@use-voltra/ios-client 2.1.0 → 2.2.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 (131) hide show
  1. package/README.md +61 -8
  2. package/build/commonjs/dynamic-widget/api.js +15 -0
  3. package/build/commonjs/dynamic-widget/api.js.map +1 -0
  4. package/build/commonjs/index.js +15 -7
  5. package/build/commonjs/index.js.map +1 -1
  6. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  7. package/build/module/dynamic-widget/api.js +10 -0
  8. package/build/module/dynamic-widget/api.js.map +1 -0
  9. package/build/module/index.js +1 -0
  10. package/build/module/index.js.map +1 -1
  11. package/build/module/native/NativeVoltra.js.map +1 -1
  12. package/build/typescript/commonjs/dynamic-widget/api.d.ts +8 -0
  13. package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -0
  14. package/build/typescript/commonjs/index.d.ts +1 -0
  15. package/build/typescript/commonjs/index.d.ts.map +1 -1
  16. package/build/typescript/commonjs/native/NativeVoltra.d.ts +1 -0
  17. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  18. package/build/typescript/module/dynamic-widget/api.d.ts +8 -0
  19. package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -0
  20. package/build/typescript/module/index.d.ts +1 -0
  21. package/build/typescript/module/index.d.ts.map +1 -1
  22. package/build/typescript/module/native/NativeVoltra.d.ts +1 -0
  23. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  24. package/expo-plugin/build/cjs/index.js +3 -1
  25. package/expo-plugin/build/cjs/index.js.map +1 -1
  26. package/expo-plugin/build/cjs/ios-widget/index.js +1 -1
  27. package/expo-plugin/build/cjs/ios-widget/index.js.map +1 -1
  28. package/expo-plugin/build/cjs/ios-widget/podfile.js +38 -12
  29. package/expo-plugin/build/cjs/ios-widget/podfile.js.map +1 -1
  30. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js +143 -107
  31. package/expo-plugin/build/cjs/ios-widget/xcode/buildPhases.js.map +1 -1
  32. package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js +30 -22
  33. package/expo-plugin/build/cjs/ios-widget/xcode/configurationList.js.map +1 -1
  34. package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js +50 -0
  35. package/expo-plugin/build/cjs/ios-widget/xcode/fileReferences.js.map +1 -0
  36. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +45 -43
  37. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
  38. package/expo-plugin/build/cjs/ios-widget/xcode/index.js +77 -91
  39. package/expo-plugin/build/cjs/ios-widget/xcode/index.js.map +1 -1
  40. package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js +41 -39
  41. package/expo-plugin/build/cjs/ios-widget/xcode/mainAppSettings.js.map +1 -1
  42. package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js +2 -2
  43. package/expo-plugin/build/cjs/ios-widget/xcode/productFile.js.map +1 -1
  44. package/expo-plugin/build/cjs/ios-widget/xcode/target.js +29 -63
  45. package/expo-plugin/build/cjs/ios-widget/xcode/target.js.map +1 -1
  46. package/expo-plugin/build/esm/index.js +3 -1
  47. package/expo-plugin/build/esm/index.js.map +1 -1
  48. package/expo-plugin/build/esm/ios-widget/index.js +1 -1
  49. package/expo-plugin/build/esm/ios-widget/index.js.map +1 -1
  50. package/expo-plugin/build/esm/ios-widget/podfile.js +36 -11
  51. package/expo-plugin/build/esm/ios-widget/podfile.js.map +1 -1
  52. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js +143 -106
  53. package/expo-plugin/build/esm/ios-widget/xcode/buildPhases.js.map +1 -1
  54. package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js +31 -22
  55. package/expo-plugin/build/esm/ios-widget/xcode/configurationList.js.map +1 -1
  56. package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js +45 -0
  57. package/expo-plugin/build/esm/ios-widget/xcode/fileReferences.js.map +1 -0
  58. package/expo-plugin/build/esm/ios-widget/xcode/groups.js +45 -42
  59. package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
  60. package/expo-plugin/build/esm/ios-widget/xcode/index.js +81 -96
  61. package/expo-plugin/build/esm/ios-widget/xcode/index.js.map +1 -1
  62. package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js +41 -39
  63. package/expo-plugin/build/esm/ios-widget/xcode/mainAppSettings.js.map +1 -1
  64. package/expo-plugin/build/esm/ios-widget/xcode/productFile.js +3 -2
  65. package/expo-plugin/build/esm/ios-widget/xcode/productFile.js.map +1 -1
  66. package/expo-plugin/build/esm/ios-widget/xcode/target.js +28 -62
  67. package/expo-plugin/build/esm/ios-widget/xcode/target.js.map +1 -1
  68. package/expo-plugin/build/types/index.d.ts.map +1 -1
  69. package/expo-plugin/build/types/ios-widget/podfile.d.ts +19 -1
  70. package/expo-plugin/build/types/ios-widget/podfile.d.ts.map +1 -1
  71. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts +3 -7
  72. package/expo-plugin/build/types/ios-widget/xcode/buildPhases.d.ts.map +1 -1
  73. package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts +15 -10
  74. package/expo-plugin/build/types/ios-widget/xcode/configurationList.d.ts.map +1 -1
  75. package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts +16 -0
  76. package/expo-plugin/build/types/ios-widget/xcode/fileReferences.d.ts.map +1 -0
  77. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts +0 -4
  78. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
  79. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts +22 -3
  80. package/expo-plugin/build/types/ios-widget/xcode/index.d.ts.map +1 -1
  81. package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts +16 -4
  82. package/expo-plugin/build/types/ios-widget/xcode/mainAppSettings.d.ts.map +1 -1
  83. package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts +0 -4
  84. package/expo-plugin/build/types/ios-widget/xcode/productFile.d.ts.map +1 -1
  85. package/expo-plugin/build/types/ios-widget/xcode/target.d.ts +11 -26
  86. package/expo-plugin/build/types/ios-widget/xcode/target.d.ts.map +1 -1
  87. package/expo-plugin/src/index.ts +5 -1
  88. package/expo-plugin/src/ios-widget/index.ts +1 -1
  89. package/expo-plugin/src/ios-widget/podfile.ts +46 -13
  90. package/expo-plugin/src/ios-widget/xcode/__fixtures__/fresh.pbxproj +197 -0
  91. package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-existing-extension.pbxproj +321 -0
  92. package/expo-plugin/src/ios-widget/xcode/__fixtures__/with-per-config-signing.pbxproj +203 -0
  93. package/expo-plugin/src/ios-widget/xcode/__snapshots__/applyXcodeChanges.node.test.ts.snap +357 -0
  94. package/expo-plugin/src/ios-widget/xcode/__tests__/pbxConsistency.ts +130 -0
  95. package/expo-plugin/src/ios-widget/xcode/buildPhases.ts +151 -152
  96. package/expo-plugin/src/ios-widget/xcode/configurationList.ts +52 -35
  97. package/expo-plugin/src/ios-widget/xcode/fileReferences.ts +51 -0
  98. package/expo-plugin/src/ios-widget/xcode/groups.ts +52 -54
  99. package/expo-plugin/src/ios-widget/xcode/index.ts +107 -119
  100. package/expo-plugin/src/ios-widget/xcode/mainAppSettings.ts +59 -54
  101. package/expo-plugin/src/ios-widget/xcode/productFile.ts +3 -2
  102. package/expo-plugin/src/ios-widget/xcode/target.ts +31 -83
  103. package/ios/Package.swift +7 -1
  104. package/ios/app/NativeVoltra.mm +19 -0
  105. package/ios/app/VoltraModule.swift +18 -0
  106. package/ios/app/VoltraModuleImpl.swift +16 -0
  107. package/ios/shared/DynamicWidgetPropsCodec.swift +51 -0
  108. package/ios/shared/DynamicWidgetPropsStorage.swift +53 -0
  109. package/ios/shared/DynamicWidgetPropsStore.swift +50 -0
  110. package/ios/shared/DynamicWidgetRenderCoordinator.swift +31 -0
  111. package/ios/shared/DynamicWidgetUpdater.swift +30 -0
  112. package/ios/shared/VoltraConstants.swift +4 -0
  113. package/ios/shared/VoltraJSRenderer.swift +3 -1
  114. package/ios/shared/VoltraNode.swift +6 -2
  115. package/ios/shared/VoltraWidgetDefaults.swift +9 -1
  116. package/ios/target/VoltraClientWidgetRuntime.swift +17 -5
  117. package/ios/target/VoltraHomeWidget.swift +6 -2
  118. package/ios/target/VoltraWidget.swift +3 -1
  119. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +3 -1
  120. package/ios/ui/Helpers/VoltraProgressDriver.swift +6 -2
  121. package/ios/ui/Layout/VoltraFlexStackLayout.swift +9 -3
  122. package/ios/ui/Style/JSColorParser.swift +24 -8
  123. package/ios/ui/Style/JSGradientParser.swift +36 -12
  124. package/ios/ui/Style/JSStyleParser.swift +36 -12
  125. package/ios/ui/Style/LayoutStyle.swift +12 -4
  126. package/ios/ui/Views/VoltraChart.swift +52 -16
  127. package/ios/ui/Views/VoltraLinearGradient.swift +6 -2
  128. package/package.json +5 -5
  129. package/src/dynamic-widget/api.ts +25 -0
  130. package/src/index.ts +1 -0
  131. package/src/native/NativeVoltra.ts +1 -0
