@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
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import UIKit
|
|
3
2
|
|
|
4
|
-
///
|
|
5
|
-
///
|
|
3
|
+
/// Fetches a Voltra payload from the server for a payload-driven widget.
|
|
4
|
+
///
|
|
5
|
+
/// Since ADR 0002 the request itself is built by `shared/WidgetServer`, the same code the Dynamic
|
|
6
|
+
/// engine uses. With no runtime settings set, that produces the request this fetcher always sent,
|
|
7
|
+
/// plus `locale` and a conditional `If-None-Match`; with settings set, this widget gains the
|
|
8
|
+
/// runtime URL, method, headers, query and body too. What the timeline does with the response is
|
|
9
|
+
/// unchanged.
|
|
6
10
|
public enum VoltraWidgetServerFetcher {
|
|
7
11
|
/// Errors that can occur during server fetch
|
|
8
12
|
public enum FetchError: Error, LocalizedError {
|
|
@@ -12,6 +16,8 @@ public enum VoltraWidgetServerFetcher {
|
|
|
12
16
|
case httpError(statusCode: Int)
|
|
13
17
|
case invalidResponse
|
|
14
18
|
case emptyResponse
|
|
19
|
+
/// `304`: what is already stored is still current, so there is nothing new to render.
|
|
20
|
+
case notModified
|
|
15
21
|
|
|
16
22
|
public var errorDescription: String? {
|
|
17
23
|
switch self {
|
|
@@ -27,142 +33,81 @@ public enum VoltraWidgetServerFetcher {
|
|
|
27
33
|
return "Invalid response from server"
|
|
28
34
|
case .emptyResponse:
|
|
29
35
|
return "Empty response from server"
|
|
36
|
+
case .notModified:
|
|
37
|
+
return "Server content is unchanged"
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
///
|
|
42
|
+
/// The URL this widget will fetch from, after runtime overrides. Callers use it to decide
|
|
43
|
+
/// whether a widget is server-driven at all before starting a timeline.
|
|
35
44
|
public static func serverUrl(for widgetId: String) -> String? {
|
|
36
|
-
|
|
37
|
-
if let urls = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerUrls) as? [String: String],
|
|
38
|
-
let url = urls[widgetId]
|
|
39
|
-
{
|
|
40
|
-
return url
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Fallback to UserDefaults (can be set at runtime)
|
|
44
|
-
if let group = VoltraConfig.groupIdentifier(),
|
|
45
|
-
let defaults = UserDefaults(suiteName: group)
|
|
46
|
-
{
|
|
47
|
-
return defaults.string(forKey: VoltraStorageKeys.widgetServerUrl(widgetId))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return nil
|
|
45
|
+
VoltraWidgetServer.resolver.resolve(.of(widgetId)).url
|
|
51
46
|
}
|
|
52
47
|
|
|
53
|
-
///
|
|
48
|
+
/// The resolved interval, in minutes. Runtime settings win over app.json.
|
|
54
49
|
public static func updateInterval(for widgetId: String) -> Int {
|
|
55
|
-
|
|
56
|
-
let interval = intervals[widgetId]
|
|
57
|
-
{
|
|
58
|
-
return interval
|
|
59
|
-
}
|
|
60
|
-
return 60 // default: 1 hour
|
|
50
|
+
VoltraWidgetServer.resolver.resolve(.of(widgetId)).intervalMinutes
|
|
61
51
|
}
|
|
62
52
|
|
|
53
|
+
/// Whether the widget draws a refresh button. Build-time only: the button is generated UI
|
|
54
|
+
/// structure, so unlike the URL and the interval it cannot be changed at runtime.
|
|
63
55
|
public static func isRefreshEnabled(for widgetId: String) -> Bool {
|
|
64
|
-
|
|
65
|
-
let enabled = refreshDict[widgetId]
|
|
66
|
-
{
|
|
67
|
-
return enabled
|
|
68
|
-
}
|
|
69
|
-
return false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private static func currentColorScheme() -> String {
|
|
73
|
-
if #available(iOSApplicationExtension 13.0, *) {
|
|
74
|
-
switch UITraitCollection.current.userInterfaceStyle {
|
|
75
|
-
case .dark:
|
|
76
|
-
return "dark"
|
|
77
|
-
case .light:
|
|
78
|
-
return "light"
|
|
79
|
-
default:
|
|
80
|
-
return "light"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return "light"
|
|
56
|
+
VoltraWidgetServer.isRefreshEnabled(for: widgetId)
|
|
84
57
|
}
|
|
85
58
|
|
|
86
59
|
/// Fetch widget content from the remote Voltra SSR server.
|
|
87
60
|
///
|
|
88
|
-
/// The request
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
///
|
|
92
|
-
/// - `theme` query parameter (`light` or `dark`)
|
|
93
|
-
/// - `Authorization: Bearer <token>` header (if credentials stored in Keychain)
|
|
94
|
-
/// - Any custom headers stored in Keychain
|
|
61
|
+
/// The request carries `widgetId`, `family`, `platform`, `theme` and `locale` as query
|
|
62
|
+
/// parameters, `Accept: application/json`, a Voltra user agent, whatever headers the app has set
|
|
63
|
+
/// — including `Authorization` from the deprecated credential API — and `If-None-Match` when a
|
|
64
|
+
/// stored ETag belongs to the URL being fetched.
|
|
95
65
|
///
|
|
96
66
|
/// Returns the raw JSON data from the server, ready to be parsed by VoltraNode.
|
|
97
67
|
public static func fetchWidgetContent(
|
|
98
68
|
widgetId: String,
|
|
99
69
|
family: String
|
|
100
70
|
) async throws -> Data {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Build URL with query parameters
|
|
106
|
-
guard var components = URLComponents(string: baseUrl) else {
|
|
107
|
-
throw FetchError.invalidUrl(baseUrl)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let theme = currentColorScheme()
|
|
71
|
+
let scope = WidgetScope.of(widgetId)
|
|
72
|
+
let settings = VoltraWidgetServer.resolver.resolve(scope)
|
|
111
73
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
queryItems.append(URLQueryItem(name: "family", value: family))
|
|
115
|
-
queryItems.append(URLQueryItem(name: "platform", value: "ios"))
|
|
116
|
-
queryItems.append(URLQueryItem(name: "theme", value: theme))
|
|
117
|
-
components.queryItems = queryItems
|
|
118
|
-
|
|
119
|
-
guard let url = components.url else {
|
|
120
|
-
throw FetchError.invalidUrl(baseUrl)
|
|
74
|
+
guard let url = settings.url else {
|
|
75
|
+
throw FetchError.noServerUrl
|
|
121
76
|
}
|
|
122
77
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
78
|
+
// No If-None-Match: a payload widget sends the request it always sent, plus locale. The last
|
|
79
|
+
// successful response is kept only for this extension process, so a 304 could arrive with
|
|
80
|
+
// nothing to fall back on and leave the widget on its initial state.
|
|
81
|
+
guard let request = WidgetServerRequestBuilder.build(
|
|
82
|
+
scope: scope,
|
|
83
|
+
settings: settings,
|
|
84
|
+
context: VoltraWidgetAppearance.requestContext(family: family)
|
|
85
|
+
) else {
|
|
86
|
+
throw FetchError.invalidUrl(url)
|
|
130
87
|
}
|
|
131
88
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
89
|
+
switch await WidgetServerFetcher.fetch(request) {
|
|
90
|
+
case let .success(body, _, _, _):
|
|
91
|
+
guard !body.isEmpty else {
|
|
92
|
+
throw FetchError.emptyResponse
|
|
136
93
|
}
|
|
137
|
-
}
|
|
138
94
|
|
|
139
|
-
|
|
140
|
-
let systemVersion = await MainActor.run {
|
|
141
|
-
UIDevice.current.systemVersion
|
|
142
|
-
}
|
|
143
|
-
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
144
|
-
request.setValue("VoltraWidget/1.0 (iOS/\(systemVersion))", forHTTPHeaderField: "User-Agent")
|
|
145
|
-
|
|
146
|
-
do {
|
|
147
|
-
let (data, response) = try await URLSession.shared.data(for: request)
|
|
95
|
+
return body
|
|
148
96
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
97
|
+
case .notModified:
|
|
98
|
+
// Only reachable if the server answers 304 unprompted; nothing was requested conditionally.
|
|
99
|
+
throw FetchError.notModified
|
|
152
100
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
101
|
+
case let .tooLarge(statusCode):
|
|
102
|
+
throw FetchError.httpError(statusCode: statusCode)
|
|
156
103
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
104
|
+
case let .httpFailure(statusCode, _):
|
|
105
|
+
throw FetchError.httpError(statusCode: statusCode)
|
|
160
106
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
throw FetchError.networkError(error)
|
|
107
|
+
case let .networkFailure(message):
|
|
108
|
+
throw FetchError.networkError(NSError(domain: "VoltraWidgetServerFetcher", code: -1, userInfo: [
|
|
109
|
+
NSLocalizedDescriptionKey: message,
|
|
110
|
+
]))
|
|
166
111
|
}
|
|
167
112
|
}
|
|
168
113
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// The appearance a widget is being drawn in, as the request contract reports it.
|
|
4
|
+
///
|
|
5
|
+
/// Split out from the rest of `WidgetServer` because it is the one piece that needs UIKit, and
|
|
6
|
+
/// keeping it here lets everything else — the resolver, the store, the request builder, the
|
|
7
|
+
/// fetcher — be compiled and tested on its own.
|
|
8
|
+
public enum VoltraWidgetAppearance {
|
|
9
|
+
public static func currentTheme() -> String {
|
|
10
|
+
UITraitCollection.current.userInterfaceStyle == .dark ? "dark" : "light"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// The device state a request carries, for callers that are already on a UIKit-capable target.
|
|
14
|
+
public static func requestContext(family: String? = nil) -> WidgetServerRequestContext {
|
|
15
|
+
VoltraWidgetServer.requestContext(theme: currentTheme(), family: family)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Build-time server-update defaults, read from the Info.plist keys the config plugin and the CLI
|
|
4
|
+
/// write.
|
|
5
|
+
///
|
|
6
|
+
/// `Voltra_WidgetServerIntervals` carries an entry for every server-driven widget, so its keys are
|
|
7
|
+
/// the set of widget ids this app may fetch for. `Voltra_WidgetServerUrls` carries only the ones
|
|
8
|
+
/// app.json gave a URL; the rest expect one at runtime from `setWidgetServerUpdate`.
|
|
9
|
+
struct ConfigWidgetServerSettingsLayer: WidgetServerSettingsLayer {
|
|
10
|
+
let name = "config"
|
|
11
|
+
|
|
12
|
+
private let urls: [String: String]
|
|
13
|
+
private let intervals: [String: Int]
|
|
14
|
+
|
|
15
|
+
init(bundle: Bundle = .main) {
|
|
16
|
+
urls = bundle.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerUrls) as? [String: String] ?? [:]
|
|
17
|
+
intervals = bundle.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerIntervals) as? [String: Int] ?? [:]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
init(urls: [String: String], intervals: [String: Int]) {
|
|
21
|
+
self.urls = urls
|
|
22
|
+
self.intervals = intervals
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func settings(for scope: WidgetScope) -> WidgetServerUpdateSettings? {
|
|
26
|
+
guard let interval = intervals[scope.widgetId] else { return nil }
|
|
27
|
+
|
|
28
|
+
return WidgetServerUpdateSettings(url: urls[scope.widgetId], intervalMinutes: interval)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func isServerDriven(_ scope: WidgetScope) -> Bool {
|
|
32
|
+
intervals[scope.widgetId] != nil
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var serverDrivenWidgetIds: Set<String> {
|
|
36
|
+
Set(intervals.keys)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Assembles the settings stack for the process.
|
|
41
|
+
///
|
|
42
|
+
/// Everything that needs server-update settings goes through here: the payload timeline, the
|
|
43
|
+
/// Dynamic Widget timeline, and the bridge methods. Neither engine reads Info.plist, the Keychain,
|
|
44
|
+
/// or the credential records on its own, which is what keeps the layer order and the merge rule in
|
|
45
|
+
/// one place.
|
|
46
|
+
public enum VoltraWidgetServer {
|
|
47
|
+
private static let configLayer = ConfigWidgetServerSettingsLayer()
|
|
48
|
+
|
|
49
|
+
/// Fixed order, lowest priority first. An instance layer will slot in above `widget`.
|
|
50
|
+
public static let resolver = WidgetServerSettingsResolver(
|
|
51
|
+
layers: [
|
|
52
|
+
configLayer,
|
|
53
|
+
CredentialsWidgetServerSettingsLayer(),
|
|
54
|
+
GlobalWidgetServerSettingsLayer(),
|
|
55
|
+
WidgetWidgetServerSettingsLayer(),
|
|
56
|
+
],
|
|
57
|
+
revisionSource: { WidgetServerSettingsStore.revision() }
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
/// Every widget app.json marked server-driven, whichever engine renders it.
|
|
61
|
+
public static var serverDrivenWidgetIds: Set<String> {
|
|
62
|
+
configLayer.serverDrivenWidgetIds
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static func isServerDriven(_ widgetId: String) -> Bool {
|
|
66
|
+
configLayer.isServerDriven(.of(widgetId))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Whether the widget draws a refresh button. Build-time only: the button is generated UI
|
|
70
|
+
/// structure, so unlike the URL and the interval it cannot be changed at runtime.
|
|
71
|
+
public static func isRefreshEnabled(for widgetId: String) -> Bool {
|
|
72
|
+
guard let refresh = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerRefresh) as? [String: Bool] else {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return refresh[widgetId] ?? false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Whether plain http to a local dev host is allowed. Release builds have App Transport Security
|
|
80
|
+
/// blocking cleartext anyway, so accepting such a URL there would only move the failure to fetch
|
|
81
|
+
/// time.
|
|
82
|
+
public static var isDebugBuild: Bool {
|
|
83
|
+
#if DEBUG
|
|
84
|
+
return true
|
|
85
|
+
#else
|
|
86
|
+
return false
|
|
87
|
+
#endif
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The device state a request carries. The theme is passed in rather than read here so this
|
|
91
|
+
/// whole module stays free of UIKit and the request contract can be tested without a simulator;
|
|
92
|
+
/// `VoltraWidgetAppearance` is the UIKit-side helper that supplies it.
|
|
93
|
+
public static func requestContext(theme: String, family: String? = nil) -> WidgetServerRequestContext {
|
|
94
|
+
WidgetServerRequestContext(
|
|
95
|
+
theme: theme,
|
|
96
|
+
locale: bcp47Locale(),
|
|
97
|
+
userAgent: userAgent(),
|
|
98
|
+
family: family
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// The device locale as a BCP-47 tag, so a backend sees `en-US` from both platforms.
|
|
103
|
+
/// `Locale.identifier` is the ICU form (`en_US`), which is not what the contract promises.
|
|
104
|
+
static func bcp47Locale(_ locale: Locale = .current) -> String {
|
|
105
|
+
if #available(iOS 16.0, macOS 13.0, *) {
|
|
106
|
+
return locale.identifier(.bcp47)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return locale.identifier.replacingOccurrences(of: "_", with: "-")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static func userAgent() -> String {
|
|
113
|
+
"VoltraWidget/\(VoltraConfig.voltraVersion()) (iOS/\(systemVersion))"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// `UIDevice.current.systemVersion` is main-actor isolated and a timeline provider is not, so
|
|
117
|
+
/// the OS version comes from `ProcessInfo`, which is not.
|
|
118
|
+
private static var systemVersion: String {
|
|
119
|
+
let version = ProcessInfo.processInfo.operatingSystemVersion
|
|
120
|
+
|
|
121
|
+
return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Resolves settings and builds the request for one scope, or returns nil when there is nothing
|
|
125
|
+
/// to fetch.
|
|
126
|
+
public static func request(
|
|
127
|
+
for scope: WidgetScope,
|
|
128
|
+
context: WidgetServerRequestContext,
|
|
129
|
+
etag: String? = nil
|
|
130
|
+
) -> URLRequest? {
|
|
131
|
+
WidgetServerRequestBuilder.build(
|
|
132
|
+
scope: scope,
|
|
133
|
+
settings: resolver.resolve(scope),
|
|
134
|
+
context: context,
|
|
135
|
+
etag: etag
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Turns a merged `env.configuration` map into the canonical form ADR 0007 sends on the wire and
|
|
4
|
+
/// hashes into an instance key.
|
|
5
|
+
///
|
|
6
|
+
/// Canonical serialization: keys sorted by Unicode scalar (code point), no whitespace, values as
|
|
7
|
+
/// JSON strings. Decoded, it is byte for byte what the widget sees as `env.configuration`, so
|
|
8
|
+
/// server and widget agree by construction. Both platforms produce this same string for the same
|
|
9
|
+
/// map, which is what lets the hash below be a stable, cross-platform instance identity.
|
|
10
|
+
public enum WidgetCanonicalConfiguration {
|
|
11
|
+
/// The canonical JSON string for `configuration`, or nil when the map is empty — a widget with
|
|
12
|
+
/// no configuration parameters has exactly one instance, the widget scope, and sends neither
|
|
13
|
+
/// `instance` nor `configuration`.
|
|
14
|
+
public static func canonicalize(_ configuration: [String: String]) -> String? {
|
|
15
|
+
guard !configuration.isEmpty else { return nil }
|
|
16
|
+
|
|
17
|
+
let sortedKeys = configuration.keys.sorted { lhs, rhs in
|
|
18
|
+
lhs.unicodeScalars.lexicographicallyPrecedes(rhs.unicodeScalars) { $0.value < $1.value }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var pieces: [String] = []
|
|
22
|
+
pieces.reserveCapacity(sortedKeys.count)
|
|
23
|
+
for key in sortedKeys {
|
|
24
|
+
let value = configuration[key] ?? ""
|
|
25
|
+
pieces.append("\(encodeJSONString(key)):\(encodeJSONString(value))")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return "{" + pieces.joined(separator: ",") + "}"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// The instance key for `configuration`: FNV-1a, 32-bit, over the UTF-8 bytes of the canonical
|
|
32
|
+
/// serialization, rendered as 8 lowercase hex digits. Nil when the map is empty, matching
|
|
33
|
+
/// `canonicalize`.
|
|
34
|
+
///
|
|
35
|
+
/// FNV-1a was picked for being small, dependency-free, and trivial to reproduce identically in
|
|
36
|
+
/// Kotlin — this is not a cryptographic hash, and nothing about instance identity depends on
|
|
37
|
+
/// collision resistance beyond "different configurations usually get different keys".
|
|
38
|
+
public static func key(_ configuration: [String: String]) -> String? {
|
|
39
|
+
guard let canonical = canonicalize(configuration) else { return nil }
|
|
40
|
+
return fnv1a32Hex(canonical)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Minimal, dependency-free JSON string encoder — just enough for configuration keys/values,
|
|
44
|
+
/// which are plain strings, not arbitrary JSON. Escapes the characters `JSONSerialization` would.
|
|
45
|
+
private static func encodeJSONString(_ value: String) -> String {
|
|
46
|
+
var result = "\""
|
|
47
|
+
for scalar in value.unicodeScalars {
|
|
48
|
+
switch scalar {
|
|
49
|
+
case "\"": result += "\\\""
|
|
50
|
+
case "\\": result += "\\\\"
|
|
51
|
+
case "\n": result += "\\n"
|
|
52
|
+
case "\r": result += "\\r"
|
|
53
|
+
case "\t": result += "\\t"
|
|
54
|
+
default:
|
|
55
|
+
if scalar.value < 0x20 {
|
|
56
|
+
result += String(format: "\\u%04x", scalar.value)
|
|
57
|
+
} else {
|
|
58
|
+
result.unicodeScalars.append(scalar)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
result += "\""
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static let fnvOffsetBasis: UInt32 = 0x811C_9DC5
|
|
67
|
+
private static let fnvPrime: UInt32 = 0x0100_0193
|
|
68
|
+
|
|
69
|
+
private static func fnv1a32Hex(_ value: String) -> String {
|
|
70
|
+
var hash = fnvOffsetBasis
|
|
71
|
+
for byte in Array(value.utf8) {
|
|
72
|
+
hash ^= UInt32(byte)
|
|
73
|
+
hash = hash &* fnvPrime
|
|
74
|
+
}
|
|
75
|
+
return String(format: "%08x", hash)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The unit everything server-driven is keyed by: settings, fetched props, the stored ETag, fetch
|
|
4
|
+
/// coalescing, and the settings revision.
|
|
5
|
+
///
|
|
6
|
+
/// `.widget` is a whole widget id. `.instance` (ADR 0007) is one distinct merged configuration of a
|
|
7
|
+
/// widget: two placements with identical configuration share one key, one fetch, one ETag and one
|
|
8
|
+
/// props slot. The settings resolver, ETag store, status store, runner and request builder take
|
|
9
|
+
/// either case with no signature change; the settings layers keep resolving by widget id, so an
|
|
10
|
+
/// instance inherits the widget's URL, interval, method, headers and body.
|
|
11
|
+
public enum WidgetScope: Hashable, Sendable {
|
|
12
|
+
case widget(id: String)
|
|
13
|
+
|
|
14
|
+
/// One distinct merged configuration of `id`. `key` is the hash of that configuration's
|
|
15
|
+
/// canonical serialization (see `WidgetCanonicalConfiguration`) — opaque, stable, and produced
|
|
16
|
+
/// identically on Android for the same configuration. WidgetKit has no placement id, so this is
|
|
17
|
+
/// the only identity an iOS placement can have.
|
|
18
|
+
case instance(id: String, key: String)
|
|
19
|
+
|
|
20
|
+
/// Widget id this scope belongs to. An instance scope reports the id it is an instance of.
|
|
21
|
+
public var widgetId: String {
|
|
22
|
+
switch self {
|
|
23
|
+
case let .widget(id):
|
|
24
|
+
return id
|
|
25
|
+
case let .instance(id, _):
|
|
26
|
+
return id
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Stable key for per-scope storage. An instance scope appends its placement key, so
|
|
31
|
+
/// widget-scoped records written today keep their keys.
|
|
32
|
+
public var storageKey: String {
|
|
33
|
+
switch self {
|
|
34
|
+
case let .widget(id):
|
|
35
|
+
return id
|
|
36
|
+
case let .instance(id, key):
|
|
37
|
+
return "\(id)#\(key)"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Convenience for the common case, so callers do not spell out the case name.
|
|
42
|
+
public static func of(_ widgetId: String) -> WidgetScope {
|
|
43
|
+
.widget(id: widgetId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The scope for one placement's merged `configuration` (ADR 0007): `.instance` when the
|
|
47
|
+
/// configuration is non-empty, `.widget` when it is — a widget with no configuration parameters
|
|
48
|
+
/// has exactly one instance, the widget scope, and its requests and storage are unchanged.
|
|
49
|
+
public static func of(_ widgetId: String, configuration: [String: String]) -> WidgetScope {
|
|
50
|
+
if let key = WidgetCanonicalConfiguration.key(configuration) {
|
|
51
|
+
return .instance(id: widgetId, key: key)
|
|
52
|
+
}
|
|
53
|
+
return .widget(id: widgetId)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The ETag from the last `200`, stored with the URL it came from.
|
|
4
|
+
///
|
|
5
|
+
/// Keeping the URL alongside it is what makes `If-None-Match` safe once the app can change the URL
|
|
6
|
+
/// at runtime: an ETag minted by one endpoint says nothing about another, and sending it could
|
|
7
|
+
/// produce a `304` that leaves the widget showing the previous endpoint's data forever.
|
|
8
|
+
///
|
|
9
|
+
/// Lives in the App Group so the widget extension and the app agree on it. Without an App Group
|
|
10
|
+
/// there is nowhere shared to put it, and the widget simply revalidates nothing.
|
|
11
|
+
public enum WidgetServerEtagStore {
|
|
12
|
+
/// The stored ETag, but only if it was minted by `url`.
|
|
13
|
+
public static func etag(for scope: WidgetScope, url: String?) -> String? {
|
|
14
|
+
guard let url, let defaults = defaults(),
|
|
15
|
+
defaults.string(forKey: urlKey(scope)) == url
|
|
16
|
+
else {
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return defaults.string(forKey: etagKey(scope))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static func put(_ etag: String?, for scope: WidgetScope, url: String) {
|
|
24
|
+
guard let defaults = defaults() else { return }
|
|
25
|
+
|
|
26
|
+
guard let etag else {
|
|
27
|
+
clear(scope)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
defaults.set(etag, forKey: etagKey(scope))
|
|
32
|
+
defaults.set(url, forKey: urlKey(scope))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static func clear(_ scope: WidgetScope) {
|
|
36
|
+
guard let defaults = defaults() else { return }
|
|
37
|
+
|
|
38
|
+
defaults.removeObject(forKey: etagKey(scope))
|
|
39
|
+
defaults.removeObject(forKey: urlKey(scope))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static func defaults() -> UserDefaults? {
|
|
43
|
+
guard let group = VoltraConfig.groupIdentifier() else { return nil }
|
|
44
|
+
|
|
45
|
+
return UserDefaults(suiteName: group)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static func etagKey(_ scope: WidgetScope) -> String {
|
|
49
|
+
"Voltra_WidgetServerEtag_\(scope.storageKey)"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static func urlKey(_ scope: WidgetScope) -> String {
|
|
53
|
+
"Voltra_WidgetServerEtagUrl_\(scope.storageKey)"
|
|
54
|
+
}
|
|
55
|
+
}
|