@use-voltra/ios-client 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +61 -8
  2. package/Voltra.podspec +1 -0
  3. package/build/commonjs/dynamic-widget/api.js +15 -0
  4. package/build/commonjs/dynamic-widget/api.js.map +1 -0
  5. package/build/commonjs/events.js +23 -0
  6. package/build/commonjs/events.js.map +1 -1
  7. package/build/commonjs/index.js +74 -7
  8. package/build/commonjs/index.js.map +1 -1
  9. package/build/commonjs/live-activity/api.js +3 -1
  10. package/build/commonjs/live-activity/api.js.map +1 -1
  11. package/build/commonjs/live-activity/dynamic-api.js +173 -0
  12. package/build/commonjs/live-activity/dynamic-api.js.map +1 -0
  13. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  14. package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js +47 -0
  15. package/build/commonjs/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
  16. package/build/commonjs/utils/enableWidgetHotReload.js +8 -11
  17. package/build/commonjs/utils/enableWidgetHotReload.js.map +1 -1
  18. package/build/commonjs/utils/useUpdateOnHMR.js +3 -7
  19. package/build/commonjs/utils/useUpdateOnHMR.js.map +1 -1
  20. package/build/commonjs/widgets/server-credentials.js +9 -0
  21. package/build/commonjs/widgets/server-credentials.js.map +1 -1
  22. package/build/commonjs/widgets/server-update.js +78 -0
  23. package/build/commonjs/widgets/server-update.js.map +1 -0
  24. package/build/module/dynamic-widget/api.js +10 -0
  25. package/build/module/dynamic-widget/api.js.map +1 -0
  26. package/build/module/events.js +23 -0
  27. package/build/module/events.js.map +1 -1
  28. package/build/module/index.js +4 -0
  29. package/build/module/index.js.map +1 -1
  30. package/build/module/live-activity/api.js +2 -2
  31. package/build/module/live-activity/api.js.map +1 -1
  32. package/build/module/live-activity/dynamic-api.js +167 -0
  33. package/build/module/live-activity/dynamic-api.js.map +1 -0
  34. package/build/module/native/NativeVoltra.js.map +1 -1
  35. package/build/module/utils/enableDynamicLiveActivityHotReload.js +43 -0
  36. package/build/module/utils/enableDynamicLiveActivityHotReload.js.map +1 -0
  37. package/build/module/utils/enableWidgetHotReload.js +9 -11
  38. package/build/module/utils/enableWidgetHotReload.js.map +1 -1
  39. package/build/module/utils/useUpdateOnHMR.js +3 -7
  40. package/build/module/utils/useUpdateOnHMR.js.map +1 -1
  41. package/build/module/widgets/server-credentials.js +9 -0
  42. package/build/module/widgets/server-credentials.js.map +1 -1
  43. package/build/module/widgets/server-update.js +72 -0
  44. package/build/module/widgets/server-update.js.map +1 -0
  45. package/build/typescript/commonjs/dynamic-widget/api.d.ts +8 -0
  46. package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -0
  47. package/build/typescript/commonjs/events.d.ts +7 -0
  48. package/build/typescript/commonjs/events.d.ts.map +1 -1
  49. package/build/typescript/commonjs/index.d.ts +4 -0
  50. package/build/typescript/commonjs/index.d.ts.map +1 -1
  51. package/build/typescript/commonjs/live-activity/api.d.ts +7 -0
  52. package/build/typescript/commonjs/live-activity/api.d.ts.map +1 -1
  53. package/build/typescript/commonjs/live-activity/dynamic-api.d.ts +54 -0
  54. package/build/typescript/commonjs/live-activity/dynamic-api.d.ts.map +1 -0
  55. package/build/typescript/commonjs/native/NativeVoltra.d.ts +21 -0
  56. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  57. package/build/typescript/commonjs/types.d.ts +1 -1
  58. package/build/typescript/commonjs/types.d.ts.map +1 -1
  59. package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
  60. package/build/typescript/commonjs/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
  61. package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts +5 -7
  62. package/build/typescript/commonjs/utils/enableWidgetHotReload.d.ts.map +1 -1
  63. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts +0 -3
  64. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts.map +1 -1
  65. package/build/typescript/commonjs/widgets/server-credentials.d.ts +8 -0
  66. package/build/typescript/commonjs/widgets/server-credentials.d.ts.map +1 -1
  67. package/build/typescript/commonjs/widgets/server-update.d.ts +64 -0
  68. package/build/typescript/commonjs/widgets/server-update.d.ts.map +1 -0
  69. package/build/typescript/module/dynamic-widget/api.d.ts +8 -0
  70. package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -0
  71. package/build/typescript/module/events.d.ts +7 -0
  72. package/build/typescript/module/events.d.ts.map +1 -1
  73. package/build/typescript/module/index.d.ts +4 -0
  74. package/build/typescript/module/index.d.ts.map +1 -1
  75. package/build/typescript/module/live-activity/api.d.ts +7 -0
  76. package/build/typescript/module/live-activity/api.d.ts.map +1 -1
  77. package/build/typescript/module/live-activity/dynamic-api.d.ts +54 -0
  78. package/build/typescript/module/live-activity/dynamic-api.d.ts.map +1 -0
  79. package/build/typescript/module/native/NativeVoltra.d.ts +21 -0
  80. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  81. package/build/typescript/module/types.d.ts +1 -1
  82. package/build/typescript/module/types.d.ts.map +1 -1
  83. package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts +15 -0
  84. package/build/typescript/module/utils/enableDynamicLiveActivityHotReload.d.ts.map +1 -0
  85. package/build/typescript/module/utils/enableWidgetHotReload.d.ts +5 -7
  86. package/build/typescript/module/utils/enableWidgetHotReload.d.ts.map +1 -1
  87. package/build/typescript/module/utils/useUpdateOnHMR.d.ts +0 -3
  88. package/build/typescript/module/utils/useUpdateOnHMR.d.ts.map +1 -1
  89. package/build/typescript/module/widgets/server-credentials.d.ts +8 -0
  90. package/build/typescript/module/widgets/server-credentials.d.ts.map +1 -1
  91. package/build/typescript/module/widgets/server-update.d.ts +64 -0
  92. package/build/typescript/module/widgets/server-update.d.ts.map +1 -0
  93. package/expo-plugin/build/cjs/constants.js +20 -1
  94. package/expo-plugin/build/cjs/constants.js.map +1 -1
  95. package/expo-plugin/build/cjs/index.js +9 -1
  96. package/expo-plugin/build/cjs/index.js.map +1 -1
  97. package/expo-plugin/build/cjs/ios/index.js +1 -0
  98. package/expo-plugin/build/cjs/ios/index.js.map +1 -1
  99. package/expo-plugin/build/cjs/ios/infoPlist.js +28 -5
  100. package/expo-plugin/build/cjs/ios/infoPlist.js.map +1 -1
  101. package/expo-plugin/build/cjs/ios/serverUpdate.js +25 -0
  102. package/expo-plugin/build/cjs/ios/serverUpdate.js.map +1 -0
  103. package/expo-plugin/build/cjs/ios-widget/clientRendered.js +4 -4
  104. package/expo-plugin/build/cjs/ios-widget/clientRendered.js.map +1 -1
  105. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +5 -5
  106. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
  107. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +130 -0
  108. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -0
  109. package/expo-plugin/build/cjs/ios-widget/files/index.js +9 -2
  110. package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
  111. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +6 -3
  112. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
  113. package/expo-plugin/build/cjs/ios-widget/files/manifest.js +24 -0
  114. package/expo-plugin/build/cjs/ios-widget/files/manifest.js.map +1 -1
  115. package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js +7 -0
  116. package/expo-plugin/build/cjs/ios-widget/files/swift-utils.js.map +1 -0
  117. package/expo-plugin/build/cjs/ios-widget/files/swift.js +56 -26
  118. package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
  119. package/expo-plugin/build/cjs/ios-widget/index.js +9 -3
  120. package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
  121. package/expo-plugin/build/cjs/ios-widget/podfile.js +38 -12
  122. package/expo-plugin/build/cjs/ios-widget/podfile.js.map +1 -1
  123. package/expo-plugin/build/cjs/ios-widget/widgetPlist.js +34 -22
  124. package/expo-plugin/build/cjs/ios-widget/widgetPlist.js.map +1 -1
  125. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +194 -118
  126. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
  127. package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js +30 -22
  128. package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js.map +1 -1
  129. package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js +50 -0
  130. package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js.map +1 -0
  131. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +45 -43
  132. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
  133. package/expo-plugin/build/cjs/ios-widget/xcode/index.js +93 -91
  134. package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
  135. package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js +41 -39
  136. package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js.map +1 -1
  137. package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js +2 -2
  138. package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js.map +1 -1
  139. package/expo-plugin/build/cjs/ios-widget/xcode/target.js +29 -63
  140. package/expo-plugin/build/cjs/ios-widget/xcode/target.js.map +1 -1
  141. package/expo-plugin/build/cjs/utils/urlScheme.js +36 -14
  142. package/expo-plugin/build/cjs/utils/urlScheme.js.map +1 -1
  143. package/expo-plugin/build/cjs/validation.js +54 -0
  144. package/expo-plugin/build/cjs/validation.js.map +1 -1
  145. package/expo-plugin/build/esm/constants.js +17 -0
  146. package/expo-plugin/build/esm/constants.js.map +1 -1
  147. package/expo-plugin/build/esm/index.js +9 -1
  148. package/expo-plugin/build/esm/index.js.map +1 -1
  149. package/expo-plugin/build/esm/ios/index.js +1 -0
  150. package/expo-plugin/build/esm/ios/index.js.map +1 -1
  151. package/expo-plugin/build/esm/ios/infoPlist.js +28 -5
  152. package/expo-plugin/build/esm/ios/infoPlist.js.map +1 -1
  153. package/expo-plugin/build/esm/ios/serverUpdate.js +21 -0
  154. package/expo-plugin/build/esm/ios/serverUpdate.js.map +1 -0
  155. package/expo-plugin/build/esm/ios-widget/clientRendered.js +5 -5
  156. package/expo-plugin/build/esm/ios-widget/clientRendered.js.map +1 -1
  157. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +6 -6
  158. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
  159. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +122 -0
  160. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -0
  161. package/expo-plugin/build/esm/ios-widget/files/index.js +10 -3
  162. package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
  163. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +6 -3
  164. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
  165. package/expo-plugin/build/esm/ios-widget/files/manifest.js +23 -1
  166. package/expo-plugin/build/esm/ios-widget/files/manifest.js.map +1 -1
  167. package/expo-plugin/build/esm/ios-widget/files/swift-utils.js +4 -0
  168. package/expo-plugin/build/esm/ios-widget/files/swift-utils.js.map +1 -0
  169. package/expo-plugin/build/esm/ios-widget/files/swift.js +50 -20
  170. package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
  171. package/expo-plugin/build/esm/ios-widget/index.js +9 -3
  172. package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
  173. package/expo-plugin/build/esm/ios-widget/podfile.js +36 -11
  174. package/expo-plugin/build/esm/ios-widget/podfile.js.map +1 -1
  175. package/expo-plugin/build/esm/ios-widget/widgetPlist.js +34 -22
  176. package/expo-plugin/build/esm/ios-widget/widgetPlist.js.map +1 -1
  177. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +194 -117
  178. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
  179. package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js +31 -22
  180. package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js.map +1 -1
  181. package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js +45 -0
  182. package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js.map +1 -0
  183. package/expo-plugin/build/esm/ios-widget/xcode/groups.js +45 -42
  184. package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
  185. package/expo-plugin/build/esm/ios-widget/xcode/index.js +97 -96
  186. package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
  187. package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js +41 -39
  188. package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js.map +1 -1
  189. package/expo-plugin/build/esm/ios-widget/xcode/productFile.js +3 -2
  190. package/expo-plugin/build/esm/ios-widget/xcode/productFile.js.map +1 -1
  191. package/expo-plugin/build/esm/ios-widget/xcode/target.js +28 -62
  192. package/expo-plugin/build/esm/ios-widget/xcode/target.js.map +1 -1
  193. package/expo-plugin/build/esm/utils/urlScheme.js +34 -14
  194. package/expo-plugin/build/esm/utils/urlScheme.js.map +1 -1
  195. package/expo-plugin/build/esm/validation.js +54 -1
  196. package/expo-plugin/build/esm/validation.js.map +1 -1
  197. package/expo-plugin/build/types/constants.d.ts +10 -1
  198. package/expo-plugin/build/types/constants.d.ts.map +1 -1
  199. package/expo-plugin/build/types/index.d.ts +1 -1
  200. package/expo-plugin/build/types/index.d.ts.map +1 -1
  201. package/expo-plugin/build/types/ios/index.d.ts +2 -0
  202. package/expo-plugin/build/types/ios/index.d.ts.map +1 -1
  203. package/expo-plugin/build/types/ios/infoPlist.d.ts +2 -0
  204. package/expo-plugin/build/types/ios/infoPlist.d.ts.map +1 -1
  205. package/expo-plugin/build/types/ios/serverUpdate.d.ts +11 -0
  206. package/expo-plugin/build/types/ios/serverUpdate.d.ts.map +1 -0
  207. package/expo-plugin/build/types/ios-widget/clientRendered.d.ts.map +1 -1
  208. package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
  209. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts +7 -0
  210. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -0
  211. package/expo-plugin/build/types/ios-widget/files/index.d.ts +3 -1
  212. package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
  213. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +2 -1
  214. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
  215. package/expo-plugin/build/types/ios-widget/files/manifest.d.ts +9 -2
  216. package/expo-plugin/build/types/ios-widget/files/manifest.d.ts.map +1 -1
  217. package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts +2 -0
  218. package/expo-plugin/build/types/ios-widget/files/swift-utils.d.ts.map +1 -0
  219. package/expo-plugin/build/types/ios-widget/files/swift.d.ts +6 -2
  220. package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
  221. package/expo-plugin/build/types/ios-widget/index.d.ts +3 -1
  222. package/expo-plugin/build/types/ios-widget/index.d.ts.map +1 -1
  223. package/expo-plugin/build/types/ios-widget/podfile.d.ts +19 -1
  224. package/expo-plugin/build/types/ios-widget/podfile.d.ts.map +1 -1
  225. package/expo-plugin/build/types/ios-widget/widgetPlist.d.ts.map +1 -1
  226. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +6 -8
  227. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
  228. package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts +15 -10
  229. package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts.map +1 -1
  230. package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts +16 -0
  231. package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts.map +1 -0
  232. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts +0 -4
  233. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
  234. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +23 -3
  235. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
  236. package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts +16 -4
  237. package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts.map +1 -1
  238. package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts +0 -4
  239. package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts.map +1 -1
  240. package/expo-plugin/build/types/ios-widget/xcode/target.d.ts +11 -26
  241. package/expo-plugin/build/types/ios-widget/xcode/target.d.ts.map +1 -1
  242. package/expo-plugin/build/types/types.d.ts +23 -4
  243. package/expo-plugin/build/types/types.d.ts.map +1 -1
  244. package/expo-plugin/build/types/utils/urlScheme.d.ts +16 -0
  245. package/expo-plugin/build/types/utils/urlScheme.d.ts.map +1 -1
  246. package/expo-plugin/build/types/validation.d.ts +2 -1
  247. package/expo-plugin/build/types/validation.d.ts.map +1 -1
  248. package/expo-plugin/src/constants.ts +24 -1
  249. package/expo-plugin/src/index.ts +14 -1
  250. package/expo-plugin/src/ios/index.ts +3 -0
  251. package/expo-plugin/src/ios/infoPlist.ts +33 -5
  252. package/expo-plugin/src/ios/serverUpdate.ts +29 -0
  253. package/expo-plugin/src/ios-widget/clientRendered.ts +9 -5
  254. package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +11 -6
  255. package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +138 -0
  256. package/expo-plugin/src/ios-widget/files/index.ts +15 -4
  257. package/expo-plugin/src/ios-widget/files/infoPlist.ts +17 -3
  258. package/expo-plugin/src/ios-widget/files/manifest.ts +42 -2
  259. package/expo-plugin/src/ios-widget/files/swift-utils.ts +3 -0
  260. package/expo-plugin/src/ios-widget/files/swift.ts +69 -22
  261. package/expo-plugin/src/ios-widget/index.ts +13 -3
  262. package/expo-plugin/src/ios-widget/podfile.ts +46 -13
  263. package/expo-plugin/src/ios-widget/widgetPlist.ts +37 -23
  264. package/expo-plugin/src/ios-widget/xcode/__fixtures__/fresh.pbxproj +197 -0
  265. package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-existing-extension.pbxproj +321 -0
  266. package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-per-config-signing.pbxproj +203 -0
  267. package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +363 -0
  268. package/expo-plugin/src/ios-widget/xcode/__tests__/pbxConsistency.ts +130 -0
  269. package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +210 -164
  270. package/expo-plugin/src/ios-widget/xcode/configurationList.ts +52 -35
  271. package/expo-plugin/src/ios-widget/xcode/fileReferences.ts +51 -0
  272. package/expo-plugin/src/ios-widget/xcode/groups.ts +52 -54
  273. package/expo-plugin/src/ios-widget/xcode/index.ts +124 -119
  274. package/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts +59 -54
  275. package/expo-plugin/src/ios-widget/xcode/productFile.ts +3 -2
  276. package/expo-plugin/src/ios-widget/xcode/target.ts +31 -83
  277. package/expo-plugin/src/types.ts +28 -4
  278. package/expo-plugin/src/utils/urlScheme.ts +47 -17
  279. package/expo-plugin/src/validation.ts +76 -1
  280. package/ios/Package.swift +39 -4
  281. package/ios/VoltraWidget.podspec +1 -0
  282. package/ios/app/NativeVoltra.mm +126 -7
  283. package/ios/app/VoltraLiveActivityManager.swift +30 -0
  284. package/ios/app/VoltraLiveActivityService.swift +77 -5
  285. package/ios/app/VoltraModule.swift +117 -1
  286. package/ios/app/VoltraModuleImpl.swift +116 -2
  287. package/ios/app/VoltraViewComponentView.mm +3 -3
  288. package/ios/app/VoltraWidgetService.swift +147 -12
  289. package/ios/app/dynamic-live-activity/VoltraDynamicLiveActivityService.swift +97 -0
  290. package/ios/app/dynamic-live-activity/VoltraLiveActivityChronology.swift +68 -0
  291. package/ios/shared/DynamicWidgetPropsCodec.swift +51 -0
  292. package/ios/shared/DynamicWidgetPropsStorage.swift +53 -0
  293. package/ios/shared/DynamicWidgetPropsStore.swift +117 -0
  294. package/ios/shared/DynamicWidgetRenderCoordinator.swift +31 -0
  295. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerFetchResolver.swift +34 -0
  296. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerProps.swift +55 -0
  297. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerPropsStore.swift +146 -0
  298. package/ios/shared/DynamicWidgetServerUpdate/DynamicWidgetServerUpdateRunner.swift +193 -0
  299. package/ios/shared/DynamicWidgetUpdater.swift +30 -0
  300. package/ios/shared/ServerWidgetContentResolver.swift +60 -0
  301. package/ios/shared/ServerWidgetResponseStore.swift +25 -0
  302. package/ios/shared/ShortNames.swift +6 -0
  303. package/ios/shared/VoltraConfig.swift +41 -0
  304. package/ios/shared/VoltraConstants.swift +19 -0
  305. package/ios/shared/VoltraElement.swift +127 -43
  306. package/ios/shared/VoltraEventBus.swift +52 -7
  307. package/ios/shared/VoltraJSRenderer.swift +93 -23
  308. package/ios/shared/VoltraNode.swift +7 -111
  309. package/ios/shared/VoltraRefreshIntent.swift +1 -1
  310. package/ios/shared/VoltraWidgetDefaults.swift +9 -1
  311. package/ios/shared/VoltraWidgetKind.swift +41 -0
  312. package/ios/shared/VoltraWidgetServerFetcher.swift +52 -107
  313. package/ios/shared/WidgetServer/VoltraWidgetAppearance.swift +17 -0
  314. package/ios/shared/WidgetServer/VoltraWidgetServer.swift +138 -0
  315. package/ios/shared/WidgetServer/WidgetCanonicalConfiguration.swift +77 -0
  316. package/ios/shared/WidgetServer/WidgetScope.swift +55 -0
  317. package/ios/shared/WidgetServer/WidgetServerEtagStore.swift +55 -0
  318. package/ios/shared/WidgetServer/WidgetServerFetcher.swift +179 -0
  319. package/ios/shared/WidgetServer/WidgetServerRequestBuilder.swift +114 -0
  320. package/ios/shared/WidgetServer/WidgetServerSettingsCodec.swift +47 -0
  321. package/ios/shared/WidgetServer/WidgetServerSettingsResolver.swift +129 -0
  322. package/ios/shared/WidgetServer/WidgetServerSettingsStore.swift +170 -0
  323. package/ios/shared/WidgetServer/WidgetServerSettingsValidator.swift +73 -0
  324. package/ios/shared/WidgetServer/WidgetServerUpdateSettings.swift +123 -0
  325. package/ios/shared/WidgetServer/WidgetServerUpdateSettingsJson.swift +115 -0
  326. package/ios/shared/dynamic-live-activity/DynamicLiveActivityPropsCodec.swift +13 -0
  327. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityBundleSource.swift +156 -0
  328. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityObserver.swift +97 -0
  329. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityOperations.swift +105 -0
  330. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityPayloadValidator.swift +55 -0
  331. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRegistry.swift +143 -0
  332. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureQueue.swift +158 -0
  333. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderFailureReporter.swift +120 -0
  334. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +370 -0
  335. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityTypes.swift +87 -0
  336. package/ios/shared/dynamic-live-activity/VoltraLiveActivityOrder.swift +19 -0
  337. package/ios/target/VoltraClientWidgetRuntime.swift +60 -10
  338. package/ios/target/VoltraDynamicWidgetServerUpdateProvider.swift +204 -0
  339. package/ios/target/VoltraHomeWidget.swift +91 -92
  340. package/ios/target/VoltraWidget.swift +3 -1
  341. package/ios/ui/Extensions/VoltraNode+View.swift +113 -0
  342. package/ios/ui/Generated/Parameters/ChartParameters.swift +3 -0
  343. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +5 -3
  344. package/ios/ui/Helpers/VoltraProgressDriver.swift +6 -2
  345. package/ios/ui/Layout/VoltraFlexStackLayout.swift +9 -3
  346. package/ios/ui/Style/JSColorParser.swift +24 -8
  347. package/ios/ui/Style/JSGradientParser.swift +36 -12
  348. package/ios/ui/Style/JSStyleParser.swift +36 -12
  349. package/ios/ui/Style/LayoutStyle.swift +12 -4
  350. package/ios/ui/Views/VoltraChart.swift +160 -16
  351. package/ios/ui/Views/VoltraLinearGradient.swift +6 -2
  352. package/package.json +5 -5
  353. package/src/dynamic-widget/api.ts +25 -0
  354. package/src/events.ts +34 -0
  355. package/src/index.ts +23 -0
  356. package/src/live-activity/api.ts +2 -2
  357. package/src/live-activity/dynamic-api.ts +240 -0
  358. package/src/native/NativeVoltra.ts +22 -0
  359. package/src/types.ts +4 -0
  360. package/src/utils/enableDynamicLiveActivityHotReload.ts +50 -0
  361. package/src/utils/enableWidgetHotReload.ts +9 -16
  362. package/src/utils/useUpdateOnHMR.ts +3 -12
  363. package/src/widgets/server-credentials.ts +8 -0
  364. package/src/widgets/server-update.ts +84 -0
  365. /package/ios/shared/{VoltraKeychainHelper.swift → WidgetServer/VoltraKeychainHelper.swift} +0 -0
