@reactvision/react-viro 2.53.1 → 2.55.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 (153) hide show
  1. package/README.md +85 -46
  2. package/android/react_viro/react_viro-release.aar +0 -0
  3. package/android/viro_renderer/viro_renderer-release.aar +0 -0
  4. package/components/AR/ViroARCamera.tsx +5 -0
  5. package/components/AR/ViroARImageMarker.tsx +5 -0
  6. package/components/AR/ViroARObjectMarker.tsx +5 -0
  7. package/components/AR/ViroARPlane.tsx +5 -0
  8. package/components/AR/ViroARPlaneSelector.tsx +5 -0
  9. package/components/AR/ViroARScene.tsx +5 -0
  10. package/components/AR/ViroARSceneNavigator.tsx +84 -0
  11. package/components/AR/ViroCommonProps.ts +11 -0
  12. package/components/Material/ViroMaterials.ts +51 -0
  13. package/components/Studio/StudioARScene.tsx +368 -0
  14. package/components/Studio/StudioSceneNavigator.tsx +191 -0
  15. package/components/Studio/VRTStudioModule.ts +40 -0
  16. package/components/Studio/domain/animationRegistry.ts +86 -0
  17. package/components/Studio/domain/collisionBindingsRuntime.ts +93 -0
  18. package/components/Studio/domain/collisionPairKey.ts +15 -0
  19. package/components/Studio/domain/dragConfiguration.ts +48 -0
  20. package/components/Studio/domain/materialConfig.ts +276 -0
  21. package/components/Studio/domain/physicsConfig.ts +204 -0
  22. package/components/Studio/domain/sceneNavigationHandler.ts +150 -0
  23. package/components/Studio/domain/studioMaterials.ts +33 -0
  24. package/components/Studio/domain/triggerImageRegistry.ts +64 -0
  25. package/components/Studio/domain/useStudioShaderTimeUniforms.ts +51 -0
  26. package/components/Studio/domain/useStudioShaderViewportUniforms.ts +52 -0
  27. package/components/Studio/domain/viroNodeFactory.tsx +323 -0
  28. package/components/Studio/index.ts +18 -0
  29. package/components/Studio/types.ts +164 -0
  30. package/components/Types/ViroEvents.ts +53 -0
  31. package/components/Utilities/VRModuleOpenXR.ts +50 -0
  32. package/components/Utilities/VRQuestNavigatorBridge.ts +168 -0
  33. package/components/Utilities/ViroPlatform.ts +52 -0
  34. package/components/Utilities/ViroUtils.tsx +48 -0
  35. package/components/Utilities/ViroVersion.ts +1 -1
  36. package/components/Utilities/useAnySourceHover.ts +55 -0
  37. package/components/Utilities/useAnySourcePressed.ts +70 -0
  38. package/components/Viro360Image.tsx +7 -0
  39. package/components/ViroQuestEntryPoint.tsx +79 -0
  40. package/components/ViroVRSceneNavigator.tsx +44 -19
  41. package/components/ViroXRSceneNavigator.tsx +217 -0
  42. package/components/VisionOS/ViroVisionOSModule.ts +93 -0
  43. package/dist/components/AR/ViroARCamera.d.ts +1 -1
  44. package/dist/components/AR/ViroARCamera.js +5 -0
  45. package/dist/components/AR/ViroARImageMarker.d.ts +1 -1
  46. package/dist/components/AR/ViroARImageMarker.js +5 -0
  47. package/dist/components/AR/ViroARObjectMarker.d.ts +1 -1
  48. package/dist/components/AR/ViroARObjectMarker.js +5 -0
  49. package/dist/components/AR/ViroARPlane.d.ts +1 -1
  50. package/dist/components/AR/ViroARPlane.js +5 -0
  51. package/dist/components/AR/ViroARPlaneSelector.d.ts +1 -1
  52. package/dist/components/AR/ViroARPlaneSelector.js +5 -0
  53. package/dist/components/AR/ViroARScene.d.ts +1 -1
  54. package/dist/components/AR/ViroARScene.js +5 -0
  55. package/dist/components/AR/ViroARSceneNavigator.d.ts +36 -0
  56. package/dist/components/AR/ViroARSceneNavigator.js +41 -0
  57. package/dist/components/AR/ViroCommonProps.d.ts +11 -0
  58. package/dist/components/Material/ViroMaterials.d.ts +12 -0
  59. package/dist/components/Material/ViroMaterials.js +25 -0
  60. package/dist/components/ReactVisionClient.d.ts +25 -0
  61. package/dist/components/ReactVisionClient.js +11 -0
  62. package/dist/components/Studio/StudioARScene.d.ts +15 -0
  63. package/dist/components/Studio/StudioARScene.js +299 -0
  64. package/dist/components/Studio/StudioSceneNavigator.d.ts +31 -0
  65. package/dist/components/Studio/StudioSceneNavigator.js +174 -0
  66. package/dist/components/Studio/VRTStudioModule.d.ts +15 -0
  67. package/dist/components/Studio/VRTStudioModule.js +31 -0
  68. package/dist/components/Studio/domain/animationRegistry.d.ts +11 -0
  69. package/dist/components/Studio/domain/animationRegistry.js +67 -0
  70. package/dist/components/Studio/domain/collisionBindingsRuntime.d.ts +21 -0
  71. package/dist/components/Studio/domain/collisionBindingsRuntime.js +54 -0
  72. package/dist/components/Studio/domain/collisionPairKey.d.ts +8 -0
  73. package/dist/components/Studio/domain/collisionPairKey.js +15 -0
  74. package/dist/components/Studio/domain/dragConfiguration.d.ts +20 -0
  75. package/dist/components/Studio/domain/dragConfiguration.js +37 -0
  76. package/dist/components/Studio/domain/materialConfig.d.ts +56 -0
  77. package/dist/components/Studio/domain/materialConfig.js +239 -0
  78. package/dist/components/Studio/domain/physicsConfig.d.ts +69 -0
  79. package/dist/components/Studio/domain/physicsConfig.js +165 -0
  80. package/dist/components/Studio/domain/sceneNavigationHandler.d.ts +12 -0
  81. package/dist/components/Studio/domain/sceneNavigationHandler.js +112 -0
  82. package/dist/components/Studio/domain/studioMaterials.d.ts +6 -0
  83. package/dist/components/Studio/domain/studioMaterials.js +30 -0
  84. package/dist/components/Studio/domain/triggerImageRegistry.d.ts +13 -0
  85. package/dist/components/Studio/domain/triggerImageRegistry.js +47 -0
  86. package/dist/components/Studio/domain/useStudioShaderTimeUniforms.d.ts +6 -0
  87. package/dist/components/Studio/domain/useStudioShaderTimeUniforms.js +48 -0
  88. package/dist/components/Studio/domain/useStudioShaderViewportUniforms.d.ts +6 -0
  89. package/dist/components/Studio/domain/useStudioShaderViewportUniforms.js +48 -0
  90. package/dist/components/Studio/domain/viroNodeFactory.d.ts +28 -0
  91. package/dist/components/Studio/domain/viroNodeFactory.js +193 -0
  92. package/dist/components/Studio/index.d.ts +3 -0
  93. package/dist/components/Studio/index.js +7 -0
  94. package/dist/components/Studio/types.d.ts +149 -0
  95. package/dist/components/Studio/types.js +4 -0
  96. package/dist/components/Types/ViroEvents.d.ts +49 -1
  97. package/dist/components/Types/ViroEvents.js +1 -0
  98. package/dist/components/Utilities/VRModuleOpenXR.d.ts +32 -0
  99. package/dist/components/Utilities/VRModuleOpenXR.js +44 -0
  100. package/dist/components/Utilities/VRQuestNavigatorBridge.d.ts +85 -0
  101. package/dist/components/Utilities/VRQuestNavigatorBridge.js +124 -0
  102. package/dist/components/Utilities/ViroPlatform.d.ts +10 -0
  103. package/dist/components/Utilities/ViroPlatform.js +43 -0
  104. package/dist/components/Utilities/ViroUtils.d.ts +19 -0
  105. package/dist/components/Utilities/ViroUtils.js +34 -0
  106. package/dist/components/Utilities/ViroVersion.d.ts +1 -1
  107. package/dist/components/Utilities/ViroVersion.js +1 -1
  108. package/dist/components/Utilities/useAnySourceHover.d.ts +36 -0
  109. package/dist/components/Utilities/useAnySourceHover.js +48 -0
  110. package/dist/components/Utilities/useAnySourcePressed.d.ts +37 -0
  111. package/dist/components/Utilities/useAnySourcePressed.js +61 -0
  112. package/dist/components/Viro360Image.d.ts +7 -0
  113. package/dist/components/ViroQuestEntryPoint.d.ts +13 -0
  114. package/dist/components/ViroQuestEntryPoint.js +104 -0
  115. package/dist/components/ViroVRSceneNavigator.d.ts +24 -10
  116. package/dist/components/ViroVRSceneNavigator.js +21 -18
  117. package/dist/components/ViroXRSceneNavigator.d.ts +54 -0
  118. package/dist/components/ViroXRSceneNavigator.js +173 -0
  119. package/dist/components/VisionOS/ViroVisionOSModule.d.ts +65 -0
  120. package/dist/components/VisionOS/ViroVisionOSModule.js +91 -0
  121. package/dist/index.d.ts +16 -3
  122. package/dist/index.js +34 -2
  123. package/dist/plugins/withViro.d.ts +28 -1
  124. package/dist/plugins/withViroAndroid.js +312 -7
  125. package/dist/plugins/withViroIos.js +17 -8
  126. package/dist/plugins/withViroVisionOS.d.ts +24 -0
  127. package/dist/plugins/withViroVisionOS.js +265 -0
  128. package/index.ts +66 -0
  129. package/ios/ViroReact.podspec +15 -5
  130. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreCoreMLSemanticsResources.bundle/Info.plist +0 -0
  131. package/ios/dist/ViroRenderer/ViroKit.framework/ARCoreResources.bundle/Info.plist +0 -0
  132. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSession.h +30 -1
  133. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROARSessioniOS.h +16 -0
  134. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROGLTFLoader.h +34 -0
  135. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputControllerBase.h +74 -0
  136. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROInputType.h +11 -3
  137. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMaterial.h +29 -0
  138. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROMorpher.h +4 -0
  139. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROPlatformUtil.h +13 -0
  140. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROPortal.h +17 -0
  141. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VRORenderContext.h +41 -0
  142. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VRORenderer.h +23 -0
  143. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROSemantics.h +14 -0
  144. package/ios/dist/ViroRenderer/ViroKit.framework/Headers/VROViewAR.h +11 -0
  145. package/ios/dist/ViroRenderer/ViroKit.framework/Info.plist +0 -0
  146. package/ios/dist/ViroRenderer/ViroKit.framework/Shaders.dat +1 -1
  147. package/ios/dist/ViroRenderer/ViroKit.framework/ViroKit +0 -0
  148. package/ios/dist/ViroRenderer/ViroKit.podspec +5 -0
  149. package/ios/dist/include/VRT360Image.h +1 -0
  150. package/ios/dist/include/VRTARSceneNavigator.h +7 -0
  151. package/ios/dist/include/VRTStudioModule.h +6 -0
  152. package/ios/dist/lib/libViroReact.a +0 -0
  153. package/package.json +8 -8
