@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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Call-time rules for `setWidgetServerUpdate`. Rejecting here rather than at fetch time means the
|
|
4
|
+
/// app learns about a bad setting from the promise it just awaited, instead of from a widget that
|
|
5
|
+
/// quietly stops updating hours later.
|
|
6
|
+
public enum WidgetServerSettingsValidator {
|
|
7
|
+
/// Hosts reachable over plain http, so a debug build can talk to a dev server.
|
|
8
|
+
static let localHttpHosts: Set<String> = ["localhost", "127.0.0.1", "::1", "10.0.2.2", "10.0.3.2"]
|
|
9
|
+
|
|
10
|
+
/// - Parameter isDebugBuild: whether plain http to a local dev host is allowed. Release builds
|
|
11
|
+
/// have App Transport Security blocking it anyway, so allowing it there would only defer the
|
|
12
|
+
/// failure.
|
|
13
|
+
/// - Returns: an error message, or nil when the settings are usable.
|
|
14
|
+
public static func validate(_ settings: WidgetServerUpdateSettings, isDebugBuild: Bool) -> String? {
|
|
15
|
+
if let url = settings.url, let error = validateUrl(url, isDebugBuild: isDebugBuild) {
|
|
16
|
+
return error
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if let interval = settings.intervalMinutes, interval <= 0 {
|
|
20
|
+
return "intervalMinutes must be a positive number of minutes"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if let method = settings.method,
|
|
24
|
+
!WidgetServerUpdateDefaults.supportedMethods.contains(method.uppercased())
|
|
25
|
+
{
|
|
26
|
+
let supported = WidgetServerUpdateDefaults.supportedMethods.sorted().joined(separator: ", ")
|
|
27
|
+
return "method '\(method)' is not supported. Use one of \(supported)"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for key in settings.query?.keys ?? [String: String]().keys
|
|
31
|
+
where WidgetServerUpdateDefaults.reservedQueryKeys.contains(key)
|
|
32
|
+
{
|
|
33
|
+
return "query key '\(key)' is reserved by Voltra and is sent on every request"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if let encoded = WidgetServerSettingsCodec.encode(settings),
|
|
37
|
+
encoded.count > WidgetServerUpdateDefaults.maxLayerBytes
|
|
38
|
+
{
|
|
39
|
+
return "settings are larger than \(WidgetServerUpdateDefaults.maxLayerBytes) bytes once serialized"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static func validateUrl(_ url: String, isDebugBuild: Bool) -> String? {
|
|
46
|
+
if url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
47
|
+
return "url must not be empty"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
guard let components = URLComponents(string: url),
|
|
51
|
+
let scheme = components.scheme?.lowercased(),
|
|
52
|
+
let host = components.host,
|
|
53
|
+
!host.isEmpty
|
|
54
|
+
else {
|
|
55
|
+
return "url '\(url)' must be an absolute http(s) URL"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if scheme == "https" {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if scheme != "http" {
|
|
63
|
+
return "url '\(url)' must be an absolute http(s) URL"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if isDebugBuild, localHttpHosts.contains(host) {
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let hosts = localHttpHosts.sorted().joined(separator: ", ")
|
|
71
|
+
return "url '\(url)' must use https. Plain http is allowed only in a debug build, and only for \(hosts)."
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Server-update settings as one layer holds them: every field is optional, and an unset field
|
|
4
|
+
/// means "this layer has no opinion, ask the layer below".
|
|
5
|
+
///
|
|
6
|
+
/// `body` is kept as the raw JSON text the app supplied rather than a parsed tree, because Voltra
|
|
7
|
+
/// never inspects it — it only forwards it as the request body.
|
|
8
|
+
public struct WidgetServerUpdateSettings: Equatable, Sendable {
|
|
9
|
+
public var url: String?
|
|
10
|
+
public var intervalMinutes: Int?
|
|
11
|
+
public var enabled: Bool?
|
|
12
|
+
public var method: String?
|
|
13
|
+
public var query: [String: String]?
|
|
14
|
+
public var headers: [String: String]?
|
|
15
|
+
public var body: String?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
url: String? = nil,
|
|
19
|
+
intervalMinutes: Int? = nil,
|
|
20
|
+
enabled: Bool? = nil,
|
|
21
|
+
method: String? = nil,
|
|
22
|
+
query: [String: String]? = nil,
|
|
23
|
+
headers: [String: String]? = nil,
|
|
24
|
+
body: String? = nil
|
|
25
|
+
) {
|
|
26
|
+
self.url = url
|
|
27
|
+
self.intervalMinutes = intervalMinutes
|
|
28
|
+
self.enabled = enabled
|
|
29
|
+
self.method = method
|
|
30
|
+
self.query = query
|
|
31
|
+
self.headers = headers
|
|
32
|
+
self.body = body
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public var isEmpty: Bool {
|
|
36
|
+
url == nil && intervalMinutes == nil && enabled == nil && method == nil
|
|
37
|
+
&& query == nil && headers == nil && body == nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static let empty = WidgetServerUpdateSettings()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The flattened settings a fetch actually runs on. Every field is decided: `intervalMinutes` has
|
|
44
|
+
/// the floor and ceiling applied, `enabled` and `method` have their defaults filled in, and `query`
|
|
45
|
+
/// and `headers` are the per-key merge of every layer.
|
|
46
|
+
///
|
|
47
|
+
/// `url` is the one field that can still be absent, and it means the widget is server-driven but
|
|
48
|
+
/// has nowhere to fetch from yet — the app is expected to supply one with `setWidgetServerUpdate`.
|
|
49
|
+
public struct ResolvedWidgetServerSettings: Equatable, Sendable {
|
|
50
|
+
public let url: String?
|
|
51
|
+
public let intervalMinutes: Int
|
|
52
|
+
public let enabled: Bool
|
|
53
|
+
public let method: String
|
|
54
|
+
public let query: [String: String]
|
|
55
|
+
public let headers: [String: String]
|
|
56
|
+
public let body: String?
|
|
57
|
+
|
|
58
|
+
public init(
|
|
59
|
+
url: String?,
|
|
60
|
+
intervalMinutes: Int,
|
|
61
|
+
enabled: Bool,
|
|
62
|
+
method: String,
|
|
63
|
+
query: [String: String],
|
|
64
|
+
headers: [String: String],
|
|
65
|
+
body: String?
|
|
66
|
+
) {
|
|
67
|
+
self.url = url
|
|
68
|
+
self.intervalMinutes = intervalMinutes
|
|
69
|
+
self.enabled = enabled
|
|
70
|
+
self.method = method
|
|
71
|
+
self.query = query
|
|
72
|
+
self.headers = headers
|
|
73
|
+
self.body = body
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// True when this widget has both a URL to fetch and permission to do it.
|
|
77
|
+
public var shouldFetch: Bool {
|
|
78
|
+
guard enabled, let url, !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public enum WidgetServerUpdateDefaults {
|
|
87
|
+
/// WidgetKit stretches a timeline asking for entries much closer together than five minutes, and
|
|
88
|
+
/// the reload budget is shared across every widget in the app, so a smaller number would only
|
|
89
|
+
/// misreport what the widget actually does. Matches Android's WorkManager floor.
|
|
90
|
+
public static let minIntervalMinutes = 15
|
|
91
|
+
|
|
92
|
+
/// A day. Past this the widget is effectively not server-driven, and `Cache-Control: max-age`
|
|
93
|
+
/// from a misconfigured server should not be able to park a widget for a week.
|
|
94
|
+
public static let maxIntervalMinutes = 24 * 60
|
|
95
|
+
|
|
96
|
+
public static let defaultIntervalMinutes = minIntervalMinutes
|
|
97
|
+
|
|
98
|
+
public static let defaultMethod = "GET"
|
|
99
|
+
|
|
100
|
+
/// Methods either platform's HTTP stack can send.
|
|
101
|
+
public static let supportedMethods: Set<String> = ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
102
|
+
|
|
103
|
+
/// Methods that cannot carry a body. A body set alongside one of these is dropped.
|
|
104
|
+
public static let bodylessMethods: Set<String> = ["GET", "HEAD"]
|
|
105
|
+
|
|
106
|
+
/// Query keys Voltra puts on every request. An app that set one of these would silently shadow
|
|
107
|
+
/// what the server relies on, so `setWidgetServerUpdate` rejects them.
|
|
108
|
+
public static let reservedQueryKeys: Set<String> = [
|
|
109
|
+
"widgetId", "platform", "family", "theme", "locale", "instance", "configuration",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
/// Serialized size cap for one layer, so a runaway `body` cannot fill the settings store.
|
|
113
|
+
public static let maxLayerBytes = 16 * 1024
|
|
114
|
+
|
|
115
|
+
/// Response bodies larger than this are refused. The widget extension has a 30 MB ceiling for
|
|
116
|
+
/// the whole render, and a widget needing a quarter of a megabyte of props will not fit on a
|
|
117
|
+
/// home screen either.
|
|
118
|
+
public static let maxBodyBytes = 256 * 1024
|
|
119
|
+
|
|
120
|
+
public static func clampIntervalMinutes(_ intervalMinutes: Int) -> Int {
|
|
121
|
+
min(max(intervalMinutes, minIntervalMinutes), maxIntervalMinutes)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Reads the settings object an app passes to `setWidgetServerUpdate`.
|
|
4
|
+
///
|
|
5
|
+
/// Separate from `WidgetServerSettingsCodec`, which is the versioned storage format: what the app
|
|
6
|
+
/// sends and what Voltra persists are allowed to diverge, and conflating them would make either one
|
|
7
|
+
/// hard to change. The one real difference today is `body`, which arrives as arbitrary JSON and is
|
|
8
|
+
/// kept as text because Voltra only forwards it.
|
|
9
|
+
public enum WidgetServerUpdateSettingsJson {
|
|
10
|
+
public enum Result: Equatable {
|
|
11
|
+
case parsed(WidgetServerUpdateSettings)
|
|
12
|
+
case invalid(reason: String)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static func parse(_ json: String) -> Result {
|
|
16
|
+
guard let object = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any] else {
|
|
17
|
+
return .invalid(reason: "settings must be a JSON object")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
guard let query = stringMap(object, "query") else {
|
|
21
|
+
return .invalid(reason: "query must be an object of strings")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
guard let headers = stringMap(object, "headers") else {
|
|
25
|
+
return .invalid(reason: "headers must be an object of strings")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var body: String?
|
|
29
|
+
|
|
30
|
+
if let rawBody = object["body"], !(rawBody is NSNull) {
|
|
31
|
+
guard let text = jsonText(rawBody) else {
|
|
32
|
+
return .invalid(reason: "body must be a JSON value")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body = text
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return .parsed(
|
|
39
|
+
WidgetServerUpdateSettings(
|
|
40
|
+
url: object["url"] as? String,
|
|
41
|
+
intervalMinutes: object["intervalMinutes"] as? Int,
|
|
42
|
+
enabled: object["enabled"] as? Bool,
|
|
43
|
+
method: (object["method"] as? String)?.uppercased(),
|
|
44
|
+
query: object["query"] == nil ? nil : query,
|
|
45
|
+
headers: object["headers"] == nil ? nil : headers,
|
|
46
|
+
body: body
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The other direction of `parse`: serializes settings back to the same JSON shape, for
|
|
52
|
+
/// `getWidgetServerUpdate` to hand across the bridge. No envelope — that is
|
|
53
|
+
/// `WidgetServerSettingsCodec`'s job for storage, not this one's for a single read.
|
|
54
|
+
public static func stringify(_ settings: WidgetServerUpdateSettings) -> String? {
|
|
55
|
+
var object: [String: Any] = [:]
|
|
56
|
+
|
|
57
|
+
if let url = settings.url {
|
|
58
|
+
object["url"] = url
|
|
59
|
+
}
|
|
60
|
+
if let intervalMinutes = settings.intervalMinutes {
|
|
61
|
+
object["intervalMinutes"] = intervalMinutes
|
|
62
|
+
}
|
|
63
|
+
if let enabled = settings.enabled {
|
|
64
|
+
object["enabled"] = enabled
|
|
65
|
+
}
|
|
66
|
+
if let method = settings.method {
|
|
67
|
+
object["method"] = method
|
|
68
|
+
}
|
|
69
|
+
if let query = settings.query {
|
|
70
|
+
object["query"] = query
|
|
71
|
+
}
|
|
72
|
+
if let headers = settings.headers {
|
|
73
|
+
object["headers"] = headers
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let body = settings.body {
|
|
77
|
+
guard let bodyData = body.data(using: .utf8),
|
|
78
|
+
let bodyValue = try? JSONSerialization.jsonObject(with: bodyData, options: [.fragmentsAllowed])
|
|
79
|
+
else {
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
object["body"] = bodyValue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object) else { return nil }
|
|
87
|
+
|
|
88
|
+
return String(data: data, encoding: .utf8)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Re-serializes a parsed value back to JSON text. A string body has to keep its quotes: without
|
|
92
|
+
/// them the request would carry something that is not JSON at all.
|
|
93
|
+
private static func jsonText(_ value: Any) -> String? {
|
|
94
|
+
if JSONSerialization.isValidJSONObject(value) {
|
|
95
|
+
return (try? JSONSerialization.data(withJSONObject: value)).flatMap { String(data: $0, encoding: .utf8) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A top-level scalar is not a valid JSON *object*, so it is wrapped, encoded and unwrapped.
|
|
99
|
+
guard let wrapped = try? JSONSerialization.data(withJSONObject: ["v": value]),
|
|
100
|
+
let text = String(data: wrapped, encoding: .utf8),
|
|
101
|
+
let start = text.firstIndex(of: ":")
|
|
102
|
+
else {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return String(text[text.index(after: start) ..< text.index(before: text.endIndex)])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Returns an empty map when the key is absent, and nil when it is present but not usable.
|
|
110
|
+
private static func stringMap(_ object: [String: Any], _ key: String) -> [String: String]? {
|
|
111
|
+
guard let raw = object[key], !(raw is NSNull) else { return [:] }
|
|
112
|
+
|
|
113
|
+
return raw as? [String: String]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum DynamicLiveActivityPropsCodec {
|
|
4
|
+
static func encode(_ props: [String: VoltraDynamicLiveActivityJSONValue]) -> String? {
|
|
5
|
+
guard
|
|
6
|
+
let data = try? JSONEncoder().encode(props),
|
|
7
|
+
let json = String(data: data, encoding: .utf8)
|
|
8
|
+
else {
|
|
9
|
+
return nil
|
|
10
|
+
}
|
|
11
|
+
return json
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum VoltraDynamicLiveActivityBundleSource {
|
|
4
|
+
private static let metroTimeout: TimeInterval = 1
|
|
5
|
+
|
|
6
|
+
enum LoadError: LocalizedError {
|
|
7
|
+
case metroHTTP(Int)
|
|
8
|
+
case metroTimeout
|
|
9
|
+
case nonUTF8
|
|
10
|
+
case bakedBundleNotFound(definitionId: String)
|
|
11
|
+
case cacheUnavailable
|
|
12
|
+
|
|
13
|
+
var errorDescription: String? {
|
|
14
|
+
switch self {
|
|
15
|
+
case let .metroHTTP(status):
|
|
16
|
+
return "Metro HTTP \(status) while loading a Dynamic Live Activity bundle"
|
|
17
|
+
case .metroTimeout:
|
|
18
|
+
return "Metro timed out while loading a Dynamic Live Activity bundle"
|
|
19
|
+
case .nonUTF8:
|
|
20
|
+
return "Dynamic Live Activity bundle was not UTF-8 text"
|
|
21
|
+
case let .bakedBundleNotFound(definitionId):
|
|
22
|
+
return "Production Dynamic Live Activity bundle missing for definitionId=\(definitionId)"
|
|
23
|
+
case .cacheUnavailable:
|
|
24
|
+
return "Dynamic Live Activity debug cache is unavailable"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func load(definitionId: String) throws -> String {
|
|
30
|
+
#if DEBUG
|
|
31
|
+
if let cached = try? loadFromDebugCache(definitionId: definitionId) {
|
|
32
|
+
return cached
|
|
33
|
+
}
|
|
34
|
+
return try loadFromMetroBounded(definitionId: definitionId)
|
|
35
|
+
#else
|
|
36
|
+
return try loadFromBakedAsset(definitionId: definitionId)
|
|
37
|
+
#endif
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// App-side preflight and Fast Refresh use an asynchronous fetch, then publish
|
|
41
|
+
/// the source into the App Group cache consumed by the extension process.
|
|
42
|
+
static func loadForApp(definitionId: String) async throws -> String {
|
|
43
|
+
#if DEBUG
|
|
44
|
+
let request = try metroRequest(definitionId: definitionId)
|
|
45
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
46
|
+
let source = try validateMetroResponse(data: data, response: response)
|
|
47
|
+
try saveToDebugCache(source, definitionId: definitionId)
|
|
48
|
+
return source
|
|
49
|
+
#else
|
|
50
|
+
return try loadFromBakedAsset(definitionId: definitionId)
|
|
51
|
+
#endif
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A cheap token that changes whenever the bytes behind `load` change, used to
|
|
55
|
+
/// invalidate the render cache. In DEBUG it is the debug-cache file's
|
|
56
|
+
/// modification time, which the app process rewrites on every hot reload; the
|
|
57
|
+
/// extension reads only the file's metadata, never decoding the whole bundle.
|
|
58
|
+
/// In release the baked asset never changes at runtime, so the token is
|
|
59
|
+
/// constant and the render cache stays valid for the process lifetime.
|
|
60
|
+
static func revision(definitionId: String) -> String {
|
|
61
|
+
#if DEBUG
|
|
62
|
+
guard
|
|
63
|
+
let url = try? debugCacheURL(definitionId: definitionId),
|
|
64
|
+
let modified = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date
|
|
65
|
+
else {
|
|
66
|
+
return ""
|
|
67
|
+
}
|
|
68
|
+
return String(modified.timeIntervalSince1970)
|
|
69
|
+
#else
|
|
70
|
+
return "baked"
|
|
71
|
+
#endif
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static func metroRequest(definitionId: String) throws -> URLRequest {
|
|
75
|
+
let base = VoltraWidgetDefaults.devServerURL() ?? "http://localhost:8081"
|
|
76
|
+
guard let url = URL(string: "\(base)/voltra/live-activities/\(definitionId).bundle?platform=ios&dev=true") else {
|
|
77
|
+
throw LoadError.metroHTTP(-1)
|
|
78
|
+
}
|
|
79
|
+
var request = URLRequest(url: url)
|
|
80
|
+
request.timeoutInterval = metroTimeout
|
|
81
|
+
return request
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// WidgetKit rendering is synchronous. On a cold debug process we permit one
|
|
85
|
+
/// short, bounded bootstrap request; all subsequent renders read the App Group
|
|
86
|
+
/// cache and never wait on the network.
|
|
87
|
+
private static func loadFromMetroBounded(definitionId: String) throws -> String {
|
|
88
|
+
let request = try metroRequest(definitionId: definitionId)
|
|
89
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
90
|
+
var result: Result<(Data, URLResponse), Error>?
|
|
91
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
92
|
+
configuration.timeoutIntervalForRequest = metroTimeout
|
|
93
|
+
configuration.timeoutIntervalForResource = metroTimeout
|
|
94
|
+
let session = URLSession(configuration: configuration)
|
|
95
|
+
let task = session.dataTask(with: request) { data, response, error in
|
|
96
|
+
if let error {
|
|
97
|
+
result = .failure(error)
|
|
98
|
+
} else if let data, let response {
|
|
99
|
+
result = .success((data, response))
|
|
100
|
+
} else {
|
|
101
|
+
result = .failure(LoadError.metroHTTP(-1))
|
|
102
|
+
}
|
|
103
|
+
semaphore.signal()
|
|
104
|
+
}
|
|
105
|
+
task.resume()
|
|
106
|
+
guard semaphore.wait(timeout: .now() + metroTimeout) == .success else {
|
|
107
|
+
task.cancel()
|
|
108
|
+
session.invalidateAndCancel()
|
|
109
|
+
throw LoadError.metroTimeout
|
|
110
|
+
}
|
|
111
|
+
session.finishTasksAndInvalidate()
|
|
112
|
+
guard let result else { throw LoadError.metroTimeout }
|
|
113
|
+
let (data, response) = try result.get()
|
|
114
|
+
let source = try validateMetroResponse(data: data, response: response)
|
|
115
|
+
try saveToDebugCache(source, definitionId: definitionId)
|
|
116
|
+
return source
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static func validateMetroResponse(data: Data, response: URLResponse) throws -> String {
|
|
120
|
+
if let httpResponse = response as? HTTPURLResponse, !(200 ... 299).contains(httpResponse.statusCode) {
|
|
121
|
+
throw LoadError.metroHTTP(httpResponse.statusCode)
|
|
122
|
+
}
|
|
123
|
+
guard let source = String(data: data, encoding: .utf8) else {
|
|
124
|
+
throw LoadError.nonUTF8
|
|
125
|
+
}
|
|
126
|
+
return source
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static func loadFromDebugCache(definitionId: String) throws -> String {
|
|
130
|
+
try String(contentsOf: debugCacheURL(definitionId: definitionId), encoding: .utf8)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static func saveToDebugCache(_ source: String, definitionId: String) throws {
|
|
134
|
+
try Data(source.utf8).write(to: debugCacheURL(definitionId: definitionId), options: .atomic)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static func debugCacheURL(definitionId: String) throws -> URL {
|
|
138
|
+
guard
|
|
139
|
+
let group = VoltraConfig.groupIdentifier(),
|
|
140
|
+
let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)
|
|
141
|
+
else {
|
|
142
|
+
throw LoadError.cacheUnavailable
|
|
143
|
+
}
|
|
144
|
+
return container.appendingPathComponent("voltra-live-activity-debug-\(definitionId).bundle")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static func loadFromBakedAsset(definitionId: String) throws -> String {
|
|
148
|
+
guard
|
|
149
|
+
let url = Bundle.main.url(forResource: "voltra-live-activity-\(definitionId)", withExtension: "bundle"),
|
|
150
|
+
let source = try? String(contentsOf: url, encoding: .utf8)
|
|
151
|
+
else {
|
|
152
|
+
throw LoadError.bakedBundleNotFound(definitionId: definitionId)
|
|
153
|
+
}
|
|
154
|
+
return source
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// Observes every concrete generated Dynamic Live Activity type. ActivityKit's
|
|
5
|
+
/// activity streams are type-specific, so the generated catalog invokes this
|
|
6
|
+
/// observer once for each bundled attributes type.
|
|
7
|
+
public actor VoltraDynamicLiveActivityObserver {
|
|
8
|
+
private let onActivityDiscovered: (@Sendable (String) -> Void)?
|
|
9
|
+
private let onTokenUpdated: (@Sendable (String, String) -> Void)?
|
|
10
|
+
private let onStateChanged: (@Sendable (String, String) -> Void)?
|
|
11
|
+
|
|
12
|
+
private var definitionTasks: [String: Task<Void, Never>] = [:]
|
|
13
|
+
private var tokenTasks: [String: Task<Void, Never>] = [:]
|
|
14
|
+
private var stateTasks: [String: Task<Void, Never>] = [:]
|
|
15
|
+
|
|
16
|
+
public init(
|
|
17
|
+
onActivityDiscovered: (@Sendable (String) -> Void)? = nil,
|
|
18
|
+
onTokenUpdated: (@Sendable (String, String) -> Void)? = nil,
|
|
19
|
+
onStateChanged: (@Sendable (String, String) -> Void)? = nil
|
|
20
|
+
) {
|
|
21
|
+
self.onActivityDiscovered = onActivityDiscovered
|
|
22
|
+
self.onTokenUpdated = onTokenUpdated
|
|
23
|
+
self.onStateChanged = onStateChanged
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public func observe<Attributes: VoltraDynamicLiveActivityDefinition>(_: Attributes.Type) {
|
|
27
|
+
let definitionId = Attributes.definitionId
|
|
28
|
+
guard definitionTasks[definitionId] == nil else { return }
|
|
29
|
+
|
|
30
|
+
definitionTasks[definitionId] = Task { [weak self] in
|
|
31
|
+
for activity in Activity<Attributes>.activities {
|
|
32
|
+
await self?.observe(activity, definitionId: definitionId)
|
|
33
|
+
}
|
|
34
|
+
for await activity in Activity<Attributes>.activityUpdates {
|
|
35
|
+
await self?.observe(activity, definitionId: definitionId)
|
|
36
|
+
}
|
|
37
|
+
await self?.removeDefinitionTask(for: definitionId)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public func stopObserving() {
|
|
42
|
+
for task in definitionTasks.values {
|
|
43
|
+
task.cancel()
|
|
44
|
+
}
|
|
45
|
+
for task in tokenTasks.values {
|
|
46
|
+
task.cancel()
|
|
47
|
+
}
|
|
48
|
+
for task in stateTasks.values {
|
|
49
|
+
task.cancel()
|
|
50
|
+
}
|
|
51
|
+
definitionTasks.removeAll()
|
|
52
|
+
tokenTasks.removeAll()
|
|
53
|
+
stateTasks.removeAll()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func observe<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
57
|
+
_ activity: Activity<Attributes>,
|
|
58
|
+
definitionId: String
|
|
59
|
+
) {
|
|
60
|
+
let key = "\(definitionId):\(activity.id)"
|
|
61
|
+
let name = activity.attributes.name
|
|
62
|
+
onActivityDiscovered?(activity.id)
|
|
63
|
+
|
|
64
|
+
if let onTokenUpdated, tokenTasks[key] == nil {
|
|
65
|
+
tokenTasks[key] = Task { [weak self] in
|
|
66
|
+
for await token in activity.pushTokenUpdates {
|
|
67
|
+
onTokenUpdated(name, token.hexString)
|
|
68
|
+
}
|
|
69
|
+
await self?.removeTokenTask(for: key)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if let onStateChanged, stateTasks[key] == nil {
|
|
74
|
+
stateTasks[key] = Task { [weak self] in
|
|
75
|
+
for await state in activity.activityStateUpdates {
|
|
76
|
+
onStateChanged(name, String(describing: state))
|
|
77
|
+
}
|
|
78
|
+
await self?.removeStateTask(for: key)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private func removeDefinitionTask(for definitionId: String) {
|
|
84
|
+
definitionTasks[definitionId]?.cancel()
|
|
85
|
+
definitionTasks.removeValue(forKey: definitionId)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func removeTokenTask(for key: String) {
|
|
89
|
+
tokenTasks[key]?.cancel()
|
|
90
|
+
tokenTasks.removeValue(forKey: key)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func removeStateTask(for key: String) {
|
|
94
|
+
stateTasks[key]?.cancel()
|
|
95
|
+
stateTasks.removeValue(forKey: key)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
public struct VoltraDynamicLiveActivityCreateRequest {
|
|
5
|
+
public let definitionId: String
|
|
6
|
+
public let name: String
|
|
7
|
+
public let deepLinkUrl: String?
|
|
8
|
+
public let props: [String: VoltraDynamicLiveActivityJSONValue]
|
|
9
|
+
public let staleDate: Date?
|
|
10
|
+
public let relevanceScore: Double
|
|
11
|
+
public let pushType: PushType?
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public struct VoltraDynamicLiveActivityUpdateRequest {
|
|
15
|
+
public let props: [String: VoltraDynamicLiveActivityJSONValue]
|
|
16
|
+
public let staleDate: Date?
|
|
17
|
+
public let relevanceScore: Double
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public struct VoltraDynamicLiveActivityReference: Hashable {
|
|
21
|
+
public let id: String
|
|
22
|
+
public let name: String
|
|
23
|
+
public let definitionId: String
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Generic ActivityKit operations called by the generated catalog. The catalog
|
|
27
|
+
/// supplies the concrete generated Attributes type without exposing it to the
|
|
28
|
+
/// legacy service or the React Native bridge.
|
|
29
|
+
public enum VoltraDynamicLiveActivityOperations {
|
|
30
|
+
public static func create<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
31
|
+
_: Attributes.Type,
|
|
32
|
+
request: VoltraDynamicLiveActivityCreateRequest
|
|
33
|
+
) async throws -> VoltraDynamicLiveActivityReference {
|
|
34
|
+
let attributes = Attributes(name: request.name, deepLinkUrl: request.deepLinkUrl)
|
|
35
|
+
let state = VoltraDynamicLiveActivityContentState(props: request.props)
|
|
36
|
+
let activity = try Activity.request(
|
|
37
|
+
attributes: attributes,
|
|
38
|
+
content: ActivityContent(state: state, staleDate: request.staleDate, relevanceScore: request.relevanceScore),
|
|
39
|
+
pushType: request.pushType
|
|
40
|
+
)
|
|
41
|
+
return VoltraDynamicLiveActivityReference(
|
|
42
|
+
id: activity.id,
|
|
43
|
+
name: activity.attributes.name,
|
|
44
|
+
definitionId: Attributes.definitionId
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static func update<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
49
|
+
_: Attributes.Type,
|
|
50
|
+
byName name: String,
|
|
51
|
+
request: VoltraDynamicLiveActivityUpdateRequest
|
|
52
|
+
) async -> Bool {
|
|
53
|
+
let activities = Activity<Attributes>.activities.filter { $0.attributes.name == name }
|
|
54
|
+
for activity in activities {
|
|
55
|
+
// State is deliberately replaced in full; V1 does not merge props.
|
|
56
|
+
await activity.update(ActivityContent(
|
|
57
|
+
state: VoltraDynamicLiveActivityContentState(props: request.props),
|
|
58
|
+
staleDate: request.staleDate,
|
|
59
|
+
relevanceScore: request.relevanceScore
|
|
60
|
+
))
|
|
61
|
+
}
|
|
62
|
+
return !activities.isEmpty
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static func end<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
66
|
+
_: Attributes.Type,
|
|
67
|
+
byName name: String,
|
|
68
|
+
dismissalPolicy: ActivityUIDismissalPolicy
|
|
69
|
+
) async -> Bool {
|
|
70
|
+
let activities = Activity<Attributes>.activities.filter { $0.attributes.name == name }
|
|
71
|
+
for activity in activities {
|
|
72
|
+
await activity.end(ActivityContent(state: activity.content.state, staleDate: nil), dismissalPolicy: dismissalPolicy)
|
|
73
|
+
}
|
|
74
|
+
return !activities.isEmpty
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static func endAll<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
78
|
+
_: Attributes.Type,
|
|
79
|
+
dismissalPolicy: ActivityUIDismissalPolicy
|
|
80
|
+
) async {
|
|
81
|
+
for activity in Activity<Attributes>.activities {
|
|
82
|
+
await activity.end(ActivityContent(state: activity.content.state, staleDate: nil), dismissalPolicy: dismissalPolicy)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public static func activities<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
87
|
+
_: Attributes.Type
|
|
88
|
+
) -> [VoltraDynamicLiveActivityReference] {
|
|
89
|
+
Activity<Attributes>.activities.map {
|
|
90
|
+
VoltraDynamicLiveActivityReference(id: $0.id, name: $0.attributes.name, definitionId: Attributes.definitionId)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public static func reload<Attributes: VoltraDynamicLiveActivityDefinition>(
|
|
95
|
+
_: Attributes.Type
|
|
96
|
+
) async {
|
|
97
|
+
for activity in Activity<Attributes>.activities {
|
|
98
|
+
await activity.update(ActivityContent(
|
|
99
|
+
state: activity.content.state,
|
|
100
|
+
staleDate: activity.content.staleDate,
|
|
101
|
+
relevanceScore: activity.content.relevanceScore
|
|
102
|
+
))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|