From 24bd6d7f254216625e61c6ed7da74d1ac7a102c9 Mon Sep 17 00:00:00 2001 From: saqibkayani2077 <176228637+saqibkayani2077@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:17:25 +0100 Subject: [PATCH 1/5] Add Dimensions.getDisplayFeatures / useDisplayFeatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a displayFeatures field to DimensionsPayload/NativeDeviceInfo (type only: 'hinge' | 'cutout', a posture, and bounds), a Dimensions.getDisplayFeatures() accessor, and a useDisplayFeatures() hook mirroring useWindowDimensions(). Modeled on the reserved regions of iPhone Duo (Apple Tech Talk 111463, "Strike a pose with adaptive layouts on iPhone Duo") so a future foldable/dual-display Android device could report through the same shape. Native (RCTDeviceInfo.mm) always reports an empty array for now. The underlying iOS 27.1 API (UIView.reservedRegions(kind:)) is Swift-only and not part of any publicly available Xcode as of 2026-09-10; this podspec target compiles no Swift sources today, so there is no safe way to wire the real query in without either guessing at an unverified Objective-C selector name or modifying the podspec's build setup blind. Left as a clearly-commented follow-up rather than guessed at. Verified: `yarn jest packages/react-native/Libraries/Utilities/__tests__/Dimensions-displayFeatures-test.js` - all 3 tests pass: Dimensions.getDisplayFeatures ✓ defaults to an empty array ✓ reports the display features passed to set() ✓ falls back to an empty array when a later update omits displayFeatures This machine's Node (v21.1.0) is below this repo's declared engine requirement (^22.13.0+); `yarn install` needed YARN_IGNORE_ENGINES=true to proceed. No other checks (flow-check, lint) were run. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LAkF81dVyenmpEChH2Nh1k --- .../Libraries/Utilities/Dimensions.js | 21 +++++++- .../Dimensions-displayFeatures-test.js | 52 +++++++++++++++++++ .../Libraries/Utilities/useDisplayFeatures.js | 40 ++++++++++++++ .../React/CoreModules/RCTDeviceInfo.mm | 17 +++++- .../modules/NativeDeviceInfo.js | 31 +++++++++++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/Libraries/Utilities/__tests__/Dimensions-displayFeatures-test.js create mode 100644 packages/react-native/Libraries/Utilities/useDisplayFeatures.js diff --git a/packages/react-native/Libraries/Utilities/Dimensions.js b/packages/react-native/Libraries/Utilities/Dimensions.js index 85a6fc5dc494..c7846fb59e2b 100644 --- a/packages/react-native/Libraries/Utilities/Dimensions.js +++ b/packages/react-native/Libraries/Utilities/Dimensions.js @@ -14,12 +14,18 @@ import EventEmitter, { } from '../vendor/emitter/EventEmitter'; import NativeDeviceInfo, { type DimensionsPayload, + type DisplayFeature, type DisplayMetrics, type DisplayMetricsAndroid, } from './NativeDeviceInfo'; import invariant from 'invariant'; -export type {DimensionsPayload, DisplayMetrics, DisplayMetricsAndroid}; +export type { + DimensionsPayload, + DisplayFeature, + DisplayMetrics, + DisplayMetricsAndroid, +}; /** @deprecated Use DisplayMetrics */ export type ScaledSize = DisplayMetrics; @@ -29,6 +35,7 @@ const eventEmitter = new EventEmitter<{ }>(); let dimensionsInitialized = false; let dimensions: DimensionsPayload; +let displayFeatures: $ReadOnlyArray = []; /** * Provides the application window's width and height. Prefer @@ -92,6 +99,7 @@ class Dimensions { } dimensions = {window, screen}; + displayFeatures = dims.displayFeatures ?? []; if (dimensionsInitialized) { // Don't fire 'change' the first time the dimensions are set. eventEmitter.emit('change', dimensions); @@ -100,6 +108,17 @@ class Dimensions { } } + /** + * Returns the display features (such as a hinge or a front-facing camera + * cutout) that content should avoid covering. Prefer `useDisplayFeatures` + * in React components. + * + * Empty on every platform today; see the `DisplayFeature` type. + */ + static getDisplayFeatures(): $ReadOnlyArray { + return displayFeatures; + } + /** * Add an event handler. Supported events: * diff --git a/packages/react-native/Libraries/Utilities/__tests__/Dimensions-displayFeatures-test.js b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-displayFeatures-test.js new file mode 100644 index 000000000000..b992ef6a9306 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-displayFeatures-test.js @@ -0,0 +1,52 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import Dimensions from '../Dimensions'; + +describe('Dimensions.getDisplayFeatures', () => { + it('defaults to an empty array', () => { + Dimensions.set({window: {width: 1, height: 1, scale: 1, fontScale: 1}}); + expect(Dimensions.getDisplayFeatures()).toEqual([]); + }); + + it('reports the display features passed to set()', () => { + const displayFeatures = [ + { + type: 'hinge', + state: 'postureHalfOpened', + bounds: {x: 0, y: 410, width: 820, height: 24}, + }, + ]; + Dimensions.set({ + window: {width: 820, height: 1180, scale: 3, fontScale: 1}, + displayFeatures, + }); + expect(Dimensions.getDisplayFeatures()).toEqual(displayFeatures); + }); + + it('falls back to an empty array when a later update omits displayFeatures', () => { + Dimensions.set({ + window: {width: 820, height: 1180, scale: 3, fontScale: 1}, + displayFeatures: [ + { + type: 'cutout', + state: 'unknown', + bounds: {x: 0, y: 0, width: 40, height: 40}, + }, + ], + }); + expect(Dimensions.getDisplayFeatures()).toHaveLength(1); + + Dimensions.set({window: {width: 390, height: 844, scale: 3, fontScale: 1}}); + expect(Dimensions.getDisplayFeatures()).toEqual([]); + }); +}); diff --git a/packages/react-native/Libraries/Utilities/useDisplayFeatures.js b/packages/react-native/Libraries/Utilities/useDisplayFeatures.js new file mode 100644 index 000000000000..64df6caa08a7 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/useDisplayFeatures.js @@ -0,0 +1,40 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import Dimensions from './Dimensions'; +import {type DisplayFeature} from './NativeDeviceInfo'; +import {useEffect, useState} from 'react'; + +/** + * React hook that provides the display features (such as a hinge or a + * front-facing camera cutout) that content should avoid covering. + * Automatically updates when the device pose changes. + * + * Returns an empty array on every platform today. iOS reserves this for the + * iOS 27.1 SDK, which is not public as of 2026-09-10; see RCTDeviceInfo.mm. + */ +export default function useDisplayFeatures(): $ReadOnlyArray { + const [displayFeatures, setDisplayFeatures] = useState(() => + Dimensions.getDisplayFeatures(), + ); + useEffect(() => { + function handleChange() { + setDisplayFeatures(Dimensions.getDisplayFeatures()); + } + const subscription = Dimensions.addEventListener('change', handleChange); + // We might have missed an update between calling `getDisplayFeatures` in + // render and `addEventListener` in this handler. + handleChange(); + return () => { + subscription.remove(); + }; + }, []); + return displayFeatures; +} diff --git a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm index 1761214fc3bd..4c86a376ca7a 100644 --- a/packages/react-native/React/CoreModules/RCTDeviceInfo.mm +++ b/packages/react-native/React/CoreModules/RCTDeviceInfo.mm @@ -215,7 +215,22 @@ static BOOL RCTIsIPhoneNotched() @"scale" : @(screen.scale), @"fontScale" : @(fontScale) }; - return @{@"window" : dimsWindow, @"screen" : dimsScreen}; + // Reserved regions (hinge, camera cutouts) that content should avoid, such + // as on iPhone Duo - see the "Designing for iPhone Duo" Human Interface + // Guidelines and Apple Tech Talk 111463, "Strike a pose with adaptive + // layouts on iPhone Duo". The underlying UIView.reservedRegions(kind:) API + // ships with the iOS 27.1 SDK, which is not part of any Xcode release + // publicly available as of 2026-09-10 (Apple lists Xcode 27.1 beta as + // "coming later this month"). RCTDeviceInfo.mm is plain Objective-C++ with + // no Swift compilation set up in this podspec target, so there is no safe + // way to reference that (Swift-only, not-yet-shipping) symbol from here + // yet without risking a build break for everyone. This always reports an + // empty array until that SDK is available and a proper native bridge (most + // likely a small Swift helper, once Swift sources are wired into this + // target's podspec, mirroring how other iPhone Duo-aware codebases in this + // ecosystem add a dedicated Swift provider file) can be added. + NSArray *displayFeatures = @[]; + return @{@"window" : dimsWindow, @"screen" : dimsScreen, @"displayFeatures" : displayFeatures}; } - (NSDictionary *)_exportedDimensions diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js index 57d18b6bd3ab..7f8f00a58f87 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeDeviceInfo.js @@ -27,11 +27,42 @@ export type DisplayMetrics = { fontScale: number, }; +// A region of the display that content should avoid, such as a hinge or a +// front-facing camera. Modeled after the reserved regions of iPhone Duo (see +// the "Designing for iPhone Duo" Human Interface Guidelines and Apple Tech +// Talk 111463, "Strike a pose with adaptive layouts on iPhone Duo") so that a +// future foldable/dual-display Android device can report through the same +// shape. +export type DisplayFeatureType = 'hinge' | 'cutout'; + +// The posture of a 'hinge' DisplayFeature. Always 'unknown' for 'cutout'. +export type DisplayFeatureState = + | 'unknown' + | 'postureFlat' + | 'postureHalfOpened'; + +export type DisplayFeatureRect = { + x: number, + y: number, + width: number, + height: number, +}; + +export type DisplayFeature = { + type: DisplayFeatureType, + state: DisplayFeatureState, + // In the same coordinate space as the 'window' DisplayMetrics, in points. + bounds: DisplayFeatureRect, +}; + export type DimensionsPayload = { window?: DisplayMetrics, screen?: DisplayMetrics, windowPhysicalPixels?: DisplayMetricsAndroid, screenPhysicalPixels?: DisplayMetricsAndroid, + // Empty on every platform today. iOS reserves the field pending the iOS + // 27.1 SDK (not public as of 2026-09-10); see RCTDeviceInfo.mm. + displayFeatures?: $ReadOnlyArray, }; export type DeviceInfoConstants = { From bb5664c7e3b14b0ca382a48a1696719bb3922ead Mon Sep 17 00:00:00 2001 From: saqibkayani2077 <176228637+saqibkayani2077@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:05:01 +0100 Subject: [PATCH 2/5] ci: standalone macOS Xcode-27.1 check for RCTDeviceInfo displayFeatures --- .github/workflows/iphone-duo-ios-check.yml | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .github/workflows/iphone-duo-ios-check.yml diff --git a/.github/workflows/iphone-duo-ios-check.yml b/.github/workflows/iphone-duo-ios-check.yml new file mode 100644 index 000000000000..7b19915c8844 --- /dev/null +++ b/.github/workflows/iphone-duo-ios-check.yml @@ -0,0 +1,86 @@ +name: iPhone Duo - iOS Native Check (Dimensions.getDisplayFeatures) + +# Standalone, manually-dispatched build that compiles the changed native file +# (RCTDeviceInfo.mm) via the traditional CocoaPods RNTester integration — +# self-contained, unlike the repo's normal iOS CI (test-ios-spm-rntester.yml / +# e2e-ios-rntester.yml), which depend on prebuilt xcframework artifacts from +# earlier jobs in test-all.yml and can't be dispatched on their own. + +on: + workflow_dispatch: {} + +jobs: + build: + runs-on: macos-15-large + timeout-minutes: 60 + env: + APP_IOS_DIR: packages/rn-tester + XCODE_PROJECT: RNTesterPods.xcworkspace + XCODE_SCHEME: RNTester + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Select Xcode (prefer 27.1, else latest installed) + id: xcode + run: | + echo "Installed Xcode versions on this runner:" + ls /Applications | grep -i '^Xcode' || true + if [ -d "/Applications/Xcode_27.1.app" ]; then + SELECTED="/Applications/Xcode_27.1.app" + else + SELECTED=$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -1) + fi + if [ -z "$SELECTED" ]; then + echo "::error::No Xcode installation found on this runner image." + exit 1 + fi + sudo xcode-select --switch "$SELECTED" + xcodebuild -version + echo "selected=$SELECTED" >> "$GITHUB_OUTPUT" + if [ "$SELECTED" != "/Applications/Xcode_27.1.app" ]; then + echo "::warning::Xcode 27.1 is not on this GitHub-hosted runner image yet; fell back to $SELECTED. This run does NOT confirm compatibility with Xcode 27.1 specifically." + fi + + - name: Setup node.js + uses: ./.github/actions/setup-node + + - name: Run yarn install + uses: ./.github/actions/yarn-install + + - name: Set Hermes prebuilt version + shell: bash + run: node ./scripts/releases/use-hermes-prebuilt.js + + - name: Run yarn install again, with the correct hermes version + uses: ./.github/actions/yarn-install + + - name: Install CocoaPods + run: pod --version || sudo gem install cocoapods --no-document + + - name: pod install (traditional integration, builds React-Core/CoreModules from source) + working-directory: ${{ env.APP_IOS_DIR }} + run: pod install --repo-update + + - name: Build RNTester Debug for iOS Simulator (compiles RCTDeviceInfo.mm) + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -workspace "$XCODE_PROJECT" \ + -scheme "$XCODE_SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + build 2>&1 | tee /tmp/xcodebuild.log + if grep -qE "^\*\* BUILD FAILED \*\*|error:" /tmp/xcodebuild.log; then + echo "::error::xcodebuild failed — see log above, especially any errors in RCTDeviceInfo.mm." + exit 1 + fi + echo "RNTester (Debug, iphonesimulator) built cleanly, including the modified RCTDeviceInfo.mm." + + - name: Upload build log + if: always() + uses: actions/upload-artifact@v4 + with: + name: iphone-duo-rn-xcodebuild-log + path: /tmp/xcodebuild.log From df9b6a68683cb6d1049ec25aee6f53f7e1dbdb16 Mon Sep 17 00:00:00 2001 From: saqibkayani2077 <176228637+saqibkayani2077@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:17:33 +0100 Subject: [PATCH 3/5] ci: use standard macos-15 runner (macos-15-large needs a paid tier) --- .github/workflows/iphone-duo-ios-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/iphone-duo-ios-check.yml b/.github/workflows/iphone-duo-ios-check.yml index 7b19915c8844..73b62983b957 100644 --- a/.github/workflows/iphone-duo-ios-check.yml +++ b/.github/workflows/iphone-duo-ios-check.yml @@ -11,7 +11,7 @@ on: jobs: build: - runs-on: macos-15-large + runs-on: macos-15 timeout-minutes: 60 env: APP_IOS_DIR: packages/rn-tester From a1d06ace0620737d4c5266c3db2d1fdb5cfc350c Mon Sep 17 00:00:00 2001 From: saqibkayani2077 <176228637+saqibkayani2077@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:20:31 +0100 Subject: [PATCH 4/5] ci: drop stale Podfile.lock before pod install (hermes-engine version mismatch) --- .github/workflows/iphone-duo-ios-check.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/iphone-duo-ios-check.yml b/.github/workflows/iphone-duo-ios-check.yml index 73b62983b957..913b2ba5a066 100644 --- a/.github/workflows/iphone-duo-ios-check.yml +++ b/.github/workflows/iphone-duo-ios-check.yml @@ -60,7 +60,12 @@ jobs: - name: pod install (traditional integration, builds React-Core/CoreModules from source) working-directory: ${{ env.APP_IOS_DIR }} - run: pod install --repo-update + run: | + # Podfile.lock in the tree pins an older hermes-engine than the one + # "Set Hermes prebuilt version" just wrote into the podspec — drop + # the stale lock so CocoaPods resolves fresh instead of erroring. + rm -f Podfile.lock + pod install --repo-update - name: Build RNTester Debug for iOS Simulator (compiles RCTDeviceInfo.mm) working-directory: ${{ env.APP_IOS_DIR }} From 907e8fcd2f299ebe425c6827f8d0ea4227bbbda2 Mon Sep 17 00:00:00 2001 From: saqibkayani2077 <176228637+saqibkayani2077@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:35:09 +0100 Subject: [PATCH 5/5] =?UTF-8?q?ci:=20fix=20false-failure=20risk=20?= =?UTF-8?q?=E2=80=94=20use=20xcodebuild=20exit=20code,=20not=20error:=20gr?= =?UTF-8?q?ep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/iphone-duo-ios-check.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/iphone-duo-ios-check.yml b/.github/workflows/iphone-duo-ios-check.yml index 913b2ba5a066..4b138bfe3292 100644 --- a/.github/workflows/iphone-duo-ios-check.yml +++ b/.github/workflows/iphone-duo-ios-check.yml @@ -70,6 +70,10 @@ jobs: - name: Build RNTester Debug for iOS Simulator (compiles RCTDeviceInfo.mm) working-directory: ${{ env.APP_IOS_DIR }} run: | + # Use xcodebuild's own exit code, not log-text grepping — a "error:" + # substring can show up harmlessly in retry/warning noise even on a + # build that ultimately succeeds. + set -o pipefail xcodebuild \ -workspace "$XCODE_PROJECT" \ -scheme "$XCODE_SCHEME" \ @@ -77,10 +81,6 @@ jobs: -sdk iphonesimulator \ -destination 'generic/platform=iOS Simulator' \ build 2>&1 | tee /tmp/xcodebuild.log - if grep -qE "^\*\* BUILD FAILED \*\*|error:" /tmp/xcodebuild.log; then - echo "::error::xcodebuild failed — see log above, especially any errors in RCTDeviceInfo.mm." - exit 1 - fi echo "RNTester (Debug, iphonesimulator) built cleanly, including the modified RCTDeviceInfo.mm." - name: Upload build log