diff --git a/android/gradle.properties b/android/gradle.properties
index 0cb7271c..f3db1d92 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -41,5 +41,19 @@ hermesEnabled=true
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
+#
+# Kept TRUE. This makes the themed header draw behind the status bar on pre-Android-15
+# devices (e.g. Moto G4) too (Android 15+ is edge-to-edge OS-enforced regardless).
+#
+# Heads-up: TRUE also forces react-native Modal's separate dialog *window* through RN
+# core's enableEdgeToEdge() (WindowUtil.kt), which hardcodes isNavigationBarContrast-
+# Enforced=true and derives nav-bar appearance from DEVICE dark mode — painting a grey
+# (or white-in-dark) scrim behind the nav bar on modal open/close. That code is in the
+# prebuilt RN Android AAR (unpatchable without building RN from source). We worked
+# around it WITHOUT turning this flag off: BottomModal renders in-tree via @gorhom/portal
+# (coverScreen={false}) on Android instead of as a native window, so it never hits that
+# code path. Any NEW Android modal should use BottomModal / the same portal pattern
+# rather than a raw native react-native Modal, or the scrim will come back.
+# See project memory: android-edge-to-edge-modal-tradeoff.
edgeToEdgeEnabled=true
diff --git a/package.json b/package.json
index 7f0911b0..1e750b2b 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@gandlaf21/bc-ur": "^1.1.12",
"@gandlaf21/bolt11-decode": "^3.1.1",
"@gocodingnow/rn-size-matters": "^0.0.4",
+ "@gorhom/portal": "^1.0.14",
"@hot-updater/react-native": "0.32.0",
"@notifee/react-native": "^9.1.8",
"@op-engineering/op-sqlite": "^16.2.0",
diff --git a/src/App.tsx b/src/App.tsx
index 76ae23be..ccb5ef95 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import {
} from '@gocodingnow/rn-size-matters'
import RNExitApp from 'react-native-exit-app'
import { changeIcon } from 'react-native-change-icon'
+import { PortalProvider } from '@gorhom/portal'
import {AppNavigator} from './navigation'
import {useInitialRootStore, useStores} from './models'
import {KeyChain, WalletKeys} from './services'
@@ -242,10 +243,14 @@ function App() {
return (
-
-
-
-
+ {/* PortalProvider hosts in-tree modals (BottomModal on Android) above the
+ navigator + bottom tab bar, avoiding react-native Modal's native window. */}
+
+
+
+
+
+
)
}
diff --git a/src/components/BottomModal.tsx b/src/components/BottomModal.tsx
index ba616a2f..87f1c6b0 100644
--- a/src/components/BottomModal.tsx
+++ b/src/components/BottomModal.tsx
@@ -1,5 +1,6 @@
import React, { ComponentType, Fragment, ReactElement, useEffect } from "react"
import Modal from 'react-native-modal'
+import { Portal } from '@gorhom/portal'
import {
StyleProp,
TextStyle,
@@ -169,11 +170,20 @@ export function BottomModal(props: ModalProps) {
// const keyboard = useAnimatedKeyboard()
//log.trace({keyboard})
+ // On Android, render the modal inside the app's React tree via a Portal instead of as a
+ // native Dialog window. The native-window path forces RN core's enableEdgeToEdge() (when
+ // the edge-to-edge flag is on), which paints a buggy nav-bar contrast scrim on open/close.
+ // coverScreen={false} keeps the modal in-tree; the Portal hoists it above the bottom tab
+ // bar so the backdrop still covers the whole screen. iOS keeps the native modal path
+ // (no such issue there, and its keyboard avoidance depends on it).
+ const ModalWrapper = Platform.OS === 'android' ? Portal : View
+
return (
-
+
-
+
)
}
diff --git a/yarn.lock b/yarn.lock
index 6a875ef6..e334c6e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4439,6 +4439,18 @@ __metadata:
languageName: node
linkType: hard
+"@gorhom/portal@npm:^1.0.14":
+ version: 1.0.14
+ resolution: "@gorhom/portal@npm:1.0.14"
+ dependencies:
+ nanoid: ^3.3.1
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 227bb96a2db854ab29bb9da8d4f3823c7f7448358de459709dd1b78522110da564c9a8734c6bc7d7153ed7c99320e0fb5d60b420c2ebb75ecaf2f0d757f410f9
+ languageName: node
+ linkType: hard
+
"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.12.6":
version: 1.14.2
resolution: "@grpc/grpc-js@npm:1.14.2"
@@ -14821,6 +14833,7 @@ __metadata:
"@gandlaf21/bc-ur": ^1.1.12
"@gandlaf21/bolt11-decode": ^3.1.1
"@gocodingnow/rn-size-matters": ^0.0.4
+ "@gorhom/portal": ^1.0.14
"@hot-updater/bare": 0.32.0
"@hot-updater/firebase": 0.32.0
"@hot-updater/react-native": 0.32.0
@@ -15228,6 +15241,15 @@ __metadata:
languageName: node
linkType: hard
+"nanoid@npm:^3.3.1":
+ version: 3.3.12
+ resolution: "nanoid@npm:3.3.12"
+ bin:
+ nanoid: bin/nanoid.cjs
+ checksum: 38699257447dc59e21e73e0510d0dfb16b7a610d9ca80633d5c3a68f9b4298c990513d30404ca8f163c2d03225ee01695ff8898bea6179183f38f0477b7635ac
+ languageName: node
+ linkType: hard
+
"nanoid@npm:^3.3.11":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"