@use-voltra/ios-client 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/Voltra.podspec +1 -0
  2. package/build/commonjs/events.js +23 -0
  3. package/build/commonjs/events.js.map +1 -1
  4. package/build/commonjs/index.js +59 -0
  5. package/build/commonjs/index.js.map +1 -1
  6. package/build/commonjs/live-activity/api.js +3 -1
  7. package/build/commonjs/live-activity/api.js.map +1 -1
  8. package/build/commonjs/live-activity/dynamic-api.js +173 -0
  9. package/build/commonjs/live-activity/dynamic-api.js.map +1 -0
  10. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  11. package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js +47 -0
  12. package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
  13. package/build/commonjs/utils/enableWidgetHotReload.js +8 -11
  14. package/build/commonjs/utils/enableWidgetHotReload.js.map +1 -1
  15. package/build/commonjs/utils/useUpdateOnHMR.js +3 -7
  16. package/build/commonjs/utils/useUpdateOnHMR.js.map +1 -1
  17. package/build/commonjs/widgets/server-credentials.js +9 -0
  18. package/build/commonjs/widgets/server-credentials.js.map +1 -1
  19. package/build/commonjs/widgets/server-update.js +78 -0
  20. package/build/commonjs/widgets/server-update.js.map +1 -0
  21. package/build/module/events.js +23 -0
  22. package/build/module/events.js.map +1 -1
  23. package/build/module/index.js +3 -0
  24. package/build/module/index.js.map +1 -1
  25. package/build/module/live-activity/api.js +2 -2
  26. package/build/module/live-activity/api.js.map +1 -1
  27. package/build/module/live-activity/dynamic-api.js +167 -0
  28. package/build/module/live-activity/dynamic-api.js.map +1 -0
  29. package/build/module/native/NativeVoltra.js.map +1 -1
  30. package/build/module/utils/enableDynamicLiveActivityHotReload.js +43 -0
  31. package/build/module/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
  32. package/build/module/utils/enableWidgetHotReload.js +9 -11
  33. package/build/module/utils/enableWidgetHotReload.js.map +1 -1
  34. package/build/module/utils/useUpdateOnHMR.js +3 -7
  35. package/build/module/utils/useUpdateOnHMR.js.map +1 -1
  36. package/build/module/widgets/server-credentials.js +9 -0
  37. package/build/module/widgets/server-credentials.js.map +1 -1
  38. package/build/module/widgets/server-update.js +72 -0
  39. package/build/module/widgets/server-update.js.map +1 -0
  40. package/build/typescript/commonjs/events.d.ts +7 -0
  41. package/build/typescript/commonjs/events.d.ts.map +1 -1
  42. package/build/typescript/commonjs/index.d.ts +3 -0
  43. package/build/typescript/commonjs/index.d.ts.map +1 -1
  44. package/build/typescript/commonjs/live-activity/api.d.ts +7 -0
  45. package/build/typescript/commonjs/live-activity/api.d.ts.map +1 -1
  46. package/build/typescript/commonjs/live-activity/dynamic-api.d.ts +54 -0
  47. package/build/typescript/commonjs/live-activity/dynamic-api.d.ts.map +1 -0
  48. package/build/typescript/commonjs/native/NativeVoltra.d.ts +20 -0
  49. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  50. package/build/typescript/commonjs/types.d.ts +1 -1
  51. package/build/typescript/commonjs/types.d.ts.map +1 -1
  52. package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
  53. package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
  54. package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts +5 -7
  55. package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts.map +1 -1
  56. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts +0 -3
  57. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts.map +1 -1
  58. package/build/typescript/commonjs/widgets/server-credentials.d.ts +8 -0
  59. package/build/typescript/commonjs/widgets/server-credentials.d.ts.map +1 -1
  60. package/build/typescript/commonjs/widgets/server-update.d.ts +64 -0
  61. package/build/typescript/commonjs/widgets/server-update.d.ts.map +1 -0
  62. package/build/typescript/module/events.d.ts +7 -0
  63. package/build/typescript/module/events.d.ts.map +1 -1
  64. package/build/typescript/module/index.d.ts +3 -0
  65. package/build/typescript/module/index.d.ts.map +1 -1
  66. package/build/typescript/module/live-activity/api.d.ts +7 -0
  67. package/build/typescript/module/live-activity/api.d.ts.map +1 -1
  68. package/build/typescript/module/live-activity/dynamic-api.d.ts +54 -0
  69. package/build/typescript/module/live-activity/dynamic-api.d.ts.map +1 -0
  70. package/build/typescript/module/native/NativeVoltra.d.ts +20 -0
  71. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  72. package/build/typescript/module/types.d.ts +1 -1
  73. package/build/typescript/module/types.d.ts.map +1 -1
  74. package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
  75. package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
  76. package/build/typescript/module/utils/enableWidgetHotReload.d.ts +5 -7
  77. package/build/typescript/module/utils/enableWidgetHotReload.d.ts.map +1 -1
  78. package/build/typescript/module/utils/useUpdateOnHMR.d.ts +0 -3
  79. package/build/typescript/module/utils/useUpdateOnHMR.d.ts.map +1 -1
  80. package/build/typescript/module/widgets/server-credentials.d.ts +8 -0
  81. package/build/typescript/module/widgets/server-credentials.d.ts.map +1 -1
  82. package/build/typescript/module/widgets/server-update.d.ts +64 -0
  83. package/build/typescript/module/widgets/server-update.d.ts.map +1 -0
  84. package/expo-plugin/build/cjs/constants.js +20 -1
  85. package/expo-plugin/build/cjs/constants.js.map +1 -1
  86. package/expo-plugin/build/cjs/index.js +6 -0
  87. package/expo-plugin/build/cjs/index.js.map +1 -1
  88. package/expo-plugin/build/cjs/ios/index.js +1 -0
  89. package/expo-plugin/build/cjs/ios/index.js.map +1 -1
  90. package/expo-plugin/build/cjs/ios/infoPlist.js +28 -5
  91. package/expo-plugin/build/cjs/ios/infoPlist.js.map +1 -1
  92. package/expo-plugin/build/cjs/ios/serverUpdate.js +25 -0
  93. package/expo-plugin/build/cjs/ios/serverUpdate.js.map +1 -0
  94. package/expo-plugin/build/cjs/ios-widget/clientRendered.js +4 -4
  95. package/expo-plugin/build/cjs/ios-widget/clientRendered.js.map +1 -1
  96. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +5 -5
  97. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
  98. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +130 -0
  99. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -0
  100. package/expo-plugin/build/cjs/ios-widget/files/index.js +9 -2
  101. package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
  102. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +6 -3
  103. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
  104. package/expo-plugin/build/cjs/ios-widget/files/manifest.js +24 -0
  105. package/expo-plugin/build/cjs/ios-widget/files/manifest.js.map +1 -1
  106. package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js +7 -0
  107. package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js.map +1 -0
  108. package/expo-plugin/build/cjs/ios-widget/files/swift.js +56 -26
  109. package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
  110. package/expo-plugin/build/cjs/ios-widget/index.js +9 -3
  111. package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
  112. package/expo-plugin/build/cjs/ios-widget/widgetPlist.js +34 -22
  113. package/expo-plugin/build/cjs/ios-widget/widgetPlist.js.map +1 -1
  114. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +51 -11
  115. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
  116. package/expo-plugin/build/cjs/ios-widget/xcode/index.js +21 -5
  117. package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
  118. package/expo-plugin/build/cjs/utils/urlScheme.js +36 -14
  119. package/expo-plugin/build/cjs/utils/urlScheme.js.map +1 -1
  120. package/expo-plugin/build/cjs/validation.js +54 -0
  121. package/expo-plugin/build/cjs/validation.js.map +1 -1
  122. package/expo-plugin/build/esm/constants.js +17 -0
  123. package/expo-plugin/build/esm/constants.js.map +1 -1
  124. package/expo-plugin/build/esm/index.js +6 -0
  125. package/expo-plugin/build/esm/index.js.map +1 -1
  126. package/expo-plugin/build/esm/ios/index.js +1 -0
  127. package/expo-plugin/build/esm/ios/index.js.map +1 -1
  128. package/expo-plugin/build/esm/ios/infoPlist.js +28 -5
  129. package/expo-plugin/build/esm/ios/infoPlist.js.map +1 -1
  130. package/expo-plugin/build/esm/ios/serverUpdate.js +21 -0
  131. package/expo-plugin/build/esm/ios/serverUpdate.js.map +1 -0
  132. package/expo-plugin/build/esm/ios-widget/clientRendered.js +5 -5
  133. package/expo-plugin/build/esm/ios-widget/clientRendered.js.map +1 -1
  134. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +6 -6
  135. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
  136. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +122 -0
  137. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -0
  138. package/expo-plugin/build/esm/ios-widget/files/index.js +10 -3
  139. package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
  140. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +6 -3
  141. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
  142. package/expo-plugin/build/esm/ios-widget/files/manifest.js +23 -1
  143. package/expo-plugin/build/esm/ios-widget/files/manifest.js.map +1 -1
  144. package/expo-plugin/build/esm/ios-widget/files/swift-utils.js +4 -0
  145. package/expo-plugin/build/esm/ios-widget/files/swift-utils.js.map +1 -0
  146. package/expo-plugin/build/esm/ios-widget/files/swift.js +50 -20
  147. package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
  148. package/expo-plugin/build/esm/ios-widget/index.js +9 -3
  149. package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
  150. package/expo-plugin/build/esm/ios-widget/widgetPlist.js +34 -22
  151. package/expo-plugin/build/esm/ios-widget/widgetPlist.js.map +1 -1
  152. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +51 -11
  153. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
  154. package/expo-plugin/build/esm/ios-widget/xcode/index.js +21 -5
  155. package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
  156. package/expo-plugin/build/esm/utils/urlScheme.js +34 -14
  157. package/expo-plugin/build/esm/utils/urlScheme.js.map +1 -1
  158. package/expo-plugin/build/esm/validation.js +54 -1
  159. package/expo-plugin/build/esm/validation.js.map +1 -1
  160. package/expo-plugin/build/types/constants.d.ts +10 -1
  161. package/expo-plugin/build/types/constants.d.ts.map +1 -1
  162. package/expo-plugin/build/types/index.d.ts +1 -1
  163. package/expo-plugin/build/types/index.d.ts.map +1 -1
  164. package/expo-plugin/build/types/ios/index.d.ts +2 -0
  165. package/expo-plugin/build/types/ios/index.d.ts.map +1 -1
  166. package/expo-plugin/build/types/ios/infoPlist.d.ts +2 -0
  167. package/expo-plugin/build/types/ios/infoPlist.d.ts.map +1 -1
  168. package/expo-plugin/build/types/ios/serverUpdate.d.ts +11 -0
  169. package/expo-plugin/build/types/ios/serverUpdate.d.ts.map +1 -0
  170. package/expo-plugin/build/types/ios-widget/clientRendered.d.ts.map +1 -1
  171. package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
  172. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts +7 -0
  173. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -0
  174. package/expo-plugin/build/types/ios-widget/files/index.d.ts +3 -1
  175. package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
  176. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +2 -1
  177. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
  178. package/expo-plugin/build/types/ios-widget/files/manifest.d.ts +9 -2
  179. package/expo-plugin/build/types/ios-widget/files/manifest.d.ts.map +1 -1
  180. package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts +2 -0
  181. package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts.map +1 -0
  182. package/expo-plugin/build/types/ios-widget/files/swift.d.ts +6 -2
  183. package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
  184. package/expo-plugin/build/types/ios-widget/index.d.ts +3 -1
  185. package/expo-plugin/build/types/ios-widget/index.d.ts.map +1 -1
  186. package/expo-plugin/build/types/ios-widget/widgetPlist.d.ts.map +1 -1
  187. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +3 -1
  188. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
  189. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +2 -1
  190. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
  191. package/expo-plugin/build/types/types.d.ts +23 -4
  192. package/expo-plugin/build/types/types.d.ts.map +1 -1
  193. package/expo-plugin/build/types/utils/urlScheme.d.ts +16 -0
  194. package/expo-plugin/build/types/utils/urlScheme.d.ts.map +1 -1
  195. package/expo-plugin/build/types/validation.d.ts +2 -1
  196. package/expo-plugin/build/types/validation.d.ts.map +1 -1
  197. package/expo-plugin/src/constants.ts +24 -1
  198. package/expo-plugin/src/index.ts +9 -0
  199. package/expo-plugin/src/ios/index.ts +3 -0
  200. package/expo-plugin/src/ios/infoPlist.ts +33 -5
  201. package/expo-plugin/src/ios/serverUpdate.ts +29 -0
  202. package/expo-plugin/src/ios-widget/clientRendered.ts +9 -5
  203. package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +11 -6
  204. package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +138 -0
  205. package/expo-plugin/src/ios-widget/files/index.ts +15 -4
  206. package/expo-plugin/src/ios-widget/files/infoPlist.ts +17 -3
  207. package/expo-plugin/src/ios-widget/files/manifest.ts +42 -2
  208. package/expo-plugin/src/ios-widget/files/swift-utils.ts +3 -0
  209. package/expo-plugin/src/ios-widget/files/swift.ts +69 -22
  210. package/expo-plugin/src/ios-widget/index.ts +13 -3
  211. package/expo-plugin/src/ios-widget/widgetPlist.ts +37 -23
  212. package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +34 -28
  213. package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +59 -12
  214. package/expo-plugin/src/ios-widget/xcode/index.ts +23 -6
  215. package/expo-plugin/src/types.ts +28 -4
  216. package/expo-plugin/src/utils/urlScheme.ts +47 -17
  217. package/expo-plugin/src/validation.ts +76 -1
  218. package/ios/Package.swift +32 -3
  219. package/ios/VoltraWidget.podspec +1 -0
  220. package/ios/app/NativeVoltra.mm +107 -7
  221. package/ios/app/VoltraLiveActivityManager.swift +30 -0
  222. package/ios/app/VoltraLiveActivityService.swift +77 -5
  223. package/ios/app/VoltraModule.swift +99 -1
  224. package/ios/app/VoltraModuleImpl.swift +100 -2
  225. package/ios/app/VoltraViewComponentView.mm +3 -3
  226. package/ios/app/VoltraWidgetService.swift +147 -12
  227. package/ios/app/dynamic-live-activity/VoltraDynamicLiveActivityService.swift +97 -0
  228. package/ios/app/dynamic-live-activity/VoltraLiveActivityChronology.swift +68 -0
  229. package/ios/shared/DynamicWidgetPropsStore.swift +73 -6
  230. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerFetchResolver.swift +34 -0
  231. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerProps.swift +55 -0
  232. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerPropsStore.swift +146 -0
  233. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerUpdateRunner.swift +193 -0
  234. package/ios/shared/ServerWidgetContentResolver.swift +60 -0
  235. package/ios/shared/ServerWidgetResponseStore.swift +25 -0
  236. package/ios/shared/ShortNames.swift +6 -0
  237. package/ios/shared/VoltraConfig.swift +41 -0
  238. package/ios/shared/VoltraConstants.swift +15 -0
  239. package/ios/shared/VoltraElement.swift +127 -43
  240. package/ios/shared/VoltraEventBus.swift +52 -7
  241. package/ios/shared/VoltraJSRenderer.swift +90 -22
  242. package/ios/shared/VoltraNode.swift +1 -109
  243. package/ios/shared/VoltraRefreshIntent.swift +1 -1
  244. package/ios/shared/VoltraWidgetKind.swift +41 -0
  245. package/ios/shared/VoltraWidgetServerFetcher.swift +52 -107
  246. package/ios/shared/WidgetServer/VoltraWidgetAppearance.swift +17 -0
  247. package/ios/shared/WidgetServer/VoltraWidgetServer.swift +138 -0
  248. package/ios/shared/WidgetServer/WidgetCanonicalConfiguration.swift +77 -0
  249. package/ios/shared/WidgetServer/WidgetScope.swift +55 -0
  250. package/ios/shared/WidgetServer/WidgetServerEtagStore.swift +55 -0
  251. package/ios/shared/WidgetServer/WidgetServerFetcher.swift +179 -0
  252. package/ios/shared/WidgetServer/WidgetServerRequestBuilder.swift +114 -0
  253. package/ios/shared/WidgetServer/WidgetServerSettingsCodec.swift +47 -0
  254. package/ios/shared/WidgetServer/WidgetServerSettingsResolver.swift +129 -0
  255. package/ios/shared/WidgetServer/WidgetServerSettingsStore.swift +170 -0
  256. package/ios/shared/WidgetServer/WidgetServerSettingsValidator.swift +73 -0
  257. package/ios/shared/WidgetServer/WidgetServerUpdateSettings.swift +123 -0
  258. package/ios/shared/WidgetServer/WidgetServerUpdateSettingsJson.swift +115 -0
  259. package/ios/shared/dynamic-live-activity/DynamicLiveActivityPropsCodec.swift +13 -0
  260. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityBundleSource.swift +156 -0
  261. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityObserver.swift +97 -0
  262. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityOperations.swift +105 -0
  263. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityPayloadValidator.swift +55 -0
  264. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRegistry.swift +143 -0
  265. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureQueue.swift +158 -0
  266. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureReporter.swift +120 -0
  267. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +370 -0
  268. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityTypes.swift +87 -0
  269. package/ios/shared/dynamic-live-activity/VoltraLiveActivityOrder.swift +19 -0
  270. package/ios/target/VoltraClientWidgetRuntime.swift +44 -6
  271. package/ios/target/VoltraDynamicWidgetServerUpdateProvider.swift +204 -0
  272. package/ios/target/VoltraHomeWidget.swift +85 -90
  273. package/ios/ui/Extensions/VoltraNode+View.swift +113 -0
  274. package/ios/ui/Generated/Parameters/ChartParameters.swift +3 -0
  275. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +2 -2
  276. package/ios/ui/Views/VoltraChart.swift +108 -0
  277. package/package.json +4 -4
  278. package/src/events.ts +34 -0
  279. package/src/index.ts +22 -0
  280. package/src/live-activity/api.ts +2 -2
  281. package/src/live-activity/dynamic-api.ts +240 -0
  282. package/src/native/NativeVoltra.ts +21 -0
  283. package/src/types.ts +4 -0
  284. package/src/utils/enableDynamicLiveActivityHotReload.ts +50 -0
  285. package/src/utils/enableWidgetHotReload.ts +9 -16
  286. package/src/utils/useUpdateOnHMR.ts +3 -12
  287. package/src/widgets/server-credentials.ts +8 -0
  288. package/src/widgets/server-update.ts +84 -0
  289. /package/ios/shared/{VoltraKeychainHelper.swift → WidgetServer/VoltraKeychainHelper.swift} +0 -0
