@use-voltra/ios-client 2.2.0 → 2.3.1

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