From f8ec7b7002d4ea349fcc7e5901c5a61e357e1fcb Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 27 Feb 2026 17:21:19 +0100 Subject: [PATCH 1/7] [age-range] bump android dependency (#43345) # Why This PR updates the Google Play age-signals dependency from version 0.0.2 to 0.0.3 to resolve test failures that were occurring with the previous version, and add new `userStatus` value. https://developer.android.com/google/play/age-signals/release-notes # How Updated the `com.google.android.play:age-signals` # Test Plan - ci, bare expo # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/expo-age-range/CHANGELOG.md | 2 ++ packages/expo-age-range/android/build.gradle | 2 +- .../src/main/java/expo/modules/agerange/AgeRangeRecords.kt | 2 ++ .../test/java/expo/modules/agerange/AgeSignalsManagerTest.kt | 2 -- packages/expo-age-range/build/ExpoAgeRange.types.d.ts | 2 +- packages/expo-age-range/build/ExpoAgeRange.types.d.ts.map | 2 +- packages/expo-age-range/build/ExpoAgeRange.types.js.map | 2 +- packages/expo-age-range/src/ExpoAgeRange.types.ts | 1 + 8 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/expo-age-range/CHANGELOG.md b/packages/expo-age-range/CHANGELOG.md index b7299bf9e01b59..1e0103d2cb9821 100644 --- a/packages/expo-age-range/CHANGELOG.md +++ b/packages/expo-age-range/CHANGELOG.md @@ -6,6 +6,8 @@ ### šŸŽ‰ New features +- bump android dependency, expose new `DECLARED` user status ([#43345](https://github.com/expo/expo/pull/43345) by [@vonovak](https://github.com/vonovak)) + ### šŸ› Bug fixes ### šŸ’” Others diff --git a/packages/expo-age-range/android/build.gradle b/packages/expo-age-range/android/build.gradle index 7c1d9a90b8c29e..8aeaab9499d839 100644 --- a/packages/expo-age-range/android/build.gradle +++ b/packages/expo-age-range/android/build.gradle @@ -20,7 +20,7 @@ repositories { dependencies { // when bumping, check if the ignored test is passing with the new version - implementation 'com.google.android.play:age-signals:0.0.2' + implementation 'com.google.android.play:age-signals:0.0.3' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.16' diff --git a/packages/expo-age-range/android/src/main/java/expo/modules/agerange/AgeRangeRecords.kt b/packages/expo-age-range/android/src/main/java/expo/modules/agerange/AgeRangeRecords.kt index 327f5a43bf7f84..3eddc9777b3869 100644 --- a/packages/expo-age-range/android/src/main/java/expo/modules/agerange/AgeRangeRecords.kt +++ b/packages/expo-age-range/android/src/main/java/expo/modules/agerange/AgeRangeRecords.kt @@ -1,6 +1,7 @@ package expo.modules.agerange import com.google.android.play.agesignals.AgeSignalsResult +import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus.DECLARED import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus.SUPERVISED import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_DENIED import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus.SUPERVISED_APPROVAL_PENDING @@ -37,6 +38,7 @@ data class AgeRangeResult( SUPERVISED_APPROVAL_PENDING -> "SUPERVISED_APPROVAL_PENDING" UNKNOWN -> "UNKNOWN" SUPERVISED_APPROVAL_DENIED -> "SUPERVISED_APPROVAL_DENIED" + DECLARED -> "DECLARED" else -> status.toString() } } diff --git a/packages/expo-age-range/android/src/test/java/expo/modules/agerange/AgeSignalsManagerTest.kt b/packages/expo-age-range/android/src/test/java/expo/modules/agerange/AgeSignalsManagerTest.kt index 392f91b45cfec9..a3197e91f64f27 100644 --- a/packages/expo-age-range/android/src/test/java/expo/modules/agerange/AgeSignalsManagerTest.kt +++ b/packages/expo-age-range/android/src/test/java/expo/modules/agerange/AgeSignalsManagerTest.kt @@ -8,7 +8,6 @@ import com.google.android.play.agesignals.model.AgeSignalsVerificationStatus import com.google.android.play.agesignals.testing.FakeAgeSignalsManager import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -21,7 +20,6 @@ import java.util.Date class AgeSignalsManagerTest { @Test - @Ignore("Temporarily skipped, fails with age-signals:0.0.2 but passed before") fun testCheckAgeSignals_verifiedAdult_success() { val mostRecentApprovalDate = LocalDate.of(2022, 1, 15).atStartOfDay().toInstant(ZoneOffset.UTC) diff --git a/packages/expo-age-range/build/ExpoAgeRange.types.d.ts b/packages/expo-age-range/build/ExpoAgeRange.types.d.ts index 89dd5135d89b74..0fd0c4a4903f17 100644 --- a/packages/expo-age-range/build/ExpoAgeRange.types.d.ts +++ b/packages/expo-age-range/build/ExpoAgeRange.types.d.ts @@ -45,7 +45,7 @@ export type AgeRangeResponse = { * * @platform android */ - userStatus?: 'VERIFIED' | 'SUPERVISED' | 'SUPERVISED_APPROVAL_PENDING' | 'SUPERVISED_APPROVAL_DENIED' | 'UNKNOWN'; + userStatus?: 'VERIFIED' | 'SUPERVISED' | 'SUPERVISED_APPROVAL_PENDING' | 'SUPERVISED_APPROVAL_DENIED' | 'DECLARED' | 'UNKNOWN'; /** * The effective date (timestamp) of the most recent significant change that was approved. * diff --git a/packages/expo-age-range/build/ExpoAgeRange.types.d.ts.map b/packages/expo-age-range/build/ExpoAgeRange.types.d.ts.map index 9b8b0a8c62b12d..846ceb3fd90ede 100644 --- a/packages/expo-age-range/build/ExpoAgeRange.types.d.ts.map +++ b/packages/expo-age-range/build/ExpoAgeRange.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ExpoAgeRange.types.d.ts","sourceRoot":"","sources":["../src/ExpoAgeRange.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,cAAc,GAAG,kBAAkB,CAAC;IAC1D;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EACP,UAAU,GACV,YAAY,GACZ,6BAA6B,GAC7B,4BAA4B,GAC5B,SAAS,CAAC;IACd;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC3E"} \ No newline at end of file +{"version":3,"file":"ExpoAgeRange.types.d.ts","sourceRoot":"","sources":["../src/ExpoAgeRange.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,cAAc,GAAG,kBAAkB,CAAC;IAC1D;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EACP,UAAU,GACV,YAAY,GACZ,6BAA6B,GAC7B,4BAA4B,GAC5B,UAAU,GACV,SAAS,CAAC;IACd;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC3E"} \ No newline at end of file diff --git a/packages/expo-age-range/build/ExpoAgeRange.types.js.map b/packages/expo-age-range/build/ExpoAgeRange.types.js.map index 68bb8226de22af..b22c9096eee309 100644 --- a/packages/expo-age-range/build/ExpoAgeRange.types.js.map +++ b/packages/expo-age-range/build/ExpoAgeRange.types.js.map @@ -1 +1 @@ -{"version":3,"file":"ExpoAgeRange.types.js","sourceRoot":"","sources":["../src/ExpoAgeRange.types.ts"],"names":[],"mappings":"","sourcesContent":["import { NativeModule } from 'expo-modules-core/types';\n\n/**\n * Options for requesting age range information from the user.\n *\n * @platform ios\n */\nexport type AgeRangeRequest = {\n /** The required minimum age for your app. */\n threshold1: number;\n /** An optional additional minimum age for your app. */\n threshold2?: number;\n /** An optional additional minimum age for your app. */\n threshold3?: number;\n};\n\n/**\n * Response containing the user's age range information.\n *\n * Contains age boundaries and platform-specific metadata.\n */\nexport type AgeRangeResponse = {\n /** The lower limit of the person’s age range. */\n lowerBound?: number;\n /** The upper limit of the person’s age range. */\n upperBound?: number;\n /**\n * Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group).\n *\n * @platform ios\n */\n ageRangeDeclaration?: 'selfDeclared' | 'guardianDeclared';\n /**\n * List of parental controls enabled and shared as a part of age range declaration.\n *\n * @platform ios\n */\n activeParentalControls?: string[];\n /**\n * An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval.\n *\n * @platform android\n */\n installId?: string;\n /**\n * The user's age verification or supervision status.\n *\n * @platform android\n */\n userStatus?:\n | 'VERIFIED'\n | 'SUPERVISED'\n | 'SUPERVISED_APPROVAL_PENDING'\n | 'SUPERVISED_APPROVAL_DENIED'\n | 'UNKNOWN';\n /**\n * The effective date (timestamp) of the most recent significant change that was approved.\n *\n * @platform android\n */\n mostRecentApprovalDate?: number;\n};\n\nexport interface ExpoAgeRangeModule extends NativeModule {\n requestAgeRangeAsync(options: AgeRangeRequest): Promise;\n}\n"]} \ No newline at end of file +{"version":3,"file":"ExpoAgeRange.types.js","sourceRoot":"","sources":["../src/ExpoAgeRange.types.ts"],"names":[],"mappings":"","sourcesContent":["import { NativeModule } from 'expo-modules-core/types';\n\n/**\n * Options for requesting age range information from the user.\n *\n * @platform ios\n */\nexport type AgeRangeRequest = {\n /** The required minimum age for your app. */\n threshold1: number;\n /** An optional additional minimum age for your app. */\n threshold2?: number;\n /** An optional additional minimum age for your app. */\n threshold3?: number;\n};\n\n/**\n * Response containing the user's age range information.\n *\n * Contains age boundaries and platform-specific metadata.\n */\nexport type AgeRangeResponse = {\n /** The lower limit of the person’s age range. */\n lowerBound?: number;\n /** The upper limit of the person’s age range. */\n upperBound?: number;\n /**\n * Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group).\n *\n * @platform ios\n */\n ageRangeDeclaration?: 'selfDeclared' | 'guardianDeclared';\n /**\n * List of parental controls enabled and shared as a part of age range declaration.\n *\n * @platform ios\n */\n activeParentalControls?: string[];\n /**\n * An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval.\n *\n * @platform android\n */\n installId?: string;\n /**\n * The user's age verification or supervision status.\n *\n * @platform android\n */\n userStatus?:\n | 'VERIFIED'\n | 'SUPERVISED'\n | 'SUPERVISED_APPROVAL_PENDING'\n | 'SUPERVISED_APPROVAL_DENIED'\n | 'DECLARED'\n | 'UNKNOWN';\n /**\n * The effective date (timestamp) of the most recent significant change that was approved.\n *\n * @platform android\n */\n mostRecentApprovalDate?: number;\n};\n\nexport interface ExpoAgeRangeModule extends NativeModule {\n requestAgeRangeAsync(options: AgeRangeRequest): Promise;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-age-range/src/ExpoAgeRange.types.ts b/packages/expo-age-range/src/ExpoAgeRange.types.ts index f4bf23305e964d..aff4aea43b813d 100644 --- a/packages/expo-age-range/src/ExpoAgeRange.types.ts +++ b/packages/expo-age-range/src/ExpoAgeRange.types.ts @@ -52,6 +52,7 @@ export type AgeRangeResponse = { | 'SUPERVISED' | 'SUPERVISED_APPROVAL_PENDING' | 'SUPERVISED_APPROVAL_DENIED' + | 'DECLARED' | 'UNKNOWN'; /** * The effective date (timestamp) of the most recent significant change that was approved. From c6d03b865ab17d19b49cfb298a93b699753ac31a Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 27 Feb 2026 17:22:39 +0100 Subject: [PATCH 2/7] [docs] update generated docs for expo-age-range (#43515) # Why Add support for the `DECLARED` user status in the expo-age-range API documentation to align with the latest Android platform capabilities. # How # Test Plan # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- docs/public/static/data/unversioned/expo-age-range.json | 2 +- docs/public/static/data/v55.0.0/expo-age-range.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/public/static/data/unversioned/expo-age-range.json b/docs/public/static/data/unversioned/expo-age-range.json index c3818e014f8cbf..ebe0e1e5639800 100644 --- a/docs/public/static/data/unversioned/expo-age-range.json +++ b/docs/public/static/data/unversioned/expo-age-range.json @@ -1 +1 @@ -{"schemaVersion":"2.0","name":"expo-age-range","variant":"project","kind":1,"children":[{"name":"AgeRangeRequest","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Options for requesting age range information from the user."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"threshold1","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The required minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold2","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold3","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"AgeRangeResponse","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Response containing the user's age range information.\n\nContains age boundaries and platform-specific metadata."}]},"children":[{"name":"activeParentalControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"List of parental controls enabled and shared as a part of age range declaration."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"intrinsic","name":"string"}}},{"name":"ageRangeDeclaration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"selfDeclared"},{"type":"literal","value":"guardianDeclared"}]}},{"name":"installId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"lowerBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The lower limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"mostRecentApprovalDate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The effective date (timestamp) of the most recent significant change that was approved."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"upperBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The upper limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"userStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The user's age verification or supervision status."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"VERIFIED"},{"type":"literal","value":"SUPERVISED"},{"type":"literal","value":"SUPERVISED_APPROVAL_PENDING"},{"type":"literal","value":"SUPERVISED_APPROVAL_DENIED"},{"type":"literal","value":"UNKNOWN"}]}}]},{"name":"requestAgeRangeAsync","variant":"declaration","kind":64,"signatures":[{"name":"requestAgeRangeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Prompts the user to share their age range with the app. Responses may be cached by the OS for future requests."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that resolves with user's age range response, or rejects with an error.\nThe user needs to be signed in on the device to get a valid response.\nWhen not supported (earlier than iOS 26 and web), the call returns "},{"kind":"code","text":"`lowerBound: 18`"},{"kind":"text","text":", which is equivalent to the response of an adult user."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 26.0+"}]}]},"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"AgeRangeRequest","package":"expo-age-range"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"AgeRangeResponse","package":"expo-age-range"}],"name":"Promise","package":"typescript"}}]}],"packageName":"expo-age-range"} \ No newline at end of file +{"schemaVersion":"2.0","name":"expo-age-range","variant":"project","kind":1,"children":[{"name":"AgeRangeRequest","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Options for requesting age range information from the user."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"threshold1","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The required minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold2","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold3","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"AgeRangeResponse","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Response containing the user's age range information.\n\nContains age boundaries and platform-specific metadata."}]},"children":[{"name":"activeParentalControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"List of parental controls enabled and shared as a part of age range declaration."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"intrinsic","name":"string"}}},{"name":"ageRangeDeclaration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"selfDeclared"},{"type":"literal","value":"guardianDeclared"}]}},{"name":"installId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"lowerBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The lower limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"mostRecentApprovalDate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The effective date (timestamp) of the most recent significant change that was approved."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"upperBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The upper limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"userStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The user's age verification or supervision status."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"VERIFIED"},{"type":"literal","value":"SUPERVISED"},{"type":"literal","value":"SUPERVISED_APPROVAL_PENDING"},{"type":"literal","value":"SUPERVISED_APPROVAL_DENIED"},{"type":"literal","value":"DECLARED"},{"type":"literal","value":"UNKNOWN"}]}}]},{"name":"requestAgeRangeAsync","variant":"declaration","kind":64,"signatures":[{"name":"requestAgeRangeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Prompts the user to share their age range with the app. Responses may be cached by the OS for future requests."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that resolves with user's age range response, or rejects with an error.\nThe user needs to be signed in on the device to get a valid response.\nWhen not supported (earlier than iOS 26 and web), the call returns "},{"kind":"code","text":"`lowerBound: 18`"},{"kind":"text","text":", which is equivalent to the response of an adult user."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 26.0+"}]}]},"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"AgeRangeRequest","package":"expo-age-range"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"AgeRangeResponse","package":"expo-age-range"}],"name":"Promise","package":"typescript"}}]}],"packageName":"expo-age-range"} \ No newline at end of file diff --git a/docs/public/static/data/v55.0.0/expo-age-range.json b/docs/public/static/data/v55.0.0/expo-age-range.json index c3818e014f8cbf..ebe0e1e5639800 100644 --- a/docs/public/static/data/v55.0.0/expo-age-range.json +++ b/docs/public/static/data/v55.0.0/expo-age-range.json @@ -1 +1 @@ -{"schemaVersion":"2.0","name":"expo-age-range","variant":"project","kind":1,"children":[{"name":"AgeRangeRequest","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Options for requesting age range information from the user."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"threshold1","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The required minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold2","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold3","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"AgeRangeResponse","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Response containing the user's age range information.\n\nContains age boundaries and platform-specific metadata."}]},"children":[{"name":"activeParentalControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"List of parental controls enabled and shared as a part of age range declaration."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"intrinsic","name":"string"}}},{"name":"ageRangeDeclaration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"selfDeclared"},{"type":"literal","value":"guardianDeclared"}]}},{"name":"installId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"lowerBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The lower limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"mostRecentApprovalDate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The effective date (timestamp) of the most recent significant change that was approved."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"upperBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The upper limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"userStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The user's age verification or supervision status."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"VERIFIED"},{"type":"literal","value":"SUPERVISED"},{"type":"literal","value":"SUPERVISED_APPROVAL_PENDING"},{"type":"literal","value":"SUPERVISED_APPROVAL_DENIED"},{"type":"literal","value":"UNKNOWN"}]}}]},{"name":"requestAgeRangeAsync","variant":"declaration","kind":64,"signatures":[{"name":"requestAgeRangeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Prompts the user to share their age range with the app. Responses may be cached by the OS for future requests."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that resolves with user's age range response, or rejects with an error.\nThe user needs to be signed in on the device to get a valid response.\nWhen not supported (earlier than iOS 26 and web), the call returns "},{"kind":"code","text":"`lowerBound: 18`"},{"kind":"text","text":", which is equivalent to the response of an adult user."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 26.0+"}]}]},"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"AgeRangeRequest","package":"expo-age-range"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"AgeRangeResponse","package":"expo-age-range"}],"name":"Promise","package":"typescript"}}]}],"packageName":"expo-age-range"} \ No newline at end of file +{"schemaVersion":"2.0","name":"expo-age-range","variant":"project","kind":1,"children":[{"name":"AgeRangeRequest","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Options for requesting age range information from the user."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"children":[{"name":"threshold1","variant":"declaration","kind":1024,"comment":{"summary":[{"kind":"text","text":"The required minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold2","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"threshold3","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An optional additional minimum age for your app."}]},"type":{"type":"intrinsic","name":"number"}}]},{"name":"AgeRangeResponse","variant":"declaration","kind":2097152,"comment":{"summary":[{"kind":"text","text":"Response containing the user's age range information.\n\nContains age boundaries and platform-specific metadata."}]},"children":[{"name":"activeParentalControls","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"List of parental controls enabled and shared as a part of age range declaration."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"array","elementType":{"type":"intrinsic","name":"string"}}},{"name":"ageRangeDeclaration","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"Indicates whether the age range was declared by the user themselves or someone else (parent, guardian, or Family Organizer in a Family Sharing group)."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"ios"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"selfDeclared"},{"type":"literal","value":"guardianDeclared"}]}},{"name":"installId","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"An ID assigned to supervised user installs by Google Play, used to notify you of revoked app approval."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"string"}},{"name":"lowerBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The lower limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"mostRecentApprovalDate","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The effective date (timestamp) of the most recent significant change that was approved."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"intrinsic","name":"number"}},{"name":"upperBound","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The upper limit of the person’s age range."}]},"type":{"type":"intrinsic","name":"number"}},{"name":"userStatus","variant":"declaration","kind":1024,"flags":{"isOptional":true},"comment":{"summary":[{"kind":"text","text":"The user's age verification or supervision status."}],"blockTags":[{"tag":"@platform","content":[{"kind":"text","text":"android"}]}]},"type":{"type":"union","types":[{"type":"literal","value":"VERIFIED"},{"type":"literal","value":"SUPERVISED"},{"type":"literal","value":"SUPERVISED_APPROVAL_PENDING"},{"type":"literal","value":"SUPERVISED_APPROVAL_DENIED"},{"type":"literal","value":"DECLARED"},{"type":"literal","value":"UNKNOWN"}]}}]},{"name":"requestAgeRangeAsync","variant":"declaration","kind":64,"signatures":[{"name":"requestAgeRangeAsync","variant":"signature","kind":4096,"comment":{"summary":[{"kind":"text","text":"Prompts the user to share their age range with the app. Responses may be cached by the OS for future requests."}],"blockTags":[{"tag":"@returns","content":[{"kind":"text","text":"A promise that resolves with user's age range response, or rejects with an error.\nThe user needs to be signed in on the device to get a valid response.\nWhen not supported (earlier than iOS 26 and web), the call returns "},{"kind":"code","text":"`lowerBound: 18`"},{"kind":"text","text":", which is equivalent to the response of an adult user."}]},{"tag":"@platform","content":[{"kind":"text","text":"android"}]},{"tag":"@platform","content":[{"kind":"text","text":"ios 26.0+"}]}]},"parameters":[{"name":"options","variant":"param","kind":32768,"type":{"type":"reference","name":"AgeRangeRequest","package":"expo-age-range"}}],"type":{"type":"reference","target":{"packageName":"typescript","packagePath":"lib/lib.es5.d.ts","qualifiedName":"Promise"},"typeArguments":[{"type":"reference","name":"AgeRangeResponse","package":"expo-age-range"}],"name":"Promise","package":"typescript"}}]}],"packageName":"expo-age-range"} \ No newline at end of file From 11d4391ee976e7c3c339d3c5f4373d57899d8314 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Fri, 27 Feb 2026 17:51:25 +0100 Subject: [PATCH 3/7] Enforce Android navigationBar / statusBar to transparent in Expo Go / Dev Launcher (#43518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Following #43514 (removal of `expo_splash_screen_status_bar_translucent` attribute) and #43516 (deletion of `withAndroidSplashLegacyMainActivity`), this PR continues the cleanup by enforcing transparent status bar and navigation bar on Android in Expo Go and dev-launcher (it was already enforced by edge-to-edge, so it's more about removing `backgroundColor` / `translucent` related code). # How - **Expo Go** (`ExperienceActivityUtils.kt`): - Removed `statusBar` `backgroundColor` handling and return value from `setStyle` - Removed `navigationBar` `backgroundColor` handling - Simplified `setStyle` to directly assign `systemUiVisibility` flags without tracking / returning the applied style - Fixed `MANIFEST_NAVIGATION_BAR_VISIBLILITY` → `MANIFEST_NAVIGATION_BAR_VISIBILITY` typo in `ExponentManifest.kt` - **Dev Launcher** (`DevLauncherExpoActivityConfigurator.kt`): - Removed `statusBar` `backgroundColor` / `translucent` handling and return value from `setStyle` - Removed `navigationBar` `backgroundColor` handling - `setTranslucent` no longer takes a `Boolean` parameter (always applies translucent insets) # Test Plan Open the Expo Go / the Bare apps on Android and verify: - Status bar is transparent and respects `barStyle` (`light-content` / `dark-content`) - Navigation bar is transparent - Navigation bar visibility and appearance options still work as expected # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com> --- .../host/exp/exponent/ExponentManifest.kt | 2 +- .../exponent/utils/ExperienceActivityUtils.kt | 75 +++------------- packages/expo-dev-launcher/CHANGELOG.md | 2 + .../DevLauncherExpoActivityConfigurator.kt | 88 +++++-------------- 4 files changed, 35 insertions(+), 132 deletions(-) diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt index a2bd6825545668..404e27344d8caa 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt @@ -179,7 +179,7 @@ class ExponentManifest @Inject constructor( // NavigationBar const val MANIFEST_NAVIGATION_BAR_KEY = "androidNavigationBar" - const val MANIFEST_NAVIGATION_BAR_VISIBLILITY = "visible" + const val MANIFEST_NAVIGATION_BAR_VISIBILITY = "visible" const val MANIFEST_NAVIGATION_BAR_APPEARANCE = "barStyle" const val MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR = "backgroundColor" diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/utils/ExperienceActivityUtils.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/utils/ExperienceActivityUtils.kt index 9974cc4e882d05..92ec7c1ca19d69 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/utils/ExperienceActivityUtils.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/utils/ExperienceActivityUtils.kt @@ -98,7 +98,6 @@ object ExperienceActivityUtils { fun configureStatusBar(manifest: Manifest, activity: Activity) { val statusBarOptions = manifest.getAndroidStatusBarOptions() val statusBarStyle = statusBarOptions?.getNullable(ExponentManifest.MANIFEST_STATUS_BAR_APPEARANCE) - val statusBarBackgroundColor = statusBarOptions?.getNullable(ExponentManifest.MANIFEST_STATUS_BAR_BACKGROUND_COLOR) val statusBarHidden = statusBarOptions != null && statusBarOptions.optBoolean( ExponentManifest.MANIFEST_STATUS_BAR_HIDDEN, @@ -113,38 +112,9 @@ object ExperienceActivityUtils { setTranslucent(activity) - val appliedStatusBarStyle = setStyle(statusBarStyle, activity) + setStyle(statusBarStyle, activity) - // Color passed from manifest is in format '#RRGGBB(AA)' and Android uses '#AARRGGBB' - val normalizedStatusBarBackgroundColor = RGBAtoARGB(statusBarBackgroundColor) - - if (normalizedStatusBarBackgroundColor == null || !ColorParser.isValid(normalizedStatusBarBackgroundColor)) { - // backgroundColor is invalid or not set - if (appliedStatusBarStyle == STATUS_BAR_STYLE_LIGHT_CONTENT) { - // appliedStatusBarStyle is "light-content" so background color should be semi transparent black - setColor(Color.parseColor("#88000000"), activity) - } else { - // otherwise it has to be transparent - setColor(Color.TRANSPARENT, activity) - } - } else { - setColor(Color.parseColor(normalizedStatusBarBackgroundColor), activity) - } - } - } - - /** - * If the string conforms to the "#RRGGBBAA" format then it's converted into the "#AARRGGBB" format. - * Otherwise noop. - */ - private fun RGBAtoARGB(rgba: String?): String? { - if (rgba == null) { - return null - } - return if (rgba.startsWith("#") && rgba.length == 9) { - "#" + rgba.substring(7, 9) + rgba.substring(1, 7) - } else { - rgba + setColor(Color.TRANSPARENT, activity) } } @@ -178,28 +148,16 @@ object ExperienceActivityUtils { * @return Effective style that is actually applied to the status bar. */ @UiThread - private fun setStyle(style: String?, activity: Activity): String { - var appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT + private fun setStyle(style: String?, activity: Activity) { val decorView = activity.window.decorView - var systemUiVisibilityFlags = decorView.systemUiVisibility - when (style) { - STATUS_BAR_STYLE_LIGHT_CONTENT -> { - systemUiVisibilityFlags = - systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT - } - STATUS_BAR_STYLE_DARK_CONTENT -> { - systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT - } - else -> { - systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT - } + decorView.systemUiVisibility = when (style) { + STATUS_BAR_STYLE_LIGHT_CONTENT -> + decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + STATUS_BAR_STYLE_DARK_CONTENT -> + decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + else -> + decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } - decorView.systemUiVisibility = systemUiVisibilityFlags - - return appliedStatusBarStyle } @UiThread @@ -246,17 +204,6 @@ object ExperienceActivityUtils { fun setNavigationBar(manifest: Manifest, activity: Activity) { val navBarOptions = manifest.getAndroidNavigationBarOptions() ?: return - // Set background color of navigation bar - val navBarColor = navBarOptions.getNullable(ExponentManifest.MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR) - if (navBarColor != null && ColorParser.isValid(navBarColor)) { - try { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) - activity.window.navigationBarColor = Color.parseColor(navBarColor) - } catch (e: Throwable) { - EXL.e(TAG, e) - } - } - // Set icon color of navigation bar val navBarAppearance = navBarOptions.getNullable(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE) if (navBarAppearance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -275,7 +222,7 @@ object ExperienceActivityUtils { } // Set visibility of navigation bar - val navBarVisible = navBarOptions.getNullable(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBLILITY) + val navBarVisible = navBarOptions.getNullable(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBILITY) if (navBarVisible != null) { // Hide both the navigation bar and the status bar. The Android docs recommend, "you should // design your app to hide the status bar whenever you hide the navigation bar." diff --git a/packages/expo-dev-launcher/CHANGELOG.md b/packages/expo-dev-launcher/CHANGELOG.md index a2145dcb9150fe..e1eca34951a22f 100644 --- a/packages/expo-dev-launcher/CHANGELOG.md +++ b/packages/expo-dev-launcher/CHANGELOG.md @@ -10,6 +10,8 @@ ### šŸ’” Others +- Enforce transparent status bar and navigation bar on Android, remove unused `backgroundColor` / `translucent` options handling. ([#43518](https://github.com/expo/expo/pull/43518) by [@zoontek](https://github.com/zoontek)) + ## 55.0.10 — 2026-02-25 _This version does not introduce any user-facing changes._ diff --git a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/launcher/configurators/DevLauncherExpoActivityConfigurator.kt b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/launcher/configurators/DevLauncherExpoActivityConfigurator.kt index c6b9681088d734..4e4a0ba43014ef 100644 --- a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/launcher/configurators/DevLauncherExpoActivityConfigurator.kt +++ b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/launcher/configurators/DevLauncherExpoActivityConfigurator.kt @@ -14,7 +14,6 @@ import android.view.WindowManager import androidx.annotation.UiThread import androidx.core.view.ViewCompat import com.facebook.react.ReactActivity -import expo.modules.devlauncher.helpers.RGBAtoARGB import expo.modules.devlauncher.helpers.isValidColor import expo.modules.devlauncher.launcher.manifest.DevLauncherNavigationBarStyle import expo.modules.devlauncher.launcher.manifest.DevLauncherNavigationBarVisibility @@ -54,59 +53,29 @@ class DevLauncherExpoActivityConfigurator( fun applyStatusBarConfiguration(activity: ReactActivity) { val statusBarOptions = manifest.getAndroidStatusBarOptions() val style = statusBarOptions?.optString("barStyle") - val backgroundColor = statusBarOptions?.optString("backgroundColor") - val translucent = statusBarOptions == null || statusBarOptions.optBoolean("translucent", true) val hidden = statusBarOptions != null && statusBarOptions.optBoolean("hidden", false) activity.runOnUiThread { // clear android:windowTranslucentStatus flag from Window as RN achieves translucency using WindowInsets activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) setHidden(hidden, activity) - setTranslucent(translucent, activity) - val appliedStyle = setStyle(style, activity) - - // Color passed from manifest is in format '#RRGGBB(AA)' and Android uses '#AARRGGBB' - val normalizedStatusBarBackgroundColor = RGBAtoARGB(backgroundColor) - - val finalBackgroundColor = if (normalizedStatusBarBackgroundColor == null || !isValidColor(normalizedStatusBarBackgroundColor)) { - // backgroundColor is invalid or not set - if (appliedStyle == DevLauncherStatusBarStyle.LIGHT) { - // appliedStatusBarStyle is "light-content" so background color should be semi transparent black - Color.parseColor("#88000000") - } else { - // otherwise it has to be transparent - Color.TRANSPARENT - } - } else { - Color.parseColor(normalizedStatusBarBackgroundColor) - } - - setColor(finalBackgroundColor, activity) + setTranslucent(activity) + setStyle(style, activity) + setColor(Color.TRANSPARENT, activity) } } @UiThread - private fun setStyle(style: String?, activity: Activity): String { - var appliedStatusBarStyle = DevLauncherStatusBarStyle.LIGHT + private fun setStyle(style: String?, activity: Activity) { val decorView = activity.window.decorView - var systemUiVisibilityFlags = decorView.systemUiVisibility - when (style) { - DevLauncherStatusBarStyle.LIGHT -> { - systemUiVisibilityFlags = systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() - appliedStatusBarStyle = DevLauncherStatusBarStyle.LIGHT - } - DevLauncherStatusBarStyle.DARK -> { - systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - appliedStatusBarStyle = DevLauncherStatusBarStyle.DARK - } - else -> { - systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR - appliedStatusBarStyle = DevLauncherStatusBarStyle.DARK - } + decorView.systemUiVisibility = when (style) { + DevLauncherStatusBarStyle.LIGHT -> + decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + DevLauncherStatusBarStyle.DARK -> + decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + else -> + decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } - decorView.systemUiVisibility = systemUiVisibilityFlags - - return appliedStatusBarStyle } @UiThread @@ -121,22 +90,18 @@ class DevLauncherExpoActivityConfigurator( } @UiThread - private fun setTranslucent(translucent: Boolean, activity: Activity) { - // If the status bar is translucent hook into the window insets calculations + private fun setTranslucent(activity: Activity) { + // As the status bar is translucent, hook into the window insets calculations // and consume all the top insets so no padding will be added under the status bar. val decorView = activity.window.decorView - if (translucent) { - decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets? -> - val defaultInsets = v.onApplyWindowInsets(insets) - defaultInsets.replaceSystemWindowInsets( - defaultInsets.systemWindowInsetLeft, - 0, - defaultInsets.systemWindowInsetRight, - defaultInsets.systemWindowInsetBottom - ) - } - } else { - decorView.setOnApplyWindowInsetsListener(null) + decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets? -> + val defaultInsets = v.onApplyWindowInsets(insets) + defaultInsets.replaceSystemWindowInsets( + defaultInsets.systemWindowInsetLeft, + 0, + defaultInsets.systemWindowInsetRight, + defaultInsets.systemWindowInsetBottom + ) } ViewCompat.requestApplyInsets(decorView) } @@ -153,17 +118,6 @@ class DevLauncherExpoActivityConfigurator( fun applyNavigationBarConfiguration(activity: ReactActivity) { val navBarOptions = manifest.getAndroidNavigationBarOptions() ?: return - // Set background color of navigation bar - val navBarColor = navBarOptions.optString("backgroundColor") - if (isValidColor(navBarColor)) { - try { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) - activity.window.navigationBarColor = Color.parseColor(navBarColor) - } catch (e: Throwable) { - Log.e(TAG, "Failed to configure androidNavigationBar.backgroundColor", e) - } - } - // Set icon color of navigation bar val navBarAppearance = navBarOptions.optString("barStyle") if (navBarAppearance != "" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { From 000d4a8f2c9f6025a9da6078e7b281b974597ab1 Mon Sep 17 00:00:00 2001 From: Wiktor Smaga Date: Fri, 27 Feb 2026 18:33:34 +0100 Subject: [PATCH 4/7] [ci] Improve ccache key for iOS and Android (#43368) # Why - Split `Restore ccache` into separate steps for Android and iOS. - Added `Podfile.lock` to the iOS key - without it, the Ccache hit rate drops, as seen here: https://github.com/expo/expo/actions/runs/22260916981/job/64417320297. - Added Xcode version to the iOS key - each Xcode version introduces changes to the clang compiler (https://gist.github.com/yamaya/2924292), which drops the Ccache hit rate to 0%. # How Implements the above improvements. # Test Plan Green CI --- .github/actions/expo-caches/action.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/actions/expo-caches/action.yml b/.github/actions/expo-caches/action.yml index 5d0e7e7cfe259b..0be62e91e92218 100644 --- a/.github/actions/expo-caches/action.yml +++ b/.github/actions/expo-caches/action.yml @@ -194,12 +194,28 @@ runs: shell: bash run: sudo $ANDROID_SDK_ROOT/tools/bin/sdkmanager --install "ndk;${{ inputs.ndk-version }}" - - name: ā™»ļø Restore ccache - if: inputs.ccache == 'true' + - name: šŸ”ļø Get Xcode version for iOS ccache key + if: inputs.ccache == 'true' && runner.os == 'macOS' + id: xcode-version + shell: bash + run: echo "hash=$(xcodebuild -version | md5)" >> $GITHUB_OUTPUT + + - name: ā™»ļø Restore ccache (iOS) + if: inputs.ccache == 'true' && runner.os == 'macOS' + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/.ccache + # It is necessary to include Xcode version in the cache key, because each Xcode version introduces changes to clang compiler, + # which is a part of ccache hashes. After an Xcode version change the hit rate would drop to 0% until a new cache is built. + key: ${{ runner.os }}-ccache-${{ steps.xcode-version.outputs.hash }}-${{ hashFiles('**/Podfile.lock', 'yarn.lock', 'packages/**/*.c', 'packages/**/*.cpp', 'packages/**/*.h', 'packages/**/*.mm', 'packages/**/*.m') }} + restore-keys: ${{ runner.os }}-ccache-${{ steps.xcode-version.outputs.hash }}- + + - name: ā™»ļø Restore ccache (Android) + if: inputs.ccache == 'true' && runner.os == 'Linux' uses: actions/cache@v4 with: path: ${{ runner.temp }}/.ccache - key: ${{ runner.os }}-ccache-${{ hashFiles('yarn.lock', 'packages/**/*.cpp', 'packages/**/*.h', 'packages/**/*.mm', 'packages/**/*.m') }} + key: ${{ runner.os }}-ccache-${{ hashFiles('yarn.lock', 'packages/**/*.c', 'packages/**/*.cpp', 'packages/**/*.h') }} restore-keys: ${{ runner.os }}-ccache- - name: šŸ”ļø Get cache key of Git LFS files From 3518b566cfad5c0fe743d0dfca3552cdc3e227e4 Mon Sep 17 00:00:00 2001 From: Thibault Malbranche Date: Fri, 27 Feb 2026 18:46:03 +0100 Subject: [PATCH 5/7] fix(babel-preset-expo): Add missing default react-compiler opt-out directives (#43521) # Why Specifying opt-out directives overrides all defaults, and doesn't add them back. They'll instead have to be added manually; See: https://github.com/facebook/react/blob/e0cc7202e14418b453c69c4f06dc00c64151f202/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L69-L86 # How - Add list of default directives # Test Plan - Tested manually against local test project - Tested with unit test added in babel-preset-expo # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --------- Co-authored-by: Phil Pluckthun --- packages/babel-preset-expo/CHANGELOG.md | 1 + packages/babel-preset-expo/build/index.js | 15 +++++++++-- .../src/__tests__/compiler.test.ts | 27 +++++++++++++++++++ packages/babel-preset-expo/src/index.ts | 16 +++++++++-- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/babel-preset-expo/CHANGELOG.md b/packages/babel-preset-expo/CHANGELOG.md index 8ef346be8e6d0c..84e9ab8bdc186d 100644 --- a/packages/babel-preset-expo/CHANGELOG.md +++ b/packages/babel-preset-expo/CHANGELOG.md @@ -9,6 +9,7 @@ ### šŸ› Bug fixes - Opt `"widget"` functions for `expo-widgets` out of react-compiler ([#43451](https://github.com/expo/expo/pull/43451) by [@kitten](https://github.com/kitten)) +- Fix `"use no memo"` and `"use no forget"` default opt-out directives being ineffective in react-compiler transform ([#43521](https://github.com/expo/expo/pull/43521) by [@Titozzz](https://github.com/Titozzz), [@kitten](https://github.com/kitten)) ### šŸ’” Others diff --git a/packages/babel-preset-expo/build/index.js b/packages/babel-preset-expo/build/index.js index 0ffca6e1802215..e3871b016b97bd 100644 --- a/packages/babel-preset-expo/build/index.js +++ b/packages/babel-preset-expo/build/index.js @@ -82,6 +82,18 @@ function babelPresetExpo(api, options = {}) { // Give users the ability to opt-out of the feature, per-platform. platformOptions['react-compiler'] !== false) { const reactCompilerOptions = platformOptions['react-compiler']; + const reactCompilerOptOutDirectives = new Set([ + // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX + 'widget', + // We need to manually include the default opt-out directives, since they get overridden + // See: + // - https://github.com/facebook/react/blob/e0cc720/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L48C1-L48C77 + // - https://github.com/facebook/react/blob/e0cc720/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L69-L86 + 'use no memo', + 'use no forget', + // Add the user's override but preserve defaults above to avoid the pitfall of them being removed + ...(reactCompilerOptions?.customOptOutDirectives ?? []), + ]); extraPlugins.push([ require('babel-plugin-react-compiler'), { @@ -93,8 +105,7 @@ function babelPresetExpo(api, options = {}) { panicThreshold: isDev ? undefined : 'NONE', ...reactCompilerOptions, // See: https://github.com/facebook/react/blob/074d96b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L160-L163 - // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX - customOptOutDirectives: [...(reactCompilerOptions?.customOptOutDirectives ?? []), 'widget'], + customOptOutDirectives: [...reactCompilerOptOutDirectives], }, ]); } diff --git a/packages/babel-preset-expo/src/__tests__/compiler.test.ts b/packages/babel-preset-expo/src/__tests__/compiler.test.ts index 130d96a3f7f0b0..3b7c71a26209a6 100644 --- a/packages/babel-preset-expo/src/__tests__/compiler.test.ts +++ b/packages/babel-preset-expo/src/__tests__/compiler.test.ts @@ -167,3 +167,30 @@ it(`skips memoizing in node modules`, () => { })!; expect(code).not.toContain('react.memo_cache_sentinel'); }); + +it.each(['widget', 'use no memo', 'use no forget'])( + `skips memoizing with opt-out directive "%s"`, + (directive) => { + const { code } = babel.transformSync( + ` + export default function App() { + ${JSON.stringify(directive)}; + return
; + } + `, + { + ...options, + filename: '/samples/Test.jsx', + caller: getCaller({ + name: 'metro', + supportsReactCompiler: true, + engine: 'hermes', + platform: 'ios', + isDev: false, + }), + } + )!; + expect(code).not.toContain('import {'); + expect(code).not.toContain('react.memo_cache_sentinel'); + } +); diff --git a/packages/babel-preset-expo/src/index.ts b/packages/babel-preset-expo/src/index.ts index 7d09600ab996f3..5a196f720076d6 100644 --- a/packages/babel-preset-expo/src/index.ts +++ b/packages/babel-preset-expo/src/index.ts @@ -174,6 +174,19 @@ function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}): platformOptions['react-compiler'] !== false ) { const reactCompilerOptions = platformOptions['react-compiler']; + const reactCompilerOptOutDirectives = new Set([ + // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX + 'widget', + // We need to manually include the default opt-out directives, since they get overridden + // See: + // - https://github.com/facebook/react/blob/e0cc720/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L48C1-L48C77 + // - https://github.com/facebook/react/blob/e0cc720/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L69-L86 + 'use no memo', + 'use no forget', + // Add the user's override but preserve defaults above to avoid the pitfall of them being removed + ...(reactCompilerOptions?.customOptOutDirectives ?? []), + ]); + extraPlugins.push([ require('babel-plugin-react-compiler'), { @@ -185,8 +198,7 @@ function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}): panicThreshold: isDev ? undefined : 'NONE', ...reactCompilerOptions, // See: https://github.com/facebook/react/blob/074d96b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L160-L163 - // We need to opt-out for our widgets, since they're stringified functions that output Swift UI JSX - customOptOutDirectives: [...(reactCompilerOptions?.customOptOutDirectives ?? []), 'widget'], + customOptOutDirectives: [...reactCompilerOptOutDirectives], }, ]); } From 2a7e36dd401982c58a0a466520040acaed3bbca2 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Sat, 28 Feb 2026 01:54:07 +0530 Subject: [PATCH 6/7] [docs] Fix internal links to Expo Router hooks API reference (#43523) --- .../router/migrate/from-react-navigation.mdx | 18 +++++++++--------- docs/pages/router/reference/testing.mdx | 10 +++++----- docs/pages/router/reference/typed-routes.mdx | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/pages/router/migrate/from-react-navigation.mdx b/docs/pages/router/migrate/from-react-navigation.mdx index 592a54f7319ba8..c8d6df4fa488eb 100644 --- a/docs/pages/router/migrate/from-react-navigation.mdx +++ b/docs/pages/router/migrate/from-react-navigation.mdx @@ -201,11 +201,11 @@ Instead, migrate `navigation` to the `useRouter` hook. -Similarly, migrate from the `route` prop to the [`useLocalSearchParams`](/router/reference/hooks/#uselocalsearchparams) hook. +Similarly, migrate from the `route` prop to the [`useLocalSearchParams`](/versions/latest/sdk/router/#uselocalsearchparams) hook. -To access the [`navigation.navigate`](https://reactnavigation.org/docs/navigation-object/#navigate), import the `navigation` prop from [`useNavigation`](/router/reference/hooks/#usenavigation) hook. +To access the [`navigation.navigate`](https://reactnavigation.org/docs/navigation-object/#navigate), import the `navigation` prop from [`useNavigation`](/versions/latest/sdk/router/#usenavigation) hook. @@ -277,11 +277,11 @@ Use `useRootNavigationState()`. #### `getCurrentRoute` -Unlike React Navigation, Expo Router can reliably represent any route with a string. Use the [`usePathname()`](/router/reference/hooks/#usepathname) or [`useSegments()`](/router/reference/hooks/#usesegments) hooks to identify the current route. +Unlike React Navigation, Expo Router can reliably represent any route with a string. Use the [`usePathname()`](/versions/latest/sdk/router/#usepathname) or [`useSegments()`](/versions/latest/sdk/router/#usesegments) hooks to identify the current route. #### `getCurrentOptions` -Use the [`useLocalSearchParams()`](/router/reference/hooks/#uselocalsearchparams) hook to get the current route's query parameters. +Use the [`useLocalSearchParams()`](/versions/latest/sdk/router/#uselocalsearchparams) hook to get the current route's query parameters. #### `addListener` @@ -289,11 +289,11 @@ The following events can be migrated: #### `state` -Use the [`usePathname()`](/router/reference/hooks/#usepathname) or [`useSegments()`](/router/reference/hooks/#usesegments) hooks to identify the current route. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. +Use the [`usePathname()`](/versions/latest/sdk/router/#usepathname) or [`useSegments()`](/versions/latest/sdk/router/#usesegments) hooks to identify the current route. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. #### `options` -Use the [`useLocalSearchParams()`](/router/reference/hooks/#uselocalsearchparams) hook to get the current route's query parameters. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. +Use the [`useLocalSearchParams()`](/versions/latest/sdk/router/#uselocalsearchparams) hook to get the current route's query parameters. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. ### props @@ -307,7 +307,7 @@ Avoid using this pattern in favor of deep linking (for example, a user opens you #### `onStateChange` -Use the [`usePathname()`](/router/reference/hooks/#usepathname), [`useSegments()`](/router/reference/hooks/#usesegments), and [`useGlobalSearchParams()`](/router/reference/hooks/#useglobalsearchparams) hooks to identify the current route state. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. +Use the [`usePathname()`](/versions/latest/sdk/router/#usepathname), [`useSegments()`](/versions/latest/sdk/router/#usesegments), and [`useGlobalSearchParams()`](/versions/latest/sdk/router/#useglobalsearchparams) hooks to identify the current route state. Use in conjunction with `useEffect(() => {}, [...])` to observe changes. - If you're attempting to track screen changes, follow the [Screen Tracking guide](/router/reference/screen-tracking/). - React Navigation recommends avoiding [`onStateChange`](https://reactnavigation.org/docs/navigation-container/#onstatechange). @@ -434,7 +434,7 @@ Expo Router wraps `expo-splash-screen` and adds special handling to ensure it's ### Navigation state observation -If you're observing the navigation state directly, migrate to the [`usePathname`](/router/reference/hooks/#usepathname), [`useSegments`](/router/reference/hooks/#usesegments), and [`useGlobalSearchParams`](/router/reference/hooks/#useglobalsearchparams) hooks. +If you're observing the navigation state directly, migrate to the [`usePathname`](/versions/latest/sdk/router/#usepathname), [`useSegments`](/versions/latest/sdk/router/#usesegments), and [`useGlobalSearchParams`](/versions/latest/sdk/router/#useglobalsearchparams) hooks. ### Pass params to nested screens @@ -457,7 +457,7 @@ In React Navigation, you can use the `initialRouteName` property of the linking ### Reset navigation state -You can use the [`reset`](https://reactnavigation.org/docs/navigation-actions/#reset) action from the React Navigation library to reset the navigation state. It is dispatched using the [`useNavigation`](/router/reference/hooks/#usenavigation) hook from Expo Router to access the `navigation` prop. +You can use the [`reset`](https://reactnavigation.org/docs/navigation-actions/#reset) action from the React Navigation library to reset the navigation state. It is dispatched using the [`useNavigation`](/versions/latest/sdk/router/#usenavigation) hook from Expo Router to access the `navigation` prop. In the below example, the `navigation` prop is accessible from the `useNavigation` hook and the `CommonActions.reset` action from `@react-navigation/native`. The object specified in the `reset` action replaces the existing navigation state with the new one. diff --git a/docs/pages/router/reference/testing.mdx b/docs/pages/router/reference/testing.mdx index d8414351ad264b..bc5533e9a04408 100644 --- a/docs/pages/router/reference/testing.mdx +++ b/docs/pages/router/reference/testing.mdx @@ -117,7 +117,7 @@ The following matches have been added to `expect` and can be used to assert valu -Assert the current pathname against a given string. The matcher uses the value of the [`usePathname`](/router/reference/hooks/#usepathname) hook on the current `screen`. +Assert the current pathname against a given string. The matcher uses the value of the [`usePathname`](/versions/latest/sdk/router/#usepathname) hook on the current `screen`. ```tsx app.test.tsx expect(screen).toHavePathname('/my-router'); @@ -137,7 +137,7 @@ expect(screen).toHavePathnameWithParams('/my-router?hello=world'); -Assert the current segments against an array of strings. The matcher uses the value of the [`useSegments`](/router/reference/hooks/#usesegments) hook on the current `screen`. +Assert the current segments against an array of strings. The matcher uses the value of the [`useSegments`](/versions/latest/sdk/router/#usesegments) hook on the current `screen`. ```tsx app.test.tsx expect(screen).toHaveSegments(['[id]']); @@ -146,7 +146,7 @@ expect(screen).toHaveSegments(['[id]']); -Assert the current local URL parameters against an object. The matcher uses the value of the [`useLocalSearchParams`](/router/reference/hooks/#uselocalsearchparams) hook on the current `screen`. +Assert the current local URL parameters against an object. The matcher uses the value of the [`useLocalSearchParams`](/versions/latest/sdk/router/#uselocalsearchparams) hook on the current `screen`. ```tsx app.test.tsx expect(screen).useLocalSearchParams({ first: 'abc' }); @@ -155,9 +155,9 @@ expect(screen).useLocalSearchParams({ first: 'abc' }); -Assert the current screen's pathname that matches a value. Compares using the value of [`useGlobalSearchParams`](/router/reference/hooks/#useglobalsearchparams) hook. +Assert the current screen's pathname that matches a value. Compares using the value of [`useGlobalSearchParams`](/versions/latest/sdk/router/#useglobalsearchparams) hook. -Assert the current global URL parameters against an object. The matcher uses the value of the [`useGlobalSearchParams`](/router/reference/hooks/#useglobalsearchparams) hook on the current `screen`. +Assert the current global URL parameters against an object. The matcher uses the value of the [`useGlobalSearchParams`](/versions/latest/sdk/router/#useglobalsearchparams) hook on the current `screen`. ```tsx app.test.tsx expect(screen).useGlobalSearchParams({ first: 'abc' }); diff --git a/docs/pages/router/reference/typed-routes.mdx b/docs/pages/router/reference/typed-routes.mdx index 05b0c7a523bb13..6bbb757d2cc048 100644 --- a/docs/pages/router/reference/typed-routes.mdx +++ b/docs/pages/router/reference/typed-routes.mdx @@ -7,7 +7,7 @@ import { FileTree } from '~/ui/components/FileTree'; > Available when using TypeScript in your project. Expo Router supports standard TypeScript out of the box. See the [TypeScript](/guides/typescript/) guide for more information on how to set it up. -Expo Router supports generating TypeScript types automatically with Expo CLI. This enables ``, and the [hooks API](/router/reference/hooks) to be statically typed. This feature is currently in beta and is not enabled by default. +Expo Router supports generating TypeScript types automatically with Expo CLI. This enables ``, and the [hooks API](/versions/latest/sdk/router/#hooks) to be statically typed. This feature is currently in beta and is not enabled by default. ## Get started From 0b99e9a27c743c588ca6af90c2f3824a8ea63093 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Sat, 28 Feb 2026 01:54:34 +0530 Subject: [PATCH 7/7] [docs] Fix broken internal links for EAS environment variables (#43525) --- docs/pages/build-reference/git-submodules.mdx | 2 +- docs/pages/build-reference/private-npm-packages.mdx | 2 +- docs/pages/build-reference/troubleshooting.mdx | 4 ++-- docs/pages/build/eas-json.mdx | 2 +- docs/pages/build/setup.mdx | 2 +- docs/pages/guides/environment-variables.mdx | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/pages/build-reference/git-submodules.mdx b/docs/pages/build-reference/git-submodules.mdx index 8e0a48bf9bc28f..b2bdf97363be0b 100644 --- a/docs/pages/build-reference/git-submodules.mdx +++ b/docs/pages/build-reference/git-submodules.mdx @@ -15,7 +15,7 @@ To initialize a submodule on EAS Build builder: -Create a [secret](/build-reference/variables/#using-secrets-in-environment-variables) with a base64 encoded private SSH key that has permission to access submodule repositories. +Create a [secret](/eas/environment-variables/#visibility-settings-for-environment-variables) with a base64 encoded private SSH key that has permission to access submodule repositories. diff --git a/docs/pages/build-reference/private-npm-packages.mdx b/docs/pages/build-reference/private-npm-packages.mdx index 4c4d3471278c6f..d96190853e4f01 100644 --- a/docs/pages/build-reference/private-npm-packages.mdx +++ b/docs/pages/build-reference/private-npm-packages.mdx @@ -37,7 +37,7 @@ The recommended way is to add the `NPM_TOKEN` secret to your account or project' src="/static/images/eas-build/environment-secrets/secrets-create-filled.png" /> -For more information on how to do that, see [secret environment variables](/build-reference/variables/#secrets-on-the-expo-website). +For more information on how to do that, see [secret environment variables](/eas/environment-variables/manage/#create-variables-in-the-dashboard). When EAS detects that the `NPM_TOKEN` environment variable is available during a build, it automatically creates the following **.npmrc**: diff --git a/docs/pages/build-reference/troubleshooting.mdx b/docs/pages/build-reference/troubleshooting.mdx index 1e3d030ef42fac..df03bc8e569e91 100644 --- a/docs/pages/build-reference/troubleshooting.mdx +++ b/docs/pages/build-reference/troubleshooting.mdx @@ -101,7 +101,7 @@ If your project imports a file listed in **.gitignore**, the build will fail wit - Encode the file with `base64`, save that string as secrets, and create the file in an EAS Build hook. See [How can I upload files to EAS Build if they are gitignored?](https://expo.fyi/eas-build-archive.md#how-can-i-upload-files-to-eas-build-if-they-are-gitignored) for more information. -- Refactor your source code to avoid importing sensitive files on the client side. If a file is an auto-generated code from a third-party provider and that provider has automatically listed files in your **.gitignore**, then that file probably contains sensitive information. You should not include it on the client side. During app development, ensure you follow secure practices, such as using environment variables or serving them through your backend. See [Using secrets in environment variables](/build-reference/variables/#using-secrets-in-environment-variables) for more information. +- Refactor your source code to avoid importing sensitive files on the client side. If a file is an auto-generated code from a third-party provider and that provider has automatically listed files in your **.gitignore**, then that file probably contains sensitive information. You should not include it on the client side. During app development, ensure you follow secure practices, such as using environment variables or serving them through your backend. See [Using secrets in environment variables](/eas/environment-variables/#visibility-settings-for-environment-variables) for more information. @@ -149,7 +149,7 @@ You can build the production bundle locally by running `npx expo export` to bypa If the logs weren't enough to immediately help you understand and fix the root cause, it's time to try to reproduce the issue locally. If your project builds and runs locally in release mode then it will also build on EAS Build, provided that the following are all true: - Relevant [Build tool versions](/build/eas-json#configuring-your-build-tools) (for example, Xcode, Node.js, npm, Yarn) are the same in both environments. -- Relevant [environment variables](/build-reference/variables) are the same in both environments. +- Relevant [environment variables](/eas/environment-variables) are the same in both environments. - The [archive](https://expo.fyi/eas-build-archive) that is uploaded to EAS Build includes the same relevant source files. You can verify that your project builds on your local machine with the `npx expo run:android` and `npx expo run:ios` commands, with variant/configuration flags set to release to most faithfully reproduce what executes on EAS Build. For more information, see [Android build process](/build-reference/android-builds) and [iOS build process](/build-reference/ios-builds). diff --git a/docs/pages/build/eas-json.mdx b/docs/pages/build/eas-json.mdx index 7cfc6c133ef708..74591012a272b6 100644 --- a/docs/pages/build/eas-json.mdx +++ b/docs/pages/build/eas-json.mdx @@ -381,7 +381,7 @@ You can configure environment variables on your build profiles using the `"env"` } ``` -The [Environment variables and secrets](/build-reference/variables) reference explains this topic in greater detail, and the [Use EAS Update](/build/updates) guide provides considerations when using this feature alongside `expo-updates`. +The [Environment variables and secrets](/eas/environment-variables) reference explains this topic in greater detail, and the [Use EAS Update](/build/updates) guide provides considerations when using this feature alongside `expo-updates`. ## More diff --git a/docs/pages/build/setup.mdx b/docs/pages/build/setup.mdx index 7678a6501d78ab..0ef41ed9a5ce8a 100644 --- a/docs/pages/build/setup.mdx +++ b/docs/pages/build/setup.mdx @@ -78,7 +78,7 @@ For development, we recommend creating a [development build](/develop/developmen Additional configuration may be required for some scenarios: -- Does your app code depend on environment variables? [Add them to your build configuration](/build-reference/variables). +- Does your app code depend on environment variables? [Add them to your build configuration](/eas/environment-variables). - Is your project inside of a monorepo? [Follow these instructions](/build-reference/build-with-monorepos). - Do you use private npm packages? [Add your npm token](/build-reference/private-npm-packages). - Does your app depend on specific versions of tools like Node, Yarn, npm, CocoaPods, or Xcode? [Specify these versions in your build configuration](/build/eas-json). diff --git a/docs/pages/guides/environment-variables.mdx b/docs/pages/guides/environment-variables.mdx index d9e1cbbc002147..3a755b6ed1c860 100644 --- a/docs/pages/guides/environment-variables.mdx +++ b/docs/pages/guides/environment-variables.mdx @@ -85,7 +85,7 @@ If you're experiencing issues with environment variables, you can try disabling ### EAS Build -[EAS Build](/build/introduction/) uses Metro Bundler to build the JavaScript bundle embedded within your app binary, so it will use **.env** files uploaded with your build job to inline `EXPO_PUBLIC_` variables into your code. EAS Build also lets you define environment variables within build profiles in **eas.json** and via EAS Secrets. Check out the EAS Build documentation on [environment variables and build secrets](/build-reference/variables/) for more information. +[EAS Build](/build/introduction/) uses Metro Bundler to build the JavaScript bundle embedded within your app binary, so it will use **.env** files uploaded with your build job to inline `EXPO_PUBLIC_` variables into your code. EAS Build also lets you define environment variables within build profiles in **eas.json** and via EAS Secrets. Check out the EAS Build documentation on [environment variables and build secrets](/eas/environment-variables) for more information. ### EAS Update