@@ -1,6 +1,7 @@
1
1
  import { XcodeProject } from '@expo/config-plugins'
2
2
 
3
3
  import type { IOSWidgetExtensionFiles } from '../../types'
4
+ import { ensureWidgetFileReference } from './fileReferences'
4
5
 
5
6
  const pbxFile = require('xcode/lib/pbxFile')
6
7
 
@@ -9,37 +10,63 @@ export interface AddPbxGroupOptions {
9
10
  widgetFiles: IOSWidgetExtensionFiles
10
11
  }
11
12
 
13
+ function collectGroupFiles(widgetFiles: IOSWidgetExtensionFiles): string[] {
14
+ const { swiftFiles, intentFiles, assetDirectories, entitlementFiles, plistFiles, localizedStringResources } =
15
+ widgetFiles
16
+ return [
17
+ ...swiftFiles,
18
+ ...intentFiles,
19
+ ...entitlementFiles,
20
+ ...plistFiles,
21
+ ...assetDirectories,
22
+ ...localizedStringResources,
23
+ ]
24
+ }
25
+
12
26
  /**
13
27
  * Adds a PBXGroup for the widget extension files.
28
+ *
29
+ * Constructs the PBXGroup and its PBXFileReference children by hand rather than delegating to
30
+ * `xcodeProject.addPbxGroup`, because that helper also creates a PBXBuildFile per file as a side
31
+ * effect. Those extra build files collide with the ones managed by the build-phase functions and
32
+ * produce the "no parent for object" consistency errors from issues #87/#32. File references are
33
+ * resolved through the widget-scoped {@link ensureWidgetFileReference} so they are shared with the
34
+ * build phases instead of duplicated.
35
+ *
36
+ * Internal create-only fallback for {@link ensurePbxGroup}; not part of the public pipeline.
14
37
  */