@@ -46,9 +46,9 @@ const withBranchAndroid = (config) => {
46
46
  const viroPlugin = config?.plugins?.find((plugin) => Array.isArray(plugin) && plugin[0] === "@reactvision/react-viro");
47
47
  if (Array.isArray(viroPlugin)) {
48
48
  if (Array.isArray(viroPlugin[1].android?.xRMode)) {
49
- viroPluginConfig = (viroPlugin[1].android?.xRMode).filter((mode) => ["AR", "GVR", "OVR_MOBILE"].includes(mode));
49
+ viroPluginConfig = (viroPlugin[1].android?.xRMode).filter((mode) => ["AR", "GVR", "OVR_MOBILE", "QUEST"].includes(mode));
50
50
  }
51
- else if (["AR", "GVR", "OVR_MOBILE"].includes(viroPlugin[1]?.android?.xRMode)) {
51
+ else if (["AR", "GVR", "OVR_MOBILE", "QUEST"].includes(viroPlugin[1]?.android?.xRMode)) {
52
52
  viroPluginConfig = [viroPlugin[1]?.android.xRMode];
53
53
  }
54
54
  }
@@ -197,6 +197,14 @@ const withViroManifest = (config) => (0, config_plugins_1.withAndroidManifest)(c
197
197
  },
198
198
  });
