Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions wallets/rn_cli_wallet/src/components/DesktopFrameWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PropsWithChildren } from 'react';

/**
* Native passthrough. The desktop frame only exists on web
* (see DesktopFrameWrapper.web.tsx); on iOS/Android the app renders full-screen.
*/
export function DesktopFrameWrapper({ children }: PropsWithChildren) {
return <>{children}</>;
}
91 changes: 91 additions & 0 deletions wallets/rn_cli_wallet/src/components/DesktopFrameWrapper.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { PropsWithChildren, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';

import SettingsStore from '@/store/SettingsStore';
import { DarkTheme, LightTheme } from '@/utils/ThemeUtil';
import { DesktopFrame } from '@/constants/DesktopFrame';

/**
* "Framed-lite" desktop frame for web.
*
* On a wide desktop viewport (>= BREAKPOINT) the app is centered in a fixed,
* phone-width surface with rounded corners and a drop shadow so it doesn't
* stretch giant across the screen. Below the breakpoint (mobile web) it renders
* full-bleed, unchanged.
*
* Unlike the pos-app frame this deliberately avoids `transform: scale()` and a
* modal portal. On web the <Modal /> renders inline (coverScreen=false) so it
* stays inside this frame and is clipped by `overflow: hidden`; <Toast /> still
* renders as a fixed full-viewport overlay and is left untouched.
*/
const RESIZE_DEBOUNCE_MS = 150;

function useIsDesktopWeb(): boolean {
const [isDesktop, setIsDesktop] = useState(
() =>
typeof window !== 'undefined' &&
window.innerWidth >= DesktopFrame.BREAKPOINT,
);

useEffect(() => {
if (typeof window === 'undefined') {
return;
}

let timeout: ReturnType<typeof setTimeout> | undefined;
const onResize = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
setIsDesktop(window.innerWidth >= DesktopFrame.BREAKPOINT);
}, RESIZE_DEBOUNCE_MS);
};

window.addEventListener('resize', onResize);
return () => {
clearTimeout(timeout);
window.removeEventListener('resize', onResize);
};
}, []);

return isDesktop;
}

