@use-voltra/ios-client 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Voltra.podspec +1 -0
- package/build/commonjs/events.js +23 -0
- package/build/commonjs/events.js.map +1 -1
- package/build/commonjs/index.js +59 -0
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/live-activity/api.js +3 -1
- package/build/commonjs/live-activity/api.js.map +1 -1
- package/build/commonjs/live-activity/dynamic-api.js +173 -0
- package/build/commonjs/live-activity/dynamic-api.js.map +1 -0
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js +47 -0
- package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
- package/build/commonjs/utils/enableWidgetHotReload.js +8 -11
- package/build/commonjs/utils/enableWidgetHotReload.js.map +1 -1
- package/build/commonjs/utils/useUpdateOnHMR.js +3 -7
- package/build/commonjs/utils/useUpdateOnHMR.js.map +1 -1
- package/build/commonjs/widgets/server-credentials.js +9 -0
- package/build/commonjs/widgets/server-credentials.js.map +1 -1
- package/build/commonjs/widgets/server-update.js +78 -0
- package/build/commonjs/widgets/server-update.js.map +1 -0
- package/build/module/events.js +23 -0
- package/build/module/events.js.map +1 -1
- package/build/module/index.js +3 -0
- package/build/module/index.js.map +1 -1
- package/build/module/live-activity/api.js +2 -2
- package/build/module/live-activity/api.js.map +1 -1
- package/build/module/live-activity/dynamic-api.js +167 -0
- package/build/module/live-activity/dynamic-api.js.map +1 -0
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/module/utils/enableDynamicLiveActivityHotReload.js +43 -0
- package/build/module/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
- package/build/module/utils/enableWidgetHotReload.js +9 -11
- package/build/module/utils/enableWidgetHotReload.js.map +1 -1
- package/build/module/utils/useUpdateOnHMR.js +3 -7
- package/build/module/utils/useUpdateOnHMR.js.map +1 -1
- package/build/module/widgets/server-credentials.js +9 -0
- package/build/module/widgets/server-credentials.js.map +1 -1
- package/build/module/widgets/server-update.js +72 -0
- package/build/module/widgets/server-update.js.map +1 -0
- package/build/typescript/commonjs/events.d.ts +7 -0
- package/build/typescript/commonjs/events.d.ts.map +1 -1
- package/build/typescript/commonjs/index.d.ts +3 -0
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/live-activity/api.d.ts +7 -0
- package/build/typescript/commonjs/live-activity/api.d.ts.map +1 -1
- package/build/typescript/commonjs/live-activity/dynamic-api.d.ts +54 -0
- package/build/typescript/commonjs/live-activity/dynamic-api.d.ts.map +1 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +20 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/commonjs/types.d.ts +1 -1
- package/build/typescript/commonjs/types.d.ts.map +1 -1
- package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
- package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
- package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts +5 -7
- package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts.map +1 -1
- package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts +0 -3
- package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts.map +1 -1
- package/build/typescript/commonjs/widgets/server-credentials.d.ts +8 -0
- package/build/typescript/commonjs/widgets/server-credentials.d.ts.map +1 -1
- package/build/typescript/commonjs/widgets/server-update.d.ts +64 -0
- package/build/typescript/commonjs/widgets/server-update.d.ts.map +1 -0
- package/build/typescript/module/events.d.ts +7 -0
- package/build/typescript/module/events.d.ts.map +1 -1
- package/build/typescript/module/index.d.ts +3 -0
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/live-activity/api.d.ts +7 -0
- package/build/typescript/module/live-activity/api.d.ts.map +1 -1
- package/build/typescript/module/live-activity/dynamic-api.d.ts +54 -0
- package/build/typescript/module/live-activity/dynamic-api.d.ts.map +1 -0
- package/build/typescript/module/native/NativeVoltra.d.ts +20 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/types.d.ts +1 -1
- package/build/typescript/module/types.d.ts.map +1 -1
- package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
- package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
- package/build/typescript/module/utils/enableWidgetHotReload.d.ts +5 -7
- package/build/typescript/module/utils/enableWidgetHotReload.d.ts.map +1 -1
- package/build/typescript/module/utils/useUpdateOnHMR.d.ts +0 -3
- package/build/typescript/module/utils/useUpdateOnHMR.d.ts.map +1 -1
- package/build/typescript/module/widgets/server-credentials.d.ts +8 -0
- package/build/typescript/module/widgets/server-credentials.d.ts.map +1 -1
- package/build/typescript/module/widgets/server-update.d.ts +64 -0
- package/build/typescript/module/widgets/server-update.d.ts.map +1 -0
- package/expo-plugin/build/cjs/constants.js +20 -1
- package/expo-plugin/build/cjs/constants.js.map +1 -1
- package/expo-plugin/build/cjs/index.js +6 -0
- package/expo-plugin/build/cjs/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios/index.js +1 -0
- package/expo-plugin/build/cjs/ios/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios/infoPlist.js +28 -5
- package/expo-plugin/build/cjs/ios/infoPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios/serverUpdate.js +25 -0
- package/expo-plugin/build/cjs/ios/serverUpdate.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/clientRendered.js +4 -4
- package/expo-plugin/build/cjs/ios-widget/clientRendered.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +5 -5
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +130 -0
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/files/index.js +9 -2
- package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +6 -3
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/manifest.js +24 -0
- package/expo-plugin/build/cjs/ios-widget/files/manifest.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js +7 -0
- package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/files/swift.js +56 -26
- package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/index.js +9 -3
- package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/widgetPlist.js +34 -22
- package/expo-plugin/build/cjs/ios-widget/widgetPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +51 -11
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js +21 -5
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/cjs/utils/urlScheme.js +36 -14
- package/expo-plugin/build/cjs/utils/urlScheme.js.map +1 -1
- package/expo-plugin/build/cjs/validation.js +54 -0
- package/expo-plugin/build/cjs/validation.js.map +1 -1
- package/expo-plugin/build/esm/constants.js +17 -0
- package/expo-plugin/build/esm/constants.js.map +1 -1
- package/expo-plugin/build/esm/index.js +6 -0
- package/expo-plugin/build/esm/index.js.map +1 -1
- package/expo-plugin/build/esm/ios/index.js +1 -0
- package/expo-plugin/build/esm/ios/index.js.map +1 -1
- package/expo-plugin/build/esm/ios/infoPlist.js +28 -5
- package/expo-plugin/build/esm/ios/infoPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios/serverUpdate.js +21 -0
- package/expo-plugin/build/esm/ios/serverUpdate.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/clientRendered.js +5 -5
- package/expo-plugin/build/esm/ios-widget/clientRendered.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +6 -6
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +122 -0
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/files/index.js +10 -3
- package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +6 -3
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/manifest.js +23 -1
- package/expo-plugin/build/esm/ios-widget/files/manifest.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/swift-utils.js +4 -0
- package/expo-plugin/build/esm/ios-widget/files/swift-utils.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/files/swift.js +50 -20
- package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/index.js +9 -3
- package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/widgetPlist.js +34 -22
- package/expo-plugin/build/esm/ios-widget/widgetPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +51 -11
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/index.js +21 -5
- package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/esm/utils/urlScheme.js +34 -14
- package/expo-plugin/build/esm/utils/urlScheme.js.map +1 -1
- package/expo-plugin/build/esm/validation.js +54 -1
- package/expo-plugin/build/esm/validation.js.map +1 -1
- package/expo-plugin/build/types/constants.d.ts +10 -1
- package/expo-plugin/build/types/constants.d.ts.map +1 -1
- package/expo-plugin/build/types/index.d.ts +1 -1
- package/expo-plugin/build/types/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios/index.d.ts +2 -0
- package/expo-plugin/build/types/ios/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios/infoPlist.d.ts +2 -0
- package/expo-plugin/build/types/ios/infoPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios/serverUpdate.d.ts +11 -0
- package/expo-plugin/build/types/ios/serverUpdate.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/clientRendered.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts +7 -0
- package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/files/index.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +2 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/manifest.d.ts +9 -2
- package/expo-plugin/build/types/ios-widget/files/manifest.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts +2 -0
- package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/files/swift.d.ts +6 -2
- package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/index.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/widgetPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +2 -1
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
- package/expo-plugin/build/types/types.d.ts +23 -4
- package/expo-plugin/build/types/types.d.ts.map +1 -1
- package/expo-plugin/build/types/utils/urlScheme.d.ts +16 -0
- package/expo-plugin/build/types/utils/urlScheme.d.ts.map +1 -1
- package/expo-plugin/build/types/validation.d.ts +2 -1
- package/expo-plugin/build/types/validation.d.ts.map +1 -1
- package/expo-plugin/src/constants.ts +24 -1
- package/expo-plugin/src/index.ts +9 -0
- package/expo-plugin/src/ios/index.ts +3 -0
- package/expo-plugin/src/ios/infoPlist.ts +33 -5
- package/expo-plugin/src/ios/serverUpdate.ts +29 -0
- package/expo-plugin/src/ios-widget/clientRendered.ts +9 -5
- package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +11 -6
- package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +138 -0
- package/expo-plugin/src/ios-widget/files/index.ts +15 -4
- package/expo-plugin/src/ios-widget/files/infoPlist.ts +17 -3
- package/expo-plugin/src/ios-widget/files/manifest.ts +42 -2
- package/expo-plugin/src/ios-widget/files/swift-utils.ts +3 -0
- package/expo-plugin/src/ios-widget/files/swift.ts +69 -22
- package/expo-plugin/src/ios-widget/index.ts +13 -3
- package/expo-plugin/src/ios-widget/widgetPlist.ts +37 -23
- package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +34 -28
- package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +59 -12
- package/expo-plugin/src/ios-widget/xcode/index.ts +23 -6
- package/expo-plugin/src/types.ts +28 -4
- package/expo-plugin/src/utils/urlScheme.ts +47 -17
- package/expo-plugin/src/validation.ts +76 -1
- package/ios/Package.swift +32 -3
- package/ios/VoltraWidget.podspec +1 -0
- package/ios/app/NativeVoltra.mm +107 -7
- package/ios/app/VoltraLiveActivityManager.swift +30 -0
- package/ios/app/VoltraLiveActivityService.swift +77 -5
- package/ios/app/VoltraModule.swift +99 -1
- package/ios/app/VoltraModuleImpl.swift +100 -2
- package/ios/app/VoltraViewComponentView.mm +3 -3
- package/ios/app/VoltraWidgetService.swift +147 -12
- package/ios/app/dynamic-live-activity/VoltraDynamicLiveActivityService.swift +97 -0
- package/ios/app/dynamic-live-activity/VoltraLiveActivityChronology.swift +68 -0
- package/ios/shared/DynamicWidgetPropsStore.swift +73 -6
- package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerFetchResolver.swift +34 -0
- package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerProps.swift +55 -0
- package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerPropsStore.swift +146 -0
- package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerUpdateRunner.swift +193 -0
- package/ios/shared/ServerWidgetContentResolver.swift +60 -0
- package/ios/shared/ServerWidgetResponseStore.swift +25 -0
- package/ios/shared/ShortNames.swift +6 -0
- package/ios/shared/VoltraConfig.swift +41 -0
- package/ios/shared/VoltraConstants.swift +15 -0
- package/ios/shared/VoltraElement.swift +127 -43
- package/ios/shared/VoltraEventBus.swift +52 -7
- package/ios/shared/VoltraJSRenderer.swift +90 -22
- package/ios/shared/VoltraNode.swift +1 -109
- package/ios/shared/VoltraRefreshIntent.swift +1 -1
- package/ios/shared/VoltraWidgetKind.swift +41 -0
- package/ios/shared/VoltraWidgetServerFetcher.swift +52 -107
- package/ios/shared/WidgetServer/VoltraWidgetAppearance.swift +17 -0
- package/ios/shared/WidgetServer/VoltraWidgetServer.swift +138 -0
- package/ios/shared/WidgetServer/WidgetCanonicalConfiguration.swift +77 -0
- package/ios/shared/WidgetServer/WidgetScope.swift +55 -0
- package/ios/shared/WidgetServer/WidgetServerEtagStore.swift +55 -0
- package/ios/shared/WidgetServer/WidgetServerFetcher.swift +179 -0
- package/ios/shared/WidgetServer/WidgetServerRequestBuilder.swift +114 -0
- package/ios/shared/WidgetServer/WidgetServerSettingsCodec.swift +47 -0
- package/ios/shared/WidgetServer/WidgetServerSettingsResolver.swift +129 -0
- package/ios/shared/WidgetServer/WidgetServerSettingsStore.swift +170 -0
- package/ios/shared/WidgetServer/WidgetServerSettingsValidator.swift +73 -0
- package/ios/shared/WidgetServer/WidgetServerUpdateSettings.swift +123 -0
- package/ios/shared/WidgetServer/WidgetServerUpdateSettingsJson.swift +115 -0
- package/ios/shared/dynamic-live-activity/DynamicLiveActivityPropsCodec.swift +13 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityBundleSource.swift +156 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityObserver.swift +97 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityOperations.swift +105 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityPayloadValidator.swift +55 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRegistry.swift +143 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureQueue.swift +158 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureReporter.swift +120 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +370 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityTypes.swift +87 -0
- package/ios/shared/dynamic-live-activity/VoltraLiveActivityOrder.swift +19 -0
- package/ios/target/VoltraClientWidgetRuntime.swift +44 -6
- package/ios/target/VoltraDynamicWidgetServerUpdateProvider.swift +204 -0
- package/ios/target/VoltraHomeWidget.swift +85 -90
- package/ios/ui/Extensions/VoltraNode+View.swift +113 -0
- package/ios/ui/Generated/Parameters/ChartParameters.swift +3 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +2 -2
- package/ios/ui/Views/VoltraChart.swift +108 -0
- package/package.json +4 -4
- package/src/events.ts +34 -0
- package/src/index.ts +22 -0
- package/src/live-activity/api.ts +2 -2
- package/src/live-activity/dynamic-api.ts +240 -0
- package/src/native/NativeVoltra.ts +21 -0
- package/src/types.ts +4 -0
- package/src/utils/enableDynamicLiveActivityHotReload.ts +50 -0
- package/src/utils/enableWidgetHotReload.ts +9 -16
- package/src/utils/useUpdateOnHMR.ts +3 -12
- package/src/widgets/server-credentials.ts +8 -0
- package/src/widgets/server-update.ts +84 -0
- /package/ios/shared/{VoltraKeychainHelper.swift → WidgetServer/VoltraKeychainHelper.swift} +0 -0
|
@@ -89,6 +89,14 @@ public class VoltraModuleImpl {
|
|
|
89
89
|
VoltraHeadlessState.shared.clear()
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
func drainDynamicLiveActivityRenderFailures() {
|
|
93
|
+
VoltraEventBus.shared.drainDynamicLiveActivityRenderFailures()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func setDynamicLiveActivityRenderFailureListenerActive(_ active: Bool) {
|
|
97
|
+
VoltraEventBus.shared.setDynamicLiveActivityRenderFailureListenerActive(active)
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
var pushNotificationsEnabled: Bool {
|
|
93
101
|
// Support both keys for compatibility with older plugin and new Voltra naming
|
|
94
102
|
let main = Bundle.main
|
|
@@ -182,6 +190,48 @@ public class VoltraModuleImpl {
|
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
func startDynamicLiveActivity(definitionId: String, propsJson: String, options: StartVoltraOptions?) async throws -> String {
|
|
194
|
+
guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
|
|
195
|
+
let name = options?.activityName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
196
|
+
let activityName = name?.isEmpty == false ? name! : UUID().uuidString
|
|
197
|
+
do {
|
|
198
|
+
let props = try VoltraDynamicLiveActivityPayloadValidator.decodeProps(propsJson)
|
|
199
|
+
let staleDate = options?.staleDate.map { Date(timeIntervalSince1970: $0.doubleValue / 1000.0) }
|
|
200
|
+
let pushType = try resolvePushType(channelId: options?.channelId)
|
|
201
|
+
let request = VoltraDynamicLiveActivityCreateRequest(
|
|
202
|
+
definitionId: definitionId,
|
|
203
|
+
name: activityName,
|
|
204
|
+
deepLinkUrl: options?.deepLinkUrl,
|
|
205
|
+
props: props,
|
|
206
|
+
staleDate: staleDate,
|
|
207
|
+
relevanceScore: options?.relevanceScore?.doubleValue ?? 0.0,
|
|
208
|
+
pushType: pushType
|
|
209
|
+
)
|
|
210
|
+
return try await liveActivityService.createDynamicActivity(request)
|
|
211
|
+
} catch {
|
|
212
|
+
VoltraLogger.module.error("startDynamicLiveActivity failed: \(error)")
|
|
213
|
+
throw mapError(error)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func updateDynamicLiveActivity(activityId: String, propsJson: String, options: UpdateVoltraOptions?) async throws {
|
|
218
|
+
guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
|
|
219
|
+
do {
|
|
220
|
+
let props = try VoltraDynamicLiveActivityPayloadValidator.decodeProps(propsJson)
|
|
221
|
+
let staleDate = options?.staleDate.map { Date(timeIntervalSince1970: $0.doubleValue / 1000.0) }
|
|
222
|
+
try await liveActivityService.updateDynamicActivity(
|
|
223
|
+
byName: activityId,
|
|
224
|
+
request: VoltraDynamicLiveActivityUpdateRequest(
|
|
225
|
+
props: props,
|
|
226
|
+
staleDate: staleDate,
|
|
227
|
+
relevanceScore: options?.relevanceScore?.doubleValue ?? 0.0
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
} catch {
|
|
231
|
+
throw mapError(error)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
185
235
|
func endLiveActivity(activityId: String, options: EndVoltraOptions?) async throws {
|
|
186
236
|
guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
|
|
187
237
|
|
|
@@ -202,12 +252,17 @@ public class VoltraModuleImpl {
|
|
|
202
252
|
|
|
203
253
|
func getLatestVoltraActivityId() -> String? {
|
|
204
254
|
guard #available(iOS 16.4, *) else { return nil }
|
|
205
|
-
return liveActivityService.
|
|
255
|
+
return liveActivityService.latestActivityId()
|
|
206
256
|
}
|
|
207
257
|
|
|
208
258
|
func listVoltraActivityIds() -> [String] {
|
|
209
259
|
guard #available(iOS 16.4, *) else { return [] }
|
|
210
|
-
return liveActivityService.
|
|
260
|
+
return liveActivityService.getAllActivityReferences().map(\.id)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func getDynamicLiveActivityDefinitionIds() -> [String] {
|
|
264
|
+
guard #available(iOS 16.4, *) else { return [] }
|
|
265
|
+
return liveActivityService.dynamicLiveActivityDefinitionIds()
|
|
211
266
|
}
|
|
212
267
|
|
|
213
268
|
func isLiveActivityActive(name: String) -> Bool {
|
|
@@ -245,6 +300,13 @@ public class VoltraModuleImpl {
|
|
|
245
300
|
}
|
|
246
301
|
}
|
|
247
302
|
|
|
303
|
+
func reloadDynamicLiveActivities(definitionIds: [String]?) async {
|
|
304
|
+
#if DEBUG
|
|
305
|
+
syncDevServerURL()
|
|
306
|
+
#endif
|
|
307
|
+
await liveActivityService.reloadDynamicActivities(definitionIds: definitionIds)
|
|
308
|
+
}
|
|
309
|
+
|
|
248
310
|
// MARK: - Image Preloading
|
|
249
311
|
|
|
250
312
|
func preloadImages(images: [PreloadImageOptions]) async throws -> PreloadImagesResult {
|
|
@@ -264,6 +326,13 @@ public class VoltraModuleImpl {
|
|
|
264
326
|
let dynamicWidgetUpdater = DynamicWidgetUpdater(
|
|
265
327
|
dynamicWidgetPropsPersistence: DynamicWidgetPropsStore(),
|
|
266
328
|
dynamicWidgetTimelineReload: { dynamicWidgetId in
|
|
329
|
+
// On a server-driven widget the reload below runs getTimeline, which would otherwise
|
|
330
|
+
// fetch and overwrite what was just written. ADR 0002 says the *next scheduled* fetch
|
|
331
|
+
// overwrites app-written props, not the reload the write itself caused.
|
|
332
|
+
if VoltraWidgetServer.isServerDriven(dynamicWidgetId) {
|
|
333
|
+
DynamicWidgetServerPropsStore().noteAppWrite(for: .of(dynamicWidgetId))
|
|
334
|
+
}
|
|
335
|
+
|
|
267
336
|
VoltraWidgetService.reloadTimeline(for: dynamicWidgetId)
|
|
268
337
|
}
|
|
269
338
|
)
|
|
@@ -307,11 +376,17 @@ public class VoltraModuleImpl {
|
|
|
307
376
|
|
|
308
377
|
func clearWidget(widgetId: String) async {
|
|
309
378
|
VoltraWidgetService.removeAllData(for: widgetId)
|
|
379
|
+
VoltraWidgetService.clearWidgetServerState(for: widgetId)
|
|
310
380
|
VoltraWidgetService.reloadTimeline(for: widgetId)
|
|
311
381
|
}
|
|
312
382
|
|
|
313
383
|
func clearAllWidgets() async {
|
|
314
384
|
VoltraWidgetService.removeAllWidgets()
|
|
385
|
+
|
|
386
|
+
for widgetId in VoltraWidgetServer.serverDrivenWidgetIds {
|
|
387
|
+
VoltraWidgetService.clearWidgetServerState(for: widgetId)
|
|
388
|
+
}
|
|
389
|
+
|
|
315
390
|
VoltraWidgetService.reloadAllTimelines()
|
|
316
391
|
}
|
|
317
392
|
|
|
@@ -319,6 +394,19 @@ public class VoltraModuleImpl {
|
|
|
319
394
|
try await VoltraWidgetService.getActiveWidgets()
|
|
320
395
|
}
|
|
321
396
|
|
|
397
|
+
/// - Returns: an error message when the settings were rejected, or nil when they were applied.
|
|
398
|
+
func setWidgetServerUpdate(settingsJson: String, widgetId: String?) -> String? {
|
|
399
|
+
VoltraWidgetService.setWidgetServerUpdate(settingsJson: settingsJson, widgetId: widgetId)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func clearWidgetServerUpdate(widgetId: String?) -> String? {
|
|
403
|
+
VoltraWidgetService.clearWidgetServerUpdate(widgetId: widgetId)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
func getWidgetServerUpdate(widgetId: String?) -> String? {
|
|
407
|
+
VoltraWidgetService.getWidgetServerUpdate(widgetId: widgetId)
|
|
408
|
+
}
|
|
409
|
+
|
|
322
410
|
func setWidgetServerCredentials(token: String, headers: [String: String]?) {
|
|
323
411
|
VoltraWidgetService.setWidgetServerCredentials(token: token, headers: headers)
|
|
324
412
|
}
|
|
@@ -342,6 +430,16 @@ public class VoltraModuleImpl {
|
|
|
342
430
|
return VoltraErrors.liveActivitiesNotEnabled
|
|
343
431
|
case .notFound:
|
|
344
432
|
return VoltraErrors.notFound
|
|
433
|
+
case .rendererMismatch:
|
|
434
|
+
return VoltraErrors.rendererMismatch
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if let dynamicError = error as? VoltraDynamicLiveActivityError {
|
|
438
|
+
switch dynamicError {
|
|
439
|
+
case .rendererMismatch:
|
|
440
|
+
return VoltraErrors.rendererMismatch
|
|
441
|
+
case .unknownDefinition, .payloadTooLarge, .resourceUnavailable:
|
|
442
|
+
return VoltraErrors.unexpectedError(error)
|
|
345
443
|
}
|
|
346
444
|
}
|
|
347
445
|
return VoltraErrors.unexpectedError(error)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#import "VoltraViewComponentView.h"
|
|
2
2
|
|
|
3
|
-
#if __has_include("Voltra/
|
|
4
|
-
#import "Voltra/
|
|
3
|
+
#if __has_include("Voltra/VoltraRuntime-Swift.h")
|
|
4
|
+
#import "Voltra/VoltraRuntime-Swift.h"
|
|
5
5
|
#else
|
|
6
|
-
#import "
|
|
6
|
+
#import "VoltraRuntime-Swift.h"
|
|
7
7
|
#endif
|
|
8
8
|
|
|
9
9
|
#import <react/renderer/components/VoltraSpec/ComponentDescriptors.h>
|
|
@@ -40,7 +40,7 @@ enum VoltraWidgetService {
|
|
|
40
40
|
// MARK: - Reload
|
|
41
41
|
|
|
42
42
|
static func reloadTimeline(for widgetId: String) {
|
|
43
|
-
WidgetCenter.shared.reloadTimelines(ofKind:
|
|
43
|
+
WidgetCenter.shared.reloadTimelines(ofKind: VoltraWidgetKind.kind(for: widgetId))
|
|
44
44
|
VoltraLogger.widget.info("Reloaded timeline for '\(widgetId)'")
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -49,10 +49,145 @@ enum VoltraWidgetService {
|
|
|
49
49
|
VoltraLogger.widget.info("Reloaded all timelines")
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// MARK: - Server
|
|
52
|
+
// MARK: - Server update settings
|
|
53
|
+
|
|
54
|
+
/// Applies runtime server-update settings and reloads the widgets they affect.
|
|
55
|
+
///
|
|
56
|
+
/// Storing is only half of what an app expects from `setWidgetServerUpdate`: a new URL should be
|
|
57
|
+
/// fetched from now on, and `enabled: false` should take effect without waiting out the current
|
|
58
|
+
/// interval. A reload makes the affected timelines re-resolve their settings straight away.
|
|
59
|
+
///
|
|
60
|
+
/// - Parameter widgetId: the widget to scope the settings to, or nil for every server-driven one.
|
|
61
|
+
/// - Returns: an error message, or nil when the settings were applied.
|
|
62
|
+
static func setWidgetServerUpdate(settingsJson: String, widgetId: String?) -> String? {
|
|
63
|
+
let settings: WidgetServerUpdateSettings
|
|
64
|
+
|
|
65
|
+
switch WidgetServerUpdateSettingsJson.parse(settingsJson) {
|
|
66
|
+
case let .invalid(reason):
|
|
67
|
+
return reason
|
|
68
|
+
case let .parsed(parsed):
|
|
69
|
+
settings = parsed
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if let widgetId, let error = rejectIfNotServerDriven(widgetId) {
|
|
73
|
+
return error
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let error = WidgetServerSettingsValidator.validate(settings, isDebugBuild: VoltraWidgetServer.isDebugBuild) {
|
|
77
|
+
return error
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
WidgetServerSettingsStore.set(settings, scope: widgetId.map { .of($0) })
|
|
81
|
+
reloadServerDrivenWidgets(widgetId: widgetId)
|
|
82
|
+
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Drops the runtime settings for one widget, or the global ones, so the widget falls back to
|
|
87
|
+
/// what app.json configured. Clearing the global settings is the logout gesture.
|
|
88
|
+
static func clearWidgetServerUpdate(widgetId: String?) -> String? {
|
|
89
|
+
if let widgetId, let error = rejectIfNotServerDriven(widgetId) {
|
|
90
|
+
return error
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
WidgetServerSettingsStore.clear(scope: widgetId.map { .of($0) })
|
|
94
|
+
|
|
95
|
+
// Clearing the global settings is logout: what the previous account's server sent has to go
|
|
96
|
+
// with it, or the widget keeps showing their data. A widget-scoped clear only drops that
|
|
97
|
+
// widget's overrides, so its props are left alone.
|
|
98
|
+
if widgetId == nil {
|
|
99
|
+
clearFetchedState()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
reloadServerDrivenWidgets(widgetId: widgetId)
|
|
103
|
+
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Drops what the server last sent for every server-driven widget, so a Dynamic Widget falls
|
|
108
|
+
/// back to `{}` with `env.serverUpdate.status` of `never` — the state it is in before its first
|
|
109
|
+
/// fetch.
|
|
110
|
+
private static func clearFetchedState() {
|
|
111
|
+
let statuses = DynamicWidgetServerPropsStore()
|
|
112
|
+
let propsStore = DynamicWidgetPropsStore()
|
|
113
|
+
|
|
114
|
+
for widgetId in VoltraWidgetServer.serverDrivenWidgetIds {
|
|
115
|
+
let scope = WidgetScope.of(widgetId)
|
|
116
|
+
|
|
117
|
+
try? propsStore.clearDynamicWidgetProps(for: widgetId)
|
|
118
|
+
statuses.clear(scope)
|
|
119
|
+
WidgetServerEtagStore.clear(scope)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// The engine is chosen at generate time, so a runtime URL cannot turn a locally-driven widget
|
|
124
|
+
/// into a server-driven one. Saying so at call time is much easier to act on than a widget that
|
|
125
|
+
/// quietly never fetches.
|
|
126
|
+
private static func rejectIfNotServerDriven(_ widgetId: String) -> String? {
|
|
127
|
+
guard !VoltraWidgetServer.isServerDriven(widgetId) else { return nil }
|
|
128
|
+
|
|
129
|
+
return "Widget '\(widgetId)' is not server-driven. Add a serverUpdate entry for it in app.json "
|
|
130
|
+
+ "and rebuild; a runtime url does not change how a widget is rendered."
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static func reloadServerDrivenWidgets(widgetId: String?) {
|
|
134
|
+
guard let widgetId else {
|
|
135
|
+
for id in VoltraWidgetServer.serverDrivenWidgetIds {
|
|
136
|
+
reloadTimeline(for: id)
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
reloadTimeline(for: widgetId)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Reads settings back rather than reasoning about what was set: with a `widgetId`, the fully
|
|
145
|
+
/// resolved settings that widget would fetch with right now (nil if it is not server-driven);
|
|
146
|
+
/// with none, the raw global layer only, no defaults applied.
|
|
147
|
+
static func getWidgetServerUpdate(widgetId: String?) -> String? {
|
|
148
|
+
if let widgetId {
|
|
149
|
+
let scope = WidgetScope.of(widgetId)
|
|
150
|
+
|
|
151
|
+
guard VoltraWidgetServer.resolver.isServerDriven(scope) else { return nil }
|
|
152
|
+
|
|
153
|
+
let resolved = VoltraWidgetServer.resolver.resolve(scope)
|
|
154
|
+
let settings = WidgetServerUpdateSettings(
|
|
155
|
+
url: resolved.url,
|
|
156
|
+
intervalMinutes: resolved.intervalMinutes,
|
|
157
|
+
enabled: resolved.enabled,
|
|
158
|
+
method: resolved.method,
|
|
159
|
+
query: resolved.query,
|
|
160
|
+
headers: resolved.headers,
|
|
161
|
+
body: resolved.body
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return WidgetServerUpdateSettingsJson.stringify(settings)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
guard let global = VoltraWidgetServer.resolver.globalSettings() else { return nil }
|
|
168
|
+
|
|
169
|
+
return WidgetServerUpdateSettingsJson.stringify(global)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Drops one widget's runtime settings and its fetch history, for `clearWidget`.
|
|
173
|
+
static func clearWidgetServerState(for widgetId: String) {
|
|
174
|
+
guard VoltraWidgetServer.isServerDriven(widgetId) else { return }
|
|
175
|
+
|
|
176
|
+
let scope = WidgetScope.of(widgetId)
|
|
177
|
+
|
|
178
|
+
WidgetServerSettingsStore.clear(scope: scope)
|
|
179
|
+
WidgetServerEtagStore.clear(scope)
|
|
180
|
+
DynamicWidgetServerPropsStore().clear(scope)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// MARK: - Server credentials (deprecated)
|
|
53
184
|
|
|
54
185
|
/// Saves widget server credentials to the Keychain and reloads all widget timelines
|
|
55
186
|
/// so extensions can use them on the next fetch.
|
|
187
|
+
///
|
|
188
|
+
/// Deprecated in favour of `setWidgetServerUpdate` with an `Authorization` header. Kept as a
|
|
189
|
+
/// wrapper over the same Keychain accounts, so an app that has not migrated keeps working and
|
|
190
|
+
/// nothing has to be moved on device.
|
|
56
191
|
static func setWidgetServerCredentials(token: String, headers: [String: String]?) {
|
|
57
192
|
VoltraKeychainHelper.saveToken(token)
|
|
58
193
|
if let headers = headers {
|
|
@@ -60,13 +195,20 @@ enum VoltraWidgetService {
|
|
|
60
195
|
} else {
|
|
61
196
|
VoltraKeychainHelper.deleteHeaders()
|
|
62
197
|
}
|
|
198
|
+
// The credentials layer does not go through the settings store, but a new token is exactly the
|
|
199
|
+
// thing a widget stuck on a 401 is waiting for, so an in-flight fetch built with the old one
|
|
200
|
+
// must not commit.
|
|
201
|
+
WidgetServerSettingsStore.bumpRevision()
|
|
63
202
|
VoltraLogger.widget.info("Server credentials saved")
|
|
64
203
|
reloadAllTimelines()
|
|
65
204
|
}
|
|
66
205
|
|
|
67
206
|
/// Clears widget server credentials from the Keychain and reloads all widget timelines.
|
|
207
|
+
///
|
|
208
|
+
/// Deprecated alongside `setWidgetServerCredentials`.
|
|
68
209
|
static func clearWidgetServerCredentials() {
|
|
69
210
|
VoltraKeychainHelper.clearAll()
|
|
211
|
+
WidgetServerSettingsStore.bumpRevision()
|
|
70
212
|
VoltraLogger.widget.info("Server credentials cleared")
|
|
71
213
|
reloadAllTimelines()
|
|
72
214
|
}
|
|
@@ -80,10 +222,7 @@ enum VoltraWidgetService {
|
|
|
80
222
|
switch result {
|
|
81
223
|
case let .success(widgetInfos):
|
|
82
224
|
let mapped = widgetInfos.map { widget -> [String: String] in
|
|
83
|
-
let
|
|
84
|
-
let name = widget.kind.hasPrefix(prefix)
|
|
85
|
-
? String(widget.kind.dropFirst(prefix.count))
|
|
86
|
-
: widget.kind
|
|
225
|
+
let name = VoltraWidgetKind.widgetId(for: widget.kind) ?? widget.kind
|
|
87
226
|
|
|
88
227
|
return [
|
|
89
228
|
"name": name,
|
|
@@ -100,17 +239,13 @@ enum VoltraWidgetService {
|
|
|
100
239
|
}
|
|
101
240
|
|
|
102
241
|
/// Returns the set of widget IDs currently installed on the device.
|
|
103
|
-
/// Only
|
|
242
|
+
/// Only Voltra widgets (default `Voltra_Widget_` kind or a pinned `kind`) are included.
|
|
104
243
|
static func getInstalledWidgetIds() async throws -> Set<String> {
|
|
105
244
|
try await withCheckedThrowingContinuation { continuation in
|
|
106
245
|
WidgetCenter.shared.getCurrentConfigurations { result in
|
|
107
246
|
switch result {
|
|
108
247
|
case let .success(configs):
|
|
109
|
-
let ids = Set(configs.compactMap {
|
|
110
|
-
let prefix = VoltraStorageKeys.widgetKindPrefix
|
|
111
|
-
guard config.kind.hasPrefix(prefix) else { return nil }
|
|
112
|
-
return String(config.kind.dropFirst(prefix.count))
|
|
113
|
-
})
|
|
248
|
+
let ids = Set(configs.compactMap { VoltraWidgetKind.widgetId(for: $0.kind) })
|
|
114
249
|
continuation.resume(returning: ids)
|
|
115
250
|
case let .failure(error):
|
|
116
251
|
continuation.resume(throwing: error)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// App-process orchestration for the dynamic renderer. The legacy service keeps
|
|
5
|
+
/// only thin cross-engine coordination points and delegates feature-specific
|
|
6
|
+
/// catalog, bundle, payload, and reload behavior here.
|
|
7
|
+
final class VoltraDynamicLiveActivityService {
|
|
8
|
+
private let chronology: VoltraLiveActivityChronology
|
|
9
|
+
|
|
10
|
+
init(chronology: VoltraLiveActivityChronology = .shared) {
|
|
11
|
+
self.chronology = chronology
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func isActive(name: String) -> Bool {
|
|
15
|
+
activityReferences().contains { $0.name == name }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func activityReferences() -> [VoltraDynamicLiveActivityReference] {
|
|
19
|
+
VoltraDynamicLiveActivityRegistry.shared.activities()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func definitionIds() -> [String] {
|
|
23
|
+
VoltraDynamicLiveActivityRegistry.shared.definitionIds()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func create(_ request: VoltraDynamicLiveActivityCreateRequest) async throws -> String {
|
|
27
|
+
guard VoltraDynamicLiveActivityRegistry.shared.contains(request.definitionId) else {
|
|
28
|
+
throw VoltraDynamicLiveActivityError.unknownDefinition(request.definitionId)
|
|
29
|
+
}
|
|
30
|
+
do {
|
|
31
|
+
let source = try await VoltraDynamicLiveActivityBundleSource.loadForApp(definitionId: request.definitionId)
|
|
32
|
+
guard VoltraJSRenderer.evaluateLiveActivityBundle(source: source, definitionId: request.definitionId) else {
|
|
33
|
+
throw VoltraDynamicLiveActivityError.resourceUnavailable(
|
|
34
|
+
NSError(
|
|
35
|
+
domain: "VoltraDynamicLiveActivity",
|
|
36
|
+
code: -1,
|
|
37
|
+
userInfo: [NSLocalizedDescriptionKey: "Dynamic Live Activity bundle could not be evaluated."]
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
} catch let error as VoltraDynamicLiveActivityError {
|
|
42
|
+
throw error
|
|
43
|
+
} catch {
|
|
44
|
+
throw VoltraDynamicLiveActivityError.resourceUnavailable(error)
|
|
45
|
+
}
|
|
46
|
+
try VoltraDynamicLiveActivityPayloadValidator.validate(
|
|
47
|
+
name: request.name,
|
|
48
|
+
deepLinkUrl: request.deepLinkUrl,
|
|
49
|
+
props: request.props
|
|
50
|
+
)
|
|
51
|
+
guard let activity = try await VoltraDynamicLiveActivityRegistry.shared.create(request) else {
|
|
52
|
+
throw VoltraDynamicLiveActivityError.unknownDefinition(request.definitionId)
|
|
53
|
+
}
|
|
54
|
+
chronology.record(activity.id)
|
|
55
|
+
return request.name
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func update(byName name: String, request: VoltraDynamicLiveActivityUpdateRequest) async throws -> Bool {
|
|
59
|
+
try VoltraDynamicLiveActivityPayloadValidator.validateContentState(request.props)
|
|
60
|
+
return await VoltraDynamicLiveActivityRegistry.shared.update(byName: name, request: request)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func end(byName name: String, dismissalPolicy: ActivityUIDismissalPolicy) async -> Bool {
|
|
64
|
+
await VoltraDynamicLiveActivityRegistry.shared.end(byName: name, dismissalPolicy: dismissalPolicy)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func endAll(dismissalPolicy: ActivityUIDismissalPolicy) async {
|
|
68
|
+
await VoltraDynamicLiveActivityRegistry.shared.endAll(dismissalPolicy: dismissalPolicy)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func reload(definitionIds: [String]?) async {
|
|
72
|
+
#if DEBUG
|
|
73
|
+
let requested = definitionIds.map(Set.init)
|
|
74
|
+
let ids = requested ?? Set(self.definitionIds())
|
|
75
|
+
var refreshed = Set<String>()
|
|
76
|
+
for definitionId in ids.sorted() {
|
|
77
|
+
guard VoltraDynamicLiveActivityRegistry.shared.contains(definitionId) else { continue }
|
|
78
|
+
do {
|
|
79
|
+
let source = try await VoltraDynamicLiveActivityBundleSource.loadForApp(definitionId: definitionId)
|
|
80
|
+
guard VoltraJSRenderer.evaluateLiveActivityBundle(source: source, definitionId: definitionId) else {
|
|
81
|
+
throw VoltraDynamicLiveActivityError.resourceUnavailable(
|
|
82
|
+
NSError(
|
|
83
|
+
domain: "VoltraDynamicLiveActivity",
|
|
84
|
+
code: -2,
|
|
85
|
+
userInfo: [NSLocalizedDescriptionKey: "Dynamic Live Activity bundle could not be evaluated."]
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
refreshed.insert(definitionId)
|
|
90
|
+
} catch {
|
|
91
|
+
VoltraLogger.activity.error("Failed to refresh Dynamic Live Activity definition '\(definitionId)': \(error)")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await VoltraDynamicLiveActivityRegistry.shared.reload(definitionIds: refreshed)
|
|
95
|
+
#endif
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Persists creation-time observations made by this app process. ActivityKit
|
|
4
|
+
/// does not expose a cross-attributes-type creation timestamp, so an instance
|
|
5
|
+
/// first encountered after a process restart must never be assigned an order
|
|
6
|
+
/// merely because a catalog happened to enumerate it first.
|
|
7
|
+
final class VoltraLiveActivityChronology: @unchecked Sendable {
|
|
8
|
+
static let shared = VoltraLiveActivityChronology()
|
|
9
|
+
|
|
10
|
+
private static let storageKey = "Voltra_LiveActivityChronology"
|
|
11
|
+
|
|
12
|
+
private let lock = NSLock()
|
|
13
|
+
private let defaults: UserDefaults
|
|
14
|
+
private var timestamps: [String: TimeInterval]
|
|
15
|
+
|
|
16
|
+
init(defaults: UserDefaults = .standard) {
|
|
17
|
+
self.defaults = defaults
|
|
18
|
+
if let stored = defaults.dictionary(forKey: Self.storageKey) as? [String: TimeInterval] {
|
|
19
|
+
timestamps = stored
|
|
20
|
+
} else {
|
|
21
|
+
// Migrate the previous observation-order ledger without reordering it.
|
|
22
|
+
let legacy = defaults.stringArray(forKey: Self.storageKey) ?? []
|
|
23
|
+
timestamps = Dictionary(uniqueKeysWithValues: legacy.enumerated().map { index, id in (id, Double(index)) })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func record(_ activityId: String) {
|
|
28
|
+
lock.lock()
|
|
29
|
+
defer { lock.unlock() }
|
|
30
|
+
guard timestamps[activityId] == nil else { return }
|
|
31
|
+
timestamps[activityId] = Date().timeIntervalSince1970
|
|
32
|
+
persist()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func order(
|
|
36
|
+
_ references: [VoltraDynamicLiveActivityReference]
|
|
37
|
+
) -> [VoltraDynamicLiveActivityReference] {
|
|
38
|
+
lock.lock()
|
|
39
|
+
defer { lock.unlock() }
|
|
40
|
+
|
|
41
|
+
let activeIds = Set(references.map(\.id))
|
|
42
|
+
let nextTimestamps = timestamps.filter { activeIds.contains($0.key) }
|
|
43
|
+
if nextTimestamps != timestamps {
|
|
44
|
+
timestamps = nextTimestamps
|
|
45
|
+
persist()
|
|
46
|
+
}
|
|
47
|
+
return references.sorted { lhs, rhs in
|
|
48
|
+
switch (timestamps[lhs.id], timestamps[rhs.id]) {
|
|
49
|
+
case let (left?, right?): return left < right
|
|
50
|
+
case (_?, nil): return true
|
|
51
|
+
case (nil, _?): return false
|
|
52
|
+
case (nil, nil): return lhs.id < rhs.id
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func latest(_ references: [VoltraDynamicLiveActivityReference]) -> VoltraDynamicLiveActivityReference? {
|
|
58
|
+
lock.lock()
|
|
59
|
+
defer { lock.unlock() }
|
|
60
|
+
return references.compactMap { reference in
|
|
61
|
+
timestamps[reference.id].map { (reference, $0) }
|
|
62
|
+
}.max { $0.1 < $1.1 }?.0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func persist() {
|
|
66
|
+
defaults.set(timestamps, forKey: Self.storageKey)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -20,28 +20,95 @@ struct DynamicWidgetPropsStore {
|
|
|
20
20
|
self.storage = .success(storage)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/// The type-level (widget-scoped) write path — the app's `updateDynamicWidget`.
|
|
23
24
|
func persistDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for dynamicWidgetID: String) throws {
|
|
25
|
+
try persist(dynamicWidgetPropsJSON, storageKey: Self.storageKey(for: dynamicWidgetID))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The instance write path — a committed per-instance server fetch (ADR 0007).
|
|
29
|
+
func persistInstanceDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for scope: WidgetScope) throws {
|
|
30
|
+
guard case let .instance(widgetId, key) = scope else {
|
|
31
|
+
try persistDynamicWidgetProps(dynamicWidgetPropsJSON, for: scope.widgetId)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
try persist(dynamicWidgetPropsJSON, storageKey: VoltraStorageKeys.dynamicWidgetInstancePropsV1(scope.storageKey))
|
|
35
|
+
try addInstanceKeyToIndex(key, for: widgetId)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private func persist(_ dynamicWidgetPropsJSON: String, storageKey: String) throws {
|
|
24
39
|
let storageEntryJSON = try DynamicWidgetPropsCodec.encodeStorageEntry(
|
|
25
40
|
dynamicWidgetPropsJSON: dynamicWidgetPropsJSON
|
|
26
41
|
)
|
|
27
42
|
let resolvedStorage = try storage.get()
|
|
28
|
-
try resolvedStorage.set(storageEntryJSON, forKey:
|
|
43
|
+
try resolvedStorage.set(storageEntryJSON, forKey: storageKey)
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
/// The type-level props only, with no instance fallback — used by the trial render.
|
|
31
47
|
func dynamicWidgetProps(for dynamicWidgetID: String) -> String {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
read(storageKey: Self.storageKey(for: dynamicWidgetID)) ?? DynamicWidgetPropsCodec.emptyDynamicWidgetPropsJSON
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Props for one placement: the instance slot for `scope`'s key, falling back to the widget slot
|
|
52
|
+
/// when the instance has not fetched yet or has no configuration (ADR 0007).
|
|
53
|
+
func dynamicWidgetProps(for scope: WidgetScope) -> String {
|
|
54
|
+
switch scope {
|
|
55
|
+
case let .widget(id):
|
|
56
|
+
return dynamicWidgetProps(for: id)
|
|
57
|
+
case .instance:
|
|
58
|
+
if let instanceProps = read(storageKey: VoltraStorageKeys.dynamicWidgetInstancePropsV1(scope.storageKey)) {
|
|
59
|
+
return instanceProps
|
|
60
|
+
}
|
|
61
|
+
return dynamicWidgetProps(for: scope.widgetId)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func read(storageKey: String) -> String? {
|
|
66
|
+
guard case let .success(resolvedStorage) = storage else { return nil }
|
|
67
|
+
guard let storageEntryJSON = (try? resolvedStorage.string(forKey: storageKey)) ?? nil else {
|
|
68
|
+
return nil
|
|
37
69
|
}
|
|
38
70
|
|
|
39
71
|
return DynamicWidgetPropsCodec.decodeDynamicWidgetProps(storageEntryJSON: storageEntryJSON)
|
|
40
72
|
}
|
|
41
73
|
|
|
74
|
+
/// Clears the widget-level props and every instance slot of `dynamicWidgetID` (ADR 0007):
|
|
75
|
+
/// `clearWidget` and logout are meant to reset a widget entirely, not leave stale per-instance
|
|
76
|
+
/// data behind that a fallback would never reveal again.
|
|
42
77
|
func clearDynamicWidgetProps(for dynamicWidgetID: String) throws {
|
|
43
78
|
let resolvedStorage = try storage.get()
|
|
79
|
+
let instanceKeys = instanceKeys(for: dynamicWidgetID)
|
|
80
|
+
|
|
44
81
|
try resolvedStorage.removeObject(forKey: Self.storageKey(for: dynamicWidgetID))
|
|
82
|
+
try resolvedStorage.removeObject(forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID))
|
|
83
|
+
for key in instanceKeys {
|
|
84
|
+
let instanceStorageKey = VoltraStorageKeys.dynamicWidgetInstancePropsV1("\(dynamicWidgetID)#\(key)")
|
|
85
|
+
try resolvedStorage.removeObject(forKey: instanceStorageKey)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// The instance keys `dynamicWidgetID` has ever fetched into, so callers can clear per-instance
|
|
90
|
+
/// state that lives in other stores (status, ETag).
|
|
91
|
+
func instanceKeys(for dynamicWidgetID: String) -> Set<String> {
|
|
92
|
+
guard
|
|
93
|
+
case let .success(resolvedStorage) = storage,
|
|
94
|
+
let raw = try? resolvedStorage.string(forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID)) ?? nil,
|
|
95
|
+
let data = raw.data(using: .utf8),
|
|
96
|
+
let keys = try? JSONDecoder().decode([String].self, from: data)
|
|
97
|
+
else {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
return Set(keys)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private func addInstanceKeyToIndex(_ key: String, for dynamicWidgetID: String) throws {
|
|
104
|
+
var current = instanceKeys(for: dynamicWidgetID)
|
|
105
|
+
guard !current.contains(key) else { return }
|
|
106
|
+
current.insert(key)
|
|
107
|
+
|
|
108
|
+
let resolvedStorage = try storage.get()
|
|
109
|
+
let data = try JSONEncoder().encode(current.sorted())
|
|
110
|
+
guard let json = String(data: data, encoding: .utf8) else { return }
|
|
111
|
+
try resolvedStorage.set(json, forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID))
|
|
45
112
|
}
|
|
46
113
|
|
|
47
114
|
static func storageKey(for dynamicWidgetID: String) -> String {
|