199
199
  }
200
+ if (pluginOptions.rvEndpoint) {
201
+ contents?.manifest?.application?.[0]["meta-data"]?.push({
202
+ $: {
203
+ "android:name": "com.reactvision.RVEndpoint",
204
+ "android:value": pluginOptions.rvEndpoint,
205
+ },
206
+ });
207
+ }
200
208
  // Add location permissions when geospatial provider is active
201
209
  if (geospatialAnchorProvider === "arcore" || geospatialAnchorProvider === "reactvision") {
202
210
  const existingPermissions = (contents.manifest["uses-permission"] || [])
@@ -281,14 +289,311 @@ const withViroManifest = (config) => (0, config_plugins_1.withAndroidManifest)(c
281
289
  "tools:replace": "required",
282
290
  },
283
291
  });
292
+ // Quest-specific features and permissions — after uses-feature is initialized
293
+ if (viroPluginConfig.includes("QUEST")) {
294
+ contents.manifest["uses-feature"].push({
295
+ $: {
296
+ "android:name": "android.hardware.vr.headtracking",
297
+ "android:required": "true",
298
+ "android:version": "1",
299
+ },
300
+ });
301
+ contents.manifest["uses-feature"].push({
302
+ $: {
303
+ "android:name": "oculus.software.handtracking",
304
+ "android:required": "false",
305
+ },
306
+ });
307
+ // XR_FB_passthrough requires this uses-feature; without it the OpenXR
308
+ // runtime silently strips the extension. required=false so apps that
309
+ // only render fully-virtual scenes still install on Quest 2 etc.
310
+ contents.manifest["uses-feature"].push({
311
+ $: {
312
+ "android:name": "com.oculus.feature.PASSTHROUGH",
313
+ "android:required": "false",
314
+ },
315
+ });
316
+ const existingPermissions = (contents.manifest["uses-permission"] || [])
317
+ .map((p) => p.$?.["android:name"]);
318
+ if (!existingPermissions.includes("com.oculus.permission.HAND_TRACKING")) {
319
+ contents.manifest["uses-permission"].push({
320
+ $: { "android:name": "com.oculus.permission.HAND_TRACKING" },
321
+ });
322
+ }
323
+ if (!existingPermissions.includes("com.oculus.permission.EYE_TRACKING")) {
324
+ contents.manifest["uses-permission"].push({
325
+ $: { "android:name": "com.oculus.permission.EYE_TRACKING" },
326
+ });
327
+ }
328
+ }
284
329
  return newConfig;
