@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
@@ -2,10 +2,12 @@ import { XcodeProject } from '@expo/config-plugins'
2
2
  import * as util from 'util'
3
3
 
4
4
  import type { IOSWidgetExtensionFiles } from '../../types'
5
+ import { ensureWidgetFileReference, normalizeRef } from './fileReferences'
5
6
 
6
7
  const pbxFile = require('xcode/lib/pbxFile')
7
8
 
8
- const WIDGET_BUNDLE_PHASE_NAME = 'Bundle Voltra Dynamic Widgets'
9
+ const WIDGET_BUNDLE_PHASE_NAME = 'Bundle Voltra Dynamic Content'
10
+ const LEGACY_WIDGET_BUNDLE_PHASE_NAME = 'Bundle Voltra Dynamic Widgets'
9
11
 
10
12
  // Release-only build phase that bakes each Dynamic Widget's production JS bundle into the
11
13
  // extension's resources. Debug builds fetch from Metro (and hot-reload), so this no-ops there. Runs
@@ -13,7 +15,8 @@ const WIDGET_BUNDLE_PHASE_NAME = 'Bundle Voltra Dynamic Widgets'
13
15
  // voltra-widget-<id>.bundle lands in the .appex (Bundle.main) where the runtime's release loader
14
16
  // reads it. SRCROOT is the ios/ dir; the project root is one level up, matching how Expo's main
15
17
  // "Bundle React Native code and images" phase resolves things.
16
- const WIDGET_BUNDLE_SHELL_SCRIPT = `if [[ "$CONFIGURATION" == *Debug* ]]; then
18
+ function bundleShellScript(content: 'all' | 'live-activities'): string {
19
+ return `if [[ "$CONFIGURATION" == *Debug* ]]; then
17
20
  echo "Voltra: Debug build — Dynamic Widgets load from Metro, skipping bundling"
18
21
  exit 0
19
22
  fi
@@ -47,6 +50,7 @@ try {
47
50
  console.error(error && error.message ? error.message : String(error))
48
51
  process.exit(1)
49
52
  }
53
+
50
54
  NODE
51
55
  )"
52
56
  if [[ -z "$BUNDLER" ]]; then
@@ -55,14 +59,23 @@ if [[ -z "$BUNDLER" ]]; then
55
59
  fi
56
60
 
57
61
  echo "Voltra: widget bundler resolved to $BUNDLER"
58
- "$NODE_BINARY" "$BUNDLER" --out-dir "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" --platform ios --project-root "$PROJECT_ROOT"
62
+ "$NODE_BINARY" "$BUNDLER" --out-dir "$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" --platform ios --project-root "$PROJECT_ROOT" --content ${content}
59
63
  `
64
+ }
65
+
66
+ function pbxQuotedShellScript(script: string): string {
67
+ return `"${script.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`
68
+ }
60
69
 
61
70
  /**
62
71
  * Adds (idempotently) the release-only shell-script phase that bakes Dynamic Widget
63
72
  * bundles into the extension. Safe to call on every prebuild; only added when absent.
64
73
  */