15
- export function addPbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupOptions): void {
38
+ function addPbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupOptions): void {
16
39
  const { targetName, widgetFiles } = options
17
- const { swiftFiles, intentFiles, assetDirectories, entitlementFiles, plistFiles, localizedStringResources } =
18
- widgetFiles
40
+ const allFiles = collectGroupFiles(widgetFiles)
19
41
 
20
- // Add PBX group with all widget files
21
- const { uuid: pbxGroupUuid } = xcodeProject.addPbxGroup(
22
- [
23
- ...swiftFiles,
24
- ...intentFiles,
25
- ...entitlementFiles,
26
- ...plistFiles,
27
- ...assetDirectories,
28
- ...localizedStringResources,
29
- ],
30
- targetName,
31
- targetName
32
- )
42
+ const pbxGroupUuid = xcodeProject.generateUuid()
43
+ const pbxGroup = {
44
+ isa: 'PBXGroup',
45
+ children: [] as { value: string; comment: string }[],
46
+ name: targetName,
47
+ path: targetName,
48
+ sourceTree: '"<group>"',
49
+ }
33
50
 
34
- // Add PBXGroup to top level group
35
51
  const groups = xcodeProject.hash.project.objects['PBXGroup']
36
- if (pbxGroupUuid) {
37
- Object.keys(groups).forEach(function (key) {
38
- if (groups[key].name === undefined && groups[key].path === undefined) {
39
- xcodeProject.addToPbxGroup(pbxGroupUuid, key)
40
- }
41
- })
52
+ groups[pbxGroupUuid] = pbxGroup
53
+ groups[`${pbxGroupUuid}_comment`] = targetName
54
+
55
+ for (const filePath of allFiles) {
56
+ const file = new pbxFile(filePath)
57
+ const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
58
+ pbxGroup.children.push({ value: fileRef, comment: file.basename })
42
59
  }
60
+
61
+ // Attach the new group to the top-level (unnamed, path-less) group.
62
+ Object.keys(groups).forEach(function (key) {
63
+ if (/_comment$/.test(key)) {
64
+ return
65
+ }
66
+ if (groups[key].name === undefined && groups[key].path === undefined) {
67
+ xcodeProject.addToPbxGroup(pbxGroupUuid, key)
68
+ }
69
+ })
43
70
  }
44
71
 
45
72
  /**
@@ -47,16 +74,7 @@ export function addPbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupOpti
47
74
  */
48
75
  export function ensurePbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupOptions): void {
49
76
  const { targetName, widgetFiles } = options
50
- const { swiftFiles, intentFiles, assetDirectories, entitlementFiles, plistFiles, localizedStringResources } =
51
- widgetFiles
52
- const allFiles = [
53
- ...swiftFiles,
54
- ...intentFiles,
55
- ...entitlementFiles,
56
- ...plistFiles,
57
- ...assetDirectories,
58
- ...localizedStringResources,
59
- ]
77
+ const allFiles = collectGroupFiles(widgetFiles)
60
78
 
61
79
  const existingGroup = xcodeProject.pbxGroupByName(targetName)
62
80
  if (!existingGroup) {
@@ -70,7 +88,7 @@ export function ensurePbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupO
70
88
 
71
89
  for (const filePath of allFiles) {
72
90
  const file = new pbxFile(filePath)
73
- const fileRef = ensureFileReference(xcodeProject, filePath)
91
+ const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
74
92
  const alreadyInGroup = existingGroup.children.some(
75
93
  (child: any) => child.value === fileRef || child.comment === file.basename
76
94
  )
@@ -79,23 +97,3 @@ export function ensurePbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupO
79
97
  }
80
98
  }
81
99
  }
