@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,204 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import WidgetKit
|
|
4
|
+
|
|
5
|
+
/// Timeline provider for a Dynamic Widget that has a `serverUpdate`.
|
|
6
|
+
///
|
|
7
|
+
/// It wraps `VoltraClientWidgetProvider` rather than replacing it: the bundle load, the render and
|
|
8
|
+
/// the fallback to the prerendered initial state are exactly what they are for any Dynamic Widget.
|
|
9
|
+
/// What this adds is a fetch on `getTimeline`, a commit of the fetched props into the widget's
|
|
10
|
+
/// existing props slot, and a schedule — a plain Dynamic Widget's timeline is `.never`, so without
|
|
11
|
+
/// one nothing would ever ask again.
|
|
12
|
+
///
|
|
13
|
+
/// `placeholder` and `getSnapshot` stay local. Apple's guidance is to keep the gallery preview off
|
|
14
|
+
/// the network, and a snapshot that waited for a fetch would show a spinner in the widget picker.
|
|
15
|
+
public struct VoltraDynamicWidgetServerUpdateProvider: TimelineProvider {
|
|
16
|
+
public let widgetId: String
|
|
17
|
+
public let initialState: Data?
|
|
18
|
+
|
|
19
|
+
public init(widgetId: String, initialState: Data? = nil) {
|
|
20
|
+
self.widgetId = widgetId
|
|
21
|
+
self.initialState = initialState
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The scope for one placement's merged configuration (ADR 0007): `.instance` when WidgetKit
|
|
25
|
+
/// handed this timeline request a non-empty configuration, `.widget` when it did not — which is
|
|
26
|
+
/// also the scope of every widget before this ADR. WidgetKit derives the key itself; there is no
|
|
27
|
+
/// separate placement id to key off, so this is the only identity an iOS placement can have.
|
|
28
|
+
private func scope(for configuration: [String: String]) -> WidgetScope {
|
|
29
|
+
.of(widgetId, configuration: configuration)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func placeholder(in _: Context) -> VoltraClientWidgetEntry {
|
|
33
|
+
VoltraClientWidgetEntry(date: Date(), widgetId: widgetId, bundleReady: false)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public func getSnapshot(in _: Context, completion: @escaping (VoltraClientWidgetEntry) -> Void) {
|
|
37
|
+
Task { completion(await localEntry(configuration: [:])) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public func getTimeline(in context: Context, completion: @escaping (Timeline<VoltraClientWidgetEntry>) -> Void) {
|
|
41
|
+
Task {
|
|
42
|
+
completion(await timeline(family: context.family, configuration: [:]))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Shared with the generated `AppIntentTimelineProvider`s, which pass the user-configured
|
|
47
|
+
/// parameters as `configuration`.
|
|
48
|
+
public static func timeline(
|
|
49
|
+
widgetId: String,
|
|
50
|
+
family: WidgetFamily,
|
|
51
|
+
configuration: [String: String]
|
|
52
|
+
) async -> Timeline<VoltraClientWidgetEntry> {
|
|
53
|
+
await VoltraDynamicWidgetServerUpdateProvider(widgetId: widgetId).timeline(
|
|
54
|
+
family: family,
|
|
55
|
+
configuration: configuration
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func timeline(family: WidgetFamily, configuration: [String: String]) async -> Timeline<VoltraClientWidgetEntry> {
|
|
60
|
+
let scope = scope(for: configuration)
|
|
61
|
+
let settings = VoltraWidgetServer.resolver.resolve(scope)
|
|
62
|
+
|
|
63
|
+
guard settings.shouldFetch else {
|
|
64
|
+
// No URL yet, or the app has taken the widget over. Either way there is nothing to schedule:
|
|
65
|
+
// the app reloads the widget itself, and setting a URL later queues a reload of its own.
|
|
66
|
+
DynamicWidgetServerPropsStore().markDisabledIfNeeded(enabled: settings.enabled, for: scope)
|
|
67
|
+
|
|
68
|
+
return Timeline(entries: [await localEntry(configuration: configuration)], policy: .never)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
var nextIntervalMinutes = settings.intervalMinutes
|
|
72
|
+
|
|
73
|
+
if await shouldFetch(scope) {
|
|
74
|
+
let result = await runner(family: family, configuration: configuration).run(scope)
|
|
75
|
+
|
|
76
|
+
// What the server asked for wins over the configured interval: `Cache-Control: max-age` on a
|
|
77
|
+
// success, `Retry-After` on a 429 or 503.
|
|
78
|
+
if let asked = result.nextIntervalMinutes {
|
|
79
|
+
nextIntervalMinutes = asked
|
|
80
|
+
} else if result.outcome == .retry || result.outcome == .failed {
|
|
81
|
+
// A failure the server did not put a number on. Coming back sooner than the widget's own
|
|
82
|
+
// interval would spend the reload budget on an endpoint that is already unhappy, so the
|
|
83
|
+
// floor is used rather than the configured value only when that is shorter.
|
|
84
|
+
nextIntervalMinutes = max(settings.intervalMinutes, WidgetServerUpdateDefaults.minIntervalMinutes)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let entry = await localEntry(configuration: configuration)
|
|
89
|
+
let nextUpdate = Date().addingTimeInterval(TimeInterval(nextIntervalMinutes * 60))
|
|
90
|
+
|
|
91
|
+
return Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Whether this timeline request should fetch, or ride on one that just happened.
|
|
95
|
+
///
|
|
96
|
+
/// Skipped for a moment after `updateDynamicWidget` writes props, so an optimistic update is not
|
|
97
|
+
/// wiped out by the very reload it triggered. ADR 0002 says the *next scheduled* fetch overwrites
|
|
98
|
+
/// app-written props, not the one the write itself caused.
|
|
99
|
+
///
|
|
100
|
+
/// Keyed by `scope`, so two placements with different configurations coalesce independently
|
|
101
|
+
/// (ADR 0007) — WidgetKit calling the timeline once per placement per family collapses onto one
|
|
102
|
+
/// fetch per distinct configuration, not one fetch total.
|
|
103
|
+
private func shouldFetch(_ scope: WidgetScope) async -> Bool {
|
|
104
|
+
if let writtenAt = DynamicWidgetServerPropsStore().appWriteAt(for: scope),
|
|
105
|
+
Date().timeIntervalSince(writtenAt) < DynamicWidgetServerFetchCoordinator.defaultCoalesceInterval
|
|
106
|
+
{
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return await DynamicWidgetServerFetchCoordinator.shared.shouldFetch(scope)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// The entry a widget renders from: the bundle, the configuration, and how the last fetch went.
|
|
114
|
+
/// The props themselves are read by the view from the same slot `updateDynamicWidget` writes, so
|
|
115
|
+
/// the render path does not know where they came from.
|
|
116
|
+
private func localEntry(configuration: [String: String]) async -> VoltraClientWidgetEntry {
|
|
117
|
+
let entry = await VoltraClientWidgetProvider.loadEntry(widgetId: widgetId, configuration: configuration)
|
|
118
|
+
|
|
119
|
+
return entry.withServerUpdate(DynamicWidgetServerPropsStore().status(for: scope(for: configuration)).toJSON())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func runner(family: WidgetFamily, configuration: [String: String]) -> DynamicWidgetServerUpdateRunner {
|
|
123
|
+
let statuses = DynamicWidgetServerPropsStore()
|
|
124
|
+
|
|
125
|
+
return DynamicWidgetServerUpdateRunner(
|
|
126
|
+
resolveSettings: { VoltraWidgetServer.resolver.resolve($0) },
|
|
127
|
+
currentRevision: { VoltraWidgetServer.resolver.revision($0) },
|
|
128
|
+
readEtag: { WidgetServerEtagStore.etag(for: $0, url: $1) },
|
|
129
|
+
fetch: { scope, settings, etag in
|
|
130
|
+
guard let request = WidgetServerRequestBuilder.build(
|
|
131
|
+
scope: scope,
|
|
132
|
+
settings: settings,
|
|
133
|
+
// No `family`: one fetch serves every size and instance of a Dynamic Widget, so its
|
|
134
|
+
// props must be size-agnostic and the entry picks its layout from env.widgetFamily.
|
|
135
|
+
context: VoltraWidgetAppearance.requestContext(),
|
|
136
|
+
etag: etag,
|
|
137
|
+
configuration: configuration
|
|
138
|
+
) else {
|
|
139
|
+
return .networkFailure(message: "Could not build a request")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return await WidgetServerFetcher.fetch(request)
|
|
143
|
+
},
|
|
144
|
+
writeEtag: { WidgetServerEtagStore.put($2, for: $0, url: $1) },
|
|
145
|
+
trialRender: { scope, props in
|
|
146
|
+
VoltraDynamicWidgetTrialRender.canRender(
|
|
147
|
+
widgetId: scope.widgetId,
|
|
148
|
+
propsJSON: props,
|
|
149
|
+
family: family,
|
|
150
|
+
configuration: configuration
|
|
151
|
+
)
|
|
152
|
+
},
|
|
153
|
+
// Instance-aware (ADR 0007): commits to the instance slot for a `.instance` scope, and the
|
|
154
|
+
// widget slot for `.widget` — a widget with no configuration parameters keeps writing
|
|
155
|
+
// exactly where it always has.
|
|
156
|
+
commitProps: { scope, props in
|
|
157
|
+
try DynamicWidgetPropsStore().persistInstanceDynamicWidgetProps(props, for: scope)
|
|
158
|
+
},
|
|
159
|
+
recordSuccess: { statuses.recordSuccess(fetchedAt: $1, httpStatus: $2, for: $0) },
|
|
160
|
+
recordFailure: { statuses.recordFailure($1, httpStatus: $2, for: $0) },
|
|
161
|
+
markDisabled: { statuses.markDisabledIfNeeded(enabled: $1, for: $0) }
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Renders fetched props once, off screen, before they are allowed anywhere near the widget.
|
|
167
|
+
///
|
|
168
|
+
/// The trial uses one environment — the family WidgetKit asked the timeline for, in the current
|
|
169
|
+
/// appearance. A widget that only throws for another family slips through, and ADR 0002 accepts
|
|
170
|
+
/// that: rendering every supported family on every fetch would cost more than the failure it
|
|
171
|
+
/// prevents, inside a 30 MB extension.
|
|
172
|
+
enum VoltraDynamicWidgetTrialRender {
|
|
173
|
+
static func canRender(
|
|
174
|
+
widgetId: String,
|
|
175
|
+
propsJSON: String,
|
|
176
|
+
family: WidgetFamily,
|
|
177
|
+
configuration: [String: String]
|
|
178
|
+
) -> Bool {
|
|
179
|
+
// env.serverUpdate is deliberately absent here. The trial asks whether the props render, and a
|
|
180
|
+
// widget that only fails when told the fetch went badly is a different problem from props that
|
|
181
|
+
// cannot be drawn.
|
|
182
|
+
let envJSON = VoltraClientWidgetEnvBuilder.build(
|
|
183
|
+
date: Date(),
|
|
184
|
+
widgetFamily: family,
|
|
185
|
+
colorScheme: nil,
|
|
186
|
+
widgetRenderingMode: .fullColor,
|
|
187
|
+
showsWidgetContainerBackground: true,
|
|
188
|
+
locale: Locale.current,
|
|
189
|
+
configuration: configuration
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
guard let resolved = VoltraJSRenderer.render(widgetId: widgetId, propsJSON: propsJSON, envJSON: envJSON),
|
|
193
|
+
let json = try? JSONValue.parse(from: resolved)
|
|
194
|
+
else {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if case .empty = VoltraNode.parse(from: json) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -122,6 +122,14 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
|
|
|
122
122
|
public let widgetId: String
|
|
123
123
|
public let initialState: Data?
|
|
124
124
|
|
|
125
|
+
/// Last successful server response per widget, shared by every provider instance in this
|
|
126
|
+
/// extension process. Backs fetch coalescing and the fallback when a fetch fails, without
|
|
127
|
+
/// depending on an App Group being configured.
|
|
128
|
+
private static let serverResponseStore = ServerWidgetResponseStore()
|
|
129
|
+
|
|
130
|
+
/// How long WidgetKit waits before asking again after a fetch failed.
|
|
131
|
+
private static let retryInterval: TimeInterval = 900
|
|
132
|
+
|
|
125
133
|
public init(widgetId: String, initialState: Data? = nil) {
|
|
126
134
|
self.widgetId = widgetId
|
|
127
135
|
self.initialState = initialState
|
|
@@ -132,126 +140,107 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
|
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
public func getSnapshot(in context: Context, completion: @escaping (VoltraHomeWidgetEntry) -> Void) {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
let firstEntry = timeline.entries.first
|
|
138
|
-
{
|
|
139
|
-
let node = parseJsonToNode(data: firstEntry.json, family: context.family)
|
|
140
|
-
completion(VoltraHomeWidgetEntry(
|
|
141
|
-
date: Date(),
|
|
142
|
-
rootNode: node,
|
|
143
|
-
widgetId: widgetId,
|
|
144
|
-
deepLinkUrl: firstEntry.deepLinkUrl
|
|
145
|
-
))
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Fallback to single-entry data
|
|
150
|
-
let data = VoltraHomeWidgetStore.readJson(widgetId: widgetId) ?? initialState
|
|
151
|
-
let node = data.flatMap { parseJsonToNode(data: $0, family: context.family) }
|
|
152
|
-
completion(VoltraHomeWidgetEntry(date: Date(), rootNode: node, widgetId: widgetId))
|
|
143
|
+
// Snapshots come from local data so the gallery preview never waits on the network.
|
|
144
|
+
completion(localTimeline(in: context, policy: .never).entries.first ?? placeholder(in: context))
|
|
153
145
|
}
|
|
154
146
|
|
|
155
147
|
public func getTimeline(in context: Context, completion: @escaping (Timeline<VoltraHomeWidgetEntry>) -> Void) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Check if server-driven updates are configured for this widget
|
|
160
|
-
if VoltraWidgetServerFetcher.serverUrl(for: widgetId) != nil {
|
|
161
|
-
getServerDrivenTimeline(in: context, completion: completion)
|
|
148
|
+
guard VoltraWidgetServerFetcher.serverUrl(for: widgetId) != nil else {
|
|
149
|
+
completion(localTimeline(in: context, policy: .never))
|
|
162
150
|
return
|
|
163
151
|
}
|
|
164
152
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let
|
|
153
|
+
Task { completion(await serverTimeline(in: context)) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// MARK: - Server-driven timeline
|
|
157
|
+
|
|
158
|
+
private func serverTimeline(in context: Context) async -> Timeline<VoltraHomeWidgetEntry> {
|
|
159
|
+
let resolver = ServerWidgetContentResolver(
|
|
160
|
+
responseStore: Self.serverResponseStore,
|
|
161
|
+
fetch: { widgetId in try await fetchServerContent(widgetId: widgetId, in: context) }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
switch await resolver.resolve(widgetId: widgetId) {
|
|
165
|
+
case let .current(data):
|
|
166
|
+
let intervalMinutes = VoltraWidgetServerFetcher.updateInterval(for: widgetId)
|
|
167
|
+
let nextUpdate = Calendar.current.date(byAdding: .minute, value: intervalMinutes, to: Date()) ?? retryDate
|
|
168
|
+
return serverTimeline(from: data, in: context, policy: .after(nextUpdate))
|
|
169
|
+
case let .lastKnown(data, error):
|
|
170
|
+
// A 304 is not a failure: the server confirmed what we already have is current, so the next
|
|
171
|
+
// fetch belongs at the normal interval rather than the shorter retry one.
|
|
172
|
+
if case VoltraWidgetServerFetcher.FetchError.notModified = error {
|
|
173
|
+
let intervalMinutes = VoltraWidgetServerFetcher.updateInterval(for: widgetId)
|
|
174
|
+
let nextUpdate = Calendar.current.date(byAdding: .minute, value: intervalMinutes, to: Date()) ?? retryDate
|
|
175
|
+
return serverTimeline(from: data, in: context, policy: .after(nextUpdate))
|
|
176
|
+
}
|
|
178
177
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Date().timeIntervalSince(lastFetch) < Self.coalesceInterval
|
|
185
|
-
{
|
|
186
|
-
VoltraLogger.widget.info("Coalescing fetch for '\(widgetId)' family '\(familyKey)' (last fetch \(Date().timeIntervalSince(lastFetch))s ago)")
|
|
187
|
-
getLocalTimeline(in: context, completion: completion)
|
|
188
|
-
return
|
|
178
|
+
VoltraLogger.widget.error("Server-driven update failed for '\(widgetId)', keeping last server content: \(error.localizedDescription)")
|
|
179
|
+
return serverTimeline(from: data, in: context, policy: .after(retryDate))
|
|
180
|
+
case let .unavailable(error):
|
|
181
|
+
VoltraLogger.widget.error("Server-driven update failed for '\(widgetId)', falling back to local data: \(error.localizedDescription)")
|
|
182
|
+
return localTimeline(in: context, policy: .after(retryDate))
|
|
189
183
|
}
|
|
184
|
+
}
|
|
190
185
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Record that we just fetched successfully
|
|
199
|
-
Self.lastFetchTimes[widgetId] = Date()
|
|
200
|
-
|
|
201
|
-
let node = parseJsonToNode(data: data, family: context.family)
|
|
186
|
+
/// Fetch, validate, and persist one server response. Throws on network errors and on responses
|
|
187
|
+
/// that do not parse into a widget tree, so they are never treated as good content.
|
|
188
|
+
private func fetchServerContent(widgetId: String, in context: Context) async throws -> Data {
|
|
189
|
+
let data = try await VoltraWidgetServerFetcher.fetchWidgetContent(
|
|
190
|
+
widgetId: widgetId,
|
|
191
|
+
family: familyToKey(context.family)
|
|
192
|
+
)
|
|
202
193
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
try? VoltraWidgetDefaults.setWidgetJson(jsonString, for: widgetId, deepLinkUrl: nil)
|
|
207
|
-
}
|
|
194
|
+
guard parseJsonToNode(data: data, family: context.family) != nil else {
|
|
195
|
+
throw UnparseableServerContentError()
|
|
196
|
+
}
|
|
208
197
|
|
|
209
|
-
|
|
198
|
+
// Best-effort on-disk copy so a fresh extension process has something to fall back to.
|
|
199
|
+
// Requires an App Group; without one the in-memory store above still covers this process.
|
|
200
|
+
if let jsonString = String(data: data, encoding: .utf8) {
|
|
201
|
+
try? VoltraWidgetDefaults.setWidgetJson(jsonString, for: widgetId, deepLinkUrl: nil)
|
|
202
|
+
}
|
|
210
203
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
VoltraLogger.widget.info("Server-driven update succeeded for '\(widgetId)'")
|
|
205
|
+
return data
|
|
206
|
+
}
|
|
214
207
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
208
|
+
private func serverTimeline(
|
|
209
|
+
from data: Data,
|
|
210
|
+
in context: Context,
|
|
211
|
+
policy: TimelineReloadPolicy
|
|
212
|
+
) -> Timeline<VoltraHomeWidgetEntry> {
|
|
213
|
+
let node = parseJsonToNode(data: data, family: context.family)
|
|
214
|
+
let entry = VoltraHomeWidgetEntry(date: Date(), rootNode: node, widgetId: widgetId)
|
|
215
|
+
return Timeline(entries: [entry], policy: policy)
|
|
216
|
+
}
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
}
|
|
218
|
+
private var retryDate: Date {
|
|
219
|
+
Date().addingTimeInterval(Self.retryInterval)
|
|
223
220
|
}
|
|
224
221
|
|
|
225
|
-
// MARK: - Local
|
|
222
|
+
// MARK: - Local timeline
|
|
226
223
|
|
|
227
224
|
/// Build a timeline from locally stored data (UserDefaults / initial state).
|
|
228
|
-
private func
|
|
225
|
+
private func localTimeline(in context: Context, policy: TimelineReloadPolicy) -> Timeline<VoltraHomeWidgetEntry> {
|
|
226
|
+
VoltraHomeWidgetStore.pruneExpiredEntries(widgetId: widgetId)
|
|
227
|
+
|
|
229
228
|
if let timeline = VoltraHomeWidgetStore.readTimeline(widgetId: widgetId), !timeline.entries.isEmpty {
|
|
230
229
|
let entries = timeline.entries.map { timelineEntry in
|
|
231
|
-
|
|
232
|
-
return VoltraHomeWidgetEntry(
|
|
230
|
+
VoltraHomeWidgetEntry(
|
|
233
231
|
date: timelineEntry.date,
|
|
234
|
-
rootNode:
|
|
232
|
+
rootNode: parseJsonToNode(data: timelineEntry.json, family: context.family),
|
|
235
233
|
widgetId: widgetId,
|
|
236
234
|
deepLinkUrl: timelineEntry.deepLinkUrl
|
|
237
235
|
)
|
|
238
236
|
}
|
|
239
|
-
|
|
240
|
-
return
|
|
237
|
+
return Timeline(entries: entries, policy: policy)
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
// Fallback to single-entry behavior
|
|
244
240
|
let data = VoltraHomeWidgetStore.readJson(widgetId: widgetId) ?? initialState
|
|
245
241
|
let node = data.flatMap { parseJsonToNode(data: $0, family: context.family) }
|
|
246
242
|
let entry = VoltraHomeWidgetEntry(date: Date(), rootNode: node, widgetId: widgetId)
|
|
247
|
-
|
|
248
|
-
// If server updates are configured but we're in fallback, retry sooner
|
|
249
|
-
if VoltraWidgetServerFetcher.serverUrl(for: widgetId) != nil {
|
|
250
|
-
let retryDate = Date().addingTimeInterval(900) // Retry in 15 minutes
|
|
251
|
-
completion(Timeline(entries: [entry], policy: .after(retryDate)))
|
|
252
|
-
} else {
|
|
253
|
-
completion(Timeline(entries: [entry], policy: .never))
|
|
254
|
-
}
|
|
243
|
+
return Timeline(entries: [entry], policy: policy)
|
|
255
244
|
}
|
|
256
245
|
|
|
257
246
|
/// Parse JSON data into a VoltraNode for the given widget family.
|
|
@@ -273,6 +262,12 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
|
|
|
273
262
|
}
|
|
274
263
|
}
|
|
275
264
|
|
|
265
|
+
private struct UnparseableServerContentError: LocalizedError {
|
|
266
|
+
var errorDescription: String? {
|
|
267
|
+
"Server response could not be parsed into a widget tree"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
276
271
|
public struct VoltraHomeWidgetView: View {
|
|
277
272
|
public var entry: VoltraHomeWidgetEntry
|
|
278
273
|
|
|
@@ -461,7 +456,9 @@ private extension View {
|
|
|
461
456
|
private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
462
457
|
// Prefer the timeline entry's deep link URL if available
|
|
463
458
|
if let entryUrl = entry.deepLinkUrl, !entryUrl.isEmpty {
|
|
464
|
-
if entryUrl.contains("://"), let url = URL(string: entryUrl) {
|
|
459
|
+
if entryUrl.contains("://"), let url = URL(string: entryUrl) {
|
|
460
|
+
return url
|
|
461
|
+
}
|
|
465
462
|
if let scheme = VoltraDeepLinkResolver.deepLinkScheme() {
|
|
466
463
|
let path = entryUrl.hasPrefix("/") ? entryUrl : "/\(entryUrl)"
|
|
467
464
|
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
@@ -470,7 +467,9 @@ private func resolveDeepLinkURL(_ entry: VoltraHomeWidgetEntry) -> URL? {
|
|
|
470
467
|
|
|
471
468
|
// Fallback to static deep link URL from UserDefaults
|
|
472
469
|
if let raw = VoltraHomeWidgetStore.readDeepLinkUrl(widgetId: entry.widgetId), !raw.isEmpty {
|
|
473
|
-
if raw.contains("://"), let url = URL(string: raw) {
|
|
470
|
+
if raw.contains("://"), let url = URL(string: raw) {
|
|
471
|
+
return url
|
|
472
|
+
}
|
|
474
473
|
if let scheme = VoltraDeepLinkResolver.deepLinkScheme() {
|
|
475
474
|
let path = raw.hasPrefix("/") ? raw : "/\(raw)"
|
|
476
475
|
return URL(string: "\(scheme)://\(path.trimmingCharacters(in: CharacterSet(charactersIn: "/")))")
|
|
@@ -9,7 +9,9 @@ public struct VoltraWidget: Widget {
|
|
|
9
9
|
/// Convert an array of nodes to a single root node for rendering
|
|
10
10
|
private func rootNode(for region: VoltraRegion, from state: VoltraAttributes.ContentState) -> VoltraNode {
|
|
11
11
|
let nodes = state.regions[region] ?? []
|
|
12
|
-
if nodes.isEmpty {
|
|
12
|
+
if nodes.isEmpty {
|
|
13
|
+
return .empty
|
|
14
|
+
}
|
|
13
15
|
return nodes.count == 1 ? nodes[0] : .array(nodes)
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// The AST itself lives in `shared/` and knows nothing about SwiftUI. Rendering is attached
|
|
5
|
+
/// here so `shared/` stays free of any dependency on `ui/` and can be compiled — and tested —
|
|
6
|
+
/// on its own. Both directories are built into a single module by the podspecs, so call sites
|
|
7
|
+
/// keep using a `VoltraNode` directly as a view.
|
|
8
|
+
extension VoltraNode: View {
|
|
9
|
+
public var body: some View {
|
|
10
|
+
switch self {
|
|
11
|
+
case let .element(element):
|
|
12
|
+
VoltraElementView(element: element)
|
|
13
|
+
case let .array(nodes):
|
|
14
|
+
// Use stable identifiers: prefer element.id, fall back to index
|
|
15
|
+
let items: [(id: String, node: VoltraNode)] = nodes.enumerated().map { offset, node in
|
|
16
|
+
let id: String
|
|
17
|
+
if case let .element(element) = node, let elementId = element.id {
|
|
18
|
+
id = elementId
|
|
19
|
+
} else {
|
|
20
|
+
id = "idx_\(offset)"
|
|
21
|
+
}
|
|
22
|
+
return (id: id, node: node)
|
|
23
|
+
}
|
|
24
|
+
ForEach(items, id: \.id) { item in
|
|
25
|
+
item.node
|
|
26
|
+
}
|
|
27
|
+
case let .text(text):
|
|
28
|
+
Text(text)
|
|
29
|
+
case .empty:
|
|
30
|
+
EmptyView()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// View that renders a VoltraElement based on its type
|
|
36
|
+
struct VoltraElementView: View {
|
|
37
|
+
let element: VoltraElement
|
|
38
|
+
|
|
39
|
+
var body: some View {
|
|
40
|
+
switch element.type {
|
|
41
|
+
case "Button":
|
|
42
|
+
VoltraButton(element)
|
|
43
|
+
|
|
44
|
+
case "Link":
|
|
45
|
+
VoltraLink(element)
|
|
46
|
+
|
|
47
|
+
case "VStack":
|
|
48
|
+
VoltraVStack(element)
|
|
49
|
+
|
|
50
|
+
case "HStack":
|
|
51
|
+
VoltraHStack(element)
|
|
52
|
+
|
|
53
|
+
case "View":
|
|
54
|
+
VoltraFlexView(element)
|
|
55
|
+
|
|
56
|
+
case "ZStack":
|
|
57
|
+
VoltraZStack(element)
|
|
58
|
+
|
|
59
|
+
case "Text":
|
|
60
|
+
VoltraText(element)
|
|
61
|
+
|
|
62
|
+
case "Image":
|
|
63
|
+
VoltraImage(element)
|
|
64
|
+
|
|
65
|
+
case "Symbol":
|
|
66
|
+
VoltraSymbol(element)
|
|
67
|
+
|
|
68
|
+
case "Divider":
|
|
69
|
+
VoltraDivider(element)
|
|
70
|
+
|
|
71
|
+
case "Spacer":
|
|
72
|
+
VoltraSpacer(element)
|
|
73
|
+
|
|
74
|
+
case "Label":
|
|
75
|
+
VoltraLabel(element)
|
|
76
|
+
|
|
77
|
+
case "Toggle":
|
|
78
|
+
VoltraToggle(element)
|
|
79
|
+
|
|
80
|
+
case "Gauge":
|
|
81
|
+
VoltraGauge(element)
|
|
82
|
+
|
|
83
|
+
case "LinearProgressView":
|
|
84
|
+
VoltraLinearProgressView(element)
|
|
85
|
+
|
|
86
|
+
case "CircularProgressView":
|
|
87
|
+
VoltraCircularProgressView(element)
|
|
88
|
+
|
|
89
|
+
case "Timer":
|
|
90
|
+
VoltraTimer(element)
|
|
91
|
+
|
|
92
|
+
case "GroupBox":
|
|
93
|
+
VoltraGroupBox(element)
|
|
94
|
+
|
|
95
|
+
case "LinearGradient":
|
|
96
|
+
VoltraLinearGradient(element)
|
|
97
|
+
|
|
98
|
+
case "GlassContainer":
|
|
99
|
+
VoltraGlassContainer(element)
|
|
100
|
+
|
|
101
|
+
case "Mask":
|
|
102
|
+
VoltraMask(element)
|
|
103
|
+
|
|
104
|
+
case "Chart":
|
|
105
|
+
if #available(iOS 16.0, macOS 13.0, *) {
|
|
106
|
+
VoltraChart(element)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
default:
|
|
110
|
+
EmptyView()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -25,6 +25,9 @@ public struct ChartParameters: ComponentParameters {
|
|
|
25
25
|
/// Configure y-axis grid line style
|
|
26
26
|
public let yAxisGridStyle: String?
|
|
27
27
|
|
|
28
|
+
/// Pin the y-axis lower and/or upper bound
|
|
29
|
+
public let yScale: String?
|
|
30
|
+
|
|
28
31
|
/// Show or hide the chart legend
|
|
29
32
|
public let legendVisibility: String?
|
|
30
33
|
|
|
@@ -2,7 +2,7 @@ import ActivityKit
|
|
|
2
2
|
import Foundation
|
|
3
3
|
import SwiftUI
|
|
4
4
|
|
|
5
|
-
enum VoltraDeepLinkResolver {
|
|
5
|
+
public enum VoltraDeepLinkResolver {
|
|
6
6
|
static func deepLinkScheme() -> String? {
|
|
7
7
|
if let types = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]] {
|
|
8
8
|
for t in types {
|
|
@@ -26,11 +26,13 @@ enum VoltraDeepLinkResolver {
|
|
|
26
26
|
/// Resolves a URL string, supporting both absolute and relative paths
|
|
27
27
|
/// - Parameter raw: The URL string (e.g., "myapp://path", "/path", or "path")
|
|
28
28
|
/// - Returns: A resolved URL, or nil if invalid
|
|
29
|
-
static func resolveUrl(_ raw: String) -> URL? {
|
|
29
|
+
public static func resolveUrl(_ raw: String) -> URL? {
|
|
30
30
|
guard !raw.isEmpty else { return nil }
|
|
31
31
|
|
|
32
32
|
// If it's already an absolute URL, use it as-is
|
|
33
|
-
if raw.contains("://"), let url = URL(string: raw) {
|
|
33
|
+
if raw.contains("://"), let url = URL(string: raw) {
|
|
34
|
+
return url
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
// Otherwise, prepend the app's URL scheme
|
|
36
38
|
if let scheme = deepLinkScheme() {
|
|
@@ -43,10 +43,14 @@ enum VoltraProgressDriver {
|
|
|
43
43
|
/// The progress value is clamped to `[0, 1]` unless `clamp` is `false`.
|
|
44
44
|
static func progress(for range: ClosedRange<Date>, at date: Date = Date(), clamp: Bool = true) -> Double {
|
|
45
45
|
let total = range.upperBound.timeIntervalSince(range.lowerBound)
|
|
46
|
-
if total <= 0 {
|
|
46
|
+
if total <= 0 {
|
|
47
|
+
return clamp ? 1 : 0
|
|
48
|
+
}
|
|
47
49
|
let elapsed = date.timeIntervalSince(range.lowerBound)
|
|
48
50
|
let ratio = elapsed / total
|
|
49
|
-
if !clamp {
|
|
51
|
+
if !clamp {
|
|
52
|
+
return ratio
|
|
53
|
+
}
|
|
50
54
|
return min(max(ratio, 0), 1)
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -250,7 +250,9 @@ struct VoltraFlexStackLayout: Layout {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
// If stretch and no explicit cross size, use full cross axis (only finite values)
|
|
253
|
-
if let proposal = proposal, proposal.isFinite {
|
|
253
|
+
if let proposal = proposal, proposal.isFinite {
|
|
254
|
+
return proposal
|
|
255
|
+
}
|
|
254
256
|
// Otherwise, measure intrinsic
|
|
255
257
|
let size = subview.sizeThatFits(ProposedViewSize(width: nil, height: nil))
|
|
256
258
|
return crossAxis(size)
|
|
@@ -266,8 +268,12 @@ struct VoltraFlexStackLayout: Layout {
|
|
|
266
268
|
|
|
267
269
|
private func clamp(_ value: CGFloat, min: CGFloat?, max: CGFloat?) -> CGFloat {
|
|
268
270
|
var result = value
|
|
269
|
-
if let min = min {
|
|
270
|
-
|
|
271
|
+
if let min = min {
|
|
272
|
+
result = Swift.max(result, min)
|
|
273
|
+
}
|
|
274
|
+
if let max = max {
|
|
275
|
+
result = Swift.min(result, max)
|
|
276
|
+
}
|
|
271
277
|
return result
|
|
272
278
|
}
|
|
273
279
|
|