Skip to content

Commit d6e04c2

Browse files
committed
Migrate stats widgets to AppIntentConfiguration and remove the SiriKit intents extension
Switches all nine Jetpack stats widgets to AppIntentConfiguration with async AppIntentTimelineProviders using the SelectSiteIntent from JetpackStatsWidgetsCore. The site option list now comes from an in-process entity query, so the JetpackIntents extension, Sites.intentdefinition, and their localizations are removed along with the target's build tooling references, and the GlotPress upload source for the ios-widget. keys moves to an unshipped en.lproj under WordPress/JetpackStatsWidgets. The system migrates existing widget configurations by matching the legacy intent class name and site identifier. fetchStats now reports a failure instead of silently dropping the completion when a refresh is already in progress.
1 parent 47d23c1 commit d6e04c2

59 files changed

Lines changed: 101 additions & 1545 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Modules/Package.swift

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,6 @@ enum XcodeSupport {
409409
name: "XcodeTarget_NotificationServiceExtension",
410410
targets: ["XcodeTarget_NotificationServiceExtension"]
411411
),
412-
.library(name: "XcodeTarget_Intents", targets: ["XcodeTarget_Intents"]),
413412
.library(name: "XcodeTarget_StatsWidget", targets: ["XcodeTarget_StatsWidget"])
414413
]
415414
}
@@ -575,24 +574,6 @@ enum XcodeSupport {
575574
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"),
576575
.product(name: "WordPressAPI", package: "wordpress-rs")
577576
]
578-
),
579-
.xcodeTarget(
580-
"XcodeTarget_Intents",
581-
dependencies: [
582-
"BuildSettingsKit",
583-
"JetpackStatsWidgetsCore",
584-
// Even though the extensions are all in Swift, we need to include the Objective-C
585-
// version of CocoaLumberjack to avoid linking issues with other dependencies that
586-
// use it.
587-
//
588-
// Example:
589-
//
590-
// Undefined symbols for architecture arm64:
591-
// "_OBJC_CLASS_$_DDLog", referenced from:
592-
// in AppExtensionsService.o
593-
.product(name: "CocoaLumberjack", package: "CocoaLumberjack"),
594-
.product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack")
595-
]
596577
)
597578
]
598579
}

Modules/Sources/XcodeSupport/XcodeTarget_Intents/Empty.swift

Lines changed: 0 additions & 3 deletions
This file was deleted.