65
- export function ensureWidgetBundleScriptPhase(xcodeProject: XcodeProject, targetUuid: string): void {
74
+ export function ensureWidgetBundleScriptPhase(
75
+ xcodeProject: XcodeProject,
76
+ targetUuid: string,
77
+ content: 'all' | 'live-activities' = 'all'
78
+ ): void {
66
79
  const nativeTargets = xcodeProject.pbxNativeTargetSection()
67
80
  const target = nativeTargets[targetUuid]
68
81
  if (!target) {
@@ -74,15 +87,37 @@ export function ensureWidgetBundleScriptPhase(xcodeProject: XcodeProject, target
74
87
 
75
88
  const shellPhases = xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {}
76
89
  const quotedName = `"${WIDGET_BUNDLE_PHASE_NAME}"`
77
- const alreadyPresent = target.buildPhases.some((entry: any) => shellPhases[entry.value]?.name === quotedName)
78
- if (alreadyPresent) {
79
- return
80
- }
81
-
82
- xcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', WIDGET_BUNDLE_PHASE_NAME, targetUuid, {
83
- shellPath: '/bin/sh',
84
- shellScript: WIDGET_BUNDLE_SHELL_SCRIPT,
90
+ const quotedLegacyName = `"${LEGACY_WIDGET_BUNDLE_PHASE_NAME}"`
91
+ const matchingEntries = target.buildPhases.filter((entry: any) => {
92
+ const name = shellPhases[entry.value]?.name
93
+ return name === quotedName || name === quotedLegacyName
85
94
  })
95
+ let phase: any | undefined
96
+ if (matchingEntries.length > 0) {
97
+ const retained = matchingEntries[0]
98
+ phase = shellPhases[retained.value]
99
+ phase.name = quotedName
100
+ target.buildPhases = target.buildPhases.filter((entry: any) => {
101
+ if (entry === retained || !matchingEntries.includes(entry)) return true
102
+ delete shellPhases[entry.value]
103
+ delete shellPhases[`${entry.value}_comment`]
104
+ return false
105
+ })
106
+ } else {
107
+ xcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', WIDGET_BUNDLE_PHASE_NAME, targetUuid, {
108
+ shellPath: '/bin/sh',
109
+ shellScript: bundleShellScript(content),
110
+ })
111
+ phase = Object.values(xcodeProject.hash.project.objects.PBXShellScriptBuildPhase || {}).find(
112
+ (candidate: any) => candidate?.name === quotedName
113
+ )
114
+ }
115
+ if (phase) {
116
+ // xcode's serializer expects an already PBX-quoted value when an existing
117
+ // phase is reconciled directly (addBuildPhase performs this for new ones).
118
+ phase.shellScript = pbxQuotedShellScript(bundleShellScript(content))
119
+ phase.alwaysOutOfDate = 1
120
+ }
86
121
 
87
122
  // The phase intentionally re-bakes on every release build (it can't statically enumerate every
88
123
  // widget source as an input). Mark it always-out-of-date so Xcode doesn't warn about the missing
@@ -98,71 +133,79 @@ export function ensureWidgetBundleScriptPhase(xcodeProject: XcodeProject, target
98
133
  }
99
134
  }
100
135
 
101
- export interface AddBuildPhasesOptions {
136
+ export interface EnsureBuildPhasesOptions {
102
137
  targetUuid: string
138
+ targetName: string
103
139
  groupName: string
104
140
  productFile: {
105
141
  uuid: string
142
+ fileRef?: string
106
143
  target?: string
107
144
  basename: string
108
145
  group: string
109
146
  }
110
147
  widgetFiles: IOSWidgetExtensionFiles
111
- }
112
-
113
- export interface EnsureBuildPhasesOptions extends AddBuildPhasesOptions {
114
148
  mainTargetUuid?: string
149
+ /** Generated ActivityKit types compiled into both the extension and the app. */
150
+ mainSwiftFiles?: string[]
115
151
  }
116
152
 
117
153
  /**
118
- * Adds all required build phases for the widget extension target.
154
+ * Finds a `PBXCopyFilesBuildPhase` on the given target that embeds app extensions
155
+ * (`dstSubfolderSpec == 13`), regardless of its display name. Returns the phase object or null.
156
+ *
157
+ * A project that already contains another app extension (Share/Watch/expo-live-activity) will have
158
+ * such a phase — typically named "Embed App Extensions" — and the widget's product must be added to
159
+ * it rather than to a second, duplicate embed phase (issue #87).
119
160
  */
120
- export function addBuildPhases(xcodeProject: XcodeProject, options: AddBuildPhasesOptions): void {
121
- const { targetUuid, groupName, productFile, widgetFiles } = options
122
- const buildPath = `""`
123
- const folderType = 'app_extension'
124
-
125
- const { swiftFiles, intentFiles, assetDirectories, localizedStringResources } = widgetFiles
126
- const resourcePaths = [...assetDirectories, ...localizedStringResources]
127
-
128
- // Sources build phase
129
- xcodeProject.addBuildPhase(
130
- [...swiftFiles, ...intentFiles],
131
- 'PBXSourcesBuildPhase',
132
- 'Sources',
133
- targetUuid,
134
- folderType,
135
- buildPath
136
- )
137
-
138
- // Copy files build phase
139
- xcodeProject.addBuildPhase(
140
- [],
141
- 'PBXCopyFilesBuildPhase',
142
- groupName,
143
- xcodeProject.getFirstTarget().uuid,
144
- folderType,
145
- buildPath
146
- )
161
+ function findExistingEmbedExtensionsPhase(xcodeProject: XcodeProject, targetUuid: string): any | null {
162
+ const nativeTargets = xcodeProject.pbxNativeTargetSection()
163
+ const target = nativeTargets[targetUuid]
164
+ if (!target?.buildPhases) {
165
+ return null
166
+ }
147
167
 
148
- xcodeProject.buildPhaseObject('PBXCopyFilesBuildPhase', groupName, productFile.target).files.push({
149
- value: productFile.uuid,
150
- comment: util.format('%s in %s', productFile.basename, productFile.group),
151
- })
152
- xcodeProject.addToPbxBuildFileSection(productFile)
168
+ const copyFilesSection = xcodeProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}
169
+ for (const entry of target.buildPhases) {
170
+ const phase = copyFilesSection[normalizeRef(entry.value)]
171
+ if (phase && String(phase.dstSubfolderSpec) === '13') {
172
+ return phase
173
+ }
174
+ }
175
+ return null
176
+ }
153
177
 
154
- // Frameworks build phase
155
- xcodeProject.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', targetUuid, folderType, buildPath)
178
+ /**
179
+ * Finds a target's build phase of the given pbx section type by walking the target's own
180
+ * `buildPhases` list and checking membership in the type's object section; the first match wins.
181
+ *
182
+ * This is deliberately semantic: pbxproj comments are decorative and other tools strip them, and
183
+ * the xcode lib's `buildPhaseObject` both relies on comments and silently falls back to a
184
+ * project-wide search that can return another target's phase (e.g. the main app's "Sources").
185
+ * The embed phase is the one exception with extra semantics — see
186
+ * {@link findExistingEmbedExtensionsPhase} (`dstSubfolderSpec == 13`).
187
+ */
188
+ function findTargetPhaseByType(xcodeProject: XcodeProject, targetUuid: string, phaseType: string): any | null {
189
+ const target = xcodeProject.pbxNativeTargetSection()[targetUuid]
190
+ if (!target?.buildPhases) {
191
+ return null
192
+ }
156
193
 
157
- // Resources build phase
158
- xcodeProject.addBuildPhase([...resourcePaths], 'PBXResourcesBuildPhase', 'Resources', targetUuid)
194
+ const phaseSection = xcodeProject.hash.project.objects[phaseType] || {}
195
+ for (const entry of target.buildPhases) {
196
+ const phase = phaseSection[normalizeRef(entry.value)]
197
+ if (phase) {
198
+ return phase
199
+ }
200
+ }
201
+ return null
159
202
  }
160
203
 
161
204
  /**
162
205
  * Ensures all required build phases and files exist for the widget extension target.
163
206
  */
164
207
  export function ensureBuildPhases(xcodeProject: XcodeProject, options: EnsureBuildPhasesOptions): void {
165
- const { targetUuid, groupName, productFile, widgetFiles } = options
208
+ const { targetUuid, targetName, groupName, productFile, widgetFiles } = options
166
209
  const buildPath = `""`
167
210
  const folderType = 'app_extension'
168
211
  const mainTargetUuid = options.mainTargetUuid ?? xcodeProject.getFirstTarget().uuid
@@ -170,60 +213,66 @@ export function ensureBuildPhases(xcodeProject: XcodeProject, options: EnsureBui
170
213
  const { swiftFiles, intentFiles, assetDirectories, localizedStringResources } = widgetFiles
171
214
  const resourcePaths = [...assetDirectories, ...localizedStringResources]
172
215
 
173
- dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXSourcesBuildPhase', 'Sources')
174
- dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase', 'Frameworks')
175
- dedupeBuildPhasesByComment(xcodeProject, mainTargetUuid, 'PBXCopyFilesBuildPhase', groupName)
216
+ dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
217
+ dedupeBuildPhasesForTarget(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase')
218
+ dedupeEmbedPhasesForTarget(xcodeProject, mainTargetUuid)
176
219
 
177
220
  // Sources build phase
178
- let sourcesPhase = xcodeProject.buildPhaseObject('PBXSourcesBuildPhase', 'Sources', targetUuid)
221
+ let sourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
179
222
  if (!sourcesPhase) {
180
- xcodeProject.addBuildPhase(
181
- [...swiftFiles, ...intentFiles],
182
- 'PBXSourcesBuildPhase',
183
- 'Sources',
184
- targetUuid,
185
- folderType,
186
- buildPath
187
- )
188
- sourcesPhase = xcodeProject.buildPhaseObject('PBXSourcesBuildPhase', 'Sources', targetUuid)
223
+ xcodeProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', targetUuid, folderType, buildPath)
224
+ sourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXSourcesBuildPhase')
189
225
  }
190
226
  if (sourcesPhase) {
191
- ensureBuildPhaseFiles(xcodeProject, sourcesPhase, [...swiftFiles, ...intentFiles])
227
+ ensureBuildPhaseFiles(xcodeProject, sourcesPhase, [...swiftFiles, ...intentFiles], targetName)
228
+ }
229
+
230
+ if (options.mainSwiftFiles && options.mainSwiftFiles.length > 0) {
231
+ let mainSourcesPhase = findTargetPhaseByType(xcodeProject, mainTargetUuid, 'PBXSourcesBuildPhase')
232
+ if (!mainSourcesPhase) {
233
+ xcodeProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', mainTargetUuid, folderType, buildPath)
234
+ mainSourcesPhase = findTargetPhaseByType(xcodeProject, mainTargetUuid, 'PBXSourcesBuildPhase')
235
+ }
236
+ if (mainSourcesPhase) {
237
+ ensureBuildPhaseFiles(xcodeProject, mainSourcesPhase, options.mainSwiftFiles, targetName)
238
+ }
192
239
  }
193
240
 
194
- // Copy files build phase (embed extension into main app)
195
- let copyFilesPhase = xcodeProject.buildPhaseObject('PBXCopyFilesBuildPhase', groupName, mainTargetUuid)
241
+ // Copy files build phase (embed extension into main app) — the embed phase is matched purely by
242
+ // its `dstSubfolderSpec == 13` semantics, both before and after creation
243
+ let copyFilesPhase = findExistingEmbedExtensionsPhase(xcodeProject, mainTargetUuid)
196
244
  if (!copyFilesPhase) {
197
245
  xcodeProject.addBuildPhase([], 'PBXCopyFilesBuildPhase', groupName, mainTargetUuid, folderType, buildPath)
198
- copyFilesPhase = xcodeProject.buildPhaseObject('PBXCopyFilesBuildPhase', groupName, mainTargetUuid)
246
+ copyFilesPhase = findExistingEmbedExtensionsPhase(xcodeProject, mainTargetUuid)
199
247
  }
200
248
  if (copyFilesPhase) {
201
249
  ensureCopyFilesPhaseProduct(xcodeProject, copyFilesPhase, productFile)
202
250
  }
203
251
 
204
252
  // Frameworks build phase
205
- const frameworksPhase = xcodeProject.buildPhaseObject('PBXFrameworksBuildPhase', 'Frameworks', targetUuid)
253
+ const frameworksPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXFrameworksBuildPhase')
206
254
  if (!frameworksPhase) {
207
255
  xcodeProject.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', targetUuid, folderType, buildPath)
208
256
  }
209
257
 
210
258
  // Resources build phase
211
- let resourcesPhase = xcodeProject.buildPhaseObject('PBXResourcesBuildPhase', 'Resources', targetUuid)
259
+ let resourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXResourcesBuildPhase')
212
260
  if (!resourcesPhase) {
213
- xcodeProject.addBuildPhase([...resourcePaths], 'PBXResourcesBuildPhase', 'Resources', targetUuid)
214
- resourcesPhase = xcodeProject.buildPhaseObject('PBXResourcesBuildPhase', 'Resources', targetUuid)
261
+ xcodeProject.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', targetUuid)
262
+ resourcesPhase = findTargetPhaseByType(xcodeProject, targetUuid, 'PBXResourcesBuildPhase')
215
263
  }
216
264
  if (resourcesPhase) {
217
- ensureBuildPhaseFiles(xcodeProject, resourcesPhase, [...resourcePaths])
265
+ ensureBuildPhaseFiles(xcodeProject, resourcesPhase, resourcePaths, targetName)
218
266
  }
219
267
  }
220
268
 
221
- function dedupeBuildPhasesForTarget(
222
- xcodeProject: XcodeProject,
223
- targetUuid: string,
224
- phaseType: string,
225
- preferredComment: string
226
- ): void {
269
+ /**
270
+ * Drops duplicate build phases of one pbx section type from a target, keeping the first phase in
271
+ * the target's `buildPhases` order. Membership in the type's object section is the only criterion —
272
+ * comments are decorative and other tools strip them. De-referenced phase objects are deleted so no
273
+ * orphans remain.
274
+ */
275
+ function dedupeBuildPhasesForTarget(xcodeProject: XcodeProject, targetUuid: string, phaseType: string): void {
227
276
  const nativeTargets = xcodeProject.pbxNativeTargetSection()
228
277
  const target = nativeTargets[targetUuid]
229
278
  if (!target?.buildPhases) {
@@ -231,114 +280,97 @@ function dedupeBuildPhasesForTarget(
231
280
  }
232
281
 
233
282
  const phaseSection = xcodeProject.hash.project.objects[phaseType] || {}
234
- const matching = target.buildPhases.filter((entry: any) => phaseSection[entry.value])
283
+ const matching = target.buildPhases.filter((entry: any) => phaseSection[normalizeRef(entry.value)])
235
284
  if (matching.length <= 1) {
236
285
  return
237
286
  }
238
287
 
239
- const keep = matching.find((entry: any) => entry.comment === preferredComment) ?? matching[0]
240
- keep.comment = preferredComment
241
-
288
+ const keep = matching[0]
289
+ const removed = matching.filter((entry: any) => entry.value !== keep.value)
242
290
  target.buildPhases = target.buildPhases.filter((entry: any) => {
243
- if (!phaseSection[entry.value]) {
291
+ if (!phaseSection[normalizeRef(entry.value)]) {
244
292
  return true
245
293
  }
246
294
  return entry.value === keep.value
247
295
  })
296
+ deleteOrphanPhaseObjects(phaseSection, removed)
248
297
  }
249
298
 
250
- function dedupeBuildPhasesByComment(
251
- xcodeProject: XcodeProject,
252
- targetUuid: string,
253
- phaseType: string,
254
- comment: string
255
- ): void {
299
+ /**
300
+ * Drops duplicate embed-app-extensions copy-files phases (`dstSubfolderSpec == 13`) from a target,
301
+ * keeping the first one in the target's `buildPhases` order. Other copy-files phases (different
302
+ * `dstSubfolderSpec`) are left untouched. De-referenced phase objects are deleted so no orphans
303
+ * remain.
304
+ */
305
+ function dedupeEmbedPhasesForTarget(xcodeProject: XcodeProject, targetUuid: string): void {
256
306
  const nativeTargets = xcodeProject.pbxNativeTargetSection()
257
307
  const target = nativeTargets[targetUuid]
258
308
  if (!target?.buildPhases) {
259
309
  return
260
310
  }
261
311
 
262
- const phaseSection = xcodeProject.hash.project.objects[phaseType] || {}
263
- const matching = target.buildPhases.filter((entry: any) => phaseSection[entry.value] && entry.comment === comment)
312
+ const phaseSection = xcodeProject.hash.project.objects['PBXCopyFilesBuildPhase'] || {}
313
+ const isEmbedEntry = (entry: any) => {
314
+ const phase = phaseSection[normalizeRef(entry.value)]
315
+ return phase && String(phase.dstSubfolderSpec) === '13'
316
+ }
317
+
318
+ const matching = target.buildPhases.filter(isEmbedEntry)
264
319
  if (matching.length <= 1) {
265
320
  return
266
321
  }
267
322
 
268
323
  const keep = matching[0]
324
+ const removed = matching.filter((entry: any) => entry.value !== keep.value)
269
325
  target.buildPhases = target.buildPhases.filter((entry: any) => {
270
- if (!phaseSection[entry.value] || entry.comment !== comment) {
326
+ if (!isEmbedEntry(entry)) {
271
327
  return true
272
328
  }
273
329
  return entry.value === keep.value
274
330
  })
331
+ deleteOrphanPhaseObjects(phaseSection, removed)
275
332
  }
276
333
 
277
- function normalizePath(value: string | undefined): string {
278
- if (!value) {
279
- return ''
280
- }
281
- return value.replace(/^"|"$/g, '')
282
- }
283
-
284
- function findFileReferenceKey(xcodeProject: XcodeProject, filePath: string): string | null {
285
- const fileReferenceSection = xcodeProject.pbxFileReferenceSection()
286
- const normalizedPath = normalizePath(filePath)
287
-
288
- for (const key of Object.keys(fileReferenceSection)) {
289
- if (/_comment$/.test(key)) {
290
- continue
291
- }
292
- const entry = fileReferenceSection[key]
293
- const entryPath = normalizePath(entry?.path)
294
- if (entryPath === normalizedPath) {
295
- return key
296
- }
297
- }
298
-
299
- return null
300
- }
301
-
302
- function findBuildFileKeyByFileRef(xcodeProject: XcodeProject, fileRef: string): string | null {
303
- const buildFileSection = xcodeProject.pbxBuildFileSection()
304
- for (const key of Object.keys(buildFileSection)) {
305
- if (/_comment$/.test(key)) {
306
- continue
307
- }
308
- const entry = buildFileSection[key]
309
- if (entry?.fileRef === fileRef) {
310
- return key
311
- }
334
+ /**
335
+ * Deletes de-referenced build-phase objects (and their `_comment` keys) from a phase section, so
336
+ * dropping duplicate phase refs from a target does not leave orphan objects that fail the
337
+ * `[Xcodeproj]` consistency check.
338
+ */
339
+ function deleteOrphanPhaseObjects(phaseSection: Record<string, any>, removedEntries: any[]): void {
340
+ for (const entry of removedEntries) {
341
+ const key = normalizeRef(entry.value)
342
+ delete phaseSection[key]
343
+ delete phaseSection[`${key}_comment`]
312
344
  }
313
- return null
314
345
  }
315
346
 
316
- function ensureFileReference(xcodeProject: XcodeProject, filePath: string) {
317
- const existingFileRef = findFileReferenceKey(xcodeProject, filePath)
347
+ /**
348
+ * Resolves (or creates) the PBXBuildFile for a widget file within a specific build phase.
349
+ *
350
+ * The lookup is scoped to the given phase's `files`: a PBXBuildFile is only reused when it is
351
+ * already part of THIS phase. This prevents adopting a build file that belongs to another target's
352
+ * phase — the same PBXFileReference (e.g. an asset catalog) may legitimately need its own build
353
+ * file per phase, and sharing one produces the "no parent for object" corruption (issue #87). When
354
+ * no match exists a new PBXBuildFile with a fresh UUID is created.
355
+ */
356
+ function ensureBuildFile(xcodeProject: XcodeProject, filePath: string, buildPhase: any, targetName: string) {
357
+ const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
318
358
  const file = new pbxFile(filePath)
319
359
 
320
- if (existingFileRef) {
321
- return { fileRef: existingFileRef, basename: file.basename, group: file.group }
322
- }
323
-
324
- file.fileRef = xcodeProject.generateUuid()
325
- xcodeProject.addToPbxFileReferenceSection(file)
326
- return { fileRef: file.fileRef, basename: file.basename, group: file.group }
327
- }
328
-
329
- function ensureBuildFile(xcodeProject: XcodeProject, filePath: string) {
330
- const fileReference = ensureFileReference(xcodeProject, filePath)
331
- const existingBuildFile = findBuildFileKeyByFileRef(xcodeProject, fileReference.fileRef)
332
-
333
- if (existingBuildFile) {
334
- return { uuid: existingBuildFile, ...fileReference }
360
+ if (buildPhase?.files) {
361
+ const buildFileSection = xcodeProject.pbxBuildFileSection()
362
+ const existingInPhase = buildPhase.files.find((entry: any) => {
363
+ const buildFile = buildFileSection[normalizeRef(entry.value)]
364
+ return buildFile?.fileRef === fileRef
365
+ })
366
+ if (existingInPhase) {
367
+ return { uuid: normalizeRef(existingInPhase.value), fileRef, basename: file.basename, group: file.group }
368
+ }
335
369
  }
336
370
 
337
- const file = new pbxFile(filePath)
338
- file.uuid = xcodeProject.generateUuid()
339
- file.fileRef = fileReference.fileRef
340
- xcodeProject.addToPbxBuildFileSection(file)
341
- return { uuid: file.uuid, ...fileReference, group: file.group }
371
+ const uuid = xcodeProject.generateUuid()
372
+ xcodeProject.addToPbxBuildFileSection({ uuid, fileRef, basename: file.basename, group: file.group } as any)
373
+ return { uuid, fileRef, basename: file.basename, group: file.group }
342
374
  }
343
375
 
344
376
  function buildPhaseHasFile(xcodeProject: XcodeProject, buildPhase: any, fileRef: string): boolean {
@@ -348,23 +380,28 @@ function buildPhaseHasFile(xcodeProject: XcodeProject, buildPhase: any, fileRef:
348
380
  const buildFileSection = xcodeProject.pbxBuildFileSection()
349
381
 
350
382
  return buildPhase.files.some((entry: any) => {
351
- const buildFile = buildFileSection[entry.value]
383
+ const buildFile = buildFileSection[normalizeRef(entry.value)]
352
384
  return buildFile?.fileRef === fileRef
353
385
  })
354
386
  }
355
387
 
356
- function ensureBuildPhaseFiles(xcodeProject: XcodeProject, buildPhase: any, filePaths: string[]): void {
388
+ function ensureBuildPhaseFiles(
389
+ xcodeProject: XcodeProject,
390
+ buildPhase: any,
391
+ filePaths: string[],
392
+ targetName: string
393
+ ): void {
357
394
  if (!buildPhase.files) {
358
395
  buildPhase.files = []
359
396
  }
360
397
 
361
398
  for (const filePath of filePaths) {
362
- const fileReference = ensureFileReference(xcodeProject, filePath)
363
- if (buildPhaseHasFile(xcodeProject, buildPhase, fileReference.fileRef)) {
399
+ const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
400
+ if (buildPhaseHasFile(xcodeProject, buildPhase, fileRef)) {
364
401
  continue
365
402
  }
366
403
 
367
- const buildFile = ensureBuildFile(xcodeProject, filePath)
404
+ const buildFile = ensureBuildFile(xcodeProject, filePath, buildPhase, targetName)
368
405
  buildPhase.files.push({
369
406
  value: buildFile.uuid,
370
407
  comment: util.format('%s in %s', buildFile.basename, buildFile.group),
@@ -377,18 +414,27 @@ function ensureCopyFilesPhaseProduct(xcodeProject: XcodeProject, buildPhase: any
377
414
  buildPhase.files = []
378
415
  }
379
416
 
380
- const alreadyExists = buildPhase.files.some((entry: any) => entry.value === productFile.uuid)
381
- if (alreadyExists) {
417
+ const buildFileSection = xcodeProject.pbxBuildFileSection()
418
+ const alreadyInPhase = buildPhase.files.some((entry: any) => {
419
+ const buildFile = buildFileSection[normalizeRef(entry.value)]
420
+ return buildFile?.fileRef === productFile.fileRef
421
+ })
422
+ if (alreadyInPhase) {
382
423
  return
383
424
  }
384
425
 
385
- const buildFileSection = xcodeProject.pbxBuildFileSection()
386
- if (!buildFileSection[productFile.uuid]) {
426
+ // Reuse the product's build-file UUID only if it isn't already registered by another phase;
427
+ // otherwise mint a fresh UUID so the two phases never share one PBXBuildFile (issue #87).
428
+ let useUuid = productFile.uuid
429
+ if (buildFileSection[productFile.uuid]) {
430
+ useUuid = xcodeProject.generateUuid()
431
+ xcodeProject.addToPbxBuildFileSection({ ...productFile, uuid: useUuid })
432
+ } else {
387
433
  xcodeProject.addToPbxBuildFileSection(productFile)
388
434
  }
389
435
 
390
436
  buildPhase.files.push({
391
- value: productFile.uuid,
437
+ value: useUuid,
392
438
  comment: util.format('%s in %s', productFile.basename, productFile.group),
393
439
  })
394
440
  }