export function DesktopFrameWrapper({ children }: PropsWithChildren) {
const { themeMode } = useSnapshot(SettingsStore.state);
const isDesktop = useIsDesktopWeb();

if (!isDesktop) {
return <>{children}</>;
}

const Theme = themeMode === 'dark' ? DarkTheme : LightTheme;
const backgroundColor =
themeMode === 'dark' ? DesktopFrame.BACKGROUND.dark : DesktopFrame.BACKGROUND.light;

return (
<div
style={{
position: 'fixed',
inset: 0,
backgroundColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: DesktopFrame.DEVICE_WIDTH,
height: `min(${DesktopFrame.DEVICE_HEIGHT}px, calc(100vh - 32px))`,
borderRadius: DesktopFrame.SCREEN_RADIUS,
boxShadow: DesktopFrame.SHADOW,
overflow: 'hidden',
backgroundColor: Theme['bg-primary'],
}}>
{children}
</div>
</div>
);
}
7 changes: 6 additions & 1 deletion wallets/rn_cli_wallet/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ export function Header() {
style={[
styles.container,
{
paddingTop: Platform.OS === 'ios' ? top : top + Spacing[2],
paddingTop:
Platform.OS === 'ios'
? top
: Platform.OS === 'web'
? Spacing[8]
: top + Spacing[2],
backgroundColor: Theme['bg-primary'],
},
]}
Expand Down
16 changes: 15 additions & 1 deletion wallets/rn_cli_wallet/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';

import ModalStore from '@/store/ModalStore';
import { Spacing } from '@/utils/ThemeUtil';
import { DesktopFrame } from '@/constants/DesktopFrame';
import SessionProposalModal from '@/modals/SessionProposalModal';
import SessionSignModal from '@/modals/SessionSignModal';
import SessionSendTransactionModal from '@/modals/SessionSendTransactionModal';
Expand Down Expand Up @@ -100,7 +101,11 @@ export default function Modal() {
statusBarTranslucent
propagateSwipe
onBackdropPress={onClose}
style={styles.modal}
style={[styles.modal, Platform.OS === 'web' ? styles.modalWeb : null]}
// On web, render inline (not via the full-screen portal) so the sheet +
// backdrop stay inside the desktop frame and get clipped to its rounded
// bottom corners instead of overflowing to the viewport edge.
coverScreen={Platform.OS !== 'web'}
isVisible={open}
>
<GestureHandlerRootView style={gestureRootStyle}>
Expand All @@ -114,6 +119,15 @@ const styles = StyleSheet.create({
modal: {
margin: Spacing[0],
},
// On web, constrain the sheet to the desktop frame width and center it so it
// doesn't stretch full-viewport. Self-adjusts: fills the width on narrow
// screens, caps + centers on wide desktop. With coverScreen=false the backdrop
// dims only the frame area (its parent), not the full page.
modalWeb: {
width: '100%',
maxWidth: DesktopFrame.DEVICE_WIDTH,
alignSelf: 'center',
},
gestureRoot: {
flex: 1,
margin: Spacing[0],
Expand Down
17 changes: 17 additions & 0 deletions wallets/rn_cli_wallet/src/constants/DesktopFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Shared dimensions for the "framed-lite" desktop-web layout.
// Used by DesktopFrameWrapper.web.tsx (the frame) and Modal.tsx (to keep sheets
// constrained to the same width on desktop web).
export const DesktopFrame = {
// Min viewport width at which the desktop frame is shown
BREAKPOINT: 1024,
// Phone-like surface width the app is constrained to
DEVICE_WIDTH: 430,
DEVICE_HEIGHT: 932,
SCREEN_RADIUS: 40,
SHADOW: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
// Neutral page background behind the frame (distinct from the screen bg for contrast)
BACKGROUND: {
light: '#F0F0F0',
dark: '#0A0A0A',
},
} as const;
11 changes: 10 additions & 1 deletion wallets/rn_cli_wallet/src/navigators/HomeTabNavigator.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Wallets from '@/screens/Wallets';
import Connections from '@/screens/Connections';
import Settings from '@/screens/Settings';
import { useTheme } from '@/hooks/useTheme';
import { Spacing } from '@/utils/ThemeUtil';

const TabNav = createBottomTabNavigator<HomeTabParamList>();

Expand All @@ -31,7 +32,15 @@ export function HomeTabNavigator() {
screenOptions={{
headerShown: false,
sceneStyle: { backgroundColor: Theme['bg-primary'] },
tabBarStyle: { backgroundColor: Theme['bg-primary'] },
tabBarStyle: {
backgroundColor: Theme['bg-primary'],
// Softer, lighter top divider than the react-navigation default
borderTopColor: Theme['border-secondary'],
// Extra height + bottom padding so labels aren't clipped in the desktop frame
height: 72,
paddingBottom: Spacing[4],
paddingTop: Spacing[1],
},
}}>
<TabNav.Screen
name="Wallets"
Expand Down
39 changes: 21 additions & 18 deletions wallets/rn_cli_wallet/src/screens/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RELAYER_EVENTS } from '@walletconnect/core';

import { RootStackNavigator } from '@/navigators/RootStackNavigator';
import Modal from '@/components/Modal';
import { DesktopFrameWrapper } from '@/components/DesktopFrameWrapper';
import useInitializeWalletKit from '@/hooks/useInitializeWalletKit';
import useWalletKitEventsManager from '@/hooks/useWalletKitEventsManager';
import { usePairing } from '@/hooks/usePairing';
Expand Down Expand Up @@ -205,24 +206,26 @@ const App = () => {
);

return (
<GestureHandlerRootView style={rootStyle}>
<SafeAreaProvider>
<KeyboardProvider>
<NavigationContainer
documentTitle={{ formatter: () => 'React N. Wallet' }}>
<StatusBar
translucent
backgroundColor="transparent"
barStyle={themeMode === 'dark' ? 'light-content' : 'dark-content'}
/>
<NavigationBar style={themeMode === 'dark' ? 'light' : 'dark'} />
<RootStackNavigator />
<Modal />
</NavigationContainer>
<Toast config={toastConfig} position="top" topOffset={0} />
</KeyboardProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
<DesktopFrameWrapper>
<GestureHandlerRootView style={rootStyle}>
<SafeAreaProvider>
<KeyboardProvider>
<NavigationContainer
documentTitle={{ formatter: () => 'React N. Wallet' }}>
<StatusBar
translucent
backgroundColor="transparent"
barStyle={themeMode === 'dark' ? 'light-content' : 'dark-content'}
/>
<NavigationBar style={themeMode === 'dark' ? 'light' : 'dark'} />
<RootStackNavigator />
<Modal />
</NavigationContainer>
<Toast config={toastConfig} position="top" topOffset={0} />
</KeyboardProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
</DesktopFrameWrapper>
);
};

Expand Down
39 changes: 25 additions & 14 deletions wallets/rn_cli_wallet/src/screens/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function Settings() {
activeThumbColor: Theme.white,
}
: {}
) as object;
) as { activeTrackColor?: string; activeThumbColor?: string };

useEffect(() => {
async function getAsyncData() {
Expand Down Expand Up @@ -81,19 +81,30 @@ export default function Settings() {
<Text variant="md-500" color="text-primary">
Dark mode
</Text>
<Switch
value={themeMode === 'dark'}
style={styles.switch}
onValueChange={toggleDarkMode}
trackColor={Platform.select({
android: {
false: Theme['foreground-tertiary'],
true: Theme['bg-accent-primary'],
},
})}
thumbColor={Platform.select({ android: Theme.white })}
{...webAccentSwitchProps}
/>
{/* On web the whole card (PressableScale) owns the toggle: a tap on
the switch bubbles up to the card, so if the Switch also fired
onValueChange it would toggle twice (net no change). Render it
display-only with pointerEvents="none" so the tap passes through.
On native there's no double-toggle, so keep the Switch fully
interactive (and accessible) with its own onValueChange. */}
{Platform.OS === 'web' ? (
<View pointerEvents="none" style={styles.switch}>
<Switch value={themeMode === 'dark'} {...webAccentSwitchProps} />
</View>
) : (
<Switch
value={themeMode === 'dark'}
style={styles.switch}
onValueChange={toggleDarkMode}
trackColor={Platform.select({
android: {
false: Theme['foreground-tertiary'],
true: Theme['bg-accent-primary'],
},
})}
thumbColor={Platform.select({ android: Theme.white })}
/>
)}
</View>
</Button>
<Card
Expand Down