285
330
  });
331
+ // ── Quest VRActivity generation ────────────────────────────────────────────────
332
+ /**
333
+ * When QUEST mode is active, generate VRActivity.kt in the app's android source
334
+ * and register it in AndroidManifest with com.oculus.intent.category.VR.
335
+ *
336
+ * VRActivity is a ReactActivity that mounts "VRQuestScene" — a root component
337
+ * the app must register via AppRegistry. Running in a separate Activity with the
338
+ * VR intent category causes Horizon OS to grant exclusive OpenXR display access.
339
+ */
340
+ const withViroQuestActivity = (config, props) => {
341
+ // Read xRMode directly from config.plugins. We cannot rely on the
342
+ // module-level `viroPluginConfig` here: that variable is only updated
343
+ // when `withBranchAndroid`'s withDangerousMod callback runs (mod-apply
344
+ // time), which is *after* this chain-time check executes. On the first
345
+ // prebuild of a new project, viroPluginConfig still holds the default
346
+ // ["AR", "GVR"] when this plugin is composed, and QUEST mods would be
347
+ // silently skipped — no VRActivity.kt and no manifest entry.
348
+ const viroPluginEntry = config?.plugins?.find((plugin) => Array.isArray(plugin) && plugin[0] === "@reactvision/react-viro");
349
+ let xrModes = ["AR", "GVR"];
350
+ if (Array.isArray(viroPluginEntry)) {
351
+ const xrMode = viroPluginEntry[1]?.android?.xRMode;
352
+ if (Array.isArray(xrMode)) {
353
+ xrModes = xrMode.filter((m) => ["AR", "GVR", "OVR_MOBILE", "QUEST"].includes(m));
354
+ }
355
+ else if (typeof xrMode === "string" &&
356
+ ["AR", "GVR", "OVR_MOBILE", "QUEST"].includes(xrMode)) {
357
+ xrModes = [xrMode];
358
+ }
359
+ }
360
+ if (!xrModes.includes("QUEST"))
361
+ return config;
362
+ // 1. Generate VRActivity.kt
363
+ config = (0, config_plugins_1.withDangerousMod)(config, [
364
+ "android",
365
+ async (config) => {
366
+ const packageName = config?.android?.package ?? "";
367
+ const packagePath = packageName.split(".");
368
+ const activityDir = path_1.default.join(config.modRequest.platformProjectRoot, "app", "src", "main", "java", ...packagePath);
369
+ if (!fs_1.default.existsSync(activityDir)) {
370
+ fs_1.default.mkdirSync(activityDir, { recursive: true });
371
+ }
372
+ const activityPath = path_1.default.join(activityDir, "VRActivity.kt");
373
+ // Only write if not already present (preserve manual edits)
374
+ if (!fs_1.default.existsSync(activityPath)) {
375
+ const kotlinContent = `package ${packageName}
376
+
377
+ import android.app.Activity
378
+ import android.app.Application
379
+ import android.os.Bundle
380
+ import android.os.Handler
381
+ import android.os.Looper
382
+ import com.facebook.react.ReactActivity
383
+ import com.facebook.react.ReactActivityDelegate
384
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
385
+ import com.facebook.react.defaults.DefaultReactActivityDelegate
386
+ import com.facebook.react.runtime.ReactHostImpl
387
+
388
+ /**
389
+ * VRActivity — generated by @reactvision/react-viro Expo plugin.
390
+ * Carries com.oculus.intent.category.VR so Horizon OS grants exclusive
391
+ * OpenXR display access. Mounts the "VRQuestScene" React root component.
392
+ * Launch via NativeModules.VRLauncher.launchVRScene().
393
+ *
394
+ * Lifecycle (requires React Native >= 0.83 / Expo >= 55):
395
+ * VRActivity and MainActivity share one ReactHostImpl singleton. On Quest,
396
+ * VRActivity launches into a separate task (FLAG_ACTIVITY_NEW_TASK), so
397
+ * MainActivity.onPause is dispatched *later* via ActivityManager IPC —
398
+ * often after VRActivity.onResume has already promoted the host to RESUMED
399
+ * with currentActivity = VRActivity. When that delayed onPause arrives,
400
+ * the standard delegate calls onHostPause(MainActivity); the identity-
401
+ * mismatch assertion is downgraded to a warning by
402
+ * skipActivityIdentityAssertionOnHostPause (set in VRLauncherModule before
403
+ * startActivity), but the rest of onHostPause runs unconditionally:
404
+ * devSupport is disabled and JavaTimerManager clears its TIMERS_EVENTS
405
+ * callback. Result: timers / requestAnimationFrame stop, Metro Fast Refresh
406
+ * dies.
407
+ *
408
+ * We catch this by listening for that delayed MainActivity.onPause via
409
+ * Application.ActivityLifecycleCallbacks.onActivityPaused(other) and re-
410
+ * promoting the host on the main looper. A Handler.post inside our own
411
+ * onResume would fire too early (the ActivityManager hasn't queued
412
+ * MainActivity.onPause yet), which is why we hook the actual pause event.
413
+ *
414
+ * On exit, VRActivity.onPause runs while currentActivity == this, so the
415
+ * identity assertion passes; MainActivity.onResume then re-promotes via
416
+ * its standard delegate. We do *not* re-promote on a self-pause — the
417
+ * onActivityPaused callback skips when other === this.
418
+ *
419
+ * Mutual exclusion with MainActivity:
420
+ * Quest will happily run MainActivity (panel) and VRActivity (immersive) at
421
+ * the same time if the user taps the app icon in the dock while VR is
422
+ * running. The same Application.ActivityLifecycleCallbacks finishes self
423
+ * when any other Activity in this app resumes — returning the user cleanly
424
+ * to the panel.
425
+ *
426
+ * Surface cleanup on destroy:
427
+ * When VRActivity is destroyed we unload its React surface (reactSurface.stop()
428
+ * in bridgeless arch) so its component tree — including all VRT* components
429
+ * — is unmounted. Without this, the React surface stayed alive in the shared
430
+ * ReactHost across VR re-entries, and on the second/third VR launch each
431
+ * stale surface remounted alongside the fresh one. The C++ scene-graph
432
+ * resolves a click hit to a single viewTag, but multiple VRTComponents
433
+ * claimed it across surfaces — most with a ReactContext bound to a
434
+ * destroyed VRActivity. Click events to the stale ReactContexts crashed in
435
+ * ComponentEventDelegate as IllegalArgumentException SoftExceptions and were
436
+ * silently dropped, so onClick took dozens of presses to register.
437
+ * We still no-op the *delegate* onDestroy() so MainActivity keeps its
438
+ * ReactHost; we only stop the surface explicitly.
439
+ */
440
+ class VRActivity : ReactActivity() {
441
+
442
+ private val mainHandler = Handler(Looper.getMainLooper())
443
+ private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
444
+
445
+ // Set true on every VRActivity.onResume; consumed by the first
446
+ // onActivityPaused(other) that follows. One-shot — defends against the
447
+ // single MainActivity.onPause that the NEW_TASK ordering schedules late.
448
+ // Without one-shot semantics, every subsequent foreign pause would re-fire
449
+ // ReactHostImpl.onHostResume, which re-fires every LifecycleEventListener
450
+ // (AppState → "active" → ViroXRSceneNavigator's AppState handler calls
451
+ // launchVRScene() again → visible scene flicker).
452
+ private var expectingForeignPause = false
453
+
454
+ override fun getMainComponentName(): String = "VRQuestScene"
455
+
456
+ override fun createReactActivityDelegate(): ReactActivityDelegate =
457
+ object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) {
458
+ // VRActivity destruction MUST NOT tear down the shared ReactHost.
459
+ // MainActivity is still alive (and re-foreground after VR exit) and
460
+ // depends on the same ReactContext. The standard delegate's
461
+ // onDestroy forwards to ReactHostImpl.onHostDestroy(VR), which
462
+ // fires LifecycleEventListener.onHostDestroy on every native module
463
+ // — releasing native resources MainActivity still uses. The next
464
+ // Cmd+R reload then runs the JS bundle re-init against a partially
465
+ // destroyed native side and fails with
466
+ // [runtime not ready]: TypeError: Cannot read property
467
+ // 'EventEmitter' of undefined
468
+ // followed by AppRegistryBinding::stopSurface failed and the
469
+ // surface never recovers. By no-op'ing onDestroy here, host
470
+ // destruction is left to MainActivity's standard delegate (the real
471
+ // last activity). The surface itself is stopped explicitly in
472
+ // VRActivity.onDestroy() below.
473
+ override fun onDestroy() {
474
+ // intentionally no-op — see comment above
475
+ }
476
+ }
477
+
478
+ override fun onResume() {
479
+ super.onResume() // delegate.onResume() -> reactHost.onHostResume(this)
480
+ expectingForeignPause = true
481
+ }
482
+
483
+ // onPause not overridden: the default delegate forwards to
484
+ // reactHost.onHostPause(this) while currentActivity matches us, which is
485
+ // what we want.
486
+
487
+ override fun onCreate(savedInstanceState: Bundle?) {
488
+ super.onCreate(savedInstanceState)
489
+ val callbacks = object : Application.ActivityLifecycleCallbacks {
490
+ override fun onActivityResumed(other: Activity) {
491
+ if (other !== this@VRActivity && !isFinishing && !isDestroyed) {
492
+ finish()
493
+ }
494
+ }
495
+ override fun onActivityPaused(other: Activity) {
496
+ if (other === this@VRActivity || isFinishing || isDestroyed) return
497
+ if (!expectingForeignPause) return
498
+ expectingForeignPause = false
499
+ // MainActivity.onPause was dispatched after our onResume (the
500
+ // NEW_TASK race). The standard delegate already called
501
+ // onHostPause(MainActivity), demoting the host to PAUSED and
502
+ // disabling devSupport / clearing JavaTimerManager's frame
503
+ // callback. Re-promote so VR keeps timers + Fast Refresh.
504
+ mainHandler.post {
505
+ if (isFinishing || isDestroyed) return@post
506
+ (reactHost as? ReactHostImpl)?.onHostResume(this@VRActivity)
507
+ }
508
+ }
509
+ override fun onActivityCreated(a: Activity, b: Bundle?) {}
510
+ override fun onActivityStarted(a: Activity) {}
511
+ override fun onActivityStopped(a: Activity) {}
512
+ override fun onActivitySaveInstanceState(a: Activity, b: Bundle) {}
513
+ override fun onActivityDestroyed(a: Activity) {}
514
+ }
515
+ application.registerActivityLifecycleCallbacks(callbacks)
516
+ lifecycleCallbacks = callbacks
517
+ }
518
+
519
+ override fun onDestroy() {
520
+ // Stop the React surface attached to this VRActivity so its component
521
+ // tree unmounts cleanly. In bridgeless arch this calls
522
+ // reactSurface.stop() + reactSurface = null, leaving the shared
523
+ // ReactHost intact for MainActivity. Without this the React tree from
524
+ // each VR session piles up across re-entries and stale VRTComponents
525
+ // intercept hit-tested viewTags with dead ReactContexts, breaking
526
+ // onClick / onHover until you press dozens of times.
527
+ try {
528
+ reactDelegate?.unloadApp()
529
+ } catch (t: Throwable) {
530
+ android.util.Log.w("VRActivity", "reactDelegate.unloadApp() failed", t)
531
+ }
532
+ lifecycleCallbacks?.let { application.unregisterActivityLifecycleCallbacks(it) }
533
+ lifecycleCallbacks = null
534
+ mainHandler.removeCallbacksAndMessages(null)
535
+ super.onDestroy()
536
+ }
537
+ }
538
+ `;
539
+ fs_1.default.writeFileSync(activityPath, kotlinContent, "utf-8");
540
+ }
541
+ return config;
542
+ },
543
+ ]);
544
+ // 2. Add VRActivity to AndroidManifest
545
+ config = (0, config_plugins_1.withAndroidManifest)(config, async (config) => {
546
+ const app = config.modResults.manifest.application?.[0];
547
+ if (!app)
548
+ return config;
549
+ if (!app.activity)
550
+ app.activity = [];
551
+ const alreadyAdded = app.activity.some((a) => a.$?.["android:name"] === ".VRActivity");
552
+ if (!alreadyAdded) {
553
+ app.activity.push({
554
+ $: {
555
+ "android:name": ".VRActivity",
556
+ "android:screenOrientation": "landscape",
557
+ "android:exported": "false",
558
+ "android:configChanges": "keyboard|keyboardHidden|orientation|screenSize|uiMode",
559
+ "android:launchMode": "singleTask",
560
+ },
561
+ "intent-filter": [
562
+ {
563
+ action: [{ $: { "android:name": "android.intent.action.MAIN" } }],
564
+ category: [
565
+ { $: { "android:name": "com.oculus.intent.category.VR" } },
566
+ ],
567
+ },
568
+ ],
569
+ });
570
+ }
571
+ // Inject com.oculus.app_id into <application> for Meta Quest App Name
572
+ const questAppId = props?.android?.questAppId;
573
+ if (questAppId) {
574
+ if (!app["meta-data"])
575
+ app["meta-data"] = [];
576
+ const alreadyHasAppId = app["meta-data"].some((m) => m.$?.["android:name"] === "com.oculus.app_id");
577
+ if (!alreadyHasAppId) {
578
+ app["meta-data"].push({
579
+ $: {
580
+ "android:name": "com.oculus.app_id",
581
+ "android:value": questAppId,
582
+ },
583
+ });
584
+ }
585
+ }
586
+ return config;
587
+ });
588
+ return config;
589
+ };
286
590
  const withViroAndroid = (config, props) => {
287
- (0, config_plugins_1.withPlugins)(config, [[withBranchAndroid, props]]);
288
- withViroProjectBuildGradle(config);
289
- withViroManifest(config);
290
- withViroSettingsGradle(config);
291
- withViroAppBuildGradle(config);
591
+ config = withBranchAndroid(config, props);
592
+ config = withViroProjectBuildGradle(config);
593
+ config = withViroManifest(config);
594
+ config = withViroSettingsGradle(config);
595
+ config = withViroAppBuildGradle(config);
596
+ config = withViroQuestActivity(config, props);
292
597
  return config;
293
598
  };
