@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
@@ -1,8 +1,12 @@
1
1
  import Foundation
2
- import UIKit
3
2
 
4
- /// Handles fetching widget content from a remote Voltra SSR server.
5
- /// Used by the TimelineProvider to pull server-driven widget updates.
3
+ /// Fetches a Voltra payload from the server for a payload-driven widget.
4
+ ///
5
+ /// Since ADR 0002 the request itself is built by `shared/WidgetServer`, the same code the Dynamic
6
+ /// engine uses. With no runtime settings set, that produces the request this fetcher always sent,
7
+ /// plus `locale` and a conditional `If-None-Match`; with settings set, this widget gains the
8
+ /// runtime URL, method, headers, query and body too. What the timeline does with the response is
9
+ /// unchanged.
6
10
  public enum VoltraWidgetServerFetcher {
7
11
  /// Errors that can occur during server fetch
8
12
  public enum FetchError: Error, LocalizedError {
@@ -12,6 +16,8 @@ public enum VoltraWidgetServerFetcher {
12
16
  case httpError(statusCode: Int)
13
17
  case invalidResponse
14
18
  case emptyResponse
19
+ /// `304`: what is already stored is still current, so there is nothing new to render.
20
+ case notModified
15
21
 
16
22
  public var errorDescription: String? {
17
23
  switch self {
@@ -27,142 +33,81 @@ public enum VoltraWidgetServerFetcher {
27
33
  return "Invalid response from server"
28
34
  case .emptyResponse:
29
35
  return "Empty response from server"
36
+ case .notModified:
37
+ return "Server content is unchanged"
30
38
  }
31
39
  }
32
40
  }
33
41
 
34
- /// Read the server update URL for a widget from Info.plist / UserDefaults config.
42
+ /// The URL this widget will fetch from, after runtime overrides. Callers use it to decide
43
+ /// whether a widget is server-driven at all before starting a timeline.
35
44
  public static func serverUrl(for widgetId: String) -> String? {
36
- // Check Info.plist first (set at build time by config plugin)
37
- if let urls = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerUrls) as? [String: String],
38
- let url = urls[widgetId]
39
- {
40
- return url
41
- }
42
-
43
- // Fallback to UserDefaults (can be set at runtime)
44
- if let group = VoltraConfig.groupIdentifier(),
45
- let defaults = UserDefaults(suiteName: group)
46
- {
47
- return defaults.string(forKey: VoltraStorageKeys.widgetServerUrl(widgetId))
48
- }
49
-
50
- return nil
45
+ VoltraWidgetServer.resolver.resolve(.of(widgetId)).url
51
46
  }
52
47
 
53
- /// Read the update interval (in minutes) for a widget.
48
+ /// The resolved interval, in minutes. Runtime settings win over app.json.
54
49
  public static func updateInterval(for widgetId: String) -> Int {
55
- if let intervals = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerIntervals) as? [String: Int],
56
- let interval = intervals[widgetId]
57
- {
58
- return interval
59
- }
60
- return 60 // default: 1 hour
50
+ VoltraWidgetServer.resolver.resolve(.of(widgetId)).intervalMinutes
61
51
  }
62
52
 
53
+ /// Whether the widget draws a refresh button. Build-time only: the button is generated UI
54
+ /// structure, so unlike the URL and the interval it cannot be changed at runtime.
63
55
  public static func isRefreshEnabled(for widgetId: String) -> Bool {
64
- if let refreshDict = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerRefresh) as? [String: Bool],
65
- let enabled = refreshDict[widgetId]
66
- {
67
- return enabled
68
- }
69
- return false
70
- }
71
-
72
- private static func currentColorScheme() -> String {
73
- if #available(iOSApplicationExtension 13.0, *) {
74
- switch UITraitCollection.current.userInterfaceStyle {
75
- case .dark:
76
- return "dark"
77
- case .light:
78
- return "light"
79
- default:
80
- return "light"
81
- }
82
- }
83
- return "light"
56
+ VoltraWidgetServer.isRefreshEnabled(for: widgetId)
84
57
  }
85
58
 
86
59
  /// Fetch widget content from the remote Voltra SSR server.
87
60
  ///