@@ -1,8 +1,7 @@
1
1
  import Foundation
2
- import SwiftUI
3
2
 
4
3
  /// Represents anything that can be rendered (like ReactNode)
5
- public indirect enum VoltraNode: Hashable, View {
4
+ public indirect enum VoltraNode: Hashable {
6
5
  /// A component element
7
6
  case element(VoltraElement)
8
7
 
@@ -55,7 +54,9 @@ public indirect enum VoltraNode: Hashable, View {
55
54
 
56
55
  /// Check if node represents nothing
57
56
  public var isEmpty: Bool {
58
- if case .empty = self { return true }
57
+ if case .empty = self {
58
+ return true
59
+ }
59
60
  return false
60
61
  }
61
62
 
@@ -72,7 +73,9 @@ public indirect enum VoltraNode: Hashable, View {
72
73
  // Extract stylesheet (key "s")
73
74
  if case let .array(stylesheetArray) = rootObject["s"] {
74
75
  stylesheet = stylesheetArray.compactMap { item in
75
- if case let .object(dict) = item { return dict }
76
+ if case let .object(dict) = item {
77
+ return dict
78
+ }
76
79
  return nil
77
80
  }
78
81
  }
@@ -84,111 +87,4 @@ public indirect enum VoltraNode: Hashable, View {
84
87
 
85
88
  return VoltraNode(from: json, stylesheet: stylesheet, sharedElements: sharedElements)
86
89
  }
87
-
88
- // MARK: - View conformance
89
-
90
- public var body: some View {
91
- switch self {
92
- case let .element(element):
93
- VoltraElementView(element: element)
94
- case let .array(nodes):
95
- // Use stable identifiers: prefer element.id, fall back to index
96
- let items: [(id: String, node: VoltraNode)] = nodes.enumerated().map { offset, node in
97
- let id: String
98
- if case let .element(element) = node, let elementId = element.id {
99
- id = elementId
100
- } else {
101
- id = "idx_\(offset)"
102
- }
103
- return (id: id, node: node)
104
- }
105
- ForEach(items, id: \.id) { item in
106
- item.node
107
- }
108
- case let .text(text):
109
- Text(text)
110
- case .empty:
111
- EmptyView()
112
- }
113
- }
114
- }
115
-
116
- /// View that renders a VoltraElement based on its type
117
- struct VoltraElementView: View {
118
- let element: VoltraElement
119
-
120
- var body: some View {
121
- switch element.type {
122
- case "Button":
123
- VoltraButton(element)
124
-
125
- case "Link":
126
- VoltraLink(element)
127
-
128
- case "VStack":
129
- VoltraVStack(element)
130
-
131
- case "HStack":
132
- VoltraHStack(element)
133
-
134
- case "View":
135
- VoltraFlexView(element)
136
-
137
- case "ZStack":
138
- VoltraZStack(element)
139
-
140
- case "Text":
141
- VoltraText(element)
142
-
143
- case "Image":
144
- VoltraImage(element)
145
-
146
- case "Symbol":
147
- VoltraSymbol(element)
148
-
149
- case "Divider":
150
- VoltraDivider(element)
151
-
152
- case "Spacer":
153
- VoltraSpacer(element)
154
-
155
- case "Label":
156
- VoltraLabel(element)
157
-
158
- case "Toggle":
159
- VoltraToggle(element)
160
-
161
- case "Gauge":
162
- VoltraGauge(element)
163
-
164
- case "LinearProgressView":
165
- VoltraLinearProgressView(element)
166
-
167
- case "CircularProgressView":
168
- VoltraCircularProgressView(element)
169
-
170
- case "Timer":
171
- VoltraTimer(element)
172
-
173
- case "GroupBox":
174
- VoltraGroupBox(element)
175
-
176
- case "LinearGradient":
177
- VoltraLinearGradient(element)
178
-
179
- case "GlassContainer":
180
- VoltraGlassContainer(element)
181
-
182
- case "Mask":
183
- VoltraMask(element)
184
-
185
- case "Chart":
186
- if #available(iOS 16.0, macOS 13.0, *) {
187
- VoltraChart(element)
188
- }
189
-
190
- default:
191
- EmptyView()
192
- }
193
- }
194
90
  }
@@ -19,7 +19,7 @@ public struct VoltraRefreshIntent: AppIntent {
19
19
 
20
20
  public func perform() async throws -> some IntentResult {
21
21
  if let widgetId = widgetId {
22
- let kind = "\(VoltraStorageKeys.widgetKindPrefix)\(widgetId)"
22
+ let kind = VoltraWidgetKind.kind(for: widgetId)
23
23
  WidgetCenter.shared.reloadTimelines(ofKind: kind)
24
24
  } else {
25
25
  WidgetCenter.shared.reloadAllTimelines()
@@ -86,13 +86,17 @@ public enum VoltraWidgetDefaults {
86
86
 
87
87
  // MARK: - Remove
88
88
 
89
- /// Removes all persisted data (json, deepLinkUrl, timeline) for a single widget.
89
+ /// Removes all persisted data (json, deepLinkUrl, timeline, Dynamic Widget props) for a single widget.
90
90
  /// This is the single source of truth for which keys exist per widget.
91
91
  public static func removeAllData(for widgetId: String) {
92
92
  guard let defaults = try? resolvedDefaults() else { return }
93
93
  defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
94
94
  defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
95
95
  defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
96
+ let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
97
+ storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
98
+ )
99
+ try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
96
100
  defaults.synchronize()
97
101
  }
98
102
 
@@ -105,11 +109,15 @@ public enum VoltraWidgetDefaults {
105
109
  /// Removes all persisted data for every widget listed in the app's Info.plist.
106
110
  public static func removeAllWidgets() {
107
111
  guard let defaults = try? resolvedDefaults() else { return }
112
+ let dynamicWidgetPropsStore = DynamicWidgetPropsStore(
113
+ storage: DynamicWidgetPropsUserDefaultsStorage(userDefaults: defaults)
114
+ )
108
115
  let widgetIds = Bundle.main.object(forInfoDictionaryKey: VoltraStorageKeys.widgetIds) as? [String] ?? []
109
116
  for widgetId in widgetIds {
110
117
  defaults.removeObject(forKey: VoltraStorageKeys.widgetJson(widgetId))
111
118
  defaults.removeObject(forKey: VoltraStorageKeys.widgetDeepLinkUrl(widgetId))
112
119
  defaults.removeObject(forKey: VoltraStorageKeys.widgetTimeline(widgetId))
120
+ try? dynamicWidgetPropsStore.clearDynamicWidgetProps(for: widgetId)
113
121
  }
114
122
  defaults.synchronize()
115
123
  }
@@ -0,0 +1,41 @@
1
+ import Foundation
2
+
3
+ /// Maps a widget id to its WidgetKit `kind` and back.
4
+ ///
5
+ /// The kind defaults to `Voltra_Widget_<id>`. A widget can pin another kind through the plugin
6
+ /// `kind` option, stored in Info.plist under `Voltra_WidgetKinds` (app and extension), so widgets
7
+ /// placed before a migration to Voltra keep their identity.
8
+ ///
9
+ /// This is the only place either half of that mapping is derived; nothing else should reach for
10
+ /// `VoltraStorageKeys.widgetKindPrefix`.
11
+ public enum VoltraWidgetKind {
12
+ /// Info.plist cannot change for the life of the process, so the map is read once.
13
+ private static let mainOverrides = VoltraConfig.widgetKinds()
14
+
15
+ public static func kind(for widgetId: String) -> String {
16
+ kind(for: widgetId, overrides: mainOverrides)
17
+ }
18
+
19
+ /// Returns nil when the kind does not belong to a Voltra widget.
20
+ public static func widgetId(for kind: String) -> String? {
21
+ widgetId(for: kind, overrides: mainOverrides)
22
+ }
23
+
24
+ /// Pure half of `kind(for:)`, split out so the mapping is covered without building a bundle.
25
+ static func kind(for widgetId: String, overrides: [String: String]) -> String {
26
+ overrides[widgetId] ?? "\(VoltraStorageKeys.widgetKindPrefix)\(widgetId)"
27
+ }
28
+
29
+ /// Pure half of `widgetId(for:)`. Overrides win over the prefix so a widget that pins a kind
30
+ /// shaped like another widget's default still resolves to its own id.
31
+ static func widgetId(for kind: String, overrides: [String: String]) -> String? {
32
+ if let match = overrides.first(where: { $0.value == kind }) {
33
+ return match.key
34
+ }
35
+
36
+ let prefix = VoltraStorageKeys.widgetKindPrefix
37
+ guard kind.hasPrefix(prefix) else { return nil }
38
+
39
+ return String(kind.dropFirst(prefix.count))
40
+ }
41
+ }
@@ -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
+ }