@use-voltra/ios-client 2.1.1 → 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/README.md +61 -8
- package/Voltra.podspec +1 -0
- package/build/commonjs/dynamic-widget/api.js +15 -0
- package/build/commonjs/dynamic-widget/api.js.map +1 -0
- package/build/commonjs/events.js +23 -0
- package/build/commonjs/events.js.map +1 -1
- package/build/commonjs/index.js +74 -7
- 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/dynamic-widget/api.js +10 -0
- package/build/module/dynamic-widget/api.js.map +1 -0
- package/build/module/events.js +23 -0
- package/build/module/events.js.map +1 -1
- package/build/module/index.js +4 -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/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.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 +4 -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 +21 -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/dynamic-widget/api.d.ts +8 -0
- package/build/typescript/module/dynamic-widget/api.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 +4 -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 +21 -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 +9 -1
- 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/podfile.js +38 -12
- package/expo-plugin/build/cjs/ios-widget/podfile.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 +194 -118
- package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js +30 -22
- package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js +50 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +45 -43
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js +93 -91
- package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js +2 -2
- package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/target.js +29 -63
- package/expo-plugin/build/cjs/ios-widget/xcode/target.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 +9 -1
- 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/podfile.js +36 -11
- package/expo-plugin/build/esm/ios-widget/podfile.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 +194 -117
- package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js +31 -22
- package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js +45 -0
- package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +45 -42
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/index.js +97 -96
- package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js +41 -39
- package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js +3 -2
- package/expo-plugin/build/esm/ios-widget/xcode/productFile.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/target.js +28 -62
- package/expo-plugin/build/esm/ios-widget/xcode/target.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/podfile.d.ts +19 -1
- package/expo-plugin/build/types/ios-widget/podfile.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 +6 -8
- package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts +15 -10
- package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts +16 -0
- package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +23 -3
- package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts +16 -4
- package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts +0 -4
- package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/target.d.ts +11 -26
- package/expo-plugin/build/types/ios-widget/xcode/target.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 +14 -1
- 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/podfile.ts +46 -13
- package/expo-plugin/src/ios-widget/widgetPlist.ts +37 -23
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/fresh.pbxproj +197 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-existing-extension.pbxproj +321 -0
- package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-per-config-signing.pbxproj +203 -0
- package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +363 -0
- package/expo-plugin/src/ios-widget/xcode/__tests__/pbxConsistency.ts +130 -0
- package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +210 -164
- package/expo-plugin/src/ios-widget/xcode/configurationList.ts +52 -35
- package/expo-plugin/src/ios-widget/xcode/fileReferences.ts +51 -0
- package/expo-plugin/src/ios-widget/xcode/groups.ts +52 -54
- package/expo-plugin/src/ios-widget/xcode/index.ts +124 -119
- package/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts +59 -54
- package/expo-plugin/src/ios-widget/xcode/productFile.ts +3 -2
- package/expo-plugin/src/ios-widget/xcode/target.ts +31 -83
- 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 +39 -4
- package/ios/VoltraWidget.podspec +1 -0
- package/ios/app/NativeVoltra.mm +126 -7
- package/ios/app/VoltraLiveActivityManager.swift +30 -0
- package/ios/app/VoltraLiveActivityService.swift +77 -5
- package/ios/app/VoltraModule.swift +117 -1
- package/ios/app/VoltraModuleImpl.swift +116 -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/DynamicWidgetPropsCodec.swift +51 -0
- package/ios/shared/DynamicWidgetPropsStorage.swift +53 -0
- package/ios/shared/DynamicWidgetPropsStore.swift +117 -0
- package/ios/shared/DynamicWidgetRenderCoordinator.swift +31 -0
- 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/DynamicWidgetUpdater.swift +30 -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 +19 -0
- package/ios/shared/VoltraElement.swift +127 -43
- package/ios/shared/VoltraEventBus.swift +52 -7
- package/ios/shared/VoltraJSRenderer.swift +93 -23
- package/ios/shared/VoltraNode.swift +7 -111
- package/ios/shared/VoltraRefreshIntent.swift +1 -1
- package/ios/shared/VoltraWidgetDefaults.swift +9 -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 +60 -10
- package/ios/target/VoltraDynamicWidgetServerUpdateProvider.swift +204 -0
- package/ios/target/VoltraHomeWidget.swift +91 -92
- package/ios/target/VoltraWidget.swift +3 -1
- package/ios/ui/Extensions/VoltraNode+View.swift +113 -0
- package/ios/ui/Generated/Parameters/ChartParameters.swift +3 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +5 -3
- package/ios/ui/Helpers/VoltraProgressDriver.swift +6 -2
- package/ios/ui/Layout/VoltraFlexStackLayout.swift +9 -3
- package/ios/ui/Style/JSColorParser.swift +24 -8
- package/ios/ui/Style/JSGradientParser.swift +36 -12
- package/ios/ui/Style/JSStyleParser.swift +36 -12
- package/ios/ui/Style/LayoutStyle.swift +12 -4
- package/ios/ui/Views/VoltraChart.swift +160 -16
- package/ios/ui/Views/VoltraLinearGradient.swift +6 -2
- package/package.json +5 -5
- package/src/dynamic-widget/api.ts +25 -0
- package/src/events.ts +34 -0
- package/src/index.ts +23 -0
- package/src/live-activity/api.ts +2 -2
- package/src/live-activity/dynamic-api.ts +240 -0
- package/src/native/NativeVoltra.ts +22 -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,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
-
import SwiftUI
|
|
3
2
|
|
|
4
3
|
/// Represents anything that can be rendered (like ReactNode)
|
|
5
|
-
public indirect enum VoltraNode: Hashable
|
|
4
|
+
public indirect enum VoltraNode: Hashable {
|
|
6
5
|
/// A component element
|
|
7
6
|
case element(VoltraElement)
|
|
8
7
|
|
|
@@ -55,7 +54,9 @@ public indirect enum VoltraNode: Hashable, View {
|
|
|
55
54
|
|
|
56
55
|
/// Check if node represents nothing
|
|
57
56
|
public var isEmpty: Bool {
|
|
58
|
-
if case .empty = self {
|
|
57
|
+
if case .empty = self {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
59
60
|
return false
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -72,7 +73,9 @@ public indirect enum VoltraNode: Hashable, View {
|
|
|
72
73
|
// Extract stylesheet (key "s")
|
|
73
74
|
if case let .array(stylesheetArray) = rootObject["s"] {
|
|
74
75
|
stylesheet = stylesheetArray.compactMap { item in
|
|
75
|
-
if case let .object(dict) = item {
|
|
76
|
+
if case let .object(dict) = item {
|
|
77
|
+
return dict
|
|
78
|
+
}
|
|
76
79
|
return nil
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -84,111 +87,4 @@ public indirect enum VoltraNode: Hashable, View {
|
|
|
84
87
|
|
|
85
88
|
return VoltraNode(from: json, stylesheet: stylesheet, sharedElements: sharedElements)
|
|
86
89
|
}
|
|
87
|
-
|
|
88
|
-
// MARK: - View conformance
|
|
89
|
-
|
|
90
|
-
public var body: some View {
|
|
91
|
-
switch self {
|
|
92
|
-
case let .element(element):
|
|
93
|
-
VoltraElementView(element: element)
|
|
94
|
-
case let .array(nodes):
|
|
95
|
-
// Use stable identifiers: prefer element.id, fall back to index
|
|
96
|
-
let items: [(id: String, node: VoltraNode)] = nodes.enumerated().map { offset, node in
|
|
97
|
-
let id: String
|
|
98
|
-
if case let .element(element) = node, let elementId = element.id {
|
|
99
|
-
id = elementId
|
|
100
|
-
} else {
|
|
101
|
-
id = "idx_\(offset)"
|
|
102
|
-
}
|
|
103
|
-
return (id: id, node: node)
|
|
104
|
-
}
|
|
105
|
-
ForEach(items, id: \.id) { item in
|
|
106
|
-
item.node
|
|
107
|
-
}
|
|
108
|
-
case let .text(text):
|
|
109
|
-
Text(text)
|
|
110
|
-
case .empty:
|
|
111
|
-
EmptyView()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/// View that renders a VoltraElement based on its type
|
|
117
|
-
struct VoltraElementView: View {
|
|
118
|
-
let element: VoltraElement
|
|
119
|
-
|
|
120
|
-
var body: some View {
|
|
121
|
-
switch element.type {
|
|
122
|
-
case "Button":
|
|
123
|
-
VoltraButton(element)
|
|
124
|
-
|
|
125
|
-
case "Link":
|
|
126
|
-
VoltraLink(element)
|
|
127
|
-
|
|
128
|
-
case "VStack":
|
|
129
|
-
VoltraVStack(element)
|
|
130
|
-
|
|
131
|
-
case "HStack":
|
|
132
|
-
VoltraHStack(element)
|
|
133
|
-
|
|
134
|
-
case "View":
|
|
135
|
-
VoltraFlexView(element)
|
|
136
|
-
|
|
137
|
-
case "ZStack":
|
|
138
|
-
VoltraZStack(element)
|
|
139
|
-
|
|
140
|
-
case "Text":
|
|
141
|
-
VoltraText(element)
|
|
142
|
-
|
|
143
|
-
case "Image":
|
|
144
|
-
VoltraImage(element)
|
|
145
|
-
|
|
146
|
-
case "Symbol":
|
|
147
|
-
VoltraSymbol(element)
|
|
148
|
-
|
|
149
|
-
case "Divider":
|
|
150
|
-
VoltraDivider(element)
|
|
151
|
-
|
|
152
|
-
case "Spacer":
|
|
153
|
-
VoltraSpacer(element)
|
|
154
|
-
|
|
155
|
-
case "Label":
|
|
156
|
-
VoltraLabel(element)
|
|
157
|
-
|
|
158
|
-
case "Toggle":
|
|
159
|
-
VoltraToggle(element)
|
|
160
|
-
|
|
161
|
-
case "Gauge":
|
|
162
|
-
VoltraGauge(element)
|
|
163
|
-
|
|
164
|
-
case "LinearProgressView":
|
|
165
|
-
VoltraLinearProgressView(element)
|
|
166
|
-
|
|
167
|
-
case "CircularProgressView":
|
|
168
|
-
VoltraCircularProgressView(element)
|
|
169
|
-
|
|
170
|
-
case "Timer":
|
|
171
|
-
VoltraTimer(element)
|
|
172
|
-
|
|
173
|
-
case "GroupBox":
|
|
174
|
-
VoltraGroupBox(element)
|
|
175
|
-
|
|
176
|
-
case "LinearGradient":
|
|
177
|
-
VoltraLinearGradient(element)
|
|
178
|
-
|
|
179
|
-
case "GlassContainer":
|
|
180
|
-
VoltraGlassContainer(element)
|
|
181
|
-
|
|
182
|
-
case "Mask":
|
|
183
|
-
VoltraMask(element)
|
|
184
|
-
|
|
185
|
-
case "Chart":
|
|
186
|
-
if #available(iOS 16.0, macOS 13.0, *) {
|
|
187
|
-
VoltraChart(element)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
default:
|
|
191
|
-
EmptyView()
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
90
|
}
|
|
@@ -19,7 +19,7 @@ public struct VoltraRefreshIntent: AppIntent {
|
|
|
19
19
|
|
|
20
20
|
public func perform() async throws -> some IntentResult {
|
|
21
21
|
if let widgetId = widgetId {
|
|
22
|
-
let kind =
|
|
22
|
+
let kind = VoltraWidgetKind.kind(for: widgetId)
|
|
23
23
|
WidgetCenter.shared.reloadTimelines(ofKind: kind)
|
|
24
24
|
} else {
|
|
25
25
|
WidgetCenter.shared.reloadAllTimelines()
|
|
@@ -86,13 +86,17 @@ public enum VoltraWidgetDefaults {
|
|
|
86
86
|
|
|
87
87
|
// MARK: - Remove
|
|
88
88
|
|
|
89
|
-
/// Removes all persisted data (json, deepLinkUrl, timeline) for a single widget.
|
|
89
|
+
/// Removes all persisted data (json, deepLinkUrl, timeline, Dynamic Widget props) for a single widget.
|
|
90
90
|
/// This is the single source of truth for which keys exist per widget.
|
|
91
91
|
public static func removeAllData(for widgetId: String) {
|
|
92
92
|
guard let defaults = try? resolvedDefaults() else { return }
|
|
93
93
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
|
|
94
94
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
|
|
95
95
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
|
|
96
|
+
let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
|
|
97
|
+
storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
|
|
98
|
+
)
|
|
99
|
+
try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
|
|
96
100
|
defaults.synchronize()
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -105,11 +109,15 @@ public enum VoltraWidgetDefaults {
|
|
|
105
109
|
/// Removes all persisted data for every widget listed in the app's Info.plist.
|
|
106
110
|
public static func removeAllWidgets() {
|
|
107
111
|
guard let defaults = try? resolvedDefaults() else { return }
|
|
112
|
+
let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
|
|
113
|
+
storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
|
|
114
|
+
)
|
|
108
115
|
let widgetIds = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetIds) as? [String] ?? []
|
|
109
116
|
for widgetId in widgetIds {
|
|
110
117
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
|
|
111
118
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
|
|
112
119
|
defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
|
|
120
|
+
try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
|
|
113
121
|
}
|
|
114
122
|
defaults.synchronize()
|
|
115
123
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Maps a widget id to its WidgetKit `kind` and back.
|
|
4
|
+
///
|
|
5
|
+
/// The kind defaults to `Voltra_Widget_<id>`. A widget can pin another kind through the plugin
|
|
6
|
+
/// `kind` option, stored in Info.plist under `Voltra_WidgetKinds` (app and extension), so widgets
|
|
7
|
+
/// placed before a migration to Voltra keep their identity.
|
|
8
|
+
///
|
|
9
|
+
/// This is the only place either half of that mapping is derived; nothing else should reach for
|
|
10
|
+
/// `VoltraStorageKeys.widgetKindPrefix`.
|
|
11
|
+
public enum VoltraWidgetKind {
|
|
12
|
+
/// Info.plist cannot change for the life of the process, so the map is read once.
|
|
13
|
+
private static let mainOverrides = VoltraConfig.widgetKinds()
|
|
14
|
+
|
|
15
|
+
public static func kind(for widgetId: String) -> String {
|
|
16
|
+
kind(for: widgetId, overrides: mainOverrides)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Returns nil when the kind does not belong to a Voltra widget.
|
|
20
|
+
public static func widgetId(for kind: String) -> String? {
|
|
21
|
+
widgetId(for: kind, overrides: mainOverrides)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Pure half of `kind(for:)`, split out so the mapping is covered without building a bundle.
|
|
25
|
+
static func kind(for widgetId: String, overrides: [String: String]) -> String {
|
|
26
|
+
overrides[widgetId] ?? "\(VoltraStorageKeys.widgetKindPrefix)\(widgetId)"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Pure half of `widgetId(for:)`. Overrides win over the prefix so a widget that pins a kind
|
|
30
|
+
/// shaped like another widget's default still resolves to its own id.
|
|
31
|
+
static func widgetId(for kind: String, overrides: [String: String]) -> String? {
|
|
32
|
+
if let match = overrides.first(where: { $0.value == kind }) {
|
|
33
|
+
return match.key
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let prefix = VoltraStorageKeys.widgetKindPrefix
|
|
37
|
+
guard kind.hasPrefix(prefix) else { return nil }
|
|
38
|
+
|
|
39
|
+
return String(kind.dropFirst(prefix.count))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|