@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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/// The only way to write server-update settings, and the storage behind the three runtime layers.
|
|
5
|
+
///
|
|
6
|
+
/// Records live in the same shared Keychain the widget credentials have always used, so nothing
|
|
7
|
+
/// migrates: the deprecated `setWidgetServerCredentials` keeps writing the accounts it always did,
|
|
8
|
+
/// and this store adds its own accounts alongside them. The Keychain is also the only store both
|
|
9
|
+
/// the app and the widget extension can reach without depending on an App Group being configured.
|
|
10
|
+
///
|
|
11
|
+
/// Callers do not read through this class. They read through `WidgetServerSettingsResolver`, which
|
|
12
|
+
/// is what keeps the layer order and the merge rule in one place.
|
|
13
|
+
public enum WidgetServerSettingsStore {
|
|
14
|
+
private static let service = "voltra-widget-server-credentials"
|
|
15
|
+
private static let globalAccount = "server_update_global"
|
|
16
|
+
private static let widgetAccountPrefix = "server_update_widget_"
|
|
17
|
+
private static let revisionAccount = "server_update_revision"
|
|
18
|
+
|
|
19
|
+
// MARK: - Writes
|
|
20
|
+
|
|
21
|
+
/// Replaces the global layer, or one widget's layer when `scope` is given.
|
|
22
|
+
@discardableResult
|
|
23
|
+
public static func set(_ settings: WidgetServerUpdateSettings, scope: WidgetScope?) -> Bool {
|
|
24
|
+
guard let data = WidgetServerSettingsCodec.encode(settings) else { return false }
|
|
25
|
+
|
|
26
|
+
let saved = write(data, account: account(for: scope))
|
|
27
|
+
bumpRevision()
|
|
28
|
+
|
|
29
|
+
return saved
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Empties the global layer, or one widget's layer when `scope` is given.
|
|
33
|
+
public static func clear(scope: WidgetScope?) {
|
|
34
|
+
delete(account: account(for: scope))
|
|
35
|
+
bumpRevision()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Bumps the revision without changing a layer. The credentials layer writes through the
|
|
39
|
+
/// deprecated credential API, which does not go through `set`, so it calls this to make sure an
|
|
40
|
+
/// in-flight fetch built with the old token does not commit.
|
|
41
|
+
public static func bumpRevision() {
|
|
42
|
+
let next = revision() &+ 1
|
|
43
|
+
write(Data("\(next)".utf8), account: revisionAccount)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static func revision() -> Int {
|
|
47
|
+
guard let data = read(account: revisionAccount),
|
|
48
|
+
let text = String(data: data, encoding: .utf8),
|
|
49
|
+
let value = Int(text)
|
|
50
|
+
else {
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// MARK: - Reads
|
|
58
|
+
|
|
59
|
+
static func settings(scope: WidgetScope?) -> WidgetServerUpdateSettings? {
|
|
60
|
+
WidgetServerSettingsCodec.decode(read(account: account(for: scope)))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static func account(for scope: WidgetScope?) -> String {
|
|
64
|
+
guard let scope else { return globalAccount }
|
|
65
|
+
|
|
66
|
+
return "\(widgetAccountPrefix)\(scope.storageKey)"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - Keychain
|
|
70
|
+
|
|
71
|
+
private static func baseQuery(account: String) -> [String: Any] {
|
|
72
|
+
var query: [String: Any] = [
|
|
73
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
74
|
+
kSecAttrService as String: service,
|
|
75
|
+
kSecAttrAccount as String: account,
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
if let group = VoltraConfig.keychainGroup() {
|
|
79
|
+
query[kSecAttrAccessGroup as String] = group
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return query
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@discardableResult
|
|
86
|
+
private static func write(_ data: Data, account: String) -> Bool {
|
|
87
|
+
delete(account: account)
|
|
88
|
+
|
|
89
|
+
var query = baseQuery(account: account)
|
|
90
|
+
query[kSecValueData as String] = data
|
|
91
|
+
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
|
92
|
+
|
|
93
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
94
|
+
|
|
95
|
+
if status != errSecSuccess {
|
|
96
|
+
VoltraLogger.keychain.error("Failed to save widget server settings: \(status, privacy: .public)")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return status == errSecSuccess
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static func read(account: String) -> Data? {
|
|
103
|
+
var query = baseQuery(account: account)
|
|
104
|
+
query[kSecReturnData as String] = true
|
|
105
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
106
|
+
|
|
107
|
+
var item: CFTypeRef?
|
|
108
|
+
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
|
109
|
+
|
|
110
|
+
guard status == errSecSuccess, let data = item as? Data else {
|
|
111
|
+
if status != errSecItemNotFound {
|
|
112
|
+
VoltraLogger.keychain.error("Failed to read widget server settings: \(status, privacy: .public)")
|
|
113
|
+
}
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return data
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private static func delete(account: String) {
|
|
121
|
+
SecItemDelete(baseQuery(account: account) as CFDictionary)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Settings the app set for every server-driven widget.
|
|
126
|
+
struct GlobalWidgetServerSettingsLayer: WidgetServerSettingsLayer {
|
|
127
|
+
let name = "global"
|
|
128
|
+
|
|
129
|
+
func settings(for _: WidgetScope) -> WidgetServerUpdateSettings? {
|
|
130
|
+
WidgetServerSettingsStore.settings(scope: nil)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Raw contents of this layer, for reading back what was set — no scope to resolve against.
|
|
134
|
+
func raw() -> WidgetServerUpdateSettings? {
|
|
135
|
+
WidgetServerSettingsStore.settings(scope: nil)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Settings the app set for one widget. Highest layer until instance scopes arrive.
|
|
140
|
+
struct WidgetWidgetServerSettingsLayer: WidgetServerSettingsLayer {
|
|
141
|
+
let name = "widget"
|
|
142
|
+
|
|
143
|
+
func settings(for scope: WidgetScope) -> WidgetServerUpdateSettings? {
|
|
144
|
+
WidgetServerSettingsStore.settings(scope: scope)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// The deprecated `setWidgetServerCredentials` API, expressed as a settings layer.
|
|
149
|
+
///
|
|
150
|
+
/// It reads the same token and header records it always has, which is why nothing migrates. It
|
|
151
|
+
/// sits below the global layer so an app that has moved to
|
|
152
|
+
/// `setWidgetServerUpdate({ headers: { Authorization: ... } })` overrides whatever an older call
|
|
153
|
+
/// left behind, rather than the other way round.
|
|
154
|
+
struct CredentialsWidgetServerSettingsLayer: WidgetServerSettingsLayer {
|
|
155
|
+
let name = "credentials"
|
|
156
|
+
|
|
157
|
+
func settings(for _: WidgetScope) -> WidgetServerUpdateSettings? {
|
|
158
|
+
var headers: [String: String] = [:]
|
|
159
|
+
|
|
160
|
+
if let token = VoltraKeychainHelper.readToken(), !token.isEmpty {
|
|
161
|
+
headers["Authorization"] = "Bearer \(token)"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if let custom = VoltraKeychainHelper.readHeaders() {
|
|
165
|
+
headers.merge(custom) { _, fromCustom in fromCustom }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return headers.isEmpty ? nil : WidgetServerUpdateSettings(headers: headers)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Call-time rules for `setWidgetServerUpdate`. Rejecting here rather than at fetch time means the
|
|
4
|
+
/// app learns about a bad setting from the promise it just awaited, instead of from a widget that
|
|
5
|
+
/// quietly stops updating hours later.
|
|
6
|
+
public enum WidgetServerSettingsValidator {
|
|
7
|
+
/// Hosts reachable over plain http, so a debug build can talk to a dev server.
|
|
8
|
+
static let localHttpHosts: Set<String> = ["localhost", "127.0.0.1", "::1", "10.0.2.2", "10.0.3.2"]
|
|
9
|
+
|
|
10
|
+
/// - Parameter isDebugBuild: whether plain http to a local dev host is allowed. Release builds
|
|
11
|
+
/// have App Transport Security blocking it anyway, so allowing it there would only defer the
|
|
12
|
+
/// failure.
|
|
13
|
+
/// - Returns: an error message, or nil when the settings are usable.
|
|
14
|
+
public static func validate(_ settings: WidgetServerUpdateSettings, isDebugBuild: Bool) -> String? {
|
|
15
|
+
if let url = settings.url, let error = validateUrl(url, isDebugBuild: isDebugBuild) {
|
|
16
|
+
return error
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if let interval = settings.intervalMinutes, interval <= 0 {
|
|
20
|
+
return "intervalMinutes must be a positive number of minutes"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if let method = settings.method,
|
|
24
|
+
!WidgetServerUpdateDefaults.supportedMethods.contains(method.uppercased())
|
|
25
|
+
{
|
|
26
|
+
let supported = WidgetServerUpdateDefaults.supportedMethods.sorted().joined(separator: ", ")
|
|
27
|
+
return "method '\(method)' is not supported. Use one of \(supported)"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for key in settings.query?.keys ?? [String: String]().keys
|
|
31
|
+
where WidgetServerUpdateDefaults.reservedQueryKeys.contains(key)
|
|
32
|
+
{
|
|
33
|
+
return "query key '\(key)' is reserved by Voltra and is sent on every request"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if let encoded = WidgetServerSettingsCodec.encode(settings),
|
|
37
|
+
encoded.count > WidgetServerUpdateDefaults.maxLayerBytes
|
|
38
|
+
{
|
|
39
|
+
return "settings are larger than \(WidgetServerUpdateDefaults.maxLayerBytes) bytes once serialized"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static func validateUrl(_ url: String, isDebugBuild: Bool) -> String? {
|
|
46
|
+
if url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
47
|
+
return "url must not be empty"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
guard let components = URLComponents(string: url),
|
|
51
|
+
let scheme = components.scheme?.lowercased(),
|
|
52
|
+
let host = components.host,
|
|
53
|
+
!host.isEmpty
|
|
54
|
+
else {
|
|
55
|
+
return "url '\(url)' must be an absolute http(s) URL"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if scheme == "https" {
|
|
59
|
+
return nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if scheme != "http" {
|
|
63
|
+
return "url '\(url)' must be an absolute http(s) URL"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if isDebugBuild, localHttpHosts.contains(host) {
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let hosts = localHttpHosts.sorted().joined(separator: ", ")
|
|
71
|
+
return "url '\(url)' must use https. Plain http is allowed only in a debug build, and only for \(hosts)."
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Server-update settings as one layer holds them: every field is optional, and an unset field
|
|
4
|
+
/// means "this layer has no opinion, ask the layer below".
|
|
5
|
+
///
|
|
6
|
+
/// `body` is kept as the raw JSON text the app supplied rather than a parsed tree, because Voltra
|
|
7
|
+
/// never inspects it — it only forwards it as the request body.
|
|
8
|
+
public struct WidgetServerUpdateSettings: Equatable, Sendable {
|
|
9
|
+
public var url: String?
|
|
10
|
+
public var intervalMinutes: Int?
|
|
11
|
+
public var enabled: Bool?
|
|
12
|
+
public var method: String?
|
|
13
|
+
public var query: [String: String]?
|
|
14
|
+
public var headers: [String: String]?
|
|
15
|
+
public var body: String?
|
|
16
|
+
|
|
17
|
+
public init(
|
|
18
|
+
url: String? = nil,
|
|
19
|
+
intervalMinutes: Int? = nil,
|
|
20
|
+
enabled: Bool? = nil,
|
|
21
|
+
method: String? = nil,
|
|
22
|
+
query: [String: String]? = nil,
|
|
23
|
+
headers: [String: String]? = nil,
|
|
24
|
+
body: String? = nil
|
|
25
|
+
) {
|
|
26
|
+
self.url = url
|
|
27
|
+
self.intervalMinutes = intervalMinutes
|
|
28
|
+
self.enabled = enabled
|
|
29
|
+
self.method = method
|
|
30
|
+
self.query = query
|
|
31
|
+
self.headers = headers
|
|
32
|
+
self.body = body
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public var isEmpty: Bool {
|
|
36
|
+
url == nil && intervalMinutes == nil && enabled == nil && method == nil
|
|
37
|
+
&& query == nil && headers == nil && body == nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static let empty = WidgetServerUpdateSettings()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The flattened settings a fetch actually runs on. Every field is decided: `intervalMinutes` has
|
|
44
|
+
/// the floor and ceiling applied, `enabled` and `method` have their defaults filled in, and `query`
|
|
45
|
+
/// and `headers` are the per-key merge of every layer.
|
|
46
|
+
///
|
|
47
|
+
/// `url` is the one field that can still be absent, and it means the widget is server-driven but
|
|
48
|
+
/// has nowhere to fetch from yet — the app is expected to supply one with `setWidgetServerUpdate`.
|
|
49
|
+
public struct ResolvedWidgetServerSettings: Equatable, Sendable {
|
|
50
|
+
public let url: String?
|
|
51
|
+
public let intervalMinutes: Int
|
|
52
|
+
public let enabled: Bool
|
|
53
|
+
public let method: String
|
|
54
|
+
public let query: [String: String]
|
|
55
|
+
public let headers: [String: String]
|
|
56
|
+
public let body: String?
|
|
57
|
+
|
|
58
|
+
public init(
|
|
59
|
+
url: String?,
|
|
60
|
+
intervalMinutes: Int,
|
|
61
|
+
enabled: Bool,
|
|
62
|
+
method: String,
|
|
63
|
+
query: [String: String],
|
|
64
|
+
headers: [String: String],
|
|
65
|
+
body: String?
|
|
66
|
+
) {
|
|
67
|
+
self.url = url
|
|
68
|
+
self.intervalMinutes = intervalMinutes
|
|
69
|
+
self.enabled = enabled
|
|
70
|
+
self.method = method
|
|
71
|
+
self.query = query
|
|
72
|
+
self.headers = headers
|
|
73
|
+
self.body = body
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// True when this widget has both a URL to fetch and permission to do it.
|
|
77
|
+
public var shouldFetch: Bool {
|
|
78
|
+
guard enabled, let url, !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public enum WidgetServerUpdateDefaults {
|
|
87
|
+
/// WidgetKit stretches a timeline asking for entries much closer together than five minutes, and
|
|
88
|
+
/// the reload budget is shared across every widget in the app, so a smaller number would only
|
|
89
|
+
/// misreport what the widget actually does. Matches Android's WorkManager floor.
|
|
90
|
+
public static let minIntervalMinutes = 15
|
|
91
|
+
|
|
92
|
+
/// A day. Past this the widget is effectively not server-driven, and `Cache-Control: max-age`
|
|
93
|
+
/// from a misconfigured server should not be able to park a widget for a week.
|
|
94
|
+
public static let maxIntervalMinutes = 24 * 60
|
|
95
|
+
|
|
96
|
+
public static let defaultIntervalMinutes = minIntervalMinutes
|
|
97
|
+
|
|
98
|
+
public static let defaultMethod = "GET"
|
|
99
|
+
|
|
100
|
+
/// Methods either platform's HTTP stack can send.
|
|
101
|
+
public static let supportedMethods: Set<String> = ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
102
|
+
|
|
103
|
+
/// Methods that cannot carry a body. A body set alongside one of these is dropped.
|
|
104
|
+
public static let bodylessMethods: Set<String> = ["GET", "HEAD"]
|
|
105
|
+
|
|
106
|
+
/// Query keys Voltra puts on every request. An app that set one of these would silently shadow
|
|
107
|
+
/// what the server relies on, so `setWidgetServerUpdate` rejects them.
|
|
108
|
+
public static let reservedQueryKeys: Set<String> = [
|
|
109
|
+
"widgetId", "platform", "family", "theme", "locale", "instance", "configuration",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
/// Serialized size cap for one layer, so a runaway `body` cannot fill the settings store.
|
|
113
|
+
public static let maxLayerBytes = 16 * 1024
|
|
114
|
+
|
|
115
|
+
/// Response bodies larger than this are refused. The widget extension has a 30 MB ceiling for
|
|
116
|
+
/// the whole render, and a widget needing a quarter of a megabyte of props will not fit on a
|
|
117
|
+
/// home screen either.
|
|
118
|
+
public static let maxBodyBytes = 256 * 1024
|
|
119
|
+
|
|
120
|
+
public static func clampIntervalMinutes(_ intervalMinutes: Int) -> Int {
|
|
121
|
+
min(max(intervalMinutes, minIntervalMinutes), maxIntervalMinutes)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Reads the settings object an app passes to `setWidgetServerUpdate`.
|
|
4
|
+
///
|
|
5
|
+
/// Separate from `WidgetServerSettingsCodec`, which is the versioned storage format: what the app
|
|
6
|
+
/// sends and what Voltra persists are allowed to diverge, and conflating them would make either one
|
|
7
|
+
/// hard to change. The one real difference today is `body`, which arrives as arbitrary JSON and is
|
|
8
|
+
/// kept as text because Voltra only forwards it.
|
|
9
|
+
public enum WidgetServerUpdateSettingsJson {
|
|
10
|
+
public enum Result: Equatable {
|
|
11
|
+
case parsed(WidgetServerUpdateSettings)
|
|
12
|
+
case invalid(reason: String)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public static func parse(_ json: String) -> Result {
|
|
16
|
+
guard let object = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any] else {
|
|
17
|
+
return .invalid(reason: "settings must be a JSON object")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
guard let query = stringMap(object, "query") else {
|
|
21
|
+
return .invalid(reason: "query must be an object of strings")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
guard let headers = stringMap(object, "headers") else {
|
|
25
|
+
return .invalid(reason: "headers must be an object of strings")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var body: String?
|
|
29
|
+
|
|
30
|
+
if let rawBody = object["body"], !(rawBody is NSNull) {
|
|
31
|
+
guard let text = jsonText(rawBody) else {
|
|
32
|
+
return .invalid(reason: "body must be a JSON value")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body = text
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return .parsed(
|
|
39
|
+
WidgetServerUpdateSettings(
|
|
40
|
+
url: object["url"] as? String,
|
|
41
|
+
intervalMinutes: object["intervalMinutes"] as? Int,
|
|
42
|
+
enabled: object["enabled"] as? Bool,
|
|
43
|
+
method: (object["method"] as? String)?.uppercased(),
|
|
44
|
+
query: object["query"] == nil ? nil : query,
|
|
45
|
+
headers: object["headers"] == nil ? nil : headers,
|
|
46
|
+
body: body
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The other direction of `parse`: serializes settings back to the same JSON shape, for
|
|
52
|
+
/// `getWidgetServerUpdate` to hand across the bridge. No envelope — that is
|
|
53
|
+
/// `WidgetServerSettingsCodec`'s job for storage, not this one's for a single read.
|
|
54
|
+
public static func stringify(_ settings: WidgetServerUpdateSettings) -> String? {
|
|
55
|
+
var object: [String: Any] = [:]
|
|
56
|
+
|
|
57
|
+
if let url = settings.url {
|
|
58
|
+
object["url"] = url
|
|
59
|
+
}
|
|
60
|
+
if let intervalMinutes = settings.intervalMinutes {
|
|
61
|
+
object["intervalMinutes"] = intervalMinutes
|
|
62
|
+
}
|
|
63
|
+
if let enabled = settings.enabled {
|
|
64
|
+
object["enabled"] = enabled
|
|
65
|
+
}
|
|
66
|
+
if let method = settings.method {
|
|
67
|
+
object["method"] = method
|
|
68
|
+
}
|
|
69
|
+
if let query = settings.query {
|
|
70
|
+
object["query"] = query
|
|
71
|
+
}
|
|
72
|
+
if let headers = settings.headers {
|
|
73
|
+
object["headers"] = headers
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if let body = settings.body {
|
|
77
|
+
guard let bodyData = body.data(using: .utf8),
|
|
78
|
+
let bodyValue = try? JSONSerialization.jsonObject(with: bodyData, options: [.fragmentsAllowed])
|
|
79
|
+
else {
|
|
80
|
+
return nil
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
object["body"] = bodyValue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
guard let data = try? JSONSerialization.data(withJSONObject: object) else { return nil }
|
|
87
|
+
|
|
88
|
+
return String(data: data, encoding: .utf8)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Re-serializes a parsed value back to JSON text. A string body has to keep its quotes: without
|
|
92
|
+
/// them the request would carry something that is not JSON at all.
|
|
93
|
+
private static func jsonText(_ value: Any) -> String? {
|
|
94
|
+
if JSONSerialization.isValidJSONObject(value) {
|
|
95
|
+
return (try? JSONSerialization.data(withJSONObject: value)).flatMap { String(data: $0, encoding: .utf8) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A top-level scalar is not a valid JSON *object*, so it is wrapped, encoded and unwrapped.
|
|
99
|
+
guard let wrapped = try? JSONSerialization.data(withJSONObject: ["v": value]),
|
|
100
|
+
let text = String(data: wrapped, encoding: .utf8),
|
|
101
|
+
let start = text.firstIndex(of: ":")
|
|
102
|
+
else {
|
|
103
|
+
return nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return String(text[text.index(after: start) ..< text.index(before: text.endIndex)])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Returns an empty map when the key is absent, and nil when it is present but not usable.
|
|
110
|
+
private static func stringMap(_ object: [String: Any], _ key: String) -> [String: String]? {
|
|
111
|
+
guard let raw = object[key], !(raw is NSNull) else { return [:] }
|
|
112
|
+
|
|
113
|
+
return raw as? [String: String]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum DynamicLiveActivityPropsCodec {
|
|
4
|
+
static func encode(_ props: [String: VoltraDynamicLiveActivityJSONValue]) -> String? {
|
|
5
|
+
guard
|
|
6
|
+
let data = try? JSONEncoder().encode(props),
|
|
7
|
+
let json = String(data: data, encoding: .utf8)
|
|
8
|
+
else {
|
|
9
|
+
return nil
|
|
10
|
+
}
|
|
11
|
+
return json
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum VoltraDynamicLiveActivityBundleSource {
|
|
4
|
+
private static let metroTimeout: TimeInterval = 1
|
|
5
|
+
|
|
6
|
+
enum LoadError: LocalizedError {
|
|
7
|
+
case metroHTTP(Int)
|
|
8
|
+
case metroTimeout
|
|
9
|
+
case nonUTF8
|
|
10
|
+
case bakedBundleNotFound(definitionId: String)
|
|
11
|
+
case cacheUnavailable
|
|
12
|
+
|
|
13
|
+
var errorDescription: String? {
|
|
14
|
+
switch self {
|
|
15
|
+
case let .metroHTTP(status):
|
|
16
|
+
return "Metro HTTP \(status) while loading a Dynamic Live Activity bundle"
|
|
17
|
+
case .metroTimeout:
|
|
18
|
+
return "Metro timed out while loading a Dynamic Live Activity bundle"
|
|
19
|
+
case .nonUTF8:
|
|
20
|
+
return "Dynamic Live Activity bundle was not UTF-8 text"
|
|
21
|
+
case let .bakedBundleNotFound(definitionId):
|
|
22
|
+
return "Production Dynamic Live Activity bundle missing for definitionId=\(definitionId)"
|
|
23
|
+
case .cacheUnavailable:
|
|
24
|
+
return "Dynamic Live Activity debug cache is unavailable"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func load(definitionId: String) throws -> String {
|
|
30
|
+
#if DEBUG
|
|
31
|
+
if let cached = try? loadFromDebugCache(definitionId: definitionId) {
|
|
32
|
+
return cached
|
|
33
|
+
}
|
|
34
|
+
return try loadFromMetroBounded(definitionId: definitionId)
|
|
35
|
+
#else
|
|
36
|
+
return try loadFromBakedAsset(definitionId: definitionId)
|
|
37
|
+
#endif
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// App-side preflight and Fast Refresh use an asynchronous fetch, then publish
|
|
41
|
+
/// the source into the App Group cache consumed by the extension process.
|
|
42
|
+
static func loadForApp(definitionId: String) async throws -> String {
|
|
43
|
+
#if DEBUG
|
|
44
|
+
let request = try metroRequest(definitionId: definitionId)
|
|
45
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
46
|
+
let source = try validateMetroResponse(data: data, response: response)
|
|
47
|
+
try saveToDebugCache(source, definitionId: definitionId)
|
|
48
|
+
return source
|
|
49
|
+
#else
|
|
50
|
+
return try loadFromBakedAsset(definitionId: definitionId)
|
|
51
|
+
#endif
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A cheap token that changes whenever the bytes behind `load` change, used to
|
|
55
|
+
/// invalidate the render cache. In DEBUG it is the debug-cache file's
|
|
56
|
+
/// modification time, which the app process rewrites on every hot reload; the
|
|
57
|
+
/// extension reads only the file's metadata, never decoding the whole bundle.
|
|
58
|
+
/// In release the baked asset never changes at runtime, so the token is
|
|
59
|
+
/// constant and the render cache stays valid for the process lifetime.
|
|
60
|
+
static func revision(definitionId: String) -> String {
|
|
61
|
+
#if DEBUG
|
|
62
|
+
guard
|
|
63
|
+
let url = try? debugCacheURL(definitionId: definitionId),
|
|
64
|
+
let modified = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date
|
|
65
|
+
else {
|
|
66
|
+
return ""
|
|
67
|
+
}
|
|
68
|
+
return String(modified.timeIntervalSince1970)
|
|
69
|
+
#else
|
|
70
|
+
return "baked"
|
|
71
|
+
#endif
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static func metroRequest(definitionId: String) throws -> URLRequest {
|
|
75
|
+
let base = VoltraWidgetDefaults.devServerURL() ?? "http://localhost:8081"
|
|
76
|
+
guard let url = URL(string: "\(base)/voltra/live-activities/\(definitionId).bundle?platform=ios&dev=true") else {
|
|
77
|
+
throw LoadError.metroHTTP(-1)
|
|
78
|
+
}
|
|
79
|
+
var request = URLRequest(url: url)
|
|
80
|
+
request.timeoutInterval = metroTimeout
|
|
81
|
+
return request
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// WidgetKit rendering is synchronous. On a cold debug process we permit one
|
|
85
|
+
/// short, bounded bootstrap request; all subsequent renders read the App Group
|
|
86
|
+
/// cache and never wait on the network.
|
|
87
|
+
private static func loadFromMetroBounded(definitionId: String) throws -> String {
|
|
88
|
+
let request = try metroRequest(definitionId: definitionId)
|
|
89
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
90
|
+
var result: Result<(Data, URLResponse), Error>?
|
|
91
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
92
|
+
configuration.timeoutIntervalForRequest = metroTimeout
|
|
93
|
+
configuration.timeoutIntervalForResource = metroTimeout
|
|
94
|
+
let session = URLSession(configuration: configuration)
|
|
95
|
+
let task = session.dataTask(with: request) { data, response, error in
|
|
96
|
+
if let error {
|
|
97
|
+
result = .failure(error)
|
|
98
|
+
} else if let data, let response {
|
|
99
|
+
result = .success((data, response))
|
|
100
|
+
} else {
|
|
101
|
+
result = .failure(LoadError.metroHTTP(-1))
|
|
102
|
+
}
|
|
103
|
+
semaphore.signal()
|
|
104
|
+
}
|
|
105
|
+
task.resume()
|
|
106
|
+
guard semaphore.wait(timeout: .now() + metroTimeout) == .success else {
|
|
107
|
+
task.cancel()
|
|
108
|
+
session.invalidateAndCancel()
|
|
109
|
+
throw LoadError.metroTimeout
|
|
110
|
+
}
|
|
111
|
+
session.finishTasksAndInvalidate()
|
|
112
|
+
guard let result else { throw LoadError.metroTimeout }
|
|
113
|
+
let (data, response) = try result.get()
|
|
114
|
+
let source = try validateMetroResponse(data: data, response: response)
|
|
115
|
+
try saveToDebugCache(source, definitionId: definitionId)
|
|
116
|
+
return source
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private static func validateMetroResponse(data: Data, response: URLResponse) throws -> String {
|
|
120
|
+
if let httpResponse = response as? HTTPURLResponse, !(200 ... 299).contains(httpResponse.statusCode) {
|
|
121
|
+
throw LoadError.metroHTTP(httpResponse.statusCode)
|
|
122
|
+
}
|
|
123
|
+
guard let source = String(data: data, encoding: .utf8) else {
|
|
124
|
+
throw LoadError.nonUTF8
|
|
125
|
+
}
|
|
126
|
+
return source
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static func loadFromDebugCache(definitionId: String) throws -> String {
|
|
130
|
+
try String(contentsOf: debugCacheURL(definitionId: definitionId), encoding: .utf8)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static func saveToDebugCache(_ source: String, definitionId: String) throws {
|
|
134
|
+
try Data(source.utf8).write(to: debugCacheURL(definitionId: definitionId), options: .atomic)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static func debugCacheURL(definitionId: String) throws -> URL {
|
|
138
|
+
guard
|
|
139
|
+
let group = VoltraConfig.groupIdentifier(),
|
|
140
|
+
let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)
|
|
141
|
+
else {
|
|
142
|
+
throw LoadError.cacheUnavailable
|
|
143
|
+
}
|
|
144
|
+
return container.appendingPathComponent("voltra-live-activity-debug-\(definitionId).bundle")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static func loadFromBakedAsset(definitionId: String) throws -> String {
|
|
148
|
+
guard
|
|
149
|
+
let url = Bundle.main.url(forResource: "voltra-live-activity-\(definitionId)", withExtension: "bundle"),
|
|
150
|
+
let source = try? String(contentsOf: url, encoding: .utf8)
|
|
151
|
+
else {
|
|
152
|
+
throw LoadError.bakedBundleNotFound(definitionId: definitionId)
|
|
153
|
+
}
|
|
154
|
+
return source
|
|
155
|
+
}
|
|
156
|
+
}
|