From cafef4f462e96a4e028a3b65f4bfe86053439690 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 2 Jun 2026 12:34:31 +0200 Subject: [PATCH] Render BottomModal in-tree via portal to fix edge-to-edge nav bar Keep edgeToEdgeEnabled=true so the themed header draws behind the status bar on pre-Android-15 devices (e.g. Moto G4). Android 15+ is edge-to-edge OS-enforced regardless. That flag also routes react-native Modal's native dialog window through RN core's enableEdgeToEdge() (WindowUtil.kt), which hardcodes nav-bar contrast enforcement and derives appearance from device dark mode, painting a grey (white-in-dark) scrim on modal open/close. That code ships in the prebuilt RN Android AAR and can't be patched. Work around it without disabling the flag: on Android, BottomModal renders in the app's React tree via @gorhom/portal (coverScreen={false}) instead of as a native window, so it never hits that code path. A PortalProvider at the app root keeps the modal overlaying the bottom tab bar. iOS keeps the native modal path unchanged. Co-Authored-By: Claude Opus 4.8 --- android/gradle.properties | 14 ++++++++++++++ package.json | 1 + src/App.tsx | 13 +++++++++---- src/components/BottomModal.tsx | 14 ++++++++++++-- yarn.lock | 22 ++++++++++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) 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"