294
599
  exports.withViroAndroid = withViroAndroid;
@@ -18,6 +18,7 @@ const withViroPods = (config) => {
18
18
  let geospatialAnchorProvider;
19
19
  let iosLinkage;
20
20
  let includeARCore;
21
+ let includeSemantics;
21
22
  if (Array.isArray(config.plugins)) {
22
23
  const pluginConfig = config?.plugins?.find((plugin) => Array.isArray(plugin) && plugin[0] === "@reactvision/react-viro");
23
24
  if (Array.isArray(pluginConfig) && pluginConfig.length > 1) {
@@ -30,6 +31,7 @@ const withViroPods = (config) => {
30
31
  geospatialAnchorProvider = legacyOpts.geospatialAnchorProvider ?? options.provider ?? defaultProvider;
31
32
  iosLinkage = options.iosLinkage;
32
33
  includeARCore = options.ios?.includeARCore;
34
+ includeSemantics = options.ios?.includeSemantics;
33
35
  }
34
36
  }
35
37
  fs_1.default.readFile(`${root}/Podfile`, "utf-8", (err, data) => {
@@ -44,36 +46,38 @@ const withViroPods = (config) => {
44
46
  ` # Automatically includes Fabric components when RCT_NEW_ARCH_ENABLED=1\n` +
45
47
  ` pod 'ViroReact', :path => '../node_modules/@reactvision/react-viro/ios'\n` +
46
48
  ` pod 'ViroKit', :path => '../node_modules/@reactvision/react-viro/ios/dist/ViroRenderer/'`;
47
- // Add ARCore pods if enabled (explicitly via includeARCore or implicitly via cloud/geospatial providers)
49
+ // Add ARCore pods if enabled (explicitly via includeARCore/includeSemantics or implicitly via providers)
48
50
  // ViroKit.podspec declares these as weak_frameworks, making ARCore optional at runtime
49
51
  const needsARCoreForFeatures = cloudAnchorProvider === "arcore" || geospatialAnchorProvider === "arcore";
50
52
  const shouldIncludeARCore = includeARCore === true || needsARCoreForFeatures;
51
- if (shouldIncludeARCore) {
53
+ const shouldIncludeSemantics = shouldIncludeARCore || includeSemantics === true;
54
+ if (shouldIncludeSemantics) {
52
55
  viroPods +=
53
56
  `\n\n # ARCore SDK - Cloud Anchors, Geospatial, and Scene Semantics API\n` +
54
57
  ` # ViroKit uses weak linking for these frameworks, making ARCore optional at runtime.\n` +
55
58
  ` # ViroKit checks availability using NSClassFromString and gracefully degrades if not present.\n` +
56
59
  ` pod 'ARCore/CloudAnchors', '~> 1.51.0'`;
57
- // Add Geospatial pod if geospatial is enabled or explicit ARCore inclusion
60
+ // Add Geospatial pod if geospatial is enabled or full ARCore inclusion
58
61
  if (geospatialAnchorProvider === "arcore" || includeARCore === true) {
59
62
  viroPods +=
60
63
  `\n pod 'ARCore/Geospatial', '~> 1.51.0'`;
61
64
  }
62
65
  // Add Semantics pod for Scene Semantics API (ML-based scene understanding)
66
+ // Included whenever ARCore is present OR includeSemantics: true
63
67
  viroPods +=
64
68
  `\n pod 'ARCore/Semantics', '~> 1.51.0'`;
65
69
  }
66
70
  // Add use_frameworks! if configured
67
- // User's iosLinkage setting is respected; if not set and ARCore is enabled, default to dynamic
68
- const effectiveLinkage = iosLinkage || (shouldIncludeARCore ? "dynamic" : undefined);
71
+ // User's iosLinkage setting is respected; if not set and ARCore/Semantics is enabled, default to dynamic
72
+ const effectiveLinkage = iosLinkage || (shouldIncludeSemantics ? "dynamic" : undefined);
69
73
  if (effectiveLinkage) {
70
74
  // Insert use_frameworks! before the target block
71
75
  let linkageComment;
72
- if (shouldIncludeARCore && effectiveLinkage === "static") {
76
+ if (shouldIncludeSemantics && effectiveLinkage === "static") {
73
77
  // Warn user that static linkage may not work with ARCore
74
78
  linkageComment = `# WARNING: ARCore SDK typically requires dynamic frameworks.\n# Static linkage is set but may cause build issues with ARCore pods.`;
75
79
  }
76
- else if (shouldIncludeARCore) {
80
+ else if (shouldIncludeSemantics) {
77
81
  linkageComment = `# Framework linkage: ${effectiveLinkage} (ARCore requires dynamic frameworks)`;
78
82
  }
79
83
  else {
@@ -167,6 +171,7 @@ const withDefaultInfoPlist = (config, _props) => {
167
171
  let googleCloudApiKey;
168
172
  let rvApiKey;
169
173
  let rvProjectId;
174
+ let rvEndpoint;
170
175
  let cloudAnchorProvider;
171
176
  let geospatialAnchorProvider;
172
177
  let includeARCore;
@@ -186,6 +191,7 @@ const withDefaultInfoPlist = (config, _props) => {
186
191
  googleCloudApiKey = pluginOptions.googleCloudApiKey;
187
192
  rvApiKey = pluginOptions.rvApiKey;
188
193
  rvProjectId = pluginOptions.rvProjectId;
194
+ rvEndpoint = pluginOptions.rvEndpoint;
189
195
  // Resolve unified provider prop; old props override for backward compat.
190
196
  // Default to "reactvision" only when rvApiKey is present (implies RV intent).
191
197
  const defaultProvider2 = pluginOptions.rvApiKey ? "reactvision" : undefined;
@@ -220,6 +226,9 @@ const withDefaultInfoPlist = (config, _props) => {
220
226
  if (rvProjectId) {
221
227
  config.ios.infoPlist.RVProjectId = rvProjectId;
222
228
  }
229
+ if (rvEndpoint) {
230
+ config.ios.infoPlist.RVEndpoint = rvEndpoint;
231
+ }
223
232
  // Add location permissions for Geospatial API
224
233
  if (geospatialAnchorProvider === "arcore" || geospatialAnchorProvider === "reactvision" || includeARCore === true) {
225
234
  config.ios.infoPlist.NSLocationWhenInUseUsageDescription =
@@ -231,7 +240,7 @@ const withDefaultInfoPlist = (config, _props) => {
231
240
  };
232
241
  exports.withDefaultInfoPlist = withDefaultInfoPlist;
233
242
  const withViroIos = (config, props) => {
234
- (0, config_plugins_1.withPlugins)(config, [[withViroPods, props]]);
243
+ config = (0, config_plugins_1.withPlugins)(config, [[withViroPods, props]]);
235
244
  (0, exports.withDefaultInfoPlist)(config, props);
236
245
  withEnabledBitcode(config);
237
246
  withExcludedSimulatorArchitectures(config);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * withViroVisionOS.ts
3
+ *
4
+ * Expo config plugin that adds a visionOS target to your app's Xcode project.
5
+ *
6
+ * What it does:
7
+ * 1. Writes the SwiftUI App entry point (ViroVisionApp.swift) into the target folder
8
+ * 2. Adds a visionOS pod target to the Podfile
9
+ * 3. Creates a visionOS Xcode target in the .pbxproj with correct SDK + build settings
10
+ *
11
+ * Usage in app.json:
12
+ * {
13
+ * "plugins": [
14
+ * ["@reactvision/react-viro", { ... }],
15
+ * "@reactvision/react-viro/plugins/withViroVisionOS"
16
+ * ]
17
+ * }
18
+ *
19
+ * After running `expo prebuild`, run `pod install` in ios/ then build the
20
+ * <AppName>Vision target in Xcode targeting the visionOS simulator.
21
+ */
22
+ import { ConfigPlugin } from "@expo/config-plugins";
23
+ export declare const withViroVisionOS: ConfigPlugin;
24
+ export default withViroVisionOS;