peachsim 0.1.1620 → 0.1.1635

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 (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -1,11 +1,13 @@
1
1
  import fs from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
3
  import path from 'node:path'
4
+ import { androidMergedResourceValuesDirectory } from './android-app-identity'
4
5
  import {
5
- canExtractNativeAppIdentity,
6
+ extractNativeAppIdentityFromFiles,
6
7
  type NativeAppIdentity,
7
- type NativeAppIdentityPlatform,
8
8
  readRootModuleName,
9
+ readGoogleSignInConfiguration,
10
+ selectNativeAppIdentityReader,
9
11
  } from './native-app-identity'
10
12
  import { resolveNativeAppIconArtwork } from './nativeAppIconContract'
11
13
  import { parsePlistSource } from './plist'
@@ -89,6 +91,11 @@ export function readNativeAppIdentityFromXcodeTarget(
89
91
  permissionUsageDescriptions[key] = value
90
92
  }
91
93
 
94
+ const infoPlistStrings: Record<string, string> = {}
95
+ for (const [key, value] of Object.entries(dictionary)) {
96
+ if (typeof value === 'string') infoPlistStrings[key] = value
97
+ }
98
+
92
99
  const querySchemes = dictionary.LSApplicationQueriesSchemes
93
100
  const urlQuerySchemes = Array.isArray(querySchemes) ? querySchemes : []
94
101
 
@@ -129,6 +136,10 @@ export function readNativeAppIdentityFromXcodeTarget(
129
136
  )
130
137
  const shareIntoAppGroupId = nonEmptyString(dictionary.ExpoShareIntoAppGroupId)
131
138
  const rtcAppGroupIdentifier = nonEmptyString(dictionary.RTCAppGroupIdentifier)
139
+ const screenShareExtensionBundleId =
140
+ typeof dictionary.ScreenShareExtensionBundleId === 'string'
141
+ ? dictionary.ScreenShareExtensionBundleId
142
+ : undefined
132
143
  const rootModuleName = readRootModuleName(
133
144
  app.allSources
134
145
  .filter((file) => /\.(?:swift|mm|m)$/.test(file) && fs.existsSync(file))
@@ -141,6 +152,15 @@ export function readNativeAppIdentityFromXcodeTarget(
141
152
  ...(rootModuleName ? { rootModuleName } : {}),
142
153
  ...(rootViewBackgroundColor !== undefined ? { rootViewBackgroundColor } : {}),
143
154
  ...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
155
+ ...(typeof dictionary.UIStatusBarHidden === 'boolean'
156
+ ? { statusBarHidden: dictionary.UIStatusBarHidden }
157
+ : {}),
158
+ ...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
159
+ ? {
160
+ viewControllerBasedStatusBarAppearance:
161
+ dictionary.UIViewControllerBasedStatusBarAppearance,
162
+ }
163
+ : {}),
144
164
  ...(typeof dictionary.DTPlatformVersion === 'string'
145
165
  ? { dtPlatformVersion: dictionary.DTPlatformVersion }
146
166
  : {}),
@@ -149,6 +169,14 @@ export function readNativeAppIdentityFromXcodeTarget(
149
169
  buildVersion,
150
170
  bundleIdentifier,
151
171
  platform: 'ios',
172
+ googleSignIn: readGoogleSignInConfiguration(
173
+ dictionary,
174
+ Object.fromEntries(
175
+ app.resources
176
+ .filter((file) => file.endsWith('.plist'))
177
+ .map((file) => [file, parseInfoPlist(fs.readFileSync(file, 'utf8'))]),
178
+ ),
179
+ ),
152
180
  supportedInterfaceOrientations,
153
181
  ipadSupportedInterfaceOrientations,
154
182
  ...(defaultScreenOrientationMask ? { defaultScreenOrientationMask } : {}),
@@ -172,9 +200,13 @@ export function readNativeAppIdentityFromXcodeTarget(
172
200
  (value) => value,
173
201
  ),
174
202
  permissionUsageDescriptions,
203
+ infoPlistStrings,
175
204
  urlQuerySchemes,
176
205
  ...(shareIntoAppGroupId ? { shareIntoAppGroupId } : {}),
177
206
  ...(rtcAppGroupIdentifier ? { rtcAppGroupIdentifier } : {}),
207
+ ...(screenShareExtensionBundleId === undefined
208
+ ? {}
209
+ : { screenShareExtensionBundleId }),
178
210
  backgroundTaskIdentifiers,
179
211
  backgroundModes,
180
212
  appIntentIdentifiers,
@@ -190,8 +222,113 @@ export function readNativeAppIdentityFromProject(
190
222
  platform: unknown,
191
223
  parseInfoPlist: (source: string) => unknown = parsePlistSource,
192
224
  iosTarget?: string,
225
+ androidMergedManifest?: string,
226
+ ): NativeAppIdentity | null {
227
+ const read = selectNativeAppIdentityReader(platform, {
228
+ android: () => readAndroidNativeAppIdentity(projectRoot, androidMergedManifest),
229
+ ios: () => readIosNativeAppIdentity(projectRoot, parseInfoPlist, iosTarget),
230
+ })
231
+ return read ? read() : null
232
+ }
233
+
234
+ // a project whose Xcode build settings do not resolve still names its app in
235
+ // the checked-in Info.plist and AppDelegate sources; android ships no such
236
+ // source identity.
237
+ export function readNativeAppIdentityFromSources(
238
+ projectRoot: string,
239
+ platform: unknown,
240
+ parseInfoPlist: (source: string) => unknown,
241
+ iosTarget: string | undefined,
242
+ ): NativeAppIdentity | null {
243
+ const read = selectNativeAppIdentityReader(platform, {
244
+ android: () => null,
245
+ ios: () => {
246
+ const iosRoot = path.join(projectRoot, 'ios')
247
+ if (!fs.existsSync(iosRoot)) return null
248
+ const files: Record<string, string> = {}
249
+ for (const entry of fs.readdirSync(iosRoot, { withFileTypes: true })) {
250
+ if (entry.name === 'Pods') continue
251
+ if (entry.isDirectory()) {
252
+ const names = entry.name.endsWith('.xcodeproj')
253
+ ? ['project.pbxproj']
254
+ : ['Info.plist', 'AppDelegate.swift', 'AppDelegate.mm', 'AppDelegate.m']
255
+ for (const name of names) {
256
+ const relative = `ios/${entry.name}/${name}`
257
+ const file = path.join(projectRoot, relative)
258
+ if (fs.existsSync(file)) files[relative] = fs.readFileSync(file, 'utf8')
259
+ }
260
+ } else if (/AppDelegate\.(?:swift|mm|m)$/.test(entry.name)) {
261
+ const relative = `ios/${entry.name}`
262
+ const file = path.join(projectRoot, relative)
263
+ if (fs.existsSync(file)) files[relative] = fs.readFileSync(file, 'utf8')
264
+ }
265
+ }
266
+ return extractNativeAppIdentityFromFiles(files, 'ios', parseInfoPlist, iosTarget)
267
+ },
268
+ })
269
+ return read ? read() : null
270
+ }
271
+
272
+ function readAndroidNativeAppIdentity(
273
+ projectRoot: string,
274
+ androidMergedManifest: string | undefined,
275
+ ): NativeAppIdentity | null {
276
+ const files: Record<string, string> = {}
277
+ if (androidMergedManifest) {
278
+ const file = path.resolve(projectRoot, androidMergedManifest)
279
+ if (!fs.existsSync(file)) return null
280
+ files[androidMergedManifest] = fs.readFileSync(file, 'utf8')
281
+ } else {
282
+ const androidRoot = path.join(projectRoot, 'android')
283
+ if (!fs.existsSync(androidRoot)) return null
284
+ const collect = (directory: string): void => {
285
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
286
+ const file = path.join(directory, entry.name)
287
+ if (entry.isDirectory()) collect(file)
288
+ else if (entry.isFile() && entry.name === 'AndroidManifest.xml')
289
+ files[path.relative(projectRoot, file).split(path.sep).join('/')] =
290
+ fs.readFileSync(file, 'utf8')
291
+ }
292
+ }
293
+ for (const module of fs.readdirSync(androidRoot, { withFileTypes: true })) {
294
+ if (!module.isDirectory()) continue
295
+ const outputs = path.join(
296
+ androidRoot,
297
+ module.name,
298
+ 'build/intermediates/merged_manifests',
299
+ )
300
+ if (fs.existsSync(outputs)) collect(outputs)
301
+ }
302
+ }
303
+ for (const manifest of Object.keys(files)) {
304
+ const resourceDirectory = androidMergedResourceValuesDirectory(manifest)
305
+ if (!resourceDirectory) continue
306
+ const absolute = path.resolve(projectRoot, resourceDirectory)
307
+ if (!fs.existsSync(absolute)) continue
308
+ for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
309
+ if (!entry.isDirectory() || !/^values(?:-|$)/.test(entry.name)) continue
310
+ for (const resource of fs.readdirSync(path.join(absolute, entry.name))) {
311
+ if (!resource.endsWith('.xml')) continue
312
+ const file = `${resourceDirectory}/${entry.name}/${resource}`
313
+ files[file] = fs.readFileSync(path.resolve(projectRoot, file), 'utf8')
314
+ }
315
+ }
316
+ }
317
+ return extractNativeAppIdentityFromFiles(
318
+ files,
319
+ 'android',
320
+ parsePlistSource,
321
+ undefined,
322
+ undefined,
323
+ androidMergedManifest,
324
+ )
325
+ }
326
+
327
+ function readIosNativeAppIdentity(
328
+ projectRoot: string,
329
+ parseInfoPlist: (source: string) => unknown,
330
+ iosTarget: string | undefined,
193
331
  ): NativeAppIdentity | null {
194
- if (!canExtractNativeAppIdentity(platform)) return null
195
332
  const iosRoot = path.join(projectRoot, 'ios')
196
333
  if (!fs.existsSync(iosRoot)) return null
197
334
  const projects = fs
@@ -251,8 +388,8 @@ export function readNativeAppIdentityFromProject(
251
388
  ? nonEmptyString(installedManifest.version)
252
389
  : undefined
253
390
  if (!installedVersion) throw new Error('Reanimated package version is malformed')
254
- // reanimated 3's native build has no static feature flag schema.
255
- if (installedVersion.startsWith('3.')) return identity
391
+ // the static feature flag schema starts with reanimated 4.
392
+ if (Number.parseInt(installedVersion, 10) < 4) return identity
256
393
  const defaults: unknown = JSON.parse(
257
394
  fs.readFileSync(
258
395
  path.join(
@@ -1,13 +1,14 @@
1
1
  // generic native app identity: the values an installed iOS/Android binary
2
2
  // carries, read from the app's own native project (`Info.plist` resolved
3
- // against the Xcode Release build settings, or `android/app/build.gradle`), so
4
- // a simulator tenant reports the same identity the native Release build does.
3
+ // against Xcode Release build settings, or a completed Android app manifest
4
+ // merger output), so a simulator tenant reports the selected native build.
5
5
  //
6
6
  // identity travels in a bundle footer or response header. the one updates
7
7
  // receipt travels only in the response header so publisher JavaScript stays
8
8
  // unchanged. tenant boot validates both before evaluation and clears the realm
9
9
  // store when neither carries identity.
10
10
 
11
+ import { nativeAppIdentityFromAndroidManifest } from './android-app-identity'
11
12
  import { findBundleFooter } from './bundleGlobalFooter'
12
13
  import {
13
14
  resolveNativeAppIconArtwork,
@@ -27,8 +28,14 @@ export interface NativeOneUpdatesReceipt {
27
28
  embeddedManifestJson: string | null
28
29
  }
29
30
 
31
+ export interface AndroidManifestPermission {
32
+ name: string
33
+ minSdkVersion: number
34
+ maxSdkVersion?: number
35
+ }
36
+
30
37
  export interface NativeAppIdentity {
31
- // the user-facing name (CFBundleDisplayName, else CFBundleName).
38
+ // the user-facing name (iOS bundle name or a resolved Android app label).
32
39
  applicationName: string | null
33
40
  // the literal CFBundleDisplayName, without a bundle-name fallback.
34
41
  applicationDisplayName?: string
@@ -41,6 +48,9 @@ export interface NativeAppIdentity {
41
48
  // UIDesignRequiresCompatibility: UIKit draws its pre-liquid-glass chrome.
42
49
  // carried only when the plist sets it true.
43
50
  designRequiresCompatibility?: true
51
+ // initial UIKit visibility and the owner of subsequent status bar preferences.
52
+ statusBarHidden?: boolean
53
+ viewControllerBasedStatusBarAppearance?: boolean
44
54
  // the literal DTPlatformVersion stamp when the app plist supplies one.
45
55
  dtPlatformVersion?: string
46
56
  // CFBundleName, which CFNetwork stamps into the default User-Agent and iOS's
@@ -51,7 +61,14 @@ export interface NativeAppIdentity {
51
61
  buildVersion: string
52
62
  bundleIdentifier: string
53
63
  platform: NativeAppIdentityPlatform
64
+ // declarations from the selected app manifest merger output; install-time
65
+ // SDK filtering belongs to the evaluation's device, not the bundle builder.
66
+ androidManifestPermissions?: readonly AndroidManifestPermission[]
54
67
  permissionUsageDescriptions?: Readonly<Record<string, unknown>>
68
+ // every top-level string the app's Info.plist declares, build settings
69
+ // resolved, for native code that reads its own plist keys.
70
+ infoPlistStrings?: Readonly<Record<string, string>>
71
+ googleSignIn?: Readonly<Record<string, unknown>>
55
72
  urlQuerySchemes?: readonly unknown[]
56
73
  appIntentIdentifiers?: readonly string[]
57
74
  backgroundTaskIdentifiers?: readonly string[]
@@ -61,6 +78,7 @@ export interface NativeAppIdentity {
61
78
  defaultScreenOrientationMask?: string
62
79
  shareIntoAppGroupId?: string
63
80
  rtcAppGroupIdentifier?: string
81
+ screenShareExtensionBundleId?: string
64
82
  alternateIconNames?: readonly string[]
65
83
  appIconArtwork?: NativeAppIconArtwork
66
84
  // resolved Reanimated compile flags from the native build inputs.
@@ -91,6 +109,9 @@ function nonEmptyString(value: unknown): string | null {
91
109
 
92
110
  export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity {
93
111
  if (!isRecord(value)) return false
112
+ for (const key of ['statusBarHidden', 'viewControllerBasedStatusBarAppearance']) {
113
+ if (value[key] !== undefined && typeof value[key] !== 'boolean') return false
114
+ }
94
115
  const oneUpdates = value.oneUpdates
95
116
  if (
96
117
  oneUpdates !== undefined &&
@@ -114,6 +135,25 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
114
135
  typeof value.dtPlatformVersion !== 'string'
115
136
  )
116
137
  return false
138
+ if (
139
+ value.androidManifestPermissions !== undefined &&
140
+ (!Array.isArray(value.androidManifestPermissions) ||
141
+ !value.androidManifestPermissions.every(
142
+ (permission: unknown) =>
143
+ isRecord(permission) &&
144
+ typeof permission.name === 'string' &&
145
+ permission.name.length > 0 &&
146
+ Number.isInteger(permission.minSdkVersion) &&
147
+ typeof permission.minSdkVersion === 'number' &&
148
+ permission.minSdkVersion > 0 &&
149
+ (permission.maxSdkVersion === undefined ||
150
+ (typeof permission.maxSdkVersion === 'number' &&
151
+ Number.isInteger(permission.maxSdkVersion) &&
152
+ permission.maxSdkVersion >= 0)),
153
+ ))
154
+ )
155
+ return false
156
+ if (value.googleSignIn !== undefined && !isRecord(value.googleSignIn)) return false
117
157
  const descriptions = value.permissionUsageDescriptions
118
158
  if (
119
159
  value.reanimatedStaticFeatureFlags !== undefined &&
@@ -124,6 +164,12 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
124
164
  )
125
165
  return false
126
166
  if (descriptions !== undefined && !isRecord(descriptions)) return false
167
+ if (
168
+ value.infoPlistStrings !== undefined &&
169
+ (!isRecord(value.infoPlistStrings) ||
170
+ !Object.values(value.infoPlistStrings).every((entry) => typeof entry === 'string'))
171
+ )
172
+ return false
127
173
  if (value.urlQuerySchemes !== undefined && !Array.isArray(value.urlQuerySchemes))
128
174
  return false
129
175
  if (
@@ -156,6 +202,11 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
156
202
  typeof value.rtcAppGroupIdentifier !== 'string'
157
203
  )
158
204
  return false
205
+ if (
206
+ value.screenShareExtensionBundleId !== undefined &&
207
+ typeof value.screenShareExtensionBundleId !== 'string'
208
+ )
209
+ return false
159
210
  if (
160
211
  value.applicationDisplayName !== undefined &&
161
212
  typeof value.applicationDisplayName !== 'string'
@@ -233,6 +284,15 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
233
284
  ...(value.designRequiresCompatibility === undefined
234
285
  ? {}
235
286
  : { designRequiresCompatibility: value.designRequiresCompatibility }),
287
+ ...(value.statusBarHidden === undefined
288
+ ? {}
289
+ : { statusBarHidden: value.statusBarHidden }),
290
+ ...(value.viewControllerBasedStatusBarAppearance === undefined
291
+ ? {}
292
+ : {
293
+ viewControllerBasedStatusBarAppearance:
294
+ value.viewControllerBasedStatusBarAppearance,
295
+ }),
236
296
  ...(value.dtPlatformVersion === undefined
237
297
  ? {}
238
298
  : { dtPlatformVersion: value.dtPlatformVersion }),
@@ -250,11 +310,24 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
250
310
  ...(value.alternateIconNames === undefined
251
311
  ? {}
252
312
  : { alternateIconNames: [...value.alternateIconNames] }),
313
+ ...(value.androidManifestPermissions === undefined
314
+ ? {}
315
+ : {
316
+ androidManifestPermissions: value.androidManifestPermissions.map(
317
+ (permission) => ({ ...permission }),
318
+ ),
319
+ }),
320
+ ...(value.googleSignIn === undefined
321
+ ? {}
322
+ : { googleSignIn: structuredClone(value.googleSignIn) }),
253
323
  ...(value.permissionUsageDescriptions === undefined
254
324
  ? {}
255
325
  : {
256
326
  permissionUsageDescriptions: { ...value.permissionUsageDescriptions },
257
327
  }),
328
+ ...(value.infoPlistStrings === undefined
329
+ ? {}
330
+ : { infoPlistStrings: { ...value.infoPlistStrings } }),
258
331
  ...(value.appIntentIdentifiers === undefined
259
332
  ? {}
260
333
  : { appIntentIdentifiers: [...value.appIntentIdentifiers] }),
@@ -285,6 +358,9 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
285
358
  ...(value.rtcAppGroupIdentifier === undefined
286
359
  ? {}
287
360
  : { rtcAppGroupIdentifier: value.rtcAppGroupIdentifier }),
361
+ ...(value.screenShareExtensionBundleId === undefined
362
+ ? {}
363
+ : { screenShareExtensionBundleId: value.screenShareExtensionBundleId }),
288
364
  ...(value.urlQuerySchemes === undefined
289
365
  ? {}
290
366
  : { urlQuerySchemes: [...value.urlQuerySchemes] }),
@@ -452,6 +528,39 @@ export function insertNativeAppIdentityFooter(
452
528
 
453
529
  // ── native project extraction ────────────────────────────────────────────────
454
530
 
531
+ // Info.plist and bundled service plists are native configuration, independent of Firebase initialization.
532
+ export function readGoogleSignInConfiguration(
533
+ plist: Record<string, unknown>,
534
+ resources: Readonly<Record<string, unknown>>,
535
+ ): Record<string, unknown> {
536
+ const clientIds: Record<string, string> = {}
537
+ for (const [file, value] of Object.entries(resources)) {
538
+ if (isRecord(value) && typeof value.CLIENT_ID === 'string')
539
+ clientIds[
540
+ file
541
+ .split('/')
542
+ .pop()
543
+ ?.replace(/\.plist$/, '') ?? file
544
+ ] = value.CLIENT_ID
545
+ }
546
+ const urlSchemes: string[] = []
547
+ for (const entry of Array.isArray(plist.CFBundleURLTypes)
548
+ ? plist.CFBundleURLTypes
549
+ : []) {
550
+ if (!isRecord(entry) || !Array.isArray(entry.CFBundleURLSchemes)) continue
551
+ for (const scheme of entry.CFBundleURLSchemes)
552
+ if (typeof scheme === 'string') urlSchemes.push(scheme)
553
+ }
554
+ return {
555
+ clientId: plist.GIDClientID,
556
+ serverClientId: plist.GIDServerClientID,
557
+ hostedDomain: plist.GIDHostedDomain,
558
+ openIDRealm: plist.GIDOpenIDRealm,
559
+ urlSchemes,
560
+ clientIds,
561
+ }
562
+ }
563
+
455
564
  function plistString(plist: string, key: string): string | null {
456
565
  const match = plist.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`))
457
566
  return match ? match[1].trim() : null
@@ -873,6 +982,13 @@ function nativeAppIdentityFromIos(
873
982
  : undefined
874
983
  if (rtcAppGroupIdentifier === null)
875
984
  throw new Error('native app Info.plist RTCAppGroupIdentifier is unresolved')
985
+ const declaredScreenShareExtension = dictionary.ScreenShareExtensionBundleId
986
+ const screenShareExtensionBundleId =
987
+ typeof declaredScreenShareExtension === 'string'
988
+ ? resolveBuildSetting(declaredScreenShareExtension, settings)
989
+ : undefined
990
+ if (screenShareExtensionBundleId === null)
991
+ throw new Error('native app Info.plist ScreenShareExtensionBundleId is unresolved')
876
992
  const querySchemes = dictionary.LSApplicationQueriesSchemes
877
993
  if (querySchemes !== undefined && !Array.isArray(querySchemes)) {
878
994
  throw new Error('native app Info.plist LSApplicationQueriesSchemes is not an array')
@@ -901,6 +1017,13 @@ function nativeAppIdentityFromIos(
901
1017
  permissionUsageDescriptions[key] = resolved
902
1018
  }
903
1019
 
1020
+ const infoPlistStrings: Record<string, string> = {}
1021
+ for (const [key, value] of Object.entries(dictionary)) {
1022
+ if (typeof value !== 'string') continue
1023
+ const resolved = resolveBuildSetting(value, settings)
1024
+ if (resolved !== null) infoPlistStrings[key] = resolved
1025
+ }
1026
+
904
1027
  // Xcode's generated Info.plist uses these build settings only when enabled.
905
1028
  // source: developer.apple.com/documentation/xcode/build-settings-reference.
906
1029
  if (settings.GENERATE_INFOPLIST_FILE === 'YES') {
@@ -974,12 +1097,29 @@ function nativeAppIdentityFromIos(
974
1097
  ...(rootModuleName === undefined ? {} : { rootModuleName }),
975
1098
  ...(rootViewBackgroundColor === undefined ? {} : { rootViewBackgroundColor }),
976
1099
  ...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
1100
+ ...(typeof dictionary.UIStatusBarHidden === 'boolean'
1101
+ ? { statusBarHidden: dictionary.UIStatusBarHidden }
1102
+ : {}),
1103
+ ...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
1104
+ ? {
1105
+ viewControllerBasedStatusBarAppearance:
1106
+ dictionary.UIViewControllerBasedStatusBarAppearance,
1107
+ }
1108
+ : {}),
977
1109
  ...(dtPlatformVersion === undefined ? {} : { dtPlatformVersion }),
978
1110
  bundleName,
979
1111
  appVersion,
980
1112
  buildVersion,
981
1113
  bundleIdentifier,
982
1114
  platform: 'ios',
1115
+ googleSignIn: readGoogleSignInConfiguration(
1116
+ dictionary,
1117
+ Object.fromEntries(
1118
+ [...resources]
1119
+ .filter((file) => file.endsWith('.plist') && files[file])
1120
+ .map((file) => [file, parseInfoPlist(files[file])]),
1121
+ ),
1122
+ ),
983
1123
  supportedInterfaceOrientations,
984
1124
  ipadSupportedInterfaceOrientations,
985
1125
  ...(defaultScreenOrientationMask === undefined
@@ -1005,9 +1145,13 @@ function nativeAppIdentityFromIos(
1005
1145
  }
1006
1146
  : {}),
1007
1147
  permissionUsageDescriptions,
1148
+ infoPlistStrings,
1008
1149
  urlQuerySchemes,
1009
1150
  ...(shareIntoAppGroupId === undefined ? {} : { shareIntoAppGroupId }),
1010
1151
  ...(rtcAppGroupIdentifier === undefined ? {} : { rtcAppGroupIdentifier }),
1152
+ ...(screenShareExtensionBundleId === undefined
1153
+ ? {}
1154
+ : { screenShareExtensionBundleId }),
1011
1155
  backgroundTaskIdentifiers: backgroundTaskIdentifiers.every((value) => value !== null)
1012
1156
  ? backgroundTaskIdentifiers
1013
1157
  : [],
@@ -1020,11 +1164,6 @@ function nativeAppIdentityFromIos(
1020
1164
  }
1021
1165
  }
1022
1166
 
1023
- // android's shipped identity requires resolving the selected flavor and
1024
- // buildType overrides (`productFlavors.prod`, `buildTypes.release`), which a
1025
- // text reader of `defaultConfig` cannot distinguish from the defaults it
1026
- // overrides. an Android project therefore ships no footer until that
1027
- // resolution exists. unsupported or absent bundle platforms ship no footer.
1028
1167
  // the module an app's own sources start react native with: the template
1029
1168
  // `startReactNative(withModuleName: "X")`, RCTAppDelegate's
1030
1169
  // `self.moduleName = @"X"` and RCTRootView's `moduleName:@"X"`. a target that
@@ -1039,8 +1178,19 @@ export function readRootModuleName(sources: readonly string[]): string | undefin
1039
1178
  return names.size === 1 ? [...names][0] : undefined
1040
1179
  }
1041
1180
 
1042
- export function canExtractNativeAppIdentity(platform: unknown): platform is 'ios' {
1043
- return platform === 'ios'
1181
+ export function canExtractNativeAppIdentity(
1182
+ platform: unknown,
1183
+ ): platform is NativeAppIdentityPlatform {
1184
+ return platform === 'ios' || platform === 'android'
1185
+ }
1186
+
1187
+ // the bundle platform's identity reader. selecting it here keeps every native
1188
+ // project reader free of platform branches.
1189
+ export function selectNativeAppIdentityReader<Reader>(
1190
+ platform: unknown,
1191
+ readers: Readonly<Record<NativeAppIdentityPlatform, Reader>>,
1192
+ ): Reader | null {
1193
+ return canExtractNativeAppIdentity(platform) ? readers[platform] : null
1044
1194
  }
1045
1195
 
1046
1196
  // read the identity a bundle builder transports. files are project-relative
@@ -1053,8 +1203,23 @@ export function extractNativeAppIdentityFromFiles(
1053
1203
  parseInfoPlist: (source: string) => unknown,
1054
1204
  iosTarget?: string,
1055
1205
  resourceUri?: (file: string) => string | undefined,
1206
+ androidMergedManifest?: string,
1056
1207
  ): NativeAppIdentity | null {
1057
- return canExtractNativeAppIdentity(platform)
1208
+ if (platform === 'android') {
1209
+ const candidates = androidMergedManifest
1210
+ ? [androidMergedManifest]
1211
+ : Object.keys(files).filter((file) =>
1212
+ /^android\/[^/]+\/build\/intermediates\/merged_manifests\/.+\/AndroidManifest\.xml$/.test(
1213
+ file,
1214
+ ),
1215
+ )
1216
+ if (candidates.length !== 1) return null
1217
+ const source = files[candidates[0]!]
1218
+ return source === undefined
1219
+ ? null
1220
+ : nativeAppIdentityFromAndroidManifest(source, candidates[0]!, files)
1221
+ }
1222
+ return platform === 'ios'
1058
1223
  ? nativeAppIdentityFromIos(files, parseInfoPlist, iosTarget, resourceUri)
1059
1224
  : null
1060
1225
  }