@@ -89,6 +89,14 @@ public class VoltraModuleImpl {
89
89
  VoltraHeadlessState.shared.clear()
90
90
  }
91
91
 
92
+ func drainDynamicLiveActivityRenderFailures() {
93
+ VoltraEventBus.shared.drainDynamicLiveActivityRenderFailures()
94
+ }
95
+
96
+ func setDynamicLiveActivityRenderFailureListenerActive(_ active: Bool) {
97
+ VoltraEventBus.shared.setDynamicLiveActivityRenderFailureListenerActive(active)
98
+ }
99
+
92
100
  var pushNotificationsEnabled: Bool {
93
101
  // Support both keys for compatibility with older plugin and new Voltra naming
94
102
  let main = Bundle.main
@@ -182,6 +190,48 @@ public class VoltraModuleImpl {
182
190
  }
183
191
  }
184
192
 
193
+ func startDynamicLiveActivity(definitionId: String, propsJson: String, options: StartVoltraOptions?) async throws -> String {
194
+ guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
195
+ let name = options?.activityName?.trimmingCharacters(in: .whitespacesAndNewlines)
196
+ let activityName = name?.isEmpty == false ? name! : UUID().uuidString
197
+ do {
198
+ let props = try VoltraDynamicLiveActivityPayloadValidator.decodeProps(propsJson)
199
+ let staleDate = options?.staleDate.map { Date(timeIntervalSince1970: $0.doubleValue / 1000.0) }
200
+ let pushType = try resolvePushType(channelId: options?.channelId)
201
+ let request = VoltraDynamicLiveActivityCreateRequest(
202
+ definitionId: definitionId,
203
+ name: activityName,
204
+ deepLinkUrl: options?.deepLinkUrl,
205
+ props: props,
206
+ staleDate: staleDate,
207
+ relevanceScore: options?.relevanceScore?.doubleValue ?? 0.0,
208
+ pushType: pushType
209
+ )
210
+ return try await liveActivityService.createDynamicActivity(request)
211
+ } catch {
212
+ VoltraLogger.module.error("startDynamicLiveActivity failed: \(error)")
213
+ throw mapError(error)
214
+ }
215
+ }
216
+
217
+ func updateDynamicLiveActivity(activityId: String, propsJson: String, options: UpdateVoltraOptions?) async throws {
218
+ guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
219
+ do {
220
+ let props = try VoltraDynamicLiveActivityPayloadValidator.decodeProps(propsJson)
221
+ let staleDate = options?.staleDate.map { Date(timeIntervalSince1970: $0.doubleValue / 1000.0) }
222
+ try await liveActivityService.updateDynamicActivity(
223
+ byName: activityId,
224
+ request: VoltraDynamicLiveActivityUpdateRequest(
225
+ props: props,
226
+ staleDate: staleDate,
227
+ relevanceScore: options?.relevanceScore?.doubleValue ?? 0.0
228
+ )
229
+ )
230
+ } catch {
231
+ throw mapError(error)
232
+ }
233
+ }
234
+
185
235
  func endLiveActivity(activityId: String, options: EndVoltraOptions?) async throws {
186
236
  guard #available(iOS 16.4, *) else { throw VoltraErrors.unsupportedOS }
187
237
 
@@ -202,12 +252,17 @@ public class VoltraModuleImpl {
202
252
 
203
253
  func getLatestVoltraActivityId() -> String? {
204
254
  guard #available(iOS 16.4, *) else { return nil }
205
- return liveActivityService.getLatestActivity()?.id
255
+ return liveActivityService.latestActivityId()
206
256
  }
207
257
 
208
258
  func listVoltraActivityIds() -> [String] {
209
259
  guard #available(iOS 16.4, *) else { return [] }
210
- return liveActivityService.getAllActivities().map(\.id)
260
+ return liveActivityService.getAllActivityReferences().map(\.id)
261
+ }
262
+
263
+ func getDynamicLiveActivityDefinitionIds() -> [String] {
264
+ guard #available(iOS 16.4, *) else { return [] }
265
+ return liveActivityService.dynamicLiveActivityDefinitionIds()
211
266
  }
212
267
 
213
268
  func isLiveActivityActive(name: String) -> Bool {
@@ -245,6 +300,13 @@ public class VoltraModuleImpl {
245
300
  }
246
301
  }
247
302
 
303
+ func reloadDynamicLiveActivities(definitionIds: [String]?) async {
304
+ #if DEBUG
305
+ syncDevServerURL()
306
+ #endif
307
+ await liveActivityService.reloadDynamicActivities(definitionIds: definitionIds)
308
+ }
309
+
248
310
  // MARK: - Image Preloading
249
311
 
250
312
  func preloadImages(images: [PreloadImageOptions]) async throws -> PreloadImagesResult {
@@ -264,6 +326,13 @@ public class VoltraModuleImpl {
264
326
  let dynamicWidgetUpdater = DynamicWidgetUpdater(
265
327
  dynamicWidgetPropsPersistence: DynamicWidgetPropsStore(),
266
328
  dynamicWidgetTimelineReload: { dynamicWidgetId in
329
+ // On a server-driven widget the reload below runs getTimeline, which would otherwise
330
+ // fetch and overwrite what was just written. ADR 0002 says the *next scheduled* fetch
331
+ // overwrites app-written props, not the reload the write itself caused.
332
+ if VoltraWidgetServer.isServerDriven(dynamicWidgetId) {
333
+ DynamicWidgetServerPropsStore().noteAppWrite(for: .of(dynamicWidgetId))
334
+ }
335
+
267
336
  VoltraWidgetService.reloadTimeline(for: dynamicWidgetId)
268
337
  }
269
338
  )
@@ -307,11 +376,17 @@ public class VoltraModuleImpl {
307
376
 
308
377
  func clearWidget(widgetId: String) async {
309
378
  VoltraWidgetService.removeAllData(for: widgetId)
379
+ VoltraWidgetService.clearWidgetServerState(for: widgetId)
310
380
  VoltraWidgetService.reloadTimeline(for: widgetId)
311
381
  }
312
382
 
313
383
  func clearAllWidgets() async {
314
384
  VoltraWidgetService.removeAllWidgets()
385
+
386
+ for widgetId in VoltraWidgetServer.serverDrivenWidgetIds {
387
+ VoltraWidgetService.clearWidgetServerState(for: widgetId)
388
+ }
389
+
315
390
  VoltraWidgetService.reloadAllTimelines()
316
391
  }
317
392
 
@@ -319,6 +394,19 @@ public class VoltraModuleImpl {
319
394
  try await VoltraWidgetService.getActiveWidgets()
320
395
  }
321
396
 
397
+ /// - Returns: an error message when the settings were rejected, or nil when they were applied.
398
+ func setWidgetServerUpdate(settingsJson: String, widgetId: String?) -> String? {
399
+ VoltraWidgetService.setWidgetServerUpdate(settingsJson: settingsJson, widgetId: widgetId)
400
+ }
401
+
402
+ func clearWidgetServerUpdate(widgetId: String?) -> String? {
403
+ VoltraWidgetService.clearWidgetServerUpdate(widgetId: widgetId)
404
+ }
405
+
406
+ func getWidgetServerUpdate(widgetId: String?) -> String? {
407
+ VoltraWidgetService.getWidgetServerUpdate(widgetId: widgetId)
408
+ }
409
+
322
410
  func setWidgetServerCredentials(token: String, headers: [String: String]?) {
323
411
  VoltraWidgetService.setWidgetServerCredentials(token: token, headers: headers)
324
412
  }
@@ -342,6 +430,16 @@ public class VoltraModuleImpl {
342
430
  return VoltraErrors.liveActivitiesNotEnabled
343
431
  case .notFound:
344
432
  return VoltraErrors.notFound
433
+ case .rendererMismatch:
434
+ return VoltraErrors.rendererMismatch
435
+ }
436
+ }
437
+ if let dynamicError = error as? VoltraDynamicLiveActivityError {
438
+ switch dynamicError {
439
+ case .rendererMismatch:
440
+ return VoltraErrors.rendererMismatch
441
+ case .unknownDefinition, .payloadTooLarge, .resourceUnavailable:
442
+ return VoltraErrors.unexpectedError(error)
345
443
  }
346
444
  }
347
445
  return VoltraErrors.unexpectedError(error)
@@ -1,9 +1,9 @@
1
1
  #import "VoltraViewComponentView.h"
2
2
 
3
- #if __has_include("Voltra/Voltra-Swift.h")
4
- #import "Voltra/Voltra-Swift.h"
3
+ #if __has_include("Voltra/VoltraRuntime-Swift.h")
4
+ #import "Voltra/VoltraRuntime-Swift.h"
5
5
  #else
6
- #import "Voltra-Swift.h"
6
+ #import "VoltraRuntime-Swift.h"
7
7
  #endif
8
8
 
9
9
  #import <react/renderer/components/VoltraSpec/ComponentDescriptors.h>
@@ -40,7 +40,7 @@ enum VoltraWidgetService {
40
40
  // MARK: - Reload
41
41
 
42
42
  static func reloadTimeline(for widgetId: String) {
43
- WidgetCenter.shared.reloadTimelines(ofKind: "\(VoltraStorageKeys.widgetKindPrefix)\(widgetId)")
43
+ WidgetCenter.shared.reloadTimelines(ofKind: VoltraWidgetKind.kind(for: widgetId))
44
44
  VoltraLogger.widget.info("Reloaded timeline for '\(widgetId)'")
45
45
  }
46
46
 
@@ -49,10 +49,145 @@ enum VoltraWidgetService {
49
49
  VoltraLogger.widget.info("Reloaded all timelines")
50
50
  }
51
51
 
52
- // MARK: - Server credentials
52
+ // MARK: - Server update settings
53
+
54
+ /// Applies runtime server-update settings and reloads the widgets they affect.
55
+ ///
56
+ /// Storing is only half of what an app expects from `setWidgetServerUpdate`: a new URL should be
57
+ /// fetched from now on, and `enabled: false` should take effect without waiting out the current
58
+ /// interval. A reload makes the affected timelines re-resolve their settings straight away.
59
+ ///
60
+ /// - Parameter widgetId: the widget to scope the settings to, or nil for every server-driven one.
61
+ /// - Returns: an error message, or nil when the settings were applied.
62
+ static func setWidgetServerUpdate(settingsJson: String, widgetId: String?) -> String? {
63
+ let settings: WidgetServerUpdateSettings
64
+
65
+ switch WidgetServerUpdateSettingsJson.parse(settingsJson) {
66
+ case let .invalid(reason):
67
+ return reason
68
+ case let .parsed(parsed):
69
+ settings = parsed
70
+ }
71
+
72
+ if let widgetId, let error = rejectIfNotServerDriven(widgetId) {
73
+ return error
74
+ }
75
+
76
+ if let error = WidgetServerSettingsValidator.validate(settings, isDebugBuild: VoltraWidgetServer.isDebugBuild) {
77
+ return error
78
+ }
79
+
80
+ WidgetServerSettingsStore.set(settings, scope: widgetId.map { .of($0) })
81
+ reloadServerDrivenWidgets(widgetId: widgetId)
82
+
83
+ return nil
84
+ }
85
+
86
+ /// Drops the runtime settings for one widget, or the global ones, so the widget falls back to
87
+ /// what app.json configured. Clearing the global settings is the logout gesture.
88
+ static func clearWidgetServerUpdate(widgetId: String?) -> String? {
89
+ if let widgetId, let error = rejectIfNotServerDriven(widgetId) {
90
+ return error
91
+ }
92
+
93
+ WidgetServerSettingsStore.clear(scope: widgetId.map { .of($0) })
94
+
95
+ // Clearing the global settings is logout: what the previous account's server sent has to go
96
+ // with it, or the widget keeps showing their data. A widget-scoped clear only drops that
97
+ // widget's overrides, so its props are left alone.
98
+ if widgetId == nil {
99
+ clearFetchedState()
100
+ }
101
+
102
+ reloadServerDrivenWidgets(widgetId: widgetId)
103
+
104
+ return nil
105
+ }
106
+
107
+ /// Drops what the server last sent for every server-driven widget, so a Dynamic Widget falls
108
+ /// back to `{}` with `env.serverUpdate.status` of `never` — the state it is in before its first
109
+ /// fetch.
110
+ private static func clearFetchedState() {
111
+ let statuses = DynamicWidgetServerPropsStore()
112
+ let propsStore = DynamicWidgetPropsStore()
113
+
114
+ for widgetId in VoltraWidgetServer.serverDrivenWidgetIds {
115
+ let scope = WidgetScope.of(widgetId)
116
+
117
+ try? propsStore.clearDynamicWidgetProps(for: widgetId)
118
+ statuses.clear(scope)
119
+ WidgetServerEtagStore.clear(scope)
120
+ }
121
+ }
122
+
123
+ /// The engine is chosen at generate time, so a runtime URL cannot turn a locally-driven widget
124
+ /// into a server-driven one. Saying so at call time is much easier to act on than a widget that
125
+ /// quietly never fetches.
126
+ private static func rejectIfNotServerDriven(_ widgetId: String) -> String? {
127
+ guard !VoltraWidgetServer.isServerDriven(widgetId) else { return nil }
128
+
129
+ return "Widget '\(widgetId)' is not server-driven. Add a serverUpdate entry for it in app.json "
130
+ + "and rebuild; a runtime url does not change how a widget is rendered."
131
+ }
132
+
133
+ private static func reloadServerDrivenWidgets(widgetId: String?) {
134
+ guard let widgetId else {
135
+ for id in VoltraWidgetServer.serverDrivenWidgetIds {
136
+ reloadTimeline(for: id)
137
+ }
138
+ return
139
+ }
140
+
141
+ reloadTimeline(for: widgetId)
142
+ }
143
+
144
+ /// Reads settings back rather than reasoning about what was set: with a `widgetId`, the fully
145
+ /// resolved settings that widget would fetch with right now (nil if it is not server-driven);
146
+ /// with none, the raw global layer only, no defaults applied.
147
+ static func getWidgetServerUpdate(widgetId: String?) -> String? {
148
+ if let widgetId {
149
+ let scope = WidgetScope.of(widgetId)
150
+
151
+ guard VoltraWidgetServer.resolver.isServerDriven(scope) else { return nil }
152
+
153
+ let resolved = VoltraWidgetServer.resolver.resolve(scope)
154
+ let settings = WidgetServerUpdateSettings(
155
+ url: resolved.url,
156
+ intervalMinutes: resolved.intervalMinutes,
157
+ enabled: resolved.enabled,
158
+ method: resolved.method,
159
+ query: resolved.query,
160
+ headers: resolved.headers,
161
+ body: resolved.body
162
+ )
163
+
164
+ return WidgetServerUpdateSettingsJson.stringify(settings)
165
+ }
166
+
167
+ guard let global = VoltraWidgetServer.resolver.globalSettings() else { return nil }
168
+
169
+ return WidgetServerUpdateSettingsJson.stringify(global)
170
+ }
171
+
172
+ /// Drops one widget's runtime settings and its fetch history, for `clearWidget`.
173
+ static func clearWidgetServerState(for widgetId: String) {
174
+ guard VoltraWidgetServer.isServerDriven(widgetId) else { return }
175
+
176
+ let scope = WidgetScope.of(widgetId)
177
+
178
+ WidgetServerSettingsStore.clear(scope: scope)
179
+ WidgetServerEtagStore.clear(scope)
180
+ DynamicWidgetServerPropsStore().clear(scope)
181
+ }
182
+
183
+ // MARK: - Server credentials (deprecated)
53
184
 
54
185
  /// Saves widget server credentials to the Keychain and reloads all widget timelines
55
186
  /// so extensions can use them on the next fetch.
187
+ ///
188
+ /// Deprecated in favour of `setWidgetServerUpdate` with an `Authorization` header. Kept as a
189
+ /// wrapper over the same Keychain accounts, so an app that has not migrated keeps working and
190
+ /// nothing has to be moved on device.
56
191
  static func setWidgetServerCredentials(token: String, headers: [String: String]?) {
57
192
  VoltraKeychainHelper.saveToken(token)
58
193
  if let headers = headers {
@@ -60,13 +195,20 @@ enum VoltraWidgetService {
60
195
  } else {
61
196
  VoltraKeychainHelper.deleteHeaders()
62
197
  }
198
+ // The credentials layer does not go through the settings store, but a new token is exactly the
199
+ // thing a widget stuck on a 401 is waiting for, so an in-flight fetch built with the old one
200
+ // must not commit.
201
+ WidgetServerSettingsStore.bumpRevision()
63
202
  VoltraLogger.widget.info("Server credentials saved")
64
203
  reloadAllTimelines()
65
204
  }
66
205
 
67
206
  /// Clears widget server credentials from the Keychain and reloads all widget timelines.
207
+ ///
208
+ /// Deprecated alongside `setWidgetServerCredentials`.
68
209
  static func clearWidgetServerCredentials() {
69
210
  VoltraKeychainHelper.clearAll()
211
+ WidgetServerSettingsStore.bumpRevision()
70
212
  VoltraLogger.widget.info("Server credentials cleared")
71
213
  reloadAllTimelines()
72
214
  }
@@ -80,10 +222,7 @@ enum VoltraWidgetService {
80
222
  switch result {
81
223
  case let .success(widgetInfos):
82
224
  let mapped = widgetInfos.map { widget -> [String: String] in
83
- let prefix = VoltraStorageKeys.widgetKindPrefix
84
- let name = widget.kind.hasPrefix(prefix)
85
- ? String(widget.kind.dropFirst(prefix.count))
86
- : widget.kind
225
+ let name = VoltraWidgetKind.widgetId(for: widget.kind) ?? widget.kind
87
226
 
88
227
  return [
89
228
  "name": name,
@@ -100,17 +239,13 @@ enum VoltraWidgetService {
100
239
  }
101
240
 
102
241
  /// Returns the set of widget IDs currently installed on the device.
103
- /// Only IDs whose kind carries the Voltra prefix are included.
242
+ /// Only Voltra widgets (default `Voltra_Widget_` kind or a pinned `kind`) are included.
104
243
  static func getInstalledWidgetIds() async throws -> Set<String> {
105
244
  try await withCheckedThrowingContinuation { continuation in
106
245
  WidgetCenter.shared.getCurrentConfigurations { result in
107
246
  switch result {
108
247
  case let .success(configs):
109
- let ids = Set(configs.compactMap { config -> String? in
110
- let prefix = VoltraStorageKeys.widgetKindPrefix
111
- guard config.kind.hasPrefix(prefix) else { return nil }
112
- return String(config.kind.dropFirst(prefix.count))
113
- })
248
+ let ids = Set(configs.compactMap { VoltraWidgetKind.widgetId(for: $0.kind) })
114
249
  continuation.resume(returning: ids)
115
250
  case let .failure(error):
116
251
  continuation.resume(throwing: error)
@@ -0,0 +1,97 @@
1
+ import ActivityKit
2
+ import Foundation
3
+
4
+ /// App-process orchestration for the dynamic renderer. The legacy service keeps
5
+ /// only thin cross-engine coordination points and delegates feature-specific
6
+ /// catalog, bundle, payload, and reload behavior here.
7
+ final class VoltraDynamicLiveActivityService {
8
+ private let chronology: VoltraLiveActivityChronology
9
+
10
+ init(chronology: VoltraLiveActivityChronology = .shared) {
11
+ self.chronology = chronology
12
+ }
13
+
14
+ func isActive(name: String) -> Bool {
15
+ activityReferences().contains { $0.name == name }
16
+ }
17
+
18
+ func activityReferences() -> [VoltraDynamicLiveActivityReference] {
19
+ VoltraDynamicLiveActivityRegistry.shared.activities()
20
+ }
21
+
22
+ func definitionIds() -> [String] {
23
+ VoltraDynamicLiveActivityRegistry.shared.definitionIds()
24
+ }
25
+
26
+ func create(_ request: VoltraDynamicLiveActivityCreateRequest) async throws -> String {
27
+ guard VoltraDynamicLiveActivityRegistry.shared.contains(request.definitionId) else {
28
+ throw VoltraDynamicLiveActivityError.unknownDefinition(request.definitionId)
29
+ }
30
+ do {
31
+ let source = try await VoltraDynamicLiveActivityBundleSource.loadForApp(definitionId: request.definitionId)
32
+ guard VoltraJSRenderer.evaluateLiveActivityBundle(source: source, definitionId: request.definitionId) else {
33
+ throw VoltraDynamicLiveActivityError.resourceUnavailable(
34
+ NSError(
35
+ domain: "VoltraDynamicLiveActivity",
36
+ code: -1,
37
+ userInfo: [NSLocalizedDescriptionKey: "Dynamic Live Activity bundle could not be evaluated."]
38
+ )
39
+ )
40
+ }
41
+ } catch let error as VoltraDynamicLiveActivityError {
42
+ throw error
43
+ } catch {
44
+ throw VoltraDynamicLiveActivityError.resourceUnavailable(error)
45
+ }
46
+ try VoltraDynamicLiveActivityPayloadValidator.validate(
47
+ name: request.name,
48
+ deepLinkUrl: request.deepLinkUrl,
49
+ props: request.props
50
+ )
51
+ guard let activity = try await VoltraDynamicLiveActivityRegistry.shared.create(request) else {
52
+ throw VoltraDynamicLiveActivityError.unknownDefinition(request.definitionId)
53
+ }
54
+ chronology.record(activity.id)
55
+ return request.name
56
+ }
57
+
58
+ func update(byName name: String, request: VoltraDynamicLiveActivityUpdateRequest) async throws -> Bool {
59
+ try VoltraDynamicLiveActivityPayloadValidator.validateContentState(request.props)
60
+ return await VoltraDynamicLiveActivityRegistry.shared.update(byName: name, request: request)
61
+ }
62
+
63
+ func end(byName name: String, dismissalPolicy: ActivityUIDismissalPolicy) async -> Bool {
64
+ await VoltraDynamicLiveActivityRegistry.shared.end(byName: name, dismissalPolicy: dismissalPolicy)
65
+ }
66
+
67
+ func endAll(dismissalPolicy: ActivityUIDismissalPolicy) async {
68
+ await VoltraDynamicLiveActivityRegistry.shared.endAll(dismissalPolicy: dismissalPolicy)
69
+ }
70
+
71
+ func reload(definitionIds: [String]?) async {
72
+ #if DEBUG
73
+ let requested = definitionIds.map(Set.init)
74
+ let ids = requested ?? Set(self.definitionIds())
75
+ var refreshed = Set<String>()
76
+ for definitionId in ids.sorted() {
77
+ guard VoltraDynamicLiveActivityRegistry.shared.contains(definitionId) else { continue }
78
+ do {
79
+ let source = try await VoltraDynamicLiveActivityBundleSource.loadForApp(definitionId: definitionId)
80
+ guard VoltraJSRenderer.evaluateLiveActivityBundle(source: source, definitionId: definitionId) else {
81
+ throw VoltraDynamicLiveActivityError.resourceUnavailable(
82
+ NSError(
83
+ domain: "VoltraDynamicLiveActivity",
84
+ code: -2,
85
+ userInfo: [NSLocalizedDescriptionKey: "Dynamic Live Activity bundle could not be evaluated."]
86
+ )
87
+ )
88
+ }
89
+ refreshed.insert(definitionId)
90
+ } catch {
91
+ VoltraLogger.activity.error("Failed to refresh Dynamic Live Activity definition '\(definitionId)': \(error)")
92
+ }
93
+ }
94
+ await VoltraDynamicLiveActivityRegistry.shared.reload(definitionIds: refreshed)
95
+ #endif
96
+ }
97
+ }
@@ -0,0 +1,68 @@
1
+ import Foundation
2
+
3
+ /// Persists creation-time observations made by this app process. ActivityKit
4
+ /// does not expose a cross-attributes-type creation timestamp, so an instance
5
+ /// first encountered after a process restart must never be assigned an order
6
+ /// merely because a catalog happened to enumerate it first.
7
+ final class VoltraLiveActivityChronology: @unchecked Sendable {
8
+ static let shared = VoltraLiveActivityChronology()
9
+
10
+ private static let storageKey = "Voltra_LiveActivityChronology"
11
+
12
+ private let lock = NSLock()
13
+ private let defaults: UserDefaults
14
+ private var timestamps: [String: TimeInterval]
15
+
16
+ init(defaults: UserDefaults = .standard) {
17
+ self.defaults = defaults
18
+ if let stored = defaults.dictionary(forKey: Self.storageKey) as? [String: TimeInterval] {
19
+ timestamps = stored
20
+ } else {
21
+ // Migrate the previous observation-order ledger without reordering it.
22
+ let legacy = defaults.stringArray(forKey: Self.storageKey) ?? []
23
+ timestamps = Dictionary(uniqueKeysWithValues: legacy.enumerated().map { index, id in (id, Double(index)) })
24
+ }
25
+ }
26
+
27
+ func record(_ activityId: String) {
28
+ lock.lock()
29
+ defer { lock.unlock() }
30
+ guard timestamps[activityId] == nil else { return }
31
+ timestamps[activityId] = Date().timeIntervalSince1970
32
+ persist()
33
+ }
34
+
35
+ func order(
36
+ _ references: [VoltraDynamicLiveActivityReference]
37
+ ) -> [VoltraDynamicLiveActivityReference] {
38
+ lock.lock()
39
+ defer { lock.unlock() }
40
+
41
+ let activeIds = Set(references.map(\.id))
42
+ let nextTimestamps = timestamps.filter { activeIds.contains($0.key) }
43
+ if nextTimestamps != timestamps {
44
+ timestamps = nextTimestamps
45
+ persist()
46
+ }
47
+ return references.sorted { lhs, rhs in
48
+ switch (timestamps[lhs.id], timestamps[rhs.id]) {
49
+ case let (left?, right?): return left < right
50
+ case (_?, nil): return true
51
+ case (nil, _?): return false
52
+ case (nil, nil): return lhs.id < rhs.id
53
+ }
54
+ }
55
+ }
56
+
57
+ func latest(_ references: [VoltraDynamicLiveActivityReference]) -> VoltraDynamicLiveActivityReference? {
58
+ lock.lock()
59
+ defer { lock.unlock() }
60
+ return references.compactMap { reference in
61
+ timestamps[reference.id].map { (reference, $0) }
62
+ }.max { $0.1 < $1.1 }?.0
63
+ }
64
+
65
+ private func persist() {
66
+ defaults.set(timestamps, forKey: Self.storageKey)
67
+ }
68
+ }
@@ -20,28 +20,95 @@ struct DynamicWidgetPropsStore {
20
20
  self.storage = .success(storage)
21
21
  }
22
22
 
23
+ /// The type-level (widget-scoped) write path — the app's `updateDynamicWidget`.
23
24
  func persistDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for dynamicWidgetID: String) throws {
25
+ try persist(dynamicWidgetPropsJSON, storageKey: Self.storageKey(for: dynamicWidgetID))
26
+ }
27
+
28
+ /// The instance write path — a committed per-instance server fetch (ADR 0007).
29
+ func persistInstanceDynamicWidgetProps(_ dynamicWidgetPropsJSON: String, for scope: WidgetScope) throws {
30
+ guard case let .instance(widgetId, key) = scope else {
31
+ try persistDynamicWidgetProps(dynamicWidgetPropsJSON, for: scope.widgetId)
32
+ return
33
+ }
34
+ try persist(dynamicWidgetPropsJSON, storageKey: VoltraStorageKeys.dynamicWidgetInstancePropsV1(scope.storageKey))
35
+ try addInstanceKeyToIndex(key, for: widgetId)
36
+ }
37
+
38
+ private func persist(_ dynamicWidgetPropsJSON: String, storageKey: String) throws {
24
39
  let storageEntryJSON = try DynamicWidgetPropsCodec.encodeStorageEntry(
25
40
  dynamicWidgetPropsJSON: dynamicWidgetPropsJSON
26
41
  )
27
42
  let resolvedStorage = try storage.get()
28
- try resolvedStorage.set(storageEntryJSON, forKey: Self.storageKey(for: dynamicWidgetID))
43
+ try resolvedStorage.set(storageEntryJSON, forKey: storageKey)
29
44
  }
30
45
 
46
+ /// The type-level props only, with no instance fallback — used by the trial render.
31
47
  func dynamicWidgetProps(for dynamicWidgetID: String) -> String {
32
- guard
33
- case let .success(resolvedStorage) = storage,
34
- let storageEntryJSON = try? resolvedStorage.string(forKey: Self.storageKey(for: dynamicWidgetID))
35
- else {
36
- return DynamicWidgetPropsCodec.emptyDynamicWidgetPropsJSON
48
+ read(storageKey: Self.storageKey(for: dynamicWidgetID)) ?? DynamicWidgetPropsCodec.emptyDynamicWidgetPropsJSON
49
+ }
50
+
51
+ /// Props for one placement: the instance slot for `scope`'s key, falling back to the widget slot
52
+ /// when the instance has not fetched yet or has no configuration (ADR 0007).
53
+ func dynamicWidgetProps(for scope: WidgetScope) -> String {
54
+ switch scope {
55
+ case let .widget(id):
56
+ return dynamicWidgetProps(for: id)
57
+ case .instance:
58
+ if let instanceProps = read(storageKey: VoltraStorageKeys.dynamicWidgetInstancePropsV1(scope.storageKey)) {
59
+ return instanceProps
60
+ }
61
+ return dynamicWidgetProps(for: scope.widgetId)
62
+ }
63
+ }
64
+
65
+ private func read(storageKey: String) -> String? {
66
+ guard case let .success(resolvedStorage) = storage else { return nil }
67
+ guard let storageEntryJSON = (try? resolvedStorage.string(forKey: storageKey)) ?? nil else {
68
+ return nil
37
69
  }
38
70
 
39
71
  return DynamicWidgetPropsCodec.decodeDynamicWidgetProps(storageEntryJSON: storageEntryJSON)
40
72
  }
41
73
 
74
+ /// Clears the widget-level props and every instance slot of `dynamicWidgetID` (ADR 0007):
75
+ /// `clearWidget` and logout are meant to reset a widget entirely, not leave stale per-instance
76
+ /// data behind that a fallback would never reveal again.
42
77
  func clearDynamicWidgetProps(for dynamicWidgetID: String) throws {
43
78
  let resolvedStorage = try storage.get()
79
+ let instanceKeys = instanceKeys(for: dynamicWidgetID)
80
+
44
81
  try resolvedStorage.removeObject(forKey: Self.storageKey(for: dynamicWidgetID))
82
+ try resolvedStorage.removeObject(forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID))
83
+ for key in instanceKeys {
84
+ let instanceStorageKey = VoltraStorageKeys.dynamicWidgetInstancePropsV1("\(dynamicWidgetID)#\(key)")
85
+ try resolvedStorage.removeObject(forKey: instanceStorageKey)
86
+ }
87
+ }
88
+
89
+ /// The instance keys `dynamicWidgetID` has ever fetched into, so callers can clear per-instance
90
+ /// state that lives in other stores (status, ETag).
91
+ func instanceKeys(for dynamicWidgetID: String) -> Set<String> {
92
+ guard
93
+ case let .success(resolvedStorage) = storage,
94
+ let raw = try? resolvedStorage.string(forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID)) ?? nil,
95
+ let data = raw.data(using: .utf8),
96
+ let keys = try? JSONDecoder().decode([String].self, from: data)
97
+ else {
98
+ return []
99
+ }
100
+ return Set(keys)
101
+ }
102
+
103
+ private func addInstanceKeyToIndex(_ key: String, for dynamicWidgetID: String) throws {
104
+ var current = instanceKeys(for: dynamicWidgetID)
105
+ guard !current.contains(key) else { return }
106
+ current.insert(key)
107
+
108
+ let resolvedStorage = try storage.get()
109
+ let data = try JSONEncoder().encode(current.sorted())
110
+ guard let json = String(data: data, encoding: .utf8) else { return }
111
+ try resolvedStorage.set(json, forKey: VoltraStorageKeys.dynamicWidgetPropsInstanceIndexV1(dynamicWidgetID))
45
112
  }
46
113
 
47
114
  static func storageKey(for dynamicWidgetID: String) -> String {