82
-
83
- function ensureFileReference(xcodeProject: XcodeProject, filePath: string): string {
84
- const fileReferenceSection = xcodeProject.pbxFileReferenceSection()
85
- const file = new pbxFile(filePath)
86
-
87
- for (const key of Object.keys(fileReferenceSection)) {
88
- if (/_comment$/.test(key)) {
89
- continue
90
- }
91
- const entry = fileReferenceSection[key]
92
- const entryPath = typeof entry?.path === 'string' ? entry.path.replace(/^"|"$/g, '') : ''
93
- if (entryPath === file.path || entryPath === filePath) {
94
- return key
95
- }
96
- }
97
-
98
- file.fileRef = xcodeProject.generateUuid()
99
- xcodeProject.addToPbxFileReferenceSection(file)
100
- return file.fileRef
101
- }
@@ -1,23 +1,118 @@
1
- import { ConfigPlugin, withXcodeProject } from '@expo/config-plugins'
1
+ import { ConfigPlugin, withXcodeProject, XcodeProject } from '@expo/config-plugins'
2
2
  import * as path from 'path'
3
3
 
4
- import type { IOSWidgetConfig } from '../../types'
4
+ import type { IOSWidgetConfig, IOSWidgetExtensionFiles } from '../../types'
5
5
  import { getIOSWidgetExtensionFiles } from '../../utils/fileDiscovery'
6
6
  import { detectClientRenderedWidgets } from '../clientRendered'
7
- import { addBuildPhases, ensureBuildPhases, ensureWidgetBundleScriptPhase } from './buildPhases'
8
- import { addXCConfigurationList, ensureXCConfigurationList } from './configurationList'
9
- import { addPbxGroup, ensurePbxGroup } from './groups'
7
+ import { ensureBuildPhases, ensureWidgetBundleScriptPhase } from './buildPhases'
8
+ import { ensureXCConfigurationList } from './configurationList'
9
+ import { ensurePbxGroup } from './groups'
10
10
  import { getMainAppTargetSettings } from './mainAppSettings'
11
- import { addProductFile, ensureProductFile } from './productFile'
12
- import { configureTarget, ensureTargetAttributes, ensureTargetDependency } from './target'
11
+ import { ensureProductFile } from './productFile'
12
+ import { createTargetSkeleton, ensureTargetAttributes, ensureTargetDependency } from './target'
13
13
 
14
14
  export interface ConfigureXcodeProjectProps {
15
15
  targetName: string
16
16
  bundleIdentifier: string
17
17
  deploymentTarget: string
18
+ /** App marketing version; becomes the widget's MARKETING_VERSION so the appex matches the app. */
19
+ version?: string
20
+ /** App build number; becomes the widget's CURRENT_PROJECT_VERSION. */
21
+ buildNumber?: string
18
22
  widgets?: IOSWidgetConfig[]
19
23
  }
20
24
 
