diff --git a/README.md b/README.md
index cd80cec0d3fe9a..c8fcd35c277ae2 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ This is a prop on `` component allowing to add hover effect. It's applie
If you want to customize it you can use the `visionos_hoverEffect` prop, like so:
-```jsx
+```tsx
Click me
@@ -77,6 +77,102 @@ If you want to customize it you can use the `visionos_hoverEffect` prop, like so
The available options are: `lift` or `highlight`.
+### `XR` API
+Manage Immersive Experiences.
+
+#### Methods
+**`requestSession`**
+```js
+requestSession: (sessionId?: string) => Promise
+```
+Opens a new [`ImmersiveSpace`](https://developer.apple.com/documentation/swiftui/immersive-spaces) given it's unique `Id`.
+
+**`endSession`**
+```js
+endSession: () => Promise
+```
+Closes currently open `ImmersiveSpace`.
+
+#### Constants
+**`supportsMultipleScenes`**
+```js
+supportsMultipleScenes: boolean
+```
+A Boolean value that indicates whether the app may display multiple scenes simultaneously. Returns the value of `UIApplicationSupportsMultipleScenes` key from `Info.plist`.
+
+### Example Usage
+
+1. Set `UIApplicationSupportsMultipleScenes` to `true` in `Info.plist`:
+```diff
+
+
+
+
+ UIApplicationSceneManifest
+
+ UIApplicationPreferredDefaultSceneSessionRole
+ UIWindowSceneSessionRoleApplication
+ UIApplicationSupportsMultipleScenes
+-
++
+ UISceneConfigurations
+
+
+
+
+
+```
+
+
+1. Inside `App.swift` add new `ImmersiveSpace`:
+```diff
+@main
+struct HelloWorldApp: App {
+ @UIApplicationDelegateAdaptor var delegate: AppDelegate
++ @State private var immersionLevel: ImmersionStyle = .mixed
+
+ var body: some Scene {
+ RCTMainWindow(moduleName: "HelloWorldApp")
++ ImmersiveSpace(id: "TestImmersiveSpace") {
++ // RealityKit content goes here
++ }
++ .immersionStyle(selection: $immersionLevel, in: .mixed, .progressive, .full)
+ }
+}
+```
+For more information about `ImmersiveSpace` API refer to [Apple documentation](https://developer.apple.com/documentation/swiftui/immersive-spaces).
+
+In the above example we set `ImmersiveSpace` id to `TestImmersiveSpace`.
+
+Now in our JS code, we can call:
+
+```js
+import {XR} from "@callstack/react-native-visionos"
+//...
+const openXRSession = async () => {
+ try {
+ if (!XR.supportsMultipleScenes) {
+ Alert.alert('Error', 'Multiple scenes are not supported');
+ return;
+ }
+ await XR.requestSession('TestImmersiveSpace'); // Pass the same identifier from `App.swift`
+ } catch (e) {
+ Alert.alert('Error', e.message);
+ }
+};
+
+const closeXRSession = async () => {
+ await XR.endSession();
+};
+```
+> [!CAUTION]
+> Opening an `ImmersiveSpace` can fail in this secarios:
+> - `ImmersiveSpace` is not declared.
+> - `UIApplicationSupportsMultipleScenes` is set to `false`.
+> - User cancels the request.
+
+For a full example usage, refer to [`XRExample.js`](https://github.com/callstack/react-native-visionos/blob/main/packages/rn-tester/js/examples/XR/XRExample.js).
+
## Contributing
1. Follow the same steps as in the `New project creation` section.
diff --git a/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec b/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec
index d5dbdafe37ff6d..531f1d8172ca7d 100644
--- a/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec
+++ b/packages/react-native/Libraries/SwiftExtensions/React-RCTSwiftExtensions.podspec
@@ -24,4 +24,5 @@ Pod::Spec.new do |s|
s.frameworks = ["UIKit", "SwiftUI"]
s.dependency "React-Core"
+ s.dependency "React-RCTXR"
end
diff --git a/packages/react-native/Libraries/XR/ImmersiveBridge.swift b/packages/react-native/Libraries/XR/ImmersiveBridge.swift
new file mode 100644
index 00000000000000..45fdbcf4087b00
--- /dev/null
+++ b/packages/react-native/Libraries/XR/ImmersiveBridge.swift
@@ -0,0 +1,55 @@
+import Foundation
+import SwiftUI
+
+@objc public enum ImmersiveSpaceResult: Int {
+ case opened
+ case userCancelled
+ case error
+}
+
+public typealias CompletionHandlerType = (_ result: ImmersiveSpaceResult) -> Void
+
+/**
+ * Utility view used to bridge the gap between SwiftUI environment and UIKit.
+ *
+ * Calls `openImmersiveSpace` when view appears in the UIKit hierarchy and `dismissImmersiveSpace` when removed.
+ */
+struct ImmersiveBridgeView: View {
+ @Environment(\.openImmersiveSpace) private var openImmersiveSpace
+ @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
+
+ var spaceId: String
+ var completionHandler: CompletionHandlerType
+
+ var body: some View {
+ EmptyView()
+ .onAppear {
+ Task {
+ let result = await openImmersiveSpace(id: spaceId)
+
+ switch result {
+ case .opened:
+ completionHandler(.opened)
+ case .error:
+ completionHandler(.error)
+ case .userCancelled:
+ completionHandler(.userCancelled)
+ default:
+ break
+ }
+ }
+ }
+ .onDisappear {
+ Task { await dismissImmersiveSpace() }
+ }
+ }
+}
+
+@objc public class ImmersiveBridgeFactory: NSObject {
+ @objc public static func makeImmersiveBridgeView(
+ spaceId: String,
+ completionHandler: @escaping CompletionHandlerType
+ ) -> UIViewController {
+ return UIHostingController(rootView: ImmersiveBridgeView(spaceId: spaceId, completionHandler: completionHandler))
+ }
+}
diff --git a/packages/react-native/Libraries/XR/NativeXRModule.js b/packages/react-native/Libraries/XR/NativeXRModule.js
new file mode 100644
index 00000000000000..c34b296bc3e4a5
--- /dev/null
+++ b/packages/react-native/Libraries/XR/NativeXRModule.js
@@ -0,0 +1,8 @@
+/**
+ * @flow strict
+ * @format
+ */
+
+export * from '../../src/private/specs/visionos_modules/NativeXRModule';
+import NativeXRModule from '../../src/private/specs/visionos_modules/NativeXRModule';
+export default NativeXRModule;
diff --git a/packages/react-native/Libraries/XR/RCTXRModule.h b/packages/react-native/Libraries/XR/RCTXRModule.h
new file mode 100644
index 00000000000000..450d8ad4681619
--- /dev/null
+++ b/packages/react-native/Libraries/XR/RCTXRModule.h
@@ -0,0 +1,6 @@
+#import
+#import
+
+@interface RCTXRModule : NSObject
+
+@end
diff --git a/packages/react-native/Libraries/XR/RCTXRModule.mm b/packages/react-native/Libraries/XR/RCTXRModule.mm
new file mode 100644
index 00000000000000..8b5db6c7cbd008
--- /dev/null
+++ b/packages/react-native/Libraries/XR/RCTXRModule.mm
@@ -0,0 +1,88 @@
+#import
+
+#import
+
+#import
+#import
+#import
+#import "RCTXR-Swift.h"
+
+@interface RCTXRModule ()
+@end
+
+@implementation RCTXRModule {
+ UIViewController *_immersiveBridgeView;
+}
+
+RCT_EXPORT_MODULE()
+
+RCT_EXPORT_METHOD(endSession
+ : (RCTPromiseResolveBlock)resolve reject
+ : (RCTPromiseRejectBlock)reject)
+{
+ [self removeImmersiveBridge];
+ resolve(nil);
+}
+
+
+RCT_EXPORT_METHOD(requestSession
+ : (NSString *)sessionId resolve
+ : (RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
+{
+ RCTExecuteOnMainQueue(^{
+ UIWindow *keyWindow = RCTKeyWindow();
+ UIViewController *rootViewController = keyWindow.rootViewController;
+
+ if (self->_immersiveBridgeView == nil) {
+ self->_immersiveBridgeView = [ImmersiveBridgeFactory makeImmersiveBridgeViewWithSpaceId:sessionId
+ completionHandler:^(enum ImmersiveSpaceResult result){
+ if (result == ImmersiveSpaceResultError) {
+ reject(@"ERROR", @"Immersive Space failed to open, the system cannot fulfill the request.", nil);
+ [self removeImmersiveBridge];
+ } else if (result == ImmersiveSpaceResultUserCancelled) {
+ reject(@"ERROR", @"Immersive Space canceled by user", nil);
+ [self removeImmersiveBridge];
+ } else if (result == ImmersiveSpaceResultOpened) {
+ resolve(nil);
+ }
+ }];
+
+ [rootViewController.view addSubview:self->_immersiveBridgeView.view];
+ [rootViewController addChildViewController:self->_immersiveBridgeView];
+ [self->_immersiveBridgeView didMoveToParentViewController:rootViewController];
+ } else {
+ reject(@"ERROR", @"Immersive Space already opened", nil);
+ }
+ });
+}
+
+- (facebook::react::ModuleConstants)constantsToExport {
+ return [self getConstants];
+}
+
+- (facebook::react::ModuleConstants)getConstants {
+ __block facebook::react::ModuleConstants constants;
+ RCTUnsafeExecuteOnMainQueueSync(^{
+ constants = facebook::react::typedConstants({
+ .supportsMultipleScenes = RCTSharedApplication().supportsMultipleScenes
+ });
+ });
+
+ return constants;
+}
+
+- (void) removeImmersiveBridge
+{
+ RCTExecuteOnMainQueue(^{
+ [self->_immersiveBridgeView willMoveToParentViewController:nil];
+ [self->_immersiveBridgeView.view removeFromSuperview];
+ [self->_immersiveBridgeView removeFromParentViewController];
+ self->_immersiveBridgeView = nil;
+ });
+}
+
+- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
+ return std::make_shared(params);
+}
+
+@end
diff --git a/packages/react-native/Libraries/XR/React-RCTXR.podspec b/packages/react-native/Libraries/XR/React-RCTXR.podspec
new file mode 100644
index 00000000000000..1073574eb266a3
--- /dev/null
+++ b/packages/react-native/Libraries/XR/React-RCTXR.podspec
@@ -0,0 +1,51 @@
+require "json"
+
+package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
+version = package['version']
+
+source = { :git => 'https://github.com/facebook/react-native.git' }
+if version == '1000.0.0'
+ # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in.
+ source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
+else
+ source[:tag] = "v#{version}"
+end
+
+folly_config = get_folly_config()
+folly_compiler_flags = folly_config[:compiler_flags]
+folly_version = folly_config[:version]
+
+header_search_paths = [
+ "\"$(PODS_ROOT)/RCT-Folly\"",
+ "\"${PODS_ROOT}/Headers/Public/React-Codegen/react/renderer/components\"",
+]
+
+Pod::Spec.new do |s|
+ s.name = "React-RCTXR"
+ s.version = version
+ s.summary = "XR module for React Native."
+ s.homepage = "https://reactnative.dev/"
+ s.documentation_url = "https://reactnative.dev/docs/settings"
+ s.license = package["license"]
+ s.author = "Callstack"
+ s.platforms = min_supported_versions
+ s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness'
+ s.source = source
+ s.source_files = "*.{m,mm,swift}"
+ s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
+ s.header_dir = "RCTXR"
+ s.pod_target_xcconfig = {
+ "USE_HEADERMAP" => "YES",
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
+ "HEADER_SEARCH_PATHS" => header_search_paths.join(' ')
+ }
+
+ s.dependency "RCT-Folly", folly_version
+ s.dependency "RCTTypeSafety"
+ s.dependency "React-jsi"
+ s.dependency "React-Core/RCTXRHeaders"
+
+ add_dependency(s, "React-Codegen", :additional_framework_paths => ["build/generated/ios"])
+ add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
+ add_dependency(s, "React-NativeModulesApple", :additional_framework_paths => ["build/generated/ios"])
+end
diff --git a/packages/react-native/Libraries/XR/XR.d.ts b/packages/react-native/Libraries/XR/XR.d.ts
new file mode 100644
index 00000000000000..11d397a4ebde34
--- /dev/null
+++ b/packages/react-native/Libraries/XR/XR.d.ts
@@ -0,0 +1,9 @@
+
+export interface XRStatic {
+ requestSession(sessionId: string): Promise;
+ endSession(): Promise;
+ supportsMultipleScenes: boolean;
+}
+
+export const XR: XRStatic;
+export type XR = XRStatic;
diff --git a/packages/react-native/Libraries/XR/XR.js b/packages/react-native/Libraries/XR/XR.js
new file mode 100644
index 00000000000000..3487429aff1bb5
--- /dev/null
+++ b/packages/react-native/Libraries/XR/XR.js
@@ -0,0 +1,33 @@
+/**
+ * @format
+ * @flow strict
+ * @jsdoc
+ */
+
+import NativeXRModule from './NativeXRModule';
+
+const XR = {
+ requestSession: (sessionId?: string): Promise => {
+ if (NativeXRModule != null && NativeXRModule.requestSession != null) {
+ return NativeXRModule.requestSession(sessionId);
+ }
+ return Promise.reject(new Error('NativeXRModule is not available'));
+ },
+ endSession: (): Promise => {
+ if (NativeXRModule != null && NativeXRModule.endSession != null) {
+ return NativeXRModule.endSession();
+ }
+ return Promise.reject(new Error('NativeXRModule is not available'));
+ },
+ // $FlowIgnore[unsafe-getters-setters]
+ get supportsMultipleScenes(): boolean {
+ if (NativeXRModule == null) {
+ return false;
+ }
+
+ const nativeConstants = NativeXRModule.getConstants();
+ return nativeConstants.supportsMultipleScenes || false;
+ },
+};
+
+module.exports = XR;
diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap
index 95a9042b77a7c4..3e4ee57b750939 100644
--- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap
+++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap
@@ -9151,6 +9151,22 @@ exports[`public API should not change unintentionally Libraries/WebSocket/WebSoc
exports[`public API should not change unintentionally Libraries/WebSocket/WebSocketInterceptor.js 1`] = `"UNTYPED MODULE"`;
+exports[`public API should not change unintentionally Libraries/XR/NativeXRModule.js 1`] = `
+"export * from \\"../../src/private/specs/visionos_modules/NativeXRModule\\";
+declare export default typeof NativeXRModule;
+"
+`;
+
+exports[`public API should not change unintentionally Libraries/XR/XR.js 1`] = `
+"declare const XR: {
+ requestSession: (sessionId?: string) => Promise,
+ endSession: () => Promise,
+ get supportsMultipleScenes(): boolean,
+};
+declare module.exports: XR;
+"
+`;
+
exports[`public API should not change unintentionally Libraries/YellowBox/YellowBoxDeprecated.js 1`] = `
"declare const React: $FlowFixMe;
type Props = $ReadOnly<{||}>;
@@ -9268,6 +9284,7 @@ declare module.exports: {
get PushNotificationIOS(): PushNotificationIOS,
get Settings(): Settings,
get Share(): Share,
+ get XR(): XR,
get StyleSheet(): StyleSheet,
get Systrace(): Systrace,
get ToastAndroid(): ToastAndroid,
diff --git a/packages/react-native/React-Core.podspec b/packages/react-native/React-Core.podspec
index fb37075d37457d..3097ad4b3b3edb 100644
--- a/packages/react-native/React-Core.podspec
+++ b/packages/react-native/React-Core.podspec
@@ -37,6 +37,7 @@ header_subspecs = {
'RCTSettingsHeaders' => 'Libraries/Settings/*.h',
'RCTTextHeaders' => 'Libraries/Text/**/*.h',
'RCTVibrationHeaders' => 'Libraries/Vibration/*.h',
+ 'RCTXRHeaders' => 'Libraries/XR/*.h',
}
frameworks_search_paths = []
diff --git a/packages/react-native/React.podspec b/packages/react-native/React.podspec
index 59b9f8319ae306..2d085e63040906 100644
--- a/packages/react-native/React.podspec
+++ b/packages/react-native/React.podspec
@@ -53,4 +53,5 @@ Pod::Spec.new do |s|
s.dependency "React-RCTSettings", version
s.dependency "React-RCTText", version
s.dependency "React-RCTVibration", version
+ s.dependency "React-RCTXR", version
end
diff --git a/packages/react-native/React/Base/RCTUtils.m b/packages/react-native/React/Base/RCTUtils.m
index aaaf3deb74baf2..7222a51f57a1ec 100644
--- a/packages/react-native/React/Base/RCTUtils.m
+++ b/packages/react-native/React/Base/RCTUtils.m
@@ -571,6 +571,14 @@ BOOL RCTRunningInAppExtension(void)
![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
+
+#if TARGET_OS_VISION
+ /// Presenting scenes over Immersive Spaces leads to crash: "Presentations are not permitted within volumetric window scenes."
+ if (scene.session.role == UISceneSessionRoleImmersiveSpaceApplication) {
+ continue;
+ }
+#endif
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
diff --git a/packages/react-native/index.js b/packages/react-native/index.js
index 7cd220bc0755a0..78ac79b6c913ac 100644
--- a/packages/react-native/index.js
+++ b/packages/react-native/index.js
@@ -89,6 +89,7 @@ import typeof Platform from './Libraries/Utilities/Platform';
import typeof useColorScheme from './Libraries/Utilities/useColorScheme';
import typeof useWindowDimensions from './Libraries/Utilities/useWindowDimensions';
import typeof Vibration from './Libraries/Vibration/Vibration';
+import typeof XR from './Libraries/XR/XR';
import typeof YellowBox from './Libraries/YellowBox/YellowBoxDeprecated';
const warnOnce = require('./Libraries/Utilities/warnOnce');
@@ -295,6 +296,9 @@ module.exports = {
get Share(): Share {
return require('./Libraries/Share/Share');
},
+ get XR(): XR {
+ return require('./Libraries/XR/XR');
+ },
get StyleSheet(): StyleSheet {
return require('./Libraries/StyleSheet/StyleSheet');
},
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index ee02f9e13fff8b..94d76fb5dfb533 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -143,6 +143,13 @@
"android": {},
"jsSrcsDir": "src"
},
+ {
+ "name": "FBReactNativeSpec_visionOS",
+ "type": "modules",
+ "ios": {},
+ "android": {},
+ "jsSrcsDir": "src/private/specs/visionos_modules"
+ },
{
"name": "rncore",
"type": "components",
diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb
index 6df005cee4fc1e..e82b38ed772cb2 100644
--- a/packages/react-native/scripts/cocoapods/utils.rb
+++ b/packages/react-native/scripts/cocoapods/utils.rb
@@ -601,6 +601,7 @@ def self.react_native_pods
"glog",
"hermes-engine",
"React-hermes",
+ "React-RCTXR", # visionOS
]
end
diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb
index 92f591e9d205c8..a5233066e55eff 100644
--- a/packages/react-native/scripts/react_native_pods.rb
+++ b/packages/react-native/scripts/react_native_pods.rb
@@ -133,6 +133,7 @@ def use_react_native! (
pod 'React-nativeconfig', :path => "#{prefix}/ReactCommon"
pod 'RCTDeprecation', :path => "#{prefix}/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
pod 'React-RCTSwiftExtensions', :path => "#{prefix}/Libraries/SwiftExtensions"
+ pod 'React-RCTXR', :path => "#{prefix}/Libraries/XR"
if hermes_enabled
setup_hermes!(:react_native_path => prefix)
diff --git a/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js b/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js
new file mode 100644
index 00000000000000..ce8d22dca68c49
--- /dev/null
+++ b/packages/react-native/src/private/specs/visionos_modules/NativeXRModule.js
@@ -0,0 +1,21 @@
+/**
+ * @flow strict
+ * @format
+ */
+
+import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport';
+
+import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry';
+
+export type XRModuleConstants = {|
+ +supportsMultipleScenes?: boolean,
+|};
+
+export interface Spec extends TurboModule {
+ +getConstants: () => XRModuleConstants;
+
+ +requestSession: (sessionId?: string) => Promise;
+ +endSession: () => Promise;
+}
+
+export default (TurboModuleRegistry.get('XRModule'): ?Spec);
diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock
index 946b0f8a795e4e..676ad01127ab66 100644
--- a/packages/rn-tester/Podfile.lock
+++ b/packages/rn-tester/Podfile.lock
@@ -89,6 +89,7 @@ PODS:
- React-RCTSettings (= 1000.0.0)
- React-RCTText (= 1000.0.0)
- React-RCTVibration (= 1000.0.0)
+ - React-RCTXR (= 1000.0.0)
- React-callinvoker (1000.0.0)
- React-Codegen (1000.0.0):
- DoubleConversion
@@ -148,6 +149,7 @@ PODS:
- React-hermes
- React-jsi
- React-jsiexecutor
+ - React-jsinspector (= 1000.0.0)
- React-perflogger
- React-runtimescheduler
- React-utils
@@ -335,6 +337,21 @@ PODS:
- React-utils
- SocketRocket (= 0.7.0.1)
- Yoga
+ - React-Core/RCTXRHeaders (1000.0.0):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.0.1)
+ - Yoga
- React-CoreModules (1000.0.0):
- RCT-Folly (= 2024.01.01.00)
- RCTTypeSafety (= 1000.0.0)
@@ -947,6 +964,7 @@ PODS:
- React-jsi (= 1000.0.0)
- React-perflogger (= 1000.0.0)
- React-jsinspector (1000.0.0):
+ - DoubleConversion
- glog
- RCT-Folly (= 2024.01.01.00)
- React-nativeconfig
@@ -1071,6 +1089,7 @@ PODS:
- ReactCommon
- React-RCTSwiftExtensions (1000.0.0):
- React-Core
+ - React-RCTXR
- React-RCTTest (1000.0.0):
- RCT-Folly (= 2024.01.01.00)
- React-Core (= 1000.0.0)
@@ -1087,6 +1106,14 @@ PODS:
- React-jsi
- React-NativeModulesApple
- ReactCommon
+ - React-RCTXR (1000.0.0):
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTTypeSafety
+ - React-Codegen
+ - React-Core/RCTXRHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - ReactCommon
- React-rendererdebug (1000.0.0):
- DoubleConversion
- fmt (= 9.1.0)
@@ -1103,6 +1130,7 @@ PODS:
- React-jserrorhandler
- React-jsi
- React-jsiexecutor
+ - React-jsinspector
- React-Mapbuffer
- React-NativeModulesApple
- React-RCTFabric
@@ -1118,6 +1146,7 @@ PODS:
- React-jserrorhandler
- React-jsi
- React-jsiexecutor
+ - React-jsinspector
- React-runtimeexecutor
- React-runtimescheduler
- React-utils
@@ -1192,6 +1221,7 @@ PODS:
- RCT-Folly (= 2024.01.01.00)
- React-callinvoker (= 1000.0.0)
- React-cxxreact (= 1000.0.0)
+ - React-debug (= 1000.0.0)
- React-jsi (= 1000.0.0)
- React-logger (= 1000.0.0)
- React-perflogger (= 1000.0.0)
@@ -1270,6 +1300,7 @@ DEPENDENCIES:
- React-RCTTest (from `./RCTTest`)
- React-RCTText (from `../react-native/Libraries/Text`)
- React-RCTVibration (from `../react-native/Libraries/Vibration`)
+ - React-RCTXR (from `../react-native/Libraries/XR`)
- React-rendererdebug (from `../react-native/ReactCommon/react/renderer/debug`)
- React-rncore (from `../react-native/ReactCommon`)
- React-RuntimeApple (from `../react-native/ReactCommon/react/runtime/platform/ios`)
@@ -1385,6 +1416,8 @@ EXTERNAL SOURCES:
:path: "../react-native/Libraries/Text"
React-RCTVibration:
:path: "../react-native/Libraries/Vibration"
+ React-RCTXR:
+ :path: "../react-native/Libraries/XR"
React-rendererdebug:
:path: "../react-native/ReactCommon/react/renderer/debug"
React-rncore:
@@ -1419,8 +1452,8 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
boost: 8f1e9b214fa11f71081fc8ecd5fad3daf221cf7f
- DoubleConversion: 71bf0761505a44e4dfddc0aa04afa049fdfb63b5
- FBLazyVector: e34ddc4948ab5a9f857262cf68a46d39d8c61ed1
+ DoubleConversion: 26c660c8d88372cca1a67f8101d2d962a7064361
+ FBLazyVector: 6d72bc9033085488dab3c20cee3dab2b6a771dda
fmt: 5d9ffa7ccba126c08b730252123601d514652320
glog: 4f05d17aa39a829fee878689fc9a41af587fabba
hermes-engine: 3fed7e58e811ae8f795063cc6450714395c0276d
@@ -1429,57 +1462,58 @@ SPEC CHECKSUMS:
OCMock: 267d92c078398b7ce11d99e811e3a402744c06bc
RCT-Folly: 70c792c856324d6a518af75b3a307c14c226343a
RCTDeprecation: 3808e36294137f9ee5668f4df2e73dc079cd1dcf
- RCTRequired: 60c97b0b0cc33a209d33b3412eb1a218cbc1b82c
- RCTTypeSafety: 2f597b431b910be1e49c6e6f40dd6879f6d1bdfd
- React: dc827cd6966c06176ea9664cb4d74d63d1a4fcca
- React-callinvoker: 3727946c98d5626aef39ac6a45b9d1be5db35527
+ RCTRequired: 6426efd0d6123b35fdac9ad5796717cbcef85ff9
+ RCTTypeSafety: 7c822d061f3b0efcfb058c61cda509369bcb8602
+ React: 2f26ffcc453dbb43a07e7396d3cc568cf328f830
+ React-callinvoker: 1a7c0d6de53398862ddf642677df14231f6697d9
React-Codegen: 04a9ac6fd2f392534ed432102c6cbf117dcbf62b
- React-Core: eb5002d75c263aaa042b97853effd3b6e2f8668c
- React-CoreModules: 1eb22960e980f9e0b80542da464c5935260fd180
- React-cxxreact: eb5eb55bffe0ccff4cbf192a04c978e37f137e8f
- React-debug: 1d5a869d405ca2410bfc95c7fe340220331696ef
- React-Fabric: 555fc115322e9b37b8cc1d4ac0ab8d4a87718077
- React-FabricImage: 657d05951f1a512496713d7c2d534d347915fe3b
- React-graphics: 989577b97b4d5e8f3cd3ff86b275eb7e4c7ff657
- React-hermes: 0003c9f0f623ed87224f4415c567f90f0d15737f
- React-ImageManager: ec80ccfd373893be8c347a23a693438b41a86e2a
- React-jserrorhandler: c4126c1c2fb42540cf64fdc80917214a0c6ca905
- React-jsi: 81558ed18c4e91824280a3298d75d598ef4cf3fa
- React-jsiexecutor: 139ad70390e61c6b00779d82c7d5f490cd3eb632
- React-jsinspector: bd27899ffc61660559bfbc8872e8d8de05b2612d
- React-jsitracing: 9639d0659449fa9a71f31e28d7dde349bcec3394
- React-logger: 142bc8479d40940294428f652fb016b809335121
- React-Mapbuffer: 2762ea21725665466ea54f09848886f5ba7e97f7
- React-nativeconfig: f984aee5151a69d520ea830756b33c62acc5c269
- React-NativeModulesApple: 5fd38cf3bd4aca4adc9522a7a53681001f98f29d
- React-perflogger: b19adeb816c255a04dff9528cf3f6bf209188d17
- React-RCTActionSheet: c07c3924b6a94602f40effc130b776faa8b6bab1
- React-RCTAnimation: 51f5a6be1479336b26d0cb282bd0e8f1feabf593
- React-RCTAppDelegate: 3cac28ed63e10086624a2d965ce2c3359bbe04b0
- React-RCTBlob: 0d6f17d4cacc18b6872b06b232186b176d4a8a18
- React-RCTFabric: 8b955856519621a30a8425ac6c594abb66788839
- React-RCTImage: c69ac72c257475c7860602ac11beaabd7862b99c
- React-RCTLinking: c3f99ac575655569f5bc108ff13b816dcdf1dbfd
- React-RCTNetwork: 39405b5687d5b16fdcc6466713930e71f8389cde
- React-RCTPushNotification: 8b6b936f8e07096db581a446d4f9844ff8b35925
- React-RCTSettings: 96dad99a283d67c573b8ba3709725901e8ac3108
- React-RCTSwiftExtensions: b372ca0daf79796645c691841b470ad5d37bba64
- React-RCTTest: 11be8ba66b91bc914d31f2b7c9abd0c39d47a3d7
- React-RCTText: 8ca2156c782dc33ceb3fa80ce8c20cfcccbcc9d1
- React-RCTVibration: 284a9575d09b5c5517d5d5b8b86d4e2d3c324b5e
- React-rendererdebug: d664023e12af482bb3911ce547c522ddbbf01ac5
- React-rncore: 5917d667ddd77e1938a9d1dabf5c503e427f9ec9
- React-RuntimeApple: 31c6a97b53e53ea3e0d071ca45471b9152d22dc3
- React-RuntimeCore: 25febc097f3f7d4043562fcf5c7bf406758ad1ce
- React-runtimeexecutor: b66459278d0c6564b5d877107e0bb3ac953a8221
- React-RuntimeHermes: 3f46517238fed32a04b36c6bc41a4ac8508baf6e
- React-runtimescheduler: 04123aa94f0fa33398b9e58cd4345cac230eaee7
- React-utils: bdfeb70a54b7b73533de6c8b679143f75c34c86a
- ReactCommon: 8f6a345ebd9558892a83e1be6b83bdad4fd99078
- ReactCommon-Samples: 090dcc2659d35c5577955d89efac618595a72e61
+ React-Core: 9397cdd14ae7018850c16dea3b31ac72c4ff0324
+ React-CoreModules: 53a00162be44e3353f2a9efad243d3dafe258cdb
+ React-cxxreact: 46f736a9cace851d42a3704c0da637d8566fd7d6
+ React-debug: b1f637660400365b9b6908bd521ffce20c713dc5
+ React-Fabric: 34c01267cb5ace971a7ae1a9c720ce5b30e92465
+ React-FabricImage: 2e076721abe31734ce2a01cf82e254801ce5503f
+ React-graphics: 5ca218f8180dda07f997fa5ad145b7d5d7f35bd9
+ React-hermes: d5707721feadf6c74d53fbc7a6212a98a140954c
+ React-ImageManager: deb13029061d5b0b08b21b8ea5883c15b7f4ba93
+ React-jserrorhandler: f719009db2ae821f7df30866fa7e16676a14e85d
+ React-jsi: 9bf49563b30132199feb9b7721289ebbe61258f5
+ React-jsiexecutor: 81e5a31965bb0e29d66df963ec9bf13496e84482
+ React-jsinspector: 3b3514d648791386506f2f4eb7f1791e8b44f663
+ React-jsitracing: 84ec35538d5d28a74431e58fc245761327dcb537
+ React-logger: a41fc9cccff7de126c97e366c8909304531813ec
+ React-Mapbuffer: 3f22d38e3c903fa729061b0e8b976d102c377306
+ React-nativeconfig: 0d00e737812420344137c8b7010033675d527982
+ React-NativeModulesApple: 803f8508c47c2963cf893d74904ccc605fae4696
+ React-perflogger: 0d57e63db7d181c2c3b9d13305b89b7783615739
+ React-RCTActionSheet: aa4ae1a2a14153412e9237b10234a40e30d2b410
+ React-RCTAnimation: 7eea83acd5da45de3d2e0c3aaa8df9d2957783bf
+ React-RCTAppDelegate: d48ddb6a45470b5d5d75d35927c2342698c47e56
+ React-RCTBlob: 038dce0c29488cfbc19daa6d60b1cfcb01e9172d
+ React-RCTFabric: 131c60dc266f637d45c75c02b81d337c4e3179f9
+ React-RCTImage: af4bbba6368f6b219915ca569ae5cf67fc8585f8
+ React-RCTLinking: ce884034ffbece4c6c2ae8e1e8b4b88fb0ee406b
+ React-RCTNetwork: 488fca1b0e9f4f9802696372216029f5d1da6bc6
+ React-RCTPushNotification: 2bb0038afd0c02f65d7b74b93bddcb2639cc6edf
+ React-RCTSettings: 0ff75db9df85764bd838b1ea9411fde23247f229
+ React-RCTSwiftExtensions: 0751aeb108e9c7ae39b26710b7fb47d79fe0bfb9
+ React-RCTTest: 3144ece03e48904e7796c2a96bd931649027516d
+ React-RCTText: abc6d97f7e36f9aed66678fc712f703d5bce2dfc
+ React-RCTVibration: 2b3b6cda5f979b0d16db391e737651d0dd4147a6
+ React-RCTXR: 07a185fd7221532ccc6a5a9f237102256f823e60
+ React-rendererdebug: 48c745f05ebf3ae447ea521c2b77bb7ee3923fd6
+ React-rncore: 84944ba68705b5de9959d2206cfe7dd3181e83f2
+ React-RuntimeApple: 33e35a0c93367647120758a61c71e21c73ce54ee
+ React-RuntimeCore: 7a4d19efece49c4087fd7bb30a643397d9ba1ab1
+ React-runtimeexecutor: fb3425d082ad47c4bc7153419b3eac899d318603
+ React-RuntimeHermes: 556f92a11c0cf9b4d4e4d7240dca228bdebb4622
+ React-runtimescheduler: c101938ad407340555eb5d56557b3409785a7396
+ React-utils: c3b2a4535dfad1e3c68d371b3946a33db4ceac4a
+ ReactCommon: 93b947fa9f432e09577a5d8c3f09c0ad7a6a4fc5
+ ReactCommon-Samples: 6a59337d27861b986269a95c52aa5353b87da4ba
ScreenshotManager: a5f596498de38f3cf3377df636ca67355503f190
SocketRocket: 0ba3e799f983d2dfa878777017659ef6c866e5c6
- Yoga: ac56a983bdf421a579c197143f79aa568f1c74a1
+ Yoga: 50572c6c7c8227a84feb5746efa5f841f75f6f1b
PODFILE CHECKSUM: 7e999b8158f1055609ef4491bc35f1ad658fdd6c
diff --git a/packages/rn-tester/RNTester-visionOS/App.swift b/packages/rn-tester/RNTester-visionOS/App.swift
index 39224b803cce1b..8e28482981e8a6 100644
--- a/packages/rn-tester/RNTester-visionOS/App.swift
+++ b/packages/rn-tester/RNTester-visionOS/App.swift
@@ -5,8 +5,11 @@ import React_RCTSwiftExtensions
@main
struct RNTesterApp: App {
@UIApplicationDelegateAdaptor var delegate: AppDelegate
+ @State private var immersionLevel: ImmersionStyle = .full
var body: some Scene {
RCTMainWindow(moduleName: "RNTesterApp")
+ ImmersiveSpace(id: "TestImmersiveSpace") {}
+ .immersionStyle(selection: $immersionLevel, in: .mixed, .progressive, .full)
}
}
diff --git a/packages/rn-tester/RNTester-visionOS/Info.plist b/packages/rn-tester/RNTester-visionOS/Info.plist
index 940d4ef45518db..20f75e2afa73d1 100644
--- a/packages/rn-tester/RNTester-visionOS/Info.plist
+++ b/packages/rn-tester/RNTester-visionOS/Info.plist
@@ -7,7 +7,7 @@
UIApplicationPreferredDefaultSceneSessionRole
UIWindowSceneSessionRoleApplication
UIApplicationSupportsMultipleScenes
-
+
UISceneConfigurations
diff --git a/packages/rn-tester/js/examples/XR/XRExample.js b/packages/rn-tester/js/examples/XR/XRExample.js
new file mode 100644
index 00000000000000..b9bd48e3f5c2c3
--- /dev/null
+++ b/packages/rn-tester/js/examples/XR/XRExample.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @format
+ * @flow
+ */
+
+'use strict';
+
+const {XR} = require('@callstack/react-native-visionos');
+const React = require('react');
+const {Alert, Button, StyleSheet, Text, View} = require('react-native');
+
+const OpenXRSession = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
+
+ const openXRSession = async () => {
+ try {
+ if (!XR.supportsMultipleScenes) {
+ Alert.alert('Error', 'Multiple scenes are not supported');
+ return;
+ }
+ await XR.requestSession('TestImmersiveSpace');
+ setIsOpen(true);
+ } catch (e) {
+ Alert.alert('Error', e.message);
+ setIsOpen(false);
+ }
+ };
+
+ const closeXRSession = async () => {
+ if (isOpen) {
+ await XR.endSession();
+ }
+ };
+
+ return (
+
+ Is XR session open: {isOpen}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ title: {
+ fontSize: 30,
+ margin: 10,
+ textAlign: 'center',
+ },
+});
+
+exports.title = 'XR';
+exports.description = 'Spatial experiences';
+exports.examples = [
+ {
+ title: 'Open XR Session',
+ render(): React.Node {
+ return ;
+ },
+ },
+];
diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js
index fa0587ccd1454c..65082c121f2b3f 100644
--- a/packages/rn-tester/js/utils/RNTesterList.ios.js
+++ b/packages/rn-tester/js/utils/RNTesterList.ios.js
@@ -179,6 +179,10 @@ const APIs: Array = ([
key: 'AppearanceExample',
module: require('../examples/Appearance/AppearanceExample'),
},
+ {
+ key: 'XRExample',
+ module: require('../examples/XR/XRExample'),
+ },
{
key: 'AppStateExample',
module: require('../examples/AppState/AppStateExample'),