From 4af3f5837efa776203db2bcf988792b7778aafc2 Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:18:30 -0300
Subject: [PATCH 1/6] fix: support expo router modal wrappers
---
.changeset/sharp-cars-approve.md | 5 +
examples/expo-multichain/app/_layout.tsx | 4 +-
jest.config.ts | 14 ++-
packages/appkit/jest.config.ts | 6 +-
.../__tests__/hooks/useAppKitTheme.test.tsx | 24 ++--
.../src/__tests__/modal/w3m-modal.test.tsx | 113 ++++++++++++++++++
packages/appkit/src/index.ts | 7 +-
packages/appkit/src/modal/w3m-modal/index.tsx | 42 ++++---
packages/common/jest.config.ts | 6 +-
packages/core/jest.config.ts | 6 +-
packages/ui/jest.config.ts | 6 +-
11 files changed, 198 insertions(+), 35 deletions(-)
create mode 100644 .changeset/sharp-cars-approve.md
create mode 100644 packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
new file mode 100644
index 000000000..3b6bde112
--- /dev/null
+++ b/.changeset/sharp-cars-approve.md
@@ -0,0 +1,5 @@
+---
+'@reown/appkit-react-native': patch
+---
+
+feat: add modalWrapper prop for custom modal window wrappers
diff --git a/examples/expo-multichain/app/_layout.tsx b/examples/expo-multichain/app/_layout.tsx
index 6119a864a..96ddf435e 100644
--- a/examples/expo-multichain/app/_layout.tsx
+++ b/examples/expo-multichain/app/_layout.tsx
@@ -96,7 +96,9 @@ export default function RootLayout() {
- {/* This is a workaround for the Android modal issue. https://github.com/expo/expo/issues/32991#issuecomment-2489620459 */}
+ {/* Mount AppKit once in the root layout to avoid Android Expo Router modal layering issues.
+ If your app already uses react-native-screens and transparentModal still hides the modal,
+ pass modalWrapper={FullWindowOverlay} here. */}
diff --git a/jest.config.ts b/jest.config.ts
index 98a897090..5d12cd6dd 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,3 +1,5 @@
+const path = require('path');
+
module.exports = {
preset: 'react-native',
modulePathIgnorePatterns: ['/node_modules', '/lib/'],
@@ -8,6 +10,16 @@ module.exports = {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { configFile: './babel.config.js' }]
},
moduleNameMapper: {
- '^@shared-jest-setup$': '/jest-shared-setup.ts'
+ '^@shared-jest-setup$': '/jest-shared-setup.ts',
+ '^@reown/appkit-react-native$': path.join(__dirname, 'packages/appkit/src/index.ts'),
+ '^@reown/appkit-bitcoin-react-native$': path.join(__dirname, 'packages/bitcoin/src/index.tsx'),
+ '^@reown/appkit-coinbase-react-native$': path.join(__dirname, 'packages/coinbase/src/index.ts'),
+ '^@reown/appkit-common-react-native$': path.join(__dirname, 'packages/common/src/index.ts'),
+ '^@reown/appkit-core-react-native$': path.join(__dirname, 'packages/core/src/index.ts'),
+ '^@reown/appkit-ethers-react-native$': path.join(__dirname, 'packages/ethers/src/index.tsx'),
+ '^@reown/appkit-react-native-cli$': path.join(__dirname, 'packages/cli/src/index.ts'),
+ '^@reown/appkit-solana-react-native$': path.join(__dirname, 'packages/solana/src/index.ts'),
+ '^@reown/appkit-ui-react-native$': path.join(__dirname, 'packages/ui/src/index.ts'),
+ '^@reown/appkit-wagmi-react-native$': path.join(__dirname, 'packages/wagmi/src/index.tsx')
}
};
diff --git a/packages/appkit/jest.config.ts b/packages/appkit/jest.config.ts
index f2fb133e4..93551e068 100644
--- a/packages/appkit/jest.config.ts
+++ b/packages/appkit/jest.config.ts
@@ -1,8 +1,10 @@
+const rootConfig = require('../../jest.config');
+
const appkitConfig = {
- ...require('../../jest.config'),
+ ...rootConfig,
setupFilesAfterEnv: ['./jest-setup.ts'],
- // Override the moduleNameMapper to use the correct path from the package
moduleNameMapper: {
+ ...rootConfig.moduleNameMapper,
'^@shared-jest-setup$': '../../jest-shared-setup.ts'
}
};
diff --git a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
index cfc5ea8dd..37a912d6d 100644
--- a/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
+++ b/packages/appkit/src/__tests__/hooks/useAppKitTheme.test.tsx
@@ -18,16 +18,20 @@ jest.mock('valtio', () => ({
}));
// Mock ThemeController
-jest.mock('@reown/appkit-core-react-native', () => ({
- ThemeController: {
- state: {
- themeMode: 'light',
- themeVariables: {}
- },
- setDefaultThemeMode: jest.fn(),
- setThemeVariables: jest.fn()
- }
-}));
+jest.mock(
+ '@reown/appkit-core-react-native',
+ () => ({
+ ThemeController: {
+ state: {
+ themeMode: 'light',
+ themeVariables: {}
+ },
+ setDefaultThemeMode: jest.fn(),
+ setThemeVariables: jest.fn()
+ }
+ }),
+ { virtual: true }
+);
describe('useAppKitTheme', () => {
const mockAppKit = {} as AppKit;
diff --git a/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
new file mode 100644
index 000000000..6c340aa6e
--- /dev/null
+++ b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { View } from 'react-native';
+
+const mockClose = jest.fn();
+const mockPrefetch = jest.fn().mockResolvedValue(undefined);
+const mockSendEvent = jest.fn();
+
+jest.mock('valtio', () => ({
+ useSnapshot: jest.fn((state: Record) => state)
+}));
+
+jest.mock('react-native-safe-area-context', () => ({
+ useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 }))
+}));
+
+jest.mock('../../AppKitContext', () => ({
+ useInternalAppKit: jest.fn(() => ({
+ close: mockClose
+ }))
+}));
+
+jest.mock('../../partials/w3m-header', () => ({
+ Header: () => 'Header'
+}));
+
+jest.mock('../../partials/w3m-snackbar', () => ({
+ Snackbar: () => 'Snackbar'
+}));
+
+jest.mock('../../modal/w3m-router', () => ({
+ AppKitRouter: () => 'Router'
+}));
+
+jest.mock(
+ '@reown/appkit-ui-react-native',
+ () => {
+ const { View: MockView } = require('react-native');
+
+ return {
+ Card: ({ children, ...props }: any) => (
+
+ {children}
+
+ ),
+ Modal: ({ children, testID }: any) => {children},
+ ThemeProvider: ({ children }: any) => <>{children}>
+ };
+ },
+ { virtual: true }
+);
+
+jest.mock(
+ '@reown/appkit-core-react-native',
+ () => ({
+ ApiController: {
+ prefetch: mockPrefetch
+ },
+ EventsController: {
+ sendEvent: mockSendEvent
+ },
+ ModalController: {
+ state: {
+ open: true
+ }
+ },
+ OptionsController: {
+ state: {
+ projectId: 'project-id'
+ }
+ },
+ RouterController: {
+ state: {
+ history: ['Connect']
+ },
+ goBack: jest.fn()
+ },
+ ThemeController: {
+ state: {
+ themeMode: 'light',
+ themeVariables: {}
+ },
+ setSystemThemeMode: jest.fn()
+ }
+ }),
+ { virtual: true }
+);
+
+const { AppKit } = require('../../modal/w3m-modal');
+
+describe('AppKit modal', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the modal without a wrapper', () => {
+ const { getByTestId, queryByTestId } = render();
+
+ expect(getByTestId('w3m-modal')).toBeTruthy();
+ expect(queryByTestId('modal-wrapper')).toBeNull();
+ });
+
+ it('wraps the modal when modalWrapper is provided', () => {
+ function ModalWrapper({ children }: { children: React.ReactNode }) {
+ return {children};
+ }
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('modal-wrapper')).toBeTruthy();
+ expect(getByTestId('w3m-modal')).toBeTruthy();
+ });
+});
diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts
index cef464c0b..57ad6f20f 100644
--- a/packages/appkit/src/index.ts
+++ b/packages/appkit/src/index.ts
@@ -14,7 +14,12 @@ export {
NetworkButton as NetworkButton,
type NetworkButtonProps as NetworkButtonProps
} from './modal/w3m-network-button';
-export { AppKit } from './modal/w3m-modal';
+export {
+ AppKit,
+ type AppKitProps,
+ type AppKitModalWrapperComponent,
+ type AppKitModalWrapperProps
+} from './modal/w3m-modal';
/********** Types **********/
export type * from '@reown/appkit-core-react-native';
diff --git a/packages/appkit/src/modal/w3m-modal/index.tsx b/packages/appkit/src/modal/w3m-modal/index.tsx
index 4ea75fa09..d5ead915b 100644
--- a/packages/appkit/src/modal/w3m-modal/index.tsx
+++ b/packages/appkit/src/modal/w3m-modal/index.tsx
@@ -1,5 +1,5 @@
import { useSnapshot } from 'valtio';
-import { useCallback, useEffect } from 'react';
+import { useCallback, useEffect, type ComponentType, type ReactNode } from 'react';
import { useColorScheme } from 'react-native';
import { Card, Modal, ThemeProvider } from '@reown/appkit-ui-react-native';
import {
@@ -18,7 +18,17 @@ import { Snackbar } from '../../partials/w3m-snackbar';
import { useInternalAppKit } from '../../AppKitContext';
import styles from './styles';
-export function AppKit() {
+export interface AppKitModalWrapperProps {
+ children: ReactNode;
+}
+
+export type AppKitModalWrapperComponent = ComponentType;
+
+export interface AppKitProps {
+ modalWrapper?: AppKitModalWrapperComponent;
+}
+
+export function AppKit({ modalWrapper: ModalWrapper }: AppKitProps) {
const theme = useColorScheme();
const { bottom, top } = useSafeAreaInsets();
const { close } = useInternalAppKit();
@@ -53,20 +63,24 @@ export function AppKit() {
}
}, [projectId, prefetch]);
+ const modal = (
+
+
+
+
+
+
+
+ );
+
return (
-
-
-
-
-
-
-
+ {ModalWrapper ? {modal} : modal}
);
}
diff --git a/packages/common/jest.config.ts b/packages/common/jest.config.ts
index 1704bb3e8..daca28c61 100644
--- a/packages/common/jest.config.ts
+++ b/packages/common/jest.config.ts
@@ -1,8 +1,10 @@
+const rootConfig = require('../../jest.config');
+
const commonConfig = {
- ...require('../../jest.config'),
+ ...rootConfig,
setupFilesAfterEnv: ['./jest-setup.ts'],
- // Override the moduleNameMapper to use the correct path from the package
moduleNameMapper: {
+ ...rootConfig.moduleNameMapper,
'^@shared-jest-setup$': '../../jest-shared-setup.ts'
}
};
diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts
index 21e9e0b9f..2666b21e3 100644
--- a/packages/core/jest.config.ts
+++ b/packages/core/jest.config.ts
@@ -1,8 +1,10 @@
+const rootConfig = require('../../jest.config');
+
const coreConfig = {
- ...require('../../jest.config'),
+ ...rootConfig,
setupFilesAfterEnv: ['./jest-setup.ts'],
- // Override the moduleNameMapper to use the correct path from the package
moduleNameMapper: {
+ ...rootConfig.moduleNameMapper,
'^@shared-jest-setup$': '../../jest-shared-setup.ts'
}
};
diff --git a/packages/ui/jest.config.ts b/packages/ui/jest.config.ts
index 2892ee1d4..aeface904 100644
--- a/packages/ui/jest.config.ts
+++ b/packages/ui/jest.config.ts
@@ -1,8 +1,10 @@
+const rootConfig = require('../../jest.config');
+
const uiConfig = {
- ...require('../../jest.config'),
+ ...rootConfig,
setupFilesAfterEnv: ['./jest-setup.ts'],
- // Override the moduleNameMapper to use the correct path from the package
moduleNameMapper: {
+ ...rootConfig.moduleNameMapper,
'^@shared-jest-setup$': '../../jest-shared-setup.ts'
}
};
From 4d9e9a59f0c8c40cb4a57f2ef6ec4eabe809596b Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:29:50 -0300
Subject: [PATCH 2/6] chore: align release bumps for appkit adapters
---
.changeset/sharp-cars-approve.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
index 3b6bde112..3abccfeba 100644
--- a/.changeset/sharp-cars-approve.md
+++ b/.changeset/sharp-cars-approve.md
@@ -1,5 +1,7 @@
---
'@reown/appkit-react-native': patch
+'@reown/appkit-ethers-react-native': patch
+'@reown/appkit-wagmi-react-native': patch
---
feat: add modalWrapper prop for custom modal window wrappers
From e4a938d124c17fdc30c52f9755cbe1ea6178bf06 Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:31:13 -0300
Subject: [PATCH 3/6] chore: bump all packages in changeset
---
.changeset/sharp-cars-approve.md | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
index 3abccfeba..a9eda01f3 100644
--- a/.changeset/sharp-cars-approve.md
+++ b/.changeset/sharp-cars-approve.md
@@ -1,6 +1,13 @@
---
-'@reown/appkit-react-native': patch
+'@reown/appkit-bitcoin-react-native': patch
+'@reown/appkit-coinbase-react-native': patch
+'@reown/appkit-common-react-native': patch
+'@reown/appkit-core-react-native': patch
'@reown/appkit-ethers-react-native': patch
+'@reown/appkit-react-native': patch
+'@reown/appkit-react-native-cli': patch
+'@reown/appkit-solana-react-native': patch
+'@reown/appkit-ui-react-native': patch
'@reown/appkit-wagmi-react-native': patch
---
From f98c49f9894d685b15744ac01e38503c51ff2937 Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 9 Apr 2026 12:39:30 -0300
Subject: [PATCH 4/6] fix: wrap modal content inside RN modal
---
.changeset/sharp-cars-approve.md | 2 +-
examples/expo-multichain/app/_layout.tsx | 2 +-
.../src/__tests__/modal/w3m-modal.test.tsx | 37 +++++++++---
packages/appkit/src/index.ts | 4 +-
packages/appkit/src/modal/w3m-modal/index.tsx | 37 ++++++------
.../ui/src/components/wui-modal/index.tsx | 59 +++++++++++++++----
packages/ui/src/index.ts | 7 ++-
7 files changed, 101 insertions(+), 47 deletions(-)
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
index a9eda01f3..0531e1f96 100644
--- a/.changeset/sharp-cars-approve.md
+++ b/.changeset/sharp-cars-approve.md
@@ -11,4 +11,4 @@
'@reown/appkit-wagmi-react-native': patch
---
-feat: add modalWrapper prop for custom modal window wrappers
+feat: add modalContentWrapper prop for custom modal content wrappers
diff --git a/examples/expo-multichain/app/_layout.tsx b/examples/expo-multichain/app/_layout.tsx
index 96ddf435e..95d4e5183 100644
--- a/examples/expo-multichain/app/_layout.tsx
+++ b/examples/expo-multichain/app/_layout.tsx
@@ -98,7 +98,7 @@ export default function RootLayout() {
{/* Mount AppKit once in the root layout to avoid Android Expo Router modal layering issues.
If your app already uses react-native-screens and transparentModal still hides the modal,
- pass modalWrapper={FullWindowOverlay} here. */}
+ pass modalContentWrapper={FullWindowOverlay} here. */}
diff --git a/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
index 6c340aa6e..02e153d19 100644
--- a/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
+++ b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
@@ -5,6 +5,9 @@ import { View } from 'react-native';
const mockClose = jest.fn();
const mockPrefetch = jest.fn().mockResolvedValue(undefined);
const mockSendEvent = jest.fn();
+const modalState = {
+ open: true
+};
jest.mock('valtio', () => ({
useSnapshot: jest.fn((state: Record) => state)
@@ -43,7 +46,15 @@ jest.mock(
{children}
),
- Modal: ({ children, testID }: any) => {children},
+ Modal: ({ children, testID, visible, contentWrapper: ContentWrapper }: any) => {
+ if (!visible) {
+ return null;
+ }
+
+ const content = ContentWrapper ? {children} : children;
+
+ return {content};
+ },
ThemeProvider: ({ children }: any) => <>{children}>
};
},
@@ -60,9 +71,7 @@ jest.mock(
sendEvent: mockSendEvent
},
ModalController: {
- state: {
- open: true
- }
+ state: modalState
},
OptionsController: {
state: {
@@ -91,6 +100,7 @@ const { AppKit } = require('../../modal/w3m-modal');
describe('AppKit modal', () => {
beforeEach(() => {
jest.clearAllMocks();
+ modalState.open = true;
});
it('renders the modal without a wrapper', () => {
@@ -100,14 +110,23 @@ describe('AppKit modal', () => {
expect(queryByTestId('modal-wrapper')).toBeNull();
});
- it('wraps the modal when modalWrapper is provided', () => {
- function ModalWrapper({ children }: { children: React.ReactNode }) {
- return {children};
+ it('wraps modal content when modalContentWrapper is provided', () => {
+ function ModalContentWrapper({ children }: { children: React.ReactNode }) {
+ return {children};
}
- const { getByTestId } = render();
+ const { getByTestId } = render();
- expect(getByTestId('modal-wrapper')).toBeTruthy();
+ expect(getByTestId('modal-content-wrapper')).toBeTruthy();
expect(getByTestId('w3m-modal')).toBeTruthy();
});
+
+ it('does not render modal content when closed', () => {
+ modalState.open = false;
+
+ const { queryByTestId } = render();
+
+ expect(queryByTestId('w3m-modal')).toBeNull();
+ expect(queryByTestId('mock-card')).toBeNull();
+ });
});
diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts
index 57ad6f20f..2b1bf9aaf 100644
--- a/packages/appkit/src/index.ts
+++ b/packages/appkit/src/index.ts
@@ -17,8 +17,8 @@ export {
export {
AppKit,
type AppKitProps,
- type AppKitModalWrapperComponent,
- type AppKitModalWrapperProps
+ type AppKitModalContentWrapperComponent,
+ type AppKitModalContentWrapperProps
} from './modal/w3m-modal';
/********** Types **********/
diff --git a/packages/appkit/src/modal/w3m-modal/index.tsx b/packages/appkit/src/modal/w3m-modal/index.tsx
index d5ead915b..ff5bd8a01 100644
--- a/packages/appkit/src/modal/w3m-modal/index.tsx
+++ b/packages/appkit/src/modal/w3m-modal/index.tsx
@@ -18,17 +18,17 @@ import { Snackbar } from '../../partials/w3m-snackbar';
import { useInternalAppKit } from '../../AppKitContext';
import styles from './styles';
-export interface AppKitModalWrapperProps {
+export interface AppKitModalContentWrapperProps {
children: ReactNode;
}
-export type AppKitModalWrapperComponent = ComponentType;
+export type AppKitModalContentWrapperComponent = ComponentType;
export interface AppKitProps {
- modalWrapper?: AppKitModalWrapperComponent;
+ modalContentWrapper?: AppKitModalContentWrapperComponent;
}
-export function AppKit({ modalWrapper: ModalWrapper }: AppKitProps) {
+export function AppKit({ modalContentWrapper }: AppKitProps) {
const theme = useColorScheme();
const { bottom, top } = useSafeAreaInsets();
const { close } = useInternalAppKit();
@@ -63,24 +63,21 @@ export function AppKit({ modalWrapper: ModalWrapper }: AppKitProps) {
}
}, [projectId, prefetch]);
- const modal = (
-
-
-
-
-
-
-
- );
-
return (
- {ModalWrapper ? {modal} : modal}
+
+
+
+
+
+
+
);
}
diff --git a/packages/ui/src/components/wui-modal/index.tsx b/packages/ui/src/components/wui-modal/index.tsx
index d814cfce2..5cba40d6e 100644
--- a/packages/ui/src/components/wui-modal/index.tsx
+++ b/packages/ui/src/components/wui-modal/index.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useRef, useState, type ComponentType, type ReactNode } from 'react';
import {
useWindowDimensions,
Modal as RNModal,
@@ -14,13 +14,27 @@ export type ModalProps = Pick<
RNModalProps,
'visible' | 'onDismiss' | 'testID' | 'onRequestClose'
> & {
- children: React.ReactNode;
+ children: ReactNode;
onBackdropPress?: () => void;
+ contentWrapper?: ModalContentWrapperComponent;
};
+export interface ModalContentWrapperProps {
+ children: ReactNode;
+}
+
+export type ModalContentWrapperComponent = ComponentType;
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
-export function Modal({ visible, onBackdropPress, onRequestClose, testID, children }: ModalProps) {
+export function Modal({
+ visible,
+ onBackdropPress,
+ onRequestClose,
+ testID,
+ children,
+ contentWrapper: ContentWrapper
+}: ModalProps) {
const { height } = useWindowDimensions();
const Theme = useTheme();
const backdropOpacity = useRef(new Animated.Value(0)).current;
@@ -120,16 +134,35 @@ export function Modal({ visible, onBackdropPress, onRequestClose, testID, childr
onRequestClose={onRequestClose}
testID={testID}
>
- {showBackdrop ? (
-
- ) : null}
-
- {children}
-
-
+ {ContentWrapper ? (
+
+ <>
+ {showBackdrop ? (
+
+ ) : null}
+
+ {children}
+
+
+ >
+
+ ) : (
+ <>
+ {showBackdrop ? (
+
+ ) : null}
+
+ {children}
+
+
+ >
+ )}
);
}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index f2e8efa4a..4de0945d8 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -50,7 +50,12 @@ export { ListTransaction, type ListTransactionProps } from './composites/wui-lis
export { ListWallet, type ListWalletProps } from './composites/wui-list-wallet';
export { Logo, type LogoProps } from './composites/wui-logo';
export { LogoSelect, type LogoSelectProps } from './composites/wui-logo-select';
-export { Modal, type ModalProps } from './components/wui-modal';
+export {
+ Modal,
+ type ModalContentWrapperComponent,
+ type ModalContentWrapperProps,
+ type ModalProps
+} from './components/wui-modal';
export { NetworkButton, type NetworkButtonProps } from './composites/wui-network-button';
export { NetworkImage, type NetworkImageProps } from './composites/wui-network-image';
export { NumericKeyboard, type NumericKeyboardProps } from './composites/wui-numeric-keyboard';
From dbe5a80225478926f5fa2531b409ca4c95e39a3c Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Thu, 9 Apr 2026 12:45:23 -0300
Subject: [PATCH 5/6] chore: code improvements
---
.changeset/sharp-cars-approve.md | 1 -
.../ui/src/components/wui-modal/index.tsx | 45 +++++++------------
2 files changed, 16 insertions(+), 30 deletions(-)
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
index 0531e1f96..a280c1443 100644
--- a/.changeset/sharp-cars-approve.md
+++ b/.changeset/sharp-cars-approve.md
@@ -5,7 +5,6 @@
'@reown/appkit-core-react-native': patch
'@reown/appkit-ethers-react-native': patch
'@reown/appkit-react-native': patch
-'@reown/appkit-react-native-cli': patch
'@reown/appkit-solana-react-native': patch
'@reown/appkit-ui-react-native': patch
'@reown/appkit-wagmi-react-native': patch
diff --git a/packages/ui/src/components/wui-modal/index.tsx b/packages/ui/src/components/wui-modal/index.tsx
index 5cba40d6e..3ee44228b 100644
--- a/packages/ui/src/components/wui-modal/index.tsx
+++ b/packages/ui/src/components/wui-modal/index.tsx
@@ -125,6 +125,21 @@ export function Modal({
}
}, [modalVisible, translateY, backdropOpacity, height]);
+ const modalContent = (
+ <>
+ {showBackdrop ? (
+
+ ) : null}
+
+ {children}
+
+
+ >
+ );
+
return (
- {ContentWrapper ? (
-
- <>
- {showBackdrop ? (
-
- ) : null}
-
- {children}
-
-
- >
-
- ) : (
- <>
- {showBackdrop ? (
-
- ) : null}
-
- {children}
-
-
- >
- )}
+ {ContentWrapper ? {modalContent} : modalContent}
);
}
From f6b43cc3e0c81aea51fbecc1534d507c463db3af Mon Sep 17 00:00:00 2001
From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:23:53 -0300
Subject: [PATCH 6/6] chore: minor changes
---
.changeset/sharp-cars-approve.md | 2 +-
packages/appkit/src/__tests__/modal/w3m-modal.test.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.changeset/sharp-cars-approve.md b/.changeset/sharp-cars-approve.md
index a280c1443..5df25ad17 100644
--- a/.changeset/sharp-cars-approve.md
+++ b/.changeset/sharp-cars-approve.md
@@ -10,4 +10,4 @@
'@reown/appkit-wagmi-react-native': patch
---
-feat: add modalContentWrapper prop for custom modal content wrappers
+fix: add modalContentWrapper prop to work around Expo Router modal layering issues
diff --git a/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
index 02e153d19..1adac9d97 100644
--- a/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
+++ b/packages/appkit/src/__tests__/modal/w3m-modal.test.tsx
@@ -107,7 +107,7 @@ describe('AppKit modal', () => {
const { getByTestId, queryByTestId } = render();
expect(getByTestId('w3m-modal')).toBeTruthy();
- expect(queryByTestId('modal-wrapper')).toBeNull();
+ expect(queryByTestId('modal-content-wrapper')).toBeNull();
});
it('wraps modal content when modalContentWrapper is provided', () => {