25
+ /**
26
+ * Applies all widget-extension changes to an already-parsed Xcode project.
27
+ *
28
+ * This is the pure core of {@link configureXcodeProject}: it takes the parsed project, the plugin
29
+ * props, the discovered widget files, and whether any widget is client-rendered, then mutates the
30
+ * project in place. It performs no filesystem or `modRequest` access, which makes it directly
31
+ * testable against pbxproj fixtures.
32
+ *
33
+ * When the widget target does not exist yet, the minimal native-target skeleton is created first
34
+ * (targets cannot be "ensured" into existence); every other object is then reconciled by the same
35
+ * idempotent ensure pipeline that runs for an already-existing target.
36
+ */
37
+ export function applyXcodeChanges(
38
+ xcodeProject: XcodeProject,
39
+ props: ConfigureXcodeProjectProps,
40
+ widgetFiles: IOSWidgetExtensionFiles,
41
+ hasClientRenderedWidgets = false
42
+ ): void {
43
+ const { targetName, bundleIdentifier, deploymentTarget, version, buildNumber } = props
44
+ const groupName = 'Embed Foundation Extensions'
45
+
46
+ // Read main app target settings to synchronize code signing (per configuration).
47
+ const mainAppSettings = getMainAppTargetSettings(xcodeProject)
48
+
49
+ // Use the deploymentTarget from plugin config (or default), ignore main app's deployment target.
50
+ // This allows the widget extension to have its own deployment target independent of the main app.
51
+
52
+ // Resolve (or create) the widget target. A target cannot be reconciled into existence, so the
53
+ // absent case creates the minimal skeleton; from here on both paths share one pipeline.
54
+ let targetUuid = xcodeProject.findTargetKey(targetName)
55
+ if (!targetUuid) {
56
+ targetUuid = xcodeProject.generateUuid()
57
+ createTargetSkeleton(xcodeProject, { targetName, targetUuid })
58
+ }
59
+ const target = xcodeProject.pbxNativeTargetSection()[targetUuid]
60
+
61
+ // Ensure the configuration list is present and its settings are in sync.
62
+ const xCConfigurationList = ensureXCConfigurationList(
63
+ xcodeProject,
64
+ {
65
+ targetName,
66
+ bundleIdentifier,
67
+ deploymentTarget,
68
+ version,
69
+ buildNumber,
70
+ mainAppSettings,
71
+ },
72
+ target.buildConfigurationList
73
+ )
74
+
75
+ // Ensure the product file (.appex) exists.
76
+ const productFile = ensureProductFile(xcodeProject, {
77
+ targetName,
78
+ groupName,
79
+ })
80
+
81
+ // Point the target at the reconciled configuration list and product file.
82
+ target.productReference = productFile.fileRef
83
+ target.buildConfigurationList = xCConfigurationList.uuid
84
+ target.productType = `"com.apple.product-type.app-extension"`
85
+ target.name = targetName
86
+ target.productName = targetName
87
+ if (!target.buildPhases) {
88
+ target.buildPhases = []
89
+ }
90
+
91
+ // Ensure the group first so the widget's file references exist before the build phases
92
+ // reference them (the phases resolve files through the widget-scoped group).
93
+ ensurePbxGroup(xcodeProject, {
94
+ targetName,
95
+ widgetFiles,
96
+ })
97
+
98
+ // Ensure build phases and their files.
99
+ ensureBuildPhases(xcodeProject, {
100
+ targetUuid,
101
+ targetName,
102
+ groupName,
103
+ productFile,
104
+ widgetFiles,
105
+ mainTargetUuid: xcodeProject.getFirstTarget().uuid,
106
+ })
107
+
108
+ if (hasClientRenderedWidgets) {
109
+ ensureWidgetBundleScriptPhase(xcodeProject, targetUuid)
110
+ }
111
+
112
+ ensureTargetAttributes(xcodeProject, targetUuid)
113
+ ensureTargetDependency(xcodeProject, targetUuid)
114
+ }
115
+
21
116
  /**
22
117
  * Plugin step that configures the Xcode project for the widget extension.
23
118
  *
@@ -28,10 +123,12 @@ export interface ConfigureXcodeProjectProps {
28
123
  * - Adds build phases (Sources, CopyFiles, Frameworks, Resources)
29
124
  * - Adds PBXGroup for widget files
30
125
  *
31
- * This should run after generateWidgetExtensionFiles so the files exist.
126
+ * This should run after generateWidgetExtensionFiles so the files exist. The wrapper is kept thin:
127
+ * it discovers widget files and detects client-rendered widgets, then delegates the actual pbxproj
128
+ * mutation to {@link applyXcodeChanges}.
32
129
  */