88
- /// The request includes:
89
- /// - `widgetId` query parameter
90
- /// - `family` query parameter (e.g., "systemSmall")
91
- /// - `platform` query parameter (`ios`)
92
- /// - `theme` query parameter (`light` or `dark`)
93
- /// - `Authorization: Bearer <token>` header (if credentials stored in Keychain)
94
- /// - Any custom headers stored in Keychain
61
+ /// The request carries `widgetId`, `family`, `platform`, `theme` and `locale` as query
62
+ /// parameters, `Accept: application/json`, a Voltra user agent, whatever headers the app has set
63
+ /// — including `Authorization` from the deprecated credential API — and `If-None-Match` when a
64
+ /// stored ETag belongs to the URL being fetched.
95
65
  ///
96
66
  /// Returns the raw JSON data from the server, ready to be parsed by VoltraNode.
97
67
  public static func fetchWidgetContent(
98
68
  widgetId: String,
99
69
  family: String
100
70
  ) async throws -> Data {
101
- guard let baseUrl = serverUrl(for: widgetId) else {
102
- throw FetchError.noServerUrl
103
- }
104
-
105
- // Build URL with query parameters
106
- guard var components = URLComponents(string: baseUrl) else {
107
- throw FetchError.invalidUrl(baseUrl)
108
- }
109
-
110
- let theme = currentColorScheme()
71
+ let scope = WidgetScope.of(widgetId)
72
+ let settings = VoltraWidgetServer.resolver.resolve(scope)
111
73
 
112
- var queryItems = components.queryItems ?? []
113
- queryItems.append(URLQueryItem(name: "widgetId", value: widgetId))
114
- queryItems.append(URLQueryItem(name: "family", value: family))
115
- queryItems.append(URLQueryItem(name: "platform", value: "ios"))
116
- queryItems.append(URLQueryItem(name: "theme", value: theme))
117
- components.queryItems = queryItems
118
-
119
- guard let url = components.url else {
120
- throw FetchError.invalidUrl(baseUrl)
74
+ guard let url = settings.url else {
75
+ throw FetchError.noServerUrl
121
76
  }
122
77
 
123
- var request = URLRequest(url: url)
124
- request.httpMethod = "GET"
125
- request.timeoutInterval = 15 // Widgets have limited execution time
126
-
127
- // Add auth token from Keychain if available
128
- if let token = VoltraKeychainHelper.readToken() {
129
- request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
78
+ // No If-None-Match: a payload widget sends the request it always sent, plus locale. The last
79
+ // successful response is kept only for this extension process, so a 304 could arrive with
80
+ // nothing to fall back on and leave the widget on its initial state.
81
+ guard let request = WidgetServerRequestBuilder.build(
82
+ scope: scope,
83
+ settings: settings,
84
+ context: VoltraWidgetAppearance.requestContext(family: family)
85
+ ) else {
86
+ throw FetchError.invalidUrl(url)
130
87
  }
131
88
 
132
- // Add custom headers from Keychain if available
133
- if let headers = VoltraKeychainHelper.readHeaders() {
134
- for (key, value) in headers {
135
- request.setValue(value, forHTTPHeaderField: key)
89
+ switch await WidgetServerFetcher.fetch(request) {
90
+ case let .success(body, _, _, _):
91
+ guard !body.isEmpty else {
92
+ throw FetchError.emptyResponse
136
93
  }
137
- }
138
94
 
139
- // Add Voltra-specific headers
140
- let systemVersion = await MainActor.run {
141
- UIDevice.current.systemVersion
142
- }
143
- request.setValue("application/json", forHTTPHeaderField: "Accept")
144
- request.setValue("VoltraWidget/1.0 (iOS/\(systemVersion))", forHTTPHeaderField: "User-Agent")
145
-
146
- do {
147
- let (data, response) = try await URLSession.shared.data(for: request)
95
+ return body
148
96
 
149
- guard let httpResponse = response as? HTTPURLResponse else {
150
- throw FetchError.invalidResponse
151
- }
97
+ case .notModified:
98
+ // Only reachable if the server answers 304 unprompted; nothing was requested conditionally.
99
+ throw FetchError.notModified
152
100
 
153
- guard (200 ... 299).contains(httpResponse.statusCode) else {
154
- throw FetchError.httpError(statusCode: httpResponse.statusCode)
155
- }
101
+ case let .tooLarge(statusCode):
102
+ throw FetchError.httpError(statusCode: statusCode)
156
103
 
157
- guard !data.isEmpty else {
158
- throw FetchError.emptyResponse
159
- }
104
+ case let .httpFailure(statusCode, _):
105
+ throw FetchError.httpError(statusCode: statusCode)
160
106
 
161
- return data
162
- } catch let error as FetchError {
163
- throw error
164
- } catch {
165
- throw FetchError.networkError(error)
107
+ case let .networkFailure(message):
108
+ throw FetchError.networkError(NSError(domain: "VoltraWidgetServerFetcher", code: -1, userInfo: [
109
+ NSLocalizedDescriptionKey: message,
110
+ ]))
166
111
  }
167
112
  }
168
113
  }
@@ -0,0 +1,17 @@
1
+ import UIKit
2
+
3
+ /// The appearance a widget is being drawn in, as the request contract reports it.
4
+ ///
5
+ /// Split out from the rest of `WidgetServer` because it is the one piece that needs UIKit, and
6
+ /// keeping it here lets everything else — the resolver, the store, the request builder, the
7
+ /// fetcher — be compiled and tested on its own.
8
+ public enum VoltraWidgetAppearance {
9
+ public static func currentTheme() -> String {
10
+ UITraitCollection.current.userInterfaceStyle == .dark ? "dark" : "light"
11
+ }
12
+
13
+ /// The device state a request carries, for callers that are already on a UIKit-capable target.
14
+ public static func requestContext(family: String? = nil) -> WidgetServerRequestContext {
15
+ VoltraWidgetServer.requestContext(theme: currentTheme(), family: family)
16
+ }
17
+ }
@@ -0,0 +1,138 @@
1
+ import Foundation
2
+
3
+ /// Build-time server-update defaults, read from the Info.plist keys the config plugin and the CLI
4
+ /// write.
5
+ ///
6
+ /// `Voltra_WidgetServerIntervals` carries an entry for every server-driven widget, so its keys are
7
+ /// the set of widget ids this app may fetch for. `Voltra_WidgetServerUrls` carries only the ones
8
+ /// app.json gave a URL; the rest expect one at runtime from `setWidgetServerUpdate`.
9
+ struct ConfigWidgetServerSettingsLayer: WidgetServerSettingsLayer {
10
+ let name = "config"
11
+
12
+ private let urls: [String: String]
13
+ private let intervals: [String: Int]
14
+
15
+ init(bundle: Bundle = .main) {
16
+ urls = bundle.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerUrls) as? [String: String] ?? [:]
17
+ intervals = bundle.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerIntervals) as? [String: Int] ?? [:]
18
+ }
19
+
20
+ init(urls: [String: String], intervals: [String: Int]) {
21
+ self.urls = urls
22
+ self.intervals = intervals
23
+ }
24
+
25
+ func settings(for scope: WidgetScope) -> WidgetServerUpdateSettings? {
26
+ guard let interval = intervals[scope.widgetId] else { return nil }
27
+
28
+ return WidgetServerUpdateSettings(url: urls[scope.widgetId], intervalMinutes: interval)
29
+ }
30
+
31
+ func isServerDriven(_ scope: WidgetScope) -> Bool {
32
+ intervals[scope.widgetId] != nil
33
+ }
34
+
35
+ var serverDrivenWidgetIds: Set<String> {
36
+ Set(intervals.keys)
37
+ }
38
+ }
39
+
40
+ /// Assembles the settings stack for the process.
41
+ ///
42
+ /// Everything that needs server-update settings goes through here: the payload timeline, the
43
+ /// Dynamic Widget timeline, and the bridge methods. Neither engine reads Info.plist, the Keychain,
44
+ /// or the credential records on its own, which is what keeps the layer order and the merge rule in
45
+ /// one place.
46
+ public enum VoltraWidgetServer {
47
+ private static let configLayer = ConfigWidgetServerSettingsLayer()
48
+
49
+ /// Fixed order, lowest priority first. An instance layer will slot in above `widget`.
50
+ public static let resolver = WidgetServerSettingsResolver(
51
+ layers: [
52
+ configLayer,
53
+ CredentialsWidgetServerSettingsLayer(),
54
+ GlobalWidgetServerSettingsLayer(),
55
+ WidgetWidgetServerSettingsLayer(),
56
+ ],
57
+ revisionSource: { WidgetServerSettingsStore.revision() }
58
+ )
59
+
60
+ /// Every widget app.json marked server-driven, whichever engine renders it.
61
+ public static var serverDrivenWidgetIds: Set<String> {
62
+ configLayer.serverDrivenWidgetIds
63
+ }
64
+
65
+ public static func isServerDriven(_ widgetId: String) -> Bool {
66
+ configLayer.isServerDriven(.of(widgetId))
67
+ }
68
+
69
+ /// Whether the widget draws a refresh button. Build-time only: the button is generated UI
70
+ /// structure, so unlike the URL and the interval it cannot be changed at runtime.
71
+ public static func isRefreshEnabled(for widgetId: String) -> Bool {
72
+ guard let refresh = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetServerRefresh) as? [String: Bool] else {
73
+ return false
74
+ }
75
+
76
+ return refresh[widgetId] ?? false
77
+ }
78
+
79
+ /// Whether plain http to a local dev host is allowed. Release builds have App Transport Security
80
+ /// blocking cleartext anyway, so accepting such a URL there would only move the failure to fetch
81
+ /// time.
82
+ public static var isDebugBuild: Bool {
83
+ #if DEBUG
84
+ return true
85
+ #else
86
+ return false
87
+ #endif
88
+ }
89
+
90
+ /// The device state a request carries. The theme is passed in rather than read here so this
91
+ /// whole module stays free of UIKit and the request contract can be tested without a simulator;
92
+ /// `VoltraWidgetAppearance` is the UIKit-side helper that supplies it.
93
+ public static func requestContext(theme: String, family: String? = nil) -> WidgetServerRequestContext {
94
+ WidgetServerRequestContext(
95
+ theme: theme,
96
+ locale: bcp47Locale(),
97
+ userAgent: userAgent(),
98
+ family: family
99
+ )
100
+ }
101
+
102
+ /// The device locale as a BCP-47 tag, so a backend sees `en-US` from both platforms.
103
+ /// `Locale.identifier` is the ICU form (`en_US`), which is not what the contract promises.
104
+ static func bcp47Locale(_ locale: Locale = .current) -> String {
105
+ if #available(iOS 16.0, macOS 13.0, *) {
106
+ return locale.identifier(.bcp47)
107
+ }
108
+
109
+ return locale.identifier.replacingOccurrences(of: "_", with: "-")
110
+ }
111
+
112
+ static func userAgent() -> String {
113
+ "VoltraWidget/\(VoltraConfig.voltraVersion()) (iOS/\(systemVersion))"
114
+ }
115
+
116
+ /// `UIDevice.current.systemVersion` is main-actor isolated and a timeline provider is not, so
117
+ /// the OS version comes from `ProcessInfo`, which is not.
118
+ private static var systemVersion: String {
119
+ let version = ProcessInfo.processInfo.operatingSystemVersion
120
+
121
+ return "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
122
+ }
123
+
124
+ /// Resolves settings and builds the request for one scope, or returns nil when there is nothing
125
+ /// to fetch.
126
+ public static func request(
127
+ for scope: WidgetScope,
128
+ context: WidgetServerRequestContext,
129
+ etag: String? = nil
130
+ ) -> URLRequest? {
131
+ WidgetServerRequestBuilder.build(
132
+ scope: scope,
133
+ settings: resolver.resolve(scope),
134
+ context: context,
135
+ etag: etag
136
+ )
137
+ }
138
+ }
@@ -0,0 +1,77 @@
1
+ import Foundation
2
+
3
+ /// Turns a merged `env.configuration` map into the canonical form ADR 0007 sends on the wire and
4
+ /// hashes into an instance key.
5
+ ///
6
+ /// Canonical serialization: keys sorted by Unicode scalar (code point), no whitespace, values as
7
+ /// JSON strings. Decoded, it is byte for byte what the widget sees as `env.configuration`, so
8
+ /// server and widget agree by construction. Both platforms produce this same string for the same
9
+ /// map, which is what lets the hash below be a stable, cross-platform instance identity.
10
+ public enum WidgetCanonicalConfiguration {
11
+ /// The canonical JSON string for `configuration`, or nil when the map is empty — a widget with
12
+ /// no configuration parameters has exactly one instance, the widget scope, and sends neither
13
+ /// `instance` nor `configuration`.
14
+ public static func canonicalize(_ configuration: [String: String]) -> String? {
15
+ guard !configuration.isEmpty else { return nil }
16
+
17
+ let sortedKeys = configuration.keys.sorted { lhs, rhs in
18
+ lhs.unicodeScalars.lexicographicallyPrecedes(rhs.unicodeScalars) { $0.value < $1.value }
19
+ }
20
+
21
+ var pieces: [String] = []
22
+ pieces.reserveCapacity(sortedKeys.count)
23
+ for key in sortedKeys {
24
+ let value = configuration[key] ?? ""
25
+ pieces.append("\(encodeJSONString(key)):\(encodeJSONString(value))")
26
+ }
27
+
28
+ return "{" + pieces.joined(separator: ",") + "}"
29
+ }
30
+
31
+ /// The instance key for `configuration`: FNV-1a, 32-bit, over the UTF-8 bytes of the canonical
32
+ /// serialization, rendered as 8 lowercase hex digits. Nil when the map is empty, matching
33
+ /// `canonicalize`.
34
+ ///
35
+ /// FNV-1a was picked for being small, dependency-free, and trivial to reproduce identically in
36
+ /// Kotlin — this is not a cryptographic hash, and nothing about instance identity depends on
37
+ /// collision resistance beyond "different configurations usually get different keys".
38
+ public static func key(_ configuration: [String: String]) -> String? {
39
+ guard let canonical = canonicalize(configuration) else { return nil }
40
+ return fnv1a32Hex(canonical)
41
+ }
42
+
43
+ /// Minimal, dependency-free JSON string encoder — just enough for configuration keys/values,
44
+ /// which are plain strings, not arbitrary JSON. Escapes the characters `JSONSerialization` would.
45
+ private static func encodeJSONString(_ value: String) -> String {
46
+ var result = "\""
47
+ for scalar in value.unicodeScalars {
48
+ switch scalar {
49
+ case "\"": result += "\\\""
50
+ case "\\": result += "\\\\"
51
+ case "\n": result += "\\n"
52
+ case "\r": result += "\\r"
53
+ case "\t": result += "\\t"
54
+ default:
55
+ if scalar.value < 0x20 {
56
+ result += String(format: "\\u%04x", scalar.value)
57
+ } else {
58
+ result.unicodeScalars.append(scalar)
59
+ }
60
+ }
61
+ }
62
+ result += "\""
63
+ return result
64
+ }
65
+
66
+ private static let fnvOffsetBasis: UInt32 = 0x811C_9DC5
67
+ private static let fnvPrime: UInt32 = 0x0100_0193
68
+
69
+ private static func fnv1a32Hex(_ value: String) -> String {
70
+ var hash = fnvOffsetBasis
71
+ for byte in Array(value.utf8) {
72
+ hash ^= UInt32(byte)
73
+ hash = hash &* fnvPrime
74
+ }
75
+ return String(format: "%08x", hash)
76
+ }
77
+ }
@@ -0,0 +1,55 @@
1
+ import Foundation
2
+
3
+ /// The unit everything server-driven is keyed by: settings, fetched props, the stored ETag, fetch
4
+ /// coalescing, and the settings revision.
5
+ ///
6
+ /// `.widget` is a whole widget id. `.instance` (ADR 0007) is one distinct merged configuration of a
7
+ /// widget: two placements with identical configuration share one key, one fetch, one ETag and one
8
+ /// props slot. The settings resolver, ETag store, status store, runner and request builder take
9
+ /// either case with no signature change; the settings layers keep resolving by widget id, so an
10
+ /// instance inherits the widget's URL, interval, method, headers and body.
11
+ public enum WidgetScope: Hashable, Sendable {
12
+ case widget(id: String)
13
+
14
+ /// One distinct merged configuration of `id`. `key` is the hash of that configuration's
15
+ /// canonical serialization (see `WidgetCanonicalConfiguration`) — opaque, stable, and produced
16
+ /// identically on Android for the same configuration. WidgetKit has no placement id, so this is
17
+ /// the only identity an iOS placement can have.
18
+ case instance(id: String, key: String)
19
+
20
+ /// Widget id this scope belongs to. An instance scope reports the id it is an instance of.
21
+ public var widgetId: String {
22
+ switch self {
23
+ case let .widget(id):
24
+ return id
25
+ case let .instance(id, _):
26
+ return id
27
+ }
28
+ }
29
+
30
+ /// Stable key for per-scope storage. An instance scope appends its placement key, so
31
+ /// widget-scoped records written today keep their keys.
32
+ public var storageKey: String {
33
+ switch self {
34
+ case let .widget(id):
35
+ return id
36
+ case let .instance(id, key):
37
+ return "\(id)#\(key)"
38
+ }
39
+ }
40
+
41
+ /// Convenience for the common case, so callers do not spell out the case name.
42
+ public static func of(_ widgetId: String) -> WidgetScope {
43
+ .widget(id: widgetId)
44
+ }
45
+
46
+ /// The scope for one placement's merged `configuration` (ADR 0007): `.instance` when the
47
+ /// configuration is non-empty, `.widget` when it is — a widget with no configuration parameters
48
+ /// has exactly one instance, the widget scope, and its requests and storage are unchanged.
49
+ public static func of(_ widgetId: String, configuration: [String: String]) -> WidgetScope {
50
+ if let key = WidgetCanonicalConfiguration.key(configuration) {
51
+ return .instance(id: widgetId, key: key)
52
+ }
53
+ return .widget(id: widgetId)
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ import Foundation
2
+
3
+ /// The ETag from the last `200`, stored with the URL it came from.
4
+ ///
5
+ /// Keeping the URL alongside it is what makes `If-None-Match` safe once the app can change the URL
6
+ /// at runtime: an ETag minted by one endpoint says nothing about another, and sending it could
7
+ /// produce a `304` that leaves the widget showing the previous endpoint's data forever.
8
+ ///
9
+ /// Lives in the App Group so the widget extension and the app agree on it. Without an App Group
10
+ /// there is nowhere shared to put it, and the widget simply revalidates nothing.
11
+ public enum WidgetServerEtagStore {
12
+ /// The stored ETag, but only if it was minted by `url`.
13
+ public static func etag(for scope: WidgetScope, url: String?) -> String? {
14
+ guard let url, let defaults = defaults(),
15
+ defaults.string(forKey: urlKey(scope)) == url
16
+ else {
17
+ return nil
18
+ }
19
+
20
+ return defaults.string(forKey: etagKey(scope))
21
+ }
22
+
23
+ public static func put(_ etag: String?, for scope: WidgetScope, url: String) {
24
+ guard let defaults = defaults() else { return }
25
+
26
+ guard let etag else {
27
+ clear(scope)
28
+ return
29
+ }
30
+
31
+ defaults.set(etag, forKey: etagKey(scope))
32
+ defaults.set(url, forKey: urlKey(scope))
33
+ }
34
+
35
+ public static func clear(_ scope: WidgetScope) {
36
+ guard let defaults = defaults() else { return }
37
+
38
+ defaults.removeObject(forKey: etagKey(scope))
39
+ defaults.removeObject(forKey: urlKey(scope))
40
+ }
41
+
42
+ private static func defaults() -> UserDefaults? {
43
+ guard let group = VoltraConfig.groupIdentifier() else { return nil }
44
+
45
+ return UserDefaults(suiteName: group)
46
+ }
47
+
48
+ private static func etagKey(_ scope: WidgetScope) -> String {
49
+ "Voltra_WidgetServerEtag_\(scope.storageKey)"
50
+ }
51
+
52
+ private static func urlKey(_ scope: WidgetScope) -> String {
53
+ "Voltra_WidgetServerEtagUrl_\(scope.storageKey)"
54
+ }
55
+ }