@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
@@ -0,0 +1,73 @@
1
+ import Foundation
2
+
3
+ /// Call-time rules for `setWidgetServerUpdate`. Rejecting here rather than at fetch time means the
4
+ /// app learns about a bad setting from the promise it just awaited, instead of from a widget that
5
+ /// quietly stops updating hours later.
6
+ public enum WidgetServerSettingsValidator {
7
+ /// Hosts reachable over plain http, so a debug build can talk to a dev server.
8
+ static let localHttpHosts: Set<String> = ["localhost", "127.0.0.1", "::1", "10.0.2.2", "10.0.3.2"]
9
+
10
+ /// - Parameter isDebugBuild: whether plain http to a local dev host is allowed. Release builds
11
+ /// have App Transport Security blocking it anyway, so allowing it there would only defer the
12
+ /// failure.
13
+ /// - Returns: an error message, or nil when the settings are usable.
14
+ public static func validate(_ settings: WidgetServerUpdateSettings, isDebugBuild: Bool) -> String? {
15
+ if let url = settings.url, let error = validateUrl(url, isDebugBuild: isDebugBuild) {
16
+ return error
17
+ }
18
+
19
+ if let interval = settings.intervalMinutes, interval <= 0 {
20
+ return "intervalMinutes must be a positive number of minutes"
21
+ }
22
+
23
+ if let method = settings.method,
24
+ !WidgetServerUpdateDefaults.supportedMethods.contains(method.uppercased())
25
+ {
26
+ let supported = WidgetServerUpdateDefaults.supportedMethods.sorted().joined(separator: ", ")
27
+ return "method '\(method)' is not supported. Use one of \(supported)"
28
+ }
29
+
30
+ for key in settings.query?.keys ?? [String: String]().keys
31
+ where WidgetServerUpdateDefaults.reservedQueryKeys.contains(key)
32
+ {
33
+ return "query key '\(key)' is reserved by Voltra and is sent on every request"
34
+ }
35
+
36
+ if let encoded = WidgetServerSettingsCodec.encode(settings),
37
+ encoded.count > WidgetServerUpdateDefaults.maxLayerBytes
38
+ {
39
+ return "settings are larger than \(WidgetServerUpdateDefaults.maxLayerBytes) bytes once serialized"
40
+ }
41
+
42
+ return nil
43
+ }
44
+
45
+ private static func validateUrl(_ url: String, isDebugBuild: Bool) -> String? {
46
+ if url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
47
+ return "url must not be empty"
48
+ }
49
+
50
+ guard let components = URLComponents(string: url),
51
+ let scheme = components.scheme?.lowercased(),
52
+ let host = components.host,
53
+ !host.isEmpty
54
+ else {
55
+ return "url '\(url)' must be an absolute http(s) URL"
56
+ }
57
+
58
+ if scheme == "https" {
59
+ return nil
60
+ }
61
+
62
+ if scheme != "http" {
63
+ return "url '\(url)' must be an absolute http(s) URL"
64
+ }
65
+
66
+ if isDebugBuild, localHttpHosts.contains(host) {
67
+ return nil
68
+ }
69
+
70
+ let hosts = localHttpHosts.sorted().joined(separator: ", ")
71
+ return "url '\(url)' must use https. Plain http is allowed only in a debug build, and only for \(hosts)."
72
+ }
73
+ }
@@ -0,0 +1,123 @@
1
+ import Foundation
2
+
3
+ /// Server-update settings as one layer holds them: every field is optional, and an unset field
4
+ /// means "this layer has no opinion, ask the layer below".
5
+ ///
6
+ /// `body` is kept as the raw JSON text the app supplied rather than a parsed tree, because Voltra
7
+ /// never inspects it — it only forwards it as the request body.
8
+ public struct WidgetServerUpdateSettings: Equatable, Sendable {
9
+ public var url: String?
10
+ public var intervalMinutes: Int?
11
+ public var enabled: Bool?
12
+ public var method: String?
13
+ public var query: [String: String]?
14
+ public var headers: [String: String]?
15
+ public var body: String?
16
+
17
+ public init(
18
+ url: String? = nil,
19
+ intervalMinutes: Int? = nil,
20
+ enabled: Bool? = nil,
21
+ method: String? = nil,
22
+ query: [String: String]? = nil,
23
+ headers: [String: String]? = nil,
24
+ body: String? = nil
25
+ ) {
26
+ self.url = url
27
+ self.intervalMinutes = intervalMinutes
28
+ self.enabled = enabled
29
+ self.method = method
30
+ self.query = query
31
+ self.headers = headers
32
+ self.body = body
33
+ }
34
+
35
+ public var isEmpty: Bool {
36
+ url == nil && intervalMinutes == nil && enabled == nil && method == nil
37
+ && query == nil && headers == nil && body == nil
38
+ }
39
+
40
+ public static let empty = WidgetServerUpdateSettings()
41
+ }
42
+
43
+ /// The flattened settings a fetch actually runs on. Every field is decided: `intervalMinutes` has
44
+ /// the floor and ceiling applied, `enabled` and `method` have their defaults filled in, and `query`
45
+ /// and `headers` are the per-key merge of every layer.
46
+ ///
47
+ /// `url` is the one field that can still be absent, and it means the widget is server-driven but
48
+ /// has nowhere to fetch from yet — the app is expected to supply one with `setWidgetServerUpdate`.
49
+ public struct ResolvedWidgetServerSettings: Equatable, Sendable {
50
+ public let url: String?
51
+ public let intervalMinutes: Int
52
+ public let enabled: Bool
53
+ public let method: String
54
+ public let query: [String: String]
55
+ public let headers: [String: String]
56
+ public let body: String?
57
+
58
+ public init(
59
+ url: String?,
60
+ intervalMinutes: Int,
61
+ enabled: Bool,
62
+ method: String,
63
+ query: [String: String],
64
+ headers: [String: String],
65
+ body: String?
66
+ ) {
67
+ self.url = url
68
+ self.intervalMinutes = intervalMinutes
69
+ self.enabled = enabled
70
+ self.method = method
71
+ self.query = query
72
+ self.headers = headers
73
+ self.body = body
74
+ }
75
+
76
+ /// True when this widget has both a URL to fetch and permission to do it.
77
+ public var shouldFetch: Bool {
78
+ guard enabled, let url, !url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
79
+ return false
80
+ }
81
+
82
+ return true
83
+ }
84
+ }
85
+
86
+ public enum WidgetServerUpdateDefaults {
87
+ /// WidgetKit stretches a timeline asking for entries much closer together than five minutes, and
88
+ /// the reload budget is shared across every widget in the app, so a smaller number would only
89
+ /// misreport what the widget actually does. Matches Android's WorkManager floor.
90
+ public static let minIntervalMinutes = 15
91
+
92
+ /// A day. Past this the widget is effectively not server-driven, and `Cache-Control: max-age`
93
+ /// from a misconfigured server should not be able to park a widget for a week.
94
+ public static let maxIntervalMinutes = 24 * 60
95
+
96
+ public static let defaultIntervalMinutes = minIntervalMinutes
97
+
98
+ public static let defaultMethod = "GET"
99
+
100
+ /// Methods either platform's HTTP stack can send.
101
+ public static let supportedMethods: Set<String> = ["GET", "POST", "PUT", "PATCH", "DELETE"]
102
+
103
+ /// Methods that cannot carry a body. A body set alongside one of these is dropped.
104
+ public static let bodylessMethods: Set<String> = ["GET", "HEAD"]
105
+
106
+ /// Query keys Voltra puts on every request. An app that set one of these would silently shadow
107
+ /// what the server relies on, so `setWidgetServerUpdate` rejects them.
108
+ public static let reservedQueryKeys: Set<String> = [
109
+ "widgetId", "platform", "family", "theme", "locale", "instance", "configuration",
110
+ ]
111
+
112
+ /// Serialized size cap for one layer, so a runaway `body` cannot fill the settings store.
113
+ public static let maxLayerBytes = 16 * 1024
114
+
115
+ /// Response bodies larger than this are refused. The widget extension has a 30 MB ceiling for
116
+ /// the whole render, and a widget needing a quarter of a megabyte of props will not fit on a
117
+ /// home screen either.
118
+ public static let maxBodyBytes = 256 * 1024
119
+
120
+ public static func clampIntervalMinutes(_ intervalMinutes: Int) -> Int {
121
+ min(max(intervalMinutes, minIntervalMinutes), maxIntervalMinutes)
122
+ }
123
+ }
@@ -0,0 +1,115 @@
1
+ import Foundation
2
+
3
+ /// Reads the settings object an app passes to `setWidgetServerUpdate`.
4
+ ///
5
+ /// Separate from `WidgetServerSettingsCodec`, which is the versioned storage format: what the app
6
+ /// sends and what Voltra persists are allowed to diverge, and conflating them would make either one
7
+ /// hard to change. The one real difference today is `body`, which arrives as arbitrary JSON and is
8
+ /// kept as text because Voltra only forwards it.
9
+ public enum WidgetServerUpdateSettingsJson {
10
+ public enum Result: Equatable {
11
+ case parsed(WidgetServerUpdateSettings)
12
+ case invalid(reason: String)
13
+ }
14
+
15
+ public static func parse(_ json: String) -> Result {
16
+ guard let object = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any] else {
17
+ return .invalid(reason: "settings must be a JSON object")
18
+ }
19
+
20
+ guard let query = stringMap(object, "query") else {
21
+ return .invalid(reason: "query must be an object of strings")
22
+ }
23
+
24
+ guard let headers = stringMap(object, "headers") else {
25
+ return .invalid(reason: "headers must be an object of strings")
26
+ }
27
+
28
+ var body: String?
29
+
30
+ if let rawBody = object["body"], !(rawBody is NSNull) {
31
+ guard let text = jsonText(rawBody) else {
32
+ return .invalid(reason: "body must be a JSON value")
33
+ }
34
+
35
+ body = text
36
+ }
37
+
38
+ return .parsed(
39
+ WidgetServerUpdateSettings(
40
+ url: object["url"] as? String,
41
+ intervalMinutes: object["intervalMinutes"] as? Int,
42
+ enabled: object["enabled"] as? Bool,
43
+ method: (object["method"] as? String)?.uppercased(),
44
+ query: object["query"] == nil ? nil : query,
45
+ headers: object["headers"] == nil ? nil : headers,
46
+ body: body
47
+ )
48
+ )
49
+ }
50
+
51
+ /// The other direction of `parse`: serializes settings back to the same JSON shape, for
52
+ /// `getWidgetServerUpdate` to hand across the bridge. No envelope — that is
53
+ /// `WidgetServerSettingsCodec`'s job for storage, not this one's for a single read.
54
+ public static func stringify(_ settings: WidgetServerUpdateSettings) -> String? {
55
+ var object: [String: Any] = [:]
56
+
57
+ if let url = settings.url {
58
+ object["url"] = url
59
+ }
60
+ if let intervalMinutes = settings.intervalMinutes {
61
+ object["intervalMinutes"] = intervalMinutes
62
+ }
63
+ if let enabled = settings.enabled {
64
+ object["enabled"] = enabled
65
+ }
66
+ if let method = settings.method {
67
+ object["method"] = method
68
+ }
69
+ if let query = settings.query {
70
+ object["query"] = query
71
+ }
72
+ if let headers = settings.headers {
73
+ object["headers"] = headers
74
+ }
75
+
76
+ if let body = settings.body {
77
+ guard let bodyData = body.data(using: .utf8),
78
+ let bodyValue = try? JSONSerialization.jsonObject(with: bodyData, options: [.fragmentsAllowed])
79
+ else {
80
+ return nil
81
+ }
82
+
83
+ object["body"] = bodyValue
84
+ }
85
+
86
+ guard let data = try? JSONSerialization.data(withJSONObject: object) else { return nil }
87
+
88
+ return String(data: data, encoding: .utf8)
89
+ }
90
+
91
+ /// Re-serializes a parsed value back to JSON text. A string body has to keep its quotes: without
92
+ /// them the request would carry something that is not JSON at all.
93
+ private static func jsonText(_ value: Any) -> String? {
94
+ if JSONSerialization.isValidJSONObject(value) {
95
+ return (try? JSONSerialization.data(withJSONObject: value)).flatMap { String(data: $0, encoding: .utf8) }
96
+ }
97
+
98
+ // A top-level scalar is not a valid JSON *object*, so it is wrapped, encoded and unwrapped.
99
+ guard let wrapped = try? JSONSerialization.data(withJSONObject: ["v": value]),
100
+ let text = String(data: wrapped, encoding: .utf8),
101
+ let start = text.firstIndex(of: ":")
102
+ else {
103
+ return nil
104
+ }
105
+
106
+ return String(text[text.index(after: start) ..< text.index(before: text.endIndex)])
107
+ }
108
+
109
+ /// Returns an empty map when the key is absent, and nil when it is present but not usable.
110
+ private static func stringMap(_ object: [String: Any], _ key: String) -> [String: String]? {
111
+ guard let raw = object[key], !(raw is NSNull) else { return [:] }
112
+
113
+ return raw as? [String: String]
114
+ }
115
+ }
@@ -0,0 +1,13 @@
1
+ import Foundation
2
+
3
+ enum DynamicLiveActivityPropsCodec {
4
+ static func encode(_ props: [String: VoltraDynamicLiveActivityJSONValue]) -> String? {
5
+ guard
6
+ let data = try? JSONEncoder().encode(props),
7
+ let json = String(data: data, encoding: .utf8)
8
+ else {
9
+ return nil
10
+ }
11
+ return json
12
+ }
13
+ }
@@ -0,0 +1,156 @@
1
+ import Foundation
2
+
3
+ enum VoltraDynamicLiveActivityBundleSource {
4
+ private static let metroTimeout: TimeInterval = 1
5
+
6
+ enum LoadError: LocalizedError {
7
+ case metroHTTP(Int)
8
+ case metroTimeout
9
+ case nonUTF8
10
+ case bakedBundleNotFound(definitionId: String)
11
+ case cacheUnavailable
12
+
13
+ var errorDescription: String? {
14
+ switch self {
15
+ case let .metroHTTP(status):
16
+ return "Metro HTTP \(status) while loading a Dynamic Live Activity bundle"
17
+ case .metroTimeout:
18
+ return "Metro timed out while loading a Dynamic Live Activity bundle"
19
+ case .nonUTF8:
20
+ return "Dynamic Live Activity bundle was not UTF-8 text"
21
+ case let .bakedBundleNotFound(definitionId):
22
+ return "Production Dynamic Live Activity bundle missing for definitionId=\(definitionId)"
23
+ case .cacheUnavailable:
24
+ return "Dynamic Live Activity debug cache is unavailable"
25
+ }
26
+ }
27
+ }
28
+
29
+ static func load(definitionId: String) throws -> String {
30
+ #if DEBUG
31
+ if let cached = try? loadFromDebugCache(definitionId: definitionId) {
32
+ return cached
33
+ }
34
+ return try loadFromMetroBounded(definitionId: definitionId)
35
+ #else
36
+ return try loadFromBakedAsset(definitionId: definitionId)
37
+ #endif
38
+ }
39
+
40
+ /// App-side preflight and Fast Refresh use an asynchronous fetch, then publish
41
+ /// the source into the App Group cache consumed by the extension process.
42
+ static func loadForApp(definitionId: String) async throws -> String {
43
+ #if DEBUG
44
+ let request = try metroRequest(definitionId: definitionId)
45
+ let (data, response) = try await URLSession.shared.data(for: request)
46
+ let source = try validateMetroResponse(data: data, response: response)
47
+ try saveToDebugCache(source, definitionId: definitionId)
48
+ return source
49
+ #else
50
+ return try loadFromBakedAsset(definitionId: definitionId)
51
+ #endif
52
+ }
53
+
54
+ /// A cheap token that changes whenever the bytes behind `load` change, used to
55
+ /// invalidate the render cache. In DEBUG it is the debug-cache file's
56
+ /// modification time, which the app process rewrites on every hot reload; the
57
+ /// extension reads only the file's metadata, never decoding the whole bundle.
58
+ /// In release the baked asset never changes at runtime, so the token is
59
+ /// constant and the render cache stays valid for the process lifetime.
60
+ static func revision(definitionId: String) -> String {
61
+ #if DEBUG
62
+ guard
63
+ let url = try? debugCacheURL(definitionId: definitionId),
64
+ let modified = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date
65
+ else {
66
+ return ""
67
+ }
68
+ return String(modified.timeIntervalSince1970)
69
+ #else
70
+ return "baked"
71
+ #endif
72
+ }
73
+
74
+ private static func metroRequest(definitionId: String) throws -> URLRequest {
75
+ let base = VoltraWidgetDefaults.devServerURL() ?? "http://localhost:8081"
76
+ guard let url = URL(string: "\(base)/voltra/live-activities/\(definitionId).bundle?platform=ios&dev=true") else {
77
+ throw LoadError.metroHTTP(-1)
78
+ }
79
+ var request = URLRequest(url: url)
80
+ request.timeoutInterval = metroTimeout
81
+ return request
82
+ }
83
+
84
+ /// WidgetKit rendering is synchronous. On a cold debug process we permit one
85
+ /// short, bounded bootstrap request; all subsequent renders read the App Group
86
+ /// cache and never wait on the network.
87
+ private static func loadFromMetroBounded(definitionId: String) throws -> String {
88
+ let request = try metroRequest(definitionId: definitionId)
89
+ let semaphore = DispatchSemaphore(value: 0)
90
+ var result: Result<(Data, URLResponse), Error>?
91
+ let configuration = URLSessionConfiguration.ephemeral
92
+ configuration.timeoutIntervalForRequest = metroTimeout
93
+ configuration.timeoutIntervalForResource = metroTimeout
94
+ let session = URLSession(configuration: configuration)
95
+ let task = session.dataTask(with: request) { data, response, error in
96
+ if let error {
97
+ result = .failure(error)
98
+ } else if let data, let response {
99
+ result = .success((data, response))
100
+ } else {
101
+ result = .failure(LoadError.metroHTTP(-1))
102
+ }
103
+ semaphore.signal()
104
+ }
105
+ task.resume()
106
+ guard semaphore.wait(timeout: .now() + metroTimeout) == .success else {
107
+ task.cancel()
108
+ session.invalidateAndCancel()
109
+ throw LoadError.metroTimeout
110
+ }
111
+ session.finishTasksAndInvalidate()
112
+ guard let result else { throw LoadError.metroTimeout }
113
+ let (data, response) = try result.get()
114
+ let source = try validateMetroResponse(data: data, response: response)
115
+ try saveToDebugCache(source, definitionId: definitionId)
116
+ return source
117
+ }
118
+
119
+ private static func validateMetroResponse(data: Data, response: URLResponse) throws -> String {
120
+ if let httpResponse = response as? HTTPURLResponse, !(200 ... 299).contains(httpResponse.statusCode) {
121
+ throw LoadError.metroHTTP(httpResponse.statusCode)
122
+ }
123
+ guard let source = String(data: data, encoding: .utf8) else {
124
+ throw LoadError.nonUTF8
125
+ }
126
+ return source
127
+ }
128
+
129
+ private static func loadFromDebugCache(definitionId: String) throws -> String {
130
+ try String(contentsOf: debugCacheURL(definitionId: definitionId), encoding: .utf8)
131
+ }
132
+
133
+ private static func saveToDebugCache(_ source: String, definitionId: String) throws {
134
+ try Data(source.utf8).write(to: debugCacheURL(definitionId: definitionId), options: .atomic)
135
+ }
136
+
137
+ private static func debugCacheURL(definitionId: String) throws -> URL {
138
+ guard
139
+ let group = VoltraConfig.groupIdentifier(),
140
+ let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)
141
+ else {
142
+ throw LoadError.cacheUnavailable
143
+ }
144
+ return container.appendingPathComponent("voltra-live-activity-debug-\(definitionId).bundle")
145
+ }
146
+
147
+ private static func loadFromBakedAsset(definitionId: String) throws -> String {
148
+ guard
149
+ let url = Bundle.main.url(forResource: "voltra-live-activity-\(definitionId)", withExtension: "bundle"),
150
+ let source = try? String(contentsOf: url, encoding: .utf8)
151
+ else {
152
+ throw LoadError.bakedBundleNotFound(definitionId: definitionId)
153
+ }
154
+ return source
155
+ }
156
+ }
@@ -0,0 +1,97 @@
1
+ import ActivityKit
2
+ import Foundation
3
+
4
+ /// Observes every concrete generated Dynamic Live Activity type. ActivityKit's
5
+ /// activity streams are type-specific, so the generated catalog invokes this
6
+ /// observer once for each bundled attributes type.
7
+ public actor VoltraDynamicLiveActivityObserver {
8
+ private let onActivityDiscovered: (@Sendable (String) -> Void)?
9
+ private let onTokenUpdated: (@Sendable (String, String) -> Void)?
10
+ private let onStateChanged: (@Sendable (String, String) -> Void)?
11
+
12
+ private var definitionTasks: [String: Task<Void, Never>] = [:]
13
+ private var tokenTasks: [String: Task<Void, Never>] = [:]
14
+ private var stateTasks: [String: Task<Void, Never>] = [:]
15
+
16
+ public init(
17
+ onActivityDiscovered: (@Sendable (String) -> Void)? = nil,
18
+ onTokenUpdated: (@Sendable (String, String) -> Void)? = nil,
19
+ onStateChanged: (@Sendable (String, String) -> Void)? = nil
20
+ ) {
21
+ self.onActivityDiscovered = onActivityDiscovered
22
+ self.onTokenUpdated = onTokenUpdated
23
+ self.onStateChanged = onStateChanged
24
+ }
25
+
26
+ public func observe<Attributes: VoltraDynamicLiveActivityDefinition>(_: Attributes.Type) {
27
+ let definitionId = Attributes.definitionId
28
+ guard definitionTasks[definitionId] == nil else { return }
29
+
30
+ definitionTasks[definitionId] = Task { [weak self] in
31
+ for activity in Activity<Attributes>.activities {
32
+ await self?.observe(activity, definitionId: definitionId)
33
+ }
34
+ for await activity in Activity<Attributes>.activityUpdates {
35
+ await self?.observe(activity, definitionId: definitionId)
36
+ }
37
+ await self?.removeDefinitionTask(for: definitionId)
38
+ }
39
+ }
40
+
41
+ public func stopObserving() {
42
+ for task in definitionTasks.values {
43
+ task.cancel()
44
+ }
45
+ for task in tokenTasks.values {
46
+ task.cancel()
47
+ }
48
+ for task in stateTasks.values {
49
+ task.cancel()
50
+ }
51
+ definitionTasks.removeAll()
52
+ tokenTasks.removeAll()
53
+ stateTasks.removeAll()
54
+ }
55
+
56
+ private func observe<Attributes: VoltraDynamicLiveActivityDefinition>(
57
+ _ activity: Activity<Attributes>,
58
+ definitionId: String
59
+ ) {
60
+ let key = "\(definitionId):\(activity.id)"
61
+ let name = activity.attributes.name
62
+ onActivityDiscovered?(activity.id)
63
+
64
+ if let onTokenUpdated, tokenTasks[key] == nil {
65
+ tokenTasks[key] = Task { [weak self] in
66
+ for await token in activity.pushTokenUpdates {
67
+ onTokenUpdated(name, token.hexString)
68
+ }
69
+ await self?.removeTokenTask(for: key)
70
+ }
71
+ }
72
+
73
+ if let onStateChanged, stateTasks[key] == nil {
74
+ stateTasks[key] = Task { [weak self] in
75
+ for await state in activity.activityStateUpdates {
76
+ onStateChanged(name, String(describing: state))
77
+ }
78
+ await self?.removeStateTask(for: key)
79
+ }
80
+ }
81
+ }
82
+
83
+ private func removeDefinitionTask(for definitionId: String) {
84
+ definitionTasks[definitionId]?.cancel()
85
+ definitionTasks.removeValue(forKey: definitionId)
86
+ }
87
+
88
+ private func removeTokenTask(for key: String) {
89
+ tokenTasks[key]?.cancel()
90
+ tokenTasks.removeValue(forKey: key)
91
+ }
92
+
93
+ private func removeStateTask(for key: String) {
94
+ stateTasks[key]?.cancel()
95
+ stateTasks.removeValue(forKey: key)
96
+ }
97
+ }
@@ -0,0 +1,105 @@
1
+ import ActivityKit
2
+ import Foundation
3
+
4
+ public struct VoltraDynamicLiveActivityCreateRequest {
5
+ public let definitionId: String
6
+ public let name: String
7
+ public let deepLinkUrl: String?
8
+ public let props: [String: VoltraDynamicLiveActivityJSONValue]
9
+ public let staleDate: Date?
10
+ public let relevanceScore: Double
11
+ public let pushType: PushType?
12
+ }
13
+
14
+ public struct VoltraDynamicLiveActivityUpdateRequest {
15
+ public let props: [String: VoltraDynamicLiveActivityJSONValue]
16
+ public let staleDate: Date?
17
+ public let relevanceScore: Double
18
+ }
19
+
20
+ public struct VoltraDynamicLiveActivityReference: Hashable {
21
+ public let id: String
22
+ public let name: String
23
+ public let definitionId: String
24
+ }
25
+
26
+ /// Generic ActivityKit operations called by the generated catalog. The catalog
27
+ /// supplies the concrete generated Attributes type without exposing it to the
28
+ /// legacy service or the React Native bridge.
29
+ public enum VoltraDynamicLiveActivityOperations {
30
+ public static func create<Attributes: VoltraDynamicLiveActivityDefinition>(
31
+ _: Attributes.Type,
32
+ request: VoltraDynamicLiveActivityCreateRequest
33
+ ) async throws -> VoltraDynamicLiveActivityReference {
34
+ let attributes = Attributes(name: request.name, deepLinkUrl: request.deepLinkUrl)
35
+ let state = VoltraDynamicLiveActivityContentState(props: request.props)
36
+ let activity = try Activity.request(
37
+ attributes: attributes,
38
+ content: ActivityContent(state: state, staleDate: request.staleDate, relevanceScore: request.relevanceScore),
39
+ pushType: request.pushType
40
+ )
41
+ return VoltraDynamicLiveActivityReference(
42
+ id: activity.id,
43
+ name: activity.attributes.name,
44
+ definitionId: Attributes.definitionId
45
+ )
46
+ }
47
+
48
+ public static func update<Attributes: VoltraDynamicLiveActivityDefinition>(
49
+ _: Attributes.Type,
50
+ byName name: String,
51
+ request: VoltraDynamicLiveActivityUpdateRequest
52
+ ) async -> Bool {
53
+ let activities = Activity<Attributes>.activities.filter { $0.attributes.name == name }
54
+ for activity in activities {
55
+ // State is deliberately replaced in full; V1 does not merge props.
56
+ await activity.update(ActivityContent(
57
+ state: VoltraDynamicLiveActivityContentState(props: request.props),
58
+ staleDate: request.staleDate,
59
+ relevanceScore: request.relevanceScore
60
+ ))
61
+ }
62
+ return !activities.isEmpty
63
+ }
64
+
65
+ public static func end<Attributes: VoltraDynamicLiveActivityDefinition>(
66
+ _: Attributes.Type,
67
+ byName name: String,
68
+ dismissalPolicy: ActivityUIDismissalPolicy
69
+ ) async -> Bool {
70
+ let activities = Activity<Attributes>.activities.filter { $0.attributes.name == name }
71
+ for activity in activities {
72
+ await activity.end(ActivityContent(state: activity.content.state, staleDate: nil), dismissalPolicy: dismissalPolicy)
73
+ }
74
+ return !activities.isEmpty
75
+ }
76
+
77
+ public static func endAll<Attributes: VoltraDynamicLiveActivityDefinition>(
78
+ _: Attributes.Type,
79
+ dismissalPolicy: ActivityUIDismissalPolicy
80
+ ) async {
81
+ for activity in Activity<Attributes>.activities {
82
+ await activity.end(ActivityContent(state: activity.content.state, staleDate: nil), dismissalPolicy: dismissalPolicy)
83
+ }
84
+ }
85
+
86
+ public static func activities<Attributes: VoltraDynamicLiveActivityDefinition>(
87
+ _: Attributes.Type
88
+ ) -> [VoltraDynamicLiveActivityReference] {
89
+ Activity<Attributes>.activities.map {
90
+ VoltraDynamicLiveActivityReference(id: $0.id, name: $0.attributes.name, definitionId: Attributes.definitionId)
91
+ }
92
+ }
93
+
94
+ public static func reload<Attributes: VoltraDynamicLiveActivityDefinition>(
95
+ _: Attributes.Type
96
+ ) async {
97
+ for activity in Activity<Attributes>.activities {
98
+ await activity.update(ActivityContent(
99
+ state: activity.content.state,
100
+ staleDate: activity.content.staleDate,
101
+ relevanceScore: activity.content.relevanceScore
102
+ ))
103
+ }
104
+ }
105
+ }