Scripts/BuildPhases/GenerateCredentials.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ if [ -f "$WORDPRESS_SECRETS_FILE" ] && [[ " ${WORDPRESS_TARGETS[*]} " == *" $TAR
7171
exit 0
7272
fi
7373

74-
JETPACK_TARGETS=("Jetpack" "JetpackStatsWidgets" "JetpackShareExtension" "JetpackDraftActionExtension" "JetpackNotificationServiceExtension" "JetpackIntents")
74+
JETPACK_TARGETS=("Jetpack" "JetpackStatsWidgets" "JetpackShareExtension" "JetpackDraftActionExtension" "JetpackNotificationServiceExtension")
7575
# If the Jetpack Secrets are available and if we're building Jetpack use them
7676
if [ -f "$JETPACK_SECRETS_FILE" ] && [[ " ${JETPACK_TARGETS[*]} " == *" $TARGET_NAME "* ]]; then
7777
echo "Applying Jetpack Secrets"

Sources/JetpackStatsWidgets/Sources/Helpers/WidgetDataReader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class WidgetDataReader<T: HomeWidgetData> {
1515
}
1616

1717
/// Returns cached widget data based on the selected site when editing widget and the default site.
18-
/// Configuration.site is nil until IntentHandler is initialized.
18+
/// Configuration.site is nil until the user picks a site in the widget configuration.
1919
/// Configuration.site can have old value after logging in with a different account. No way to reset configuration when the user logs out.
2020
/// Using defaultSiteID if both of these cases.
2121
/// - Parameters:
@@ -31,7 +31,7 @@ final class WidgetDataReader<T: HomeWidgetData> {
3131
}
3232

3333
return cacheReader.widgetData(
34-
forSiteIdentifier: configuration.site?.identifier,
34+
forSiteIdentifier: configuration.site?.id,
3535
defaultSiteID: defaultSiteID,
3636
userLoggedIn: defaults.bool(forKey: WidgetStatsConfiguration.userDefaultsLoggedInKey)
3737
)

Sources/JetpackStatsWidgets/Sources/LockScreenWidgets/LockScreenSiteListProvider.swift

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import Foundation
2-
import CocoaLumberjackSwift
3-
import WidgetKit
4-
import SwiftUI
51
import BuildSettingsKit
2+
import CocoaLumberjackSwift
3+
import Foundation
64
import JetpackStatsWidgetsCore
5+
import WidgetKit
76

8-
struct LockScreenSiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
7+
struct LockScreenSiteListProvider<T: HomeWidgetData>: AppIntentTimelineProvider {
98
let service: StatsWidgetsService
109
let placeholderContent: T
1110

@@ -15,8 +14,7 @@ struct LockScreenSiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
1514
let minElapsedTimeToRefresh = 1
1615

1716
private var defaultSiteID: Int? {
18-
UserDefaults(suiteName: BuildSettings.current.appGroupName)?
19-
.object(forKey: WidgetStatsConfiguration.userDefaultsSiteIdKey) as? Int
17+
WidgetStatsConfiguration.defaultSiteID(appGroup: BuildSettings.current.appGroupName)
2018
}
2119

2220
private let widgetDataLoader = WidgetDataReader<T>()
@@ -25,71 +23,58 @@ struct LockScreenSiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
2523
LockScreenStatsWidgetEntry.siteSelected(placeholderContent, context)
2624
}
2725

28-
func getSnapshot(
29-
for configuration: SelectSiteIntent,
30-
in context: Context,
31-
completion: @escaping (LockScreenStatsWidgetEntry<T>) -> Void
32-
) {
26+
func snapshot(for configuration: SelectSiteIntent, in context: Context) async -> LockScreenStatsWidgetEntry<T> {
3327
switch widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) {
3428
case .success(let widgetData):
35-
completion(.siteSelected(widgetData, context))
29+
return .siteSelected(widgetData, context)
3630
case .failure:
37-
completion(.siteSelected(placeholderContent, context))
31+
return .siteSelected(placeholderContent, context)
3832
}
3933
}
4034

41-
func getTimeline(
35+
func timeline(
4236
for configuration: SelectSiteIntent,
43-
in context: Context,
44-
completion: @escaping (Timeline<LockScreenStatsWidgetEntry<T>>) -> Void
45-
) {
46-
switch widgetDataLoader.widgetData(
47-
for: configuration,
48-
defaultSiteID: defaultSiteID
49-
) {
37+
in context: Context
38+
) async -> Timeline<LockScreenStatsWidgetEntry<T>> {
39+
switch widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) {
5040
case .success(let widgetData):
5141
let date = Date()
5242
let nextRefreshDate = Calendar.current.date(byAdding: .minute, value: refreshInterval, to: date) ?? date
5343
let elapsedTime = abs(
5444
Calendar.current.dateComponents([.minute], from: widgetData.date, to: date).minute ?? 0
5545
)
5646

57-
let privateCompletion = { (timelineEntry: LockScreenStatsWidgetEntry<T>) in
58-
let timeline = Timeline(entries: [timelineEntry], policy: .after(nextRefreshDate))
59-
completion(timeline)
60-
}
6147
// if cached data are "too old", refresh them from the backend, otherwise keep them
6248
guard elapsedTime > minElapsedTimeToRefresh else {
63-
privateCompletion(.siteSelected(widgetData, context))
64-
return
49+
return Timeline(entries: [.siteSelected(widgetData, context)], policy: .after(nextRefreshDate))
6550
}
6651

67-
service.fetchStats(for: widgetData) { result in
68-
switch result {
69-
case .failure(let error):
70-
DDLogError(
71-
"LockScreen StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)"
72-
)
73-
privateCompletion(.siteSelected(widgetData, context))
74-
case .success(let newWidgetData):
75-
if let newWidgetData = newWidgetData as? T {
76-
privateCompletion(.siteSelected(newWidgetData, context))
77-
} else {
78-
privateCompletion(.siteSelected(widgetData, context))
79-
}
52+
let entry: LockScreenStatsWidgetEntry<T>
53+
do {
54+
let newWidgetData = try await service.fetchStats(for: widgetData)
55+
if let newWidgetData = newWidgetData as? T {
56+
entry = .siteSelected(newWidgetData, context)
57+
} else {
58+
entry = .siteSelected(widgetData, context)
8059
}
60+
} catch {
61+
DDLogError(
62+
"LockScreen StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)"
63+
)
64+
entry = .siteSelected(widgetData, context)
8165
}
66+
return Timeline(entries: [entry], policy: .after(nextRefreshDate))
8267
case .failure(let error):
8368
switch error {
8469
case .noData:
85-
completion(Timeline(entries: [.noData], policy: .never))
70+
return Timeline(entries: [.noData], policy: .never)
8671
case .noSite:
87-
completion(Timeline(entries: [.noSite], policy: .never))
72+
return Timeline(entries: [.noSite], policy: .never)
8873
case .loggedOut:
89-
completion(Timeline(entries: [.loggedOut], policy: .never))
74+
return Timeline(entries: [.loggedOut], policy: .never)
9075
case .jetpackFeatureDisabled:
9176
DDLogError("LockScreen StatsWidgets: lock screen widget should have Jetpack feature disable error")
92-
completion(Timeline(entries: [.noData], policy: .never))
77+
return Timeline(entries: [.noData], policy: .never)
9378
}
9479
}
9580
}

Sources/JetpackStatsWidgets/Sources/LockScreenWidgets/LockScreenStatsWidget.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct LockScreenStatsWidget<T: LockScreenStatsWidgetConfig>: Widget {
1717
}
1818

1919
var body: some WidgetConfiguration {
20-
IntentConfiguration(
20+
AppIntentConfiguration(
2121
kind: config.kind.rawValue,
2222
intent: SelectSiteIntent.self,
2323
provider: LockScreenSiteListProvider<T.WidgetData>(
@@ -33,6 +33,6 @@ struct LockScreenStatsWidget<T: LockScreenStatsWidgetConfig>: Widget {
3333
.configurationDisplayName(config.displayName)
3434
.description(config.description)
3535
.supportedFamilies(config.supportFamilies)
36-
.iOS17ContentMarginsDisabled()/// Temporarily disable additional iOS17 margins for widgets
36+
.iOS17ContentMarginsDisabled() // Temporarily disable additional iOS17 margins for widgets
3737
}
3838
}

Sources/JetpackStatsWidgets/Sources/Services/StatsWidgetsService.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,26 @@ class StatsWidgetsService {
2828

2929
private var state: State = .ready
3030

31-
func fetchStats(
31+
enum FetchError: Error {
32+
case refreshAlreadyInProgress
33+
case unsupportedWidgetData
34+
}
35+
36+
func fetchStats(for widgetData: HomeWidgetData) async throws -> ResultType {
37+
try await withCheckedThrowingContinuation { continuation in
38+
fetchStats(for: widgetData) { result in
39+
continuation.resume(with: result)
40+
}
41+
}
42+
}
43+
44+
private func fetchStats(
3245
for widgetData: HomeWidgetData,
3346
completion: @escaping (Result<ResultType, Error>) -> Void
3447
) {
3548

3649
guard !state.isLoading else {
50+
completion(.failure(FetchError.refreshAlreadyInProgress))
3751
return
3852
}
3953
state = .loading
@@ -46,6 +60,9 @@ class StatsWidgetsService {
4660
fetchAllTimeStats(widgetData: widgetData, completion: completion)
4761
} else if let widgetData = widgetData as? HomeWidgetThisWeekData {
4862
fetchThisWeekStats(widgetData: widgetData, completion: completion)
63+
} else {
64+
state = .error
65+
completion(.failure(FetchError.unsupportedWidgetData))
4966
}
5067
}
5168

Sources/JetpackStatsWidgets/Sources/SiteListProvider.swift

Lines changed: 25 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import Foundation
2-
import CocoaLumberjackSwift
3-
import WidgetKit
41
import BuildSettingsKit
5-
import SwiftUI
2+
import CocoaLumberjackSwift
3+
import Foundation
64
import JetpackStatsWidgetsCore
5+
import WidgetKit
76

8-
struct SiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
7+
struct SiteListProvider<T: HomeWidgetData>: AppIntentTimelineProvider {
98

109
let service: StatsWidgetsService
1110
let placeholderContent: T
@@ -17,8 +16,7 @@ struct SiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
1716
let minElapsedTimeToRefresh = 1
1817

1918
private var defaultSiteID: Int? {
20-
UserDefaults(suiteName: BuildSettings.current.appGroupName)?
21-
.object(forKey: WidgetStatsConfiguration.userDefaultsSiteIdKey) as? Int
19+
WidgetStatsConfiguration.defaultSiteID(appGroup: BuildSettings.current.appGroupName)
2220
}
2321

2422
private let widgetDataLoader = WidgetDataReader<T>()
@@ -27,66 +25,50 @@ struct SiteListProvider<T: HomeWidgetData>: IntentTimelineProvider {
2725
StatsWidgetEntry.siteSelected(placeholderContent, context)
2826
}
2927

30-
func getSnapshot(
31-
for configuration: SelectSiteIntent,
32-
in context: Context,
33-
completion: @escaping (StatsWidgetEntry) -> Void
34-
) {
28+
func snapshot(for configuration: SelectSiteIntent, in context: Context) async -> StatsWidgetEntry {
3529
switch widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) {
3630
case .success(let widgetData):
37-
completion(.siteSelected(widgetData, context))
31+
return .siteSelected(widgetData, context)
3832
case .failure:
39-
completion(.siteSelected(placeholderContent, context))
33+
return .siteSelected(placeholderContent, context)
4034
}
4135
}
4236

43-
func getTimeline(
44-
for configuration: SelectSiteIntent,
45-
in context: Context,
46-
completion: @escaping (Timeline<StatsWidgetEntry>) -> Void
47-
) {
48-
switch widgetDataLoader.widgetData(
49-
for: configuration,
50-
defaultSiteID: defaultSiteID
51-
) {
37+
func timeline(for configuration: SelectSiteIntent, in context: Context) async -> Timeline<StatsWidgetEntry> {
38+
switch widgetDataLoader.widgetData(for: configuration, defaultSiteID: defaultSiteID) {
5239
case .success(let widgetData):
5340
let date = Date()
5441
let nextRefreshDate = Calendar.current.date(byAdding: .minute, value: refreshInterval, to: date) ?? date
5542
let elapsedTime = abs(
5643
Calendar.current.dateComponents([.minute], from: widgetData.date, to: date).minute ?? 0
5744
)
5845

59-
let privateCompletion = { (timelineEntry: StatsWidgetEntry) in
60-
let timeline = Timeline(entries: [timelineEntry], policy: .after(nextRefreshDate))
61-
completion(timeline)
62-
}
6346
// if cached data are "too old", refresh them from the backend, otherwise keep them
6447
guard elapsedTime > minElapsedTimeToRefresh else {
65-
privateCompletion(.siteSelected(widgetData, context))
66-
return
48+
return Timeline(entries: [.siteSelected(widgetData, context)], policy: .after(nextRefreshDate))
6749
}
6850

69-
service.fetchStats(for: widgetData) { result in
70-
switch result {
71-
case .failure(let error):
72-
DDLogError(
73-
"StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)"
74-
)
75-
privateCompletion(.siteSelected(widgetData, context))
76-
case .success(let newWidgetData):
77-
privateCompletion(.siteSelected(newWidgetData, context))
78-
}
51+
let entry: StatsWidgetEntry
52+
do {
53+
let newWidgetData = try await service.fetchStats(for: widgetData)
54+
entry = .siteSelected(newWidgetData, context)
55+
} catch {
56+
DDLogError(
57+
"StatsWidgets: failed to fetch remote stats. Returned error: \(error.localizedDescription)"
58+
)
59+
entry = .siteSelected(widgetData, context)
7960
}
61+
return Timeline(entries: [entry], policy: .after(nextRefreshDate))
8062
case .failure(let error):
8163
switch error {
8264
case .noData:
83-
completion(Timeline(entries: [.noData(widgetKind)], policy: .never))
65+
return Timeline(entries: [.noData(widgetKind)], policy: .never)
8466
case .noSite:
85-
completion(Timeline(entries: [.noSite(widgetKind)], policy: .never))
67+
return Timeline(entries: [.noSite(widgetKind)], policy: .never)
8668
case .loggedOut:
87-
completion(Timeline(entries: [.loggedOut(widgetKind)], policy: .never))
69+
return Timeline(entries: [.loggedOut(widgetKind)], policy: .never)
8870
case .jetpackFeatureDisabled:
89-
completion(Timeline(entries: [.disabled(widgetKind)], policy: .never))
71+
return Timeline(entries: [.disabled(widgetKind)], policy: .never)
9072
}
9173
}
9274
}

Sources/JetpackStatsWidgets/Sources/StatsWidgets.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import BuildSettingsKit
2+
import JetpackStatsWidgetsCore
13
import SwiftUI
24
import WidgetKit
3-
import BuildSettingsKit
45

56
@main
67
struct JetpackStatsWidgets: WidgetBundle {

Sources/JetpackStatsWidgets/Sources/Widgets/HomeWidgetAllTime.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ struct HomeWidgetAllTime: Widget {
2121
)
2222

2323
var body: some WidgetConfiguration {
24-
IntentConfiguration(
24+
AppIntentConfiguration(
2525
kind: WidgetStatsConfiguration.Kind.homeAllTime.rawValue,
2626
intent: SelectSiteIntent.self,
2727
provider: SiteListProvider<HomeWidgetAllTimeData>(
@@ -35,6 +35,6 @@ struct HomeWidgetAllTime: Widget {
3535
.configurationDisplayName(LocalizableStrings.allTimeWidgetTitle)
3636
.description(LocalizableStrings.allTimePreviewDescription)
3737
.supportedFamilies([.systemSmall, .systemMedium])
38-
.iOS17ContentMarginsDisabled()/// Temporarily disable additional iOS17 margins for widgets
38+
.iOS17ContentMarginsDisabled() // Temporarily disable additional iOS17 margins for widgets
3939
}
4040
}

0 commit comments

Comments
 (0)