33
130
  export const configureXcodeProject: ConfigPlugin<ConfigureXcodeProjectProps> = (config, props) => {
34
- const { targetName, bundleIdentifier, deploymentTarget, widgets } = props
131
+ const { targetName, widgets } = props
35
132
 
36
133
  return withXcodeProject(config, (config) => {
37
134
  if (config.modRequest.introspect) {
@@ -39,7 +136,6 @@ export const configureXcodeProject: ConfigPlugin<ConfigureXcodeProjectProps> = (
39
136
  }
40
137
 
41
138
  const xcodeProject = config.modResults
42
- const groupName = 'Embed Foundation Extensions'
43
139
 
44
140
  // The release widget-bundling phase is only needed when a widget is a Dynamic Widget (server
45
141
  // widgets carry no JS bundle). Detect once so both the create and update paths agree.
@@ -47,119 +143,11 @@ export const configureXcodeProject: ConfigPlugin<ConfigureXcodeProjectProps> = (
47
143
  !!widgets &&
48
144
  detectClientRenderedWidgets(widgets, config.modRequest.projectRoot).some((widget) => widget.clientRendered)
49
145
 
50
- // Check if target already exists
51
- const nativeTargets = xcodeProject.pbxNativeTargetSection()
52
- const existingTargetKey = xcodeProject.findTargetKey(targetName)
53
- const existingTarget = existingTargetKey ? nativeTargets[existingTargetKey] : null
54
-
55
146
  const { platformProjectRoot } = config.modRequest
56
147
  const targetPath = path.join(platformProjectRoot, targetName)
57
148
  const widgetFiles = getIOSWidgetExtensionFiles(targetPath, targetName)
58
149
 
59
- // Read main app target settings to synchronize code signing
60
- const mainAppSettings = getMainAppTargetSettings(xcodeProject)
61
-
62
- // Use the deploymentTarget from plugin config (or default), ignore main app's deployment target
63
- // This allows the widget extension to have its own deployment target independent of the main app
64
-
65
- if (existingTarget && existingTargetKey) {
66
- // Ensure configuration list is up to date
67
- const xCConfigurationList = ensureXCConfigurationList(
68
- xcodeProject,
69
- {
70
- targetName,
71
- bundleIdentifier,
72
- deploymentTarget,
73
- codeSignStyle: mainAppSettings?.codeSignStyle,
74
- developmentTeam: mainAppSettings?.developmentTeam,
75
- provisioningProfileSpecifier: mainAppSettings?.provisioningProfileSpecifier,
76
- },
77
- existingTarget.buildConfigurationList
78
- )
79
-
80
- // Ensure product file exists
81
- const productFile = ensureProductFile(xcodeProject, {
82
- targetName,
83
- groupName,
84
- })
85
-
86
- // Update target references
87
- existingTarget.productReference = productFile.fileRef
88
- existingTarget.buildConfigurationList = xCConfigurationList.uuid
89
- existingTarget.productType = `"com.apple.product-type.app-extension"`
90
- existingTarget.name = targetName
91
- existingTarget.productName = targetName
92
- if (!existingTarget.buildPhases) {
93
- existingTarget.buildPhases = []
94
- }
95
-
96
- // Ensure build phases and groups
97
- ensureBuildPhases(xcodeProject, {
98
- targetUuid: existingTargetKey,
99
- groupName,
100
- productFile,
101
- widgetFiles,
102
- mainTargetUuid: xcodeProject.getFirstTarget().uuid,
103
- })
104
-
105
- if (hasClientRenderedWidgets) {
106
- ensureWidgetBundleScriptPhase(xcodeProject, existingTargetKey)
107
- }
108
-
109
- ensurePbxGroup(xcodeProject, {
110
- targetName,
111
- widgetFiles,
112
- })
113
-
114
- ensureTargetAttributes(xcodeProject, existingTargetKey)
115
- ensureTargetDependency(xcodeProject, existingTargetKey)
116
-
117
- return config
118
- }
119
-
120
- // Add configuration list
121
- const targetUuid = xcodeProject.generateUuid()
122
-
123
- const xCConfigurationList = addXCConfigurationList(xcodeProject, {
124
- targetName,
125
- bundleIdentifier,
126
- deploymentTarget,
127
- codeSignStyle: mainAppSettings?.codeSignStyle,
128
- developmentTeam: mainAppSettings?.developmentTeam,
129
- provisioningProfileSpecifier: mainAppSettings?.provisioningProfileSpecifier,
130
- })
131
-
132
- // Add product file
133
- const productFile = addProductFile(xcodeProject, {
134
- targetName,
135
- groupName,
136
- })
137
-
138
- // Configure target (native target, project section, dependency)
139
- configureTarget(xcodeProject, {
140
- targetName,
141
- targetUuid,
142
- productFile,
143
- xCConfigurationList,
144
- })
145
-
146
- // Add build phases
147
- addBuildPhases(xcodeProject, {
148
- targetUuid,
149
- groupName,
150
- productFile,
151
- widgetFiles,
152
- })
153
-
154
- if (hasClientRenderedWidgets) {
155
- ensureWidgetBundleScriptPhase(xcodeProject, targetUuid)
156
- }
157
-
158
- // Add PBX group
159
- addPbxGroup(xcodeProject, {
160
- targetName,
161
- widgetFiles,
162
- })
150
+ applyXcodeChanges(xcodeProject, props, widgetFiles, hasClientRenderedWidgets)
163
151
 
164
152
  return config
165
153
  })
@@ -1,27 +1,55 @@
1
1
  import { XcodeProject } from '@expo/config-plugins'
2
2
 
3
- export interface MainAppTargetSettings {
4
- deploymentTarget: string
3
+ /** Code-signing settings read from one of the main app target's build configurations. */
4
+ export interface MainAppSigningSettings {
5
5
  codeSignStyle?: string
6
6
  developmentTeam?: string
7
7
  provisioningProfileSpecifier?: string
8
8
  }
9
9
 
10
+ export interface MainAppTargetSettings {
11
+ /**
12
+ * Signing settings keyed by build-configuration name (e.g. `Debug`, `Release`). Release
13
+ * provisioning often differs from Debug, so the widget must mirror the main app per
14
+ * configuration rather than copying one configuration's signing into both.
15
+ */
16
+ byConfigurationName: Record<string, MainAppSigningSettings>
17
+ /** The first configuration's settings, used for widget configurations with no name match. */
18
+ fallback: MainAppSigningSettings
19
+ }
20
+
21
+ /** Strips surrounding quotes from a raw build-setting value. */
22
+ function unquote(value: unknown): string | undefined {
23
+ if (typeof value !== 'string') {
24
+ return undefined
25
+ }
26
+ return value.replace(/^"|"$/g, '')
27
+ }
28
+
29
+ /** Reads the signing-related build settings from a single XCBuildConfiguration. */
30
+ function readSigningSettings(buildSettings: Record<string, unknown>): MainAppSigningSettings {
31
+ return {
32
+ codeSignStyle: unquote(buildSettings.CODE_SIGN_STYLE),
33
+ developmentTeam: unquote(buildSettings.DEVELOPMENT_TEAM),
34
+ provisioningProfileSpecifier: unquote(buildSettings.PROVISIONING_PROFILE_SPECIFIER),
35
+ }
36
+ }
37
+
10
38
  /**
11
- * Reads build settings from the main app target to synchronize code signing with the widget extension.
39
+ * Reads code-signing build settings from every build configuration of the main app target, so the
40
+ * widget extension can synchronize signing per configuration (Debug→Debug, Release→Release).
12
41
  *
13
42
  * @param xcodeProject The Xcode project instance
14
- * @returns The main app target's build settings, or null if not found
43
+ * @returns Signing settings keyed by configuration name plus a first-configuration fallback, or
44
+ * null when the main app target or its configurations cannot be resolved
15
45
  */
16
46
  export function getMainAppTargetSettings(xcodeProject: XcodeProject): MainAppTargetSettings | null {
17
47
  try {
18
48
  const mainAppTarget = xcodeProject.getFirstTarget()?.firstTarget
19
-
20
49
  if (!mainAppTarget) {
21
50
  return null
22
51
  }
23
52
 
24
- // Get the build configuration list for the main app target
25
53
  const buildConfigurationListId = mainAppTarget.buildConfigurationList
26
54
  if (!buildConfigurationListId) {
27
55
  return null
@@ -32,62 +60,39 @@ export function getMainAppTargetSettings(xcodeProject: XcodeProject): MainAppTar
32
60
 
33
61
  const buildConfigurationList =
34
62
  xcodeProject.hash.project.objects['XCConfigurationList']?.[buildConfigurationListUuid]
35
- if (!buildConfigurationList || !buildConfigurationList.buildConfigurations) {
36
- return null
37
- }
38
-
39
- // Get the first build configuration (usually Debug) to read settings
40
- const firstConfigRef = buildConfigurationList.buildConfigurations[0]
41
- if (!firstConfigRef) {
42
- return null
43
- }
44
-
45
- // Extract UUID from config reference (it might include a comment)
46
- const firstConfigUuid =
47
- typeof firstConfigRef === 'string' ? firstConfigRef.split(' ')[0] : firstConfigRef.value?.split(' ')[0]
48
- if (!firstConfigUuid) {
63
+ if (!buildConfigurationList?.buildConfigurations) {
49
64
  return null
50
65
  }
51
66
 
52
- const buildConfiguration = xcodeProject.hash.project.objects['XCBuildConfiguration']?.[firstConfigUuid]
53
- if (!buildConfiguration || !buildConfiguration.buildSettings) {
54
- return null
67
+ const buildConfigurationSection = xcodeProject.hash.project.objects['XCBuildConfiguration'] ?? {}
68
+ const byConfigurationName: Record<string, MainAppSigningSettings> = {}
69
+ let fallback: MainAppSigningSettings | null = null
70
+
71
+ for (const configRef of buildConfigurationList.buildConfigurations) {
72
+ // Extract UUID from the config reference (it might include a comment)
73
+ const configUuid = typeof configRef === 'string' ? configRef.split(' ')[0] : configRef.value?.split(' ')[0]
74
+ if (!configUuid) {
75
+ continue
76
+ }
77
+
78
+ const buildConfiguration = buildConfigurationSection[configUuid]
79
+ if (!buildConfiguration?.buildSettings) {
80
+ continue
81
+ }
82
+
83
+ const settings = readSigningSettings(buildConfiguration.buildSettings)
84
+ const configName = unquote(buildConfiguration.name)
85
+ if (configName && !(configName in byConfigurationName)) {
86
+ byConfigurationName[configName] = settings
87
+ }
88
+ fallback = fallback ?? settings
55
89
  }
56
90
 
57
- const buildSettings = buildConfiguration.buildSettings
58
-
59
- // Extract deployment target (remove quotes if present)
60
- const deploymentTarget =
61
- buildSettings.IPHONEOS_DEPLOYMENT_TARGET?.replace(/^"|"$/g, '') ||
62
- buildSettings['IPHONEOS_DEPLOYMENT_TARGET']?.replace(/^"|"$/g, '') ||
63
- null
64
-
65
- // Extract code signing settings
66
- const codeSignStyle =
67
- buildSettings.CODE_SIGN_STYLE?.replace(/^"|"$/g, '') ||
68
- buildSettings['CODE_SIGN_STYLE']?.replace(/^"|"$/g, '') ||
69
- null
70
-
71
- const developmentTeam =
72
- buildSettings.DEVELOPMENT_TEAM?.replace(/^"|"$/g, '') ||
73
- buildSettings['DEVELOPMENT_TEAM']?.replace(/^"|"$/g, '') ||
74
- null
75
-
76
- const provisioningProfileSpecifier =
77
- buildSettings.PROVISIONING_PROFILE_SPECIFIER?.replace(/^"|"$/g, '') ||
78
- buildSettings['PROVISIONING_PROFILE_SPECIFIER']?.replace(/^"|"$/g, '') ||
79
- null
80
-
81
- if (!deploymentTarget) {
91
+ if (!fallback) {
82
92
  return null
83
93
  }
84
94
 
85
- return {
86
- deploymentTarget,
87
- codeSignStyle: codeSignStyle || undefined,
88
- developmentTeam: developmentTeam || undefined,
89
- provisioningProfileSpecifier: provisioningProfileSpecifier || undefined,
90
- }
95
+ return { byConfigurationName, fallback }
91
96
  } catch {
92
97
  // If we can't read the settings, return null and use fallback values
93
98
  return null
@@ -42,9 +42,10 @@ function findBuildFileForFileRef(xcodeProject: XcodeProject, fileRef: string): {
42
42
  }
43
43
 
44
44
  /**
45
- * Adds the product file (.appex) for the widget extension.
45
+ * Adds the product file (.appex) for the widget extension. Internal create-only fallback for
46
+ * {@link ensureProductFile}; not part of the public pipeline.
46
47
  */
47
- export function addProductFile(xcodeProject: XcodeProject, options: AddProductFileOptions) {
48
+ function addProductFile(xcodeProject: XcodeProject, options: AddProductFileOptions) {
48
49
  const { targetName, groupName } = options
49
50
 
50
51
  const productFileOptions = {