diff --git a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx index 773494d8..3435bffc 100644 --- a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx +++ b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx @@ -9,6 +9,9 @@ import {useLogs} from '@/hooks/useLogs'; import { AppKitLogList } from '@/screens/AppKitLogList'; import NetworkSettingsScreen from '@/screens/NetworkSettings'; import PayWebView from '@/screens/PayWebView'; +import OmenScreen from '@/screens/Omen'; +import OmenDepositWebView from '@/screens/Omen/OmenDepositWebView'; +import GoodDepositConfirm from '@/screens/GoodWallet'; const StackNavigator = createNativeStackNavigator(); @@ -76,6 +79,34 @@ export function RootStackNavigator() { headerTintColor: Theme['fg-100'], }} /> + + + ); } diff --git a/dapps/appkit-wagmi/src/screens/Connections/components/OmenDemoButton.tsx b/dapps/appkit-wagmi/src/screens/Connections/components/OmenDemoButton.tsx new file mode 100644 index 00000000..9b6c44fe --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Connections/components/OmenDemoButton.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {Button} from '@reown/appkit-ui-react-native'; +import {useNavigation} from '@react-navigation/native'; +import type {NativeStackNavigationProp} from '@react-navigation/native-stack'; + +import {RootStackParamList} from '@/utils/TypesUtil'; + +// Entry point for the Omen deposit demo: opens the mock funded-account screen, which in turn +// opens the WalletConnect Pay deposit flow in an in-app WebView. Always visible (unlike +// PasteUrlButton, which self-hides when connected) so the demo is reachable regardless of state. +export function OmenDemoButton() { + const navigation = useNavigation>(); + + return ( + + ); +} diff --git a/dapps/appkit-wagmi/src/screens/Connections/index.tsx b/dapps/appkit-wagmi/src/screens/Connections/index.tsx index 12e66200..873516ba 100644 --- a/dapps/appkit-wagmi/src/screens/Connections/index.tsx +++ b/dapps/appkit-wagmi/src/screens/Connections/index.tsx @@ -7,6 +7,7 @@ import {ActionsView} from './components/ActionsView'; import { EventsView } from './components/EventsView'; import { WalletInfoView } from './components/WalletInfoView'; import { PasteUrlButton } from './components/PasteUrlButton'; +import { OmenDemoButton } from './components/OmenDemoButton'; function ConnectionsScreen() { return ( @@ -17,6 +18,7 @@ function ConnectionsScreen() { + diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/GoodWalletLogo.tsx b/dapps/appkit-wagmi/src/screens/GoodWallet/GoodWalletLogo.tsx new file mode 100644 index 00000000..65efd5e9 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/GoodWalletLogo.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import {StyleSheet, Text, View} from 'react-native'; + +import {GOODWALLET_COLORS} from './theme'; + +/** + * GoodWallet brand mark — drawn in code (no image asset). An emerald rounded-square badge with a + * checkmark glyph next to the "GoodWallet" wordmark, matching the wallet's own (light) skin. + */ +function GoodWalletLogo() { + return ( + + + + + GoodWallet + + ); +} + +export default GoodWalletLogo; + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + badge: { + width: 28, + height: 28, + borderRadius: 9, + backgroundColor: GOODWALLET_COLORS.accent, + alignItems: 'center', + justifyContent: 'center', + }, + glyph: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '800', + lineHeight: 20, + }, + wordmark: { + color: GOODWALLET_COLORS.textPrimary, + fontSize: 18, + fontWeight: '700', + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx new file mode 100644 index 00000000..bc3d413a --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx @@ -0,0 +1,544 @@ +import React, {useMemo, useState} from 'react'; +import { + ActivityIndicator, + Image, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +import {RootStackScreenProps} from '@/utils/TypesUtil'; +import OmenStore from '@/stores/OmenStore'; + +import GoodWalletLogo from './GoodWalletLogo'; +import {GOODWALLET_COLORS as C} from './theme'; +import { + WALLET_NETWORKS, + WalletNetwork, + WalletToken, + networkImageUrl, + usdValue, +} from './tokens'; + +type Phase = 'select' | 'sending' | 'success'; + +function fmtToken(n: number): string { + return n.toLocaleString('en-US', {maximumFractionDigits: 6}); +} + +function fmtUsd(n: number): string { + return `$${n.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`; +} + +function shortAddr(addr?: string): string { + if (!addr) { + return ''; + } + return addr.length > 12 ? `${addr.slice(0, 6)}…${addr.slice(-4)}` : addr; +} + +function mockTxHash(): string { + return `0x${Date.now().toString(16).padStart(64, '0')}`; +} + +/** A network logo from the WC assets CDN, with a neutral fallback if it fails to load. */ +function NetworkImage({caip2, size}: {caip2: string; size: number}) { + const [ok, setOk] = useState(true); + const style = {width: size, height: size, borderRadius: size / 2}; + if (!ok) { + return ; + } + return ( + setOk(false)} + /> + ); +} + +/** + * GoodWallet — a mock wallet's native "deposit intent" screen. Reached when the BX deposit page + * hands off `DEPOSIT_OPEN_WALLET` (via OmenDepositWebView) and presented as a full-screen modal so + * it reads as a separate wallet. Pick a network → a token you hold → an amount, see the destination + * + requesting app, Confirm; the tx is mocked, credits Omen, and returns. Network/token arrive + * pre-selected from BX but stay changeable. + */ +function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepositConfirm'>) { + const insets = useSafeAreaInsets(); + const {to, app, amount: amountParam, network, token} = route.params; + const appName = app || 'the app'; + + const initialNetwork = + WALLET_NETWORKS.find(n => n.name === network) ?? WALLET_NETWORKS[0]; + const initialToken = + initialNetwork.tokens.find(t => t.symbol === token) ?? initialNetwork.tokens[0]; + + const [selectedNetwork, setSelectedNetwork] = useState(initialNetwork); + const [selectedToken, setSelectedToken] = useState(initialToken); + const [amountStr, setAmountStr] = useState( + amountParam && amountParam > 0 ? String(amountParam) : '', + ); + const [phase, setPhase] = useState('select'); + + const amount = Number.parseFloat(amountStr) || 0; + const isValid = amount > 0 && amount <= selectedToken.balance; + const credited = useMemo(() => usdValue(selectedToken, amount), [selectedToken, amount]); + + function pickNetwork(n: WalletNetwork) { + setSelectedNetwork(n); + // Keep the token symbol if the new network holds it, else its first holding. + setSelectedToken(n.tokens.find(t => t.symbol === selectedToken.symbol) ?? n.tokens[0]); + setAmountStr(''); + } + + function handleConfirm() { + if (!isValid) { + return; + } + setPhase('sending'); + setTimeout(() => { + OmenStore.credit({amount: credited, source: 'wallet', txHash: mockTxHash()}); + setPhase('success'); + setTimeout(() => navigation.popToTop(), 1500); + }, 1200); + } + + if (phase !== 'select') { + return ( + + {phase === 'sending' ? ( + <> + + Sending… + + {fmtToken(amount)} {selectedToken.symbol} on {selectedNetwork.name} + + + ) : ( + <> + + + + {fmtUsd(credited)} sent + Returning to {appName}… + + )} + + ); + } + + return ( + + + + navigation.goBack()} + hitSlop={12} + style={({pressed}) => [styles.closeBtn, pressed && styles.closeBtnPressed]}> + + + + + + + + {appName.slice(0, 1).toUpperCase()} + + + Deposit to + {appName} + {to ? {shortAddr(to)} : null} + + + + Network + + {WALLET_NETWORKS.map(n => { + const isSel = n.name === selectedNetwork.name; + return ( + pickNetwork(n)} + style={[styles.chip, isSel && styles.chipSelected]}> + + {n.name} + + ); + })} + + + Asset + + {selectedNetwork.tokens.map((tok, i) => { + const isSel = tok.symbol === selectedToken.symbol; + return ( + { + setSelectedToken(tok); + setAmountStr(''); + }} + style={[ + styles.tokenRow, + i > 0 && styles.tokenRowDivider, + isSel && styles.tokenRowSelected, + ]}> + + {tok.symbol.slice(0, 1)} + + + {tok.name} + + {fmtToken(tok.balance)} {tok.symbol} + + + + {fmtUsd(tok.balance * tok.priceUsd)} + + {isSel ? : null} + + + + ); + })} + + + Amount + + + setAmountStr(t.replace(/[^0-9.]/g, ''))} + keyboardType="decimal-pad" + placeholder="0" + placeholderTextColor={C.textMuted} + style={styles.amountInput} + /> + {selectedToken.symbol} + setAmountStr(String(selectedToken.balance))} + style={styles.maxBtn}> + Max + + + + {amount > 0 + ? `≈ ${fmtUsd(credited)}` + : `Balance ${fmtToken(selectedToken.balance)} ${selectedToken.symbol}`} + + + + + + + + {amount > 0 ? `Deposit ${fmtUsd(credited)}` : 'Enter an amount'} + + + + + ); +} + +export default GoodDepositConfirm; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: C.bg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingBottom: 14, + backgroundColor: C.surface, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: C.border, + }, + closeBtn: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: C.surfaceAlt, + alignItems: 'center', + justifyContent: 'center', + }, + closeBtnPressed: { + opacity: 0.6, + }, + closeGlyph: { + color: C.textSecondary, + fontSize: 16, + fontWeight: '600', + lineHeight: 18, + }, + content: { + padding: 16, + gap: 8, + }, + requestCard: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + backgroundColor: C.surface, + borderRadius: 18, + borderWidth: 1, + borderColor: C.border, + padding: 16, + marginBottom: 6, + }, + appAvatar: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: C.accent, + alignItems: 'center', + justifyContent: 'center', + }, + appAvatarText: { + color: '#FFFFFF', + fontSize: 20, + fontWeight: '700', + }, + requestText: { + flex: 1, + }, + requestLabel: { + color: C.textMuted, + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + requestApp: { + color: C.textPrimary, + fontSize: 18, + fontWeight: '700', + }, + requestAddr: { + color: C.textSecondary, + fontSize: 13, + marginTop: 1, + }, + sectionLabel: { + color: C.textSecondary, + fontSize: 13, + fontWeight: '600', + marginTop: 8, + marginBottom: 2, + }, + chipRow: { + gap: 8, + paddingVertical: 2, + paddingRight: 4, + }, + chip: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 999, + borderWidth: 1, + borderColor: C.border, + backgroundColor: C.surface, + }, + chipSelected: { + borderColor: C.accent, + backgroundColor: C.accentTint, + }, + chipText: { + color: C.textSecondary, + fontSize: 14, + fontWeight: '600', + }, + chipTextSelected: { + color: C.textPrimary, + }, + tokenList: { + backgroundColor: C.surface, + borderRadius: 18, + borderWidth: 1, + borderColor: C.border, + overflow: 'hidden', + }, + tokenRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingHorizontal: 14, + paddingVertical: 13, + }, + tokenRowDivider: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: C.border, + }, + tokenRowSelected: { + backgroundColor: C.accentTint, + }, + tokenBadge: { + width: 38, + height: 38, + borderRadius: 19, + alignItems: 'center', + justifyContent: 'center', + }, + tokenBadgeText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '700', + }, + tokenText: { + flex: 1, + }, + tokenName: { + color: C.textPrimary, + fontSize: 15, + fontWeight: '600', + }, + tokenSub: { + color: C.textSecondary, + fontSize: 13, + }, + tokenRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + tokenUsd: { + color: C.textSecondary, + fontSize: 13, + fontWeight: '600', + }, + radio: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 2, + borderColor: C.border, + alignItems: 'center', + justifyContent: 'center', + }, + radioSelected: { + borderColor: C.accent, + }, + radioDot: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: C.accent, + }, + amountCard: { + backgroundColor: C.surface, + borderRadius: 18, + borderWidth: 1, + borderColor: C.border, + paddingHorizontal: 16, + paddingVertical: 14, + gap: 6, + }, + amountRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, + }, + amountInput: { + flex: 1, + color: C.textPrimary, + fontSize: 30, + fontWeight: '700', + padding: 0, + }, + amountSymbol: { + color: C.textSecondary, + fontSize: 16, + fontWeight: '700', + }, + maxBtn: { + backgroundColor: C.accentTint, + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 6, + }, + maxText: { + color: C.accent, + fontSize: 13, + fontWeight: '700', + }, + amountHint: { + color: C.textMuted, + fontSize: 13, + }, + footer: { + paddingHorizontal: 16, + paddingTop: 12, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: C.border, + backgroundColor: C.surface, + }, + confirm: { + backgroundColor: C.accent, + borderRadius: 16, + paddingVertical: 17, + alignItems: 'center', + }, + confirmDisabled: { + backgroundColor: C.textMuted, + opacity: 0.4, + }, + confirmText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '700', + }, + centered: { + alignItems: 'center', + justifyContent: 'center', + gap: 12, + padding: 24, + }, + centerTitle: { + color: C.textPrimary, + fontSize: 22, + fontWeight: '700', + marginTop: 8, + }, + centerSub: { + color: C.textSecondary, + fontSize: 15, + textAlign: 'center', + }, + successBadge: { + width: 72, + height: 72, + borderRadius: 36, + backgroundColor: C.accent, + alignItems: 'center', + justifyContent: 'center', + }, + successGlyph: { + color: '#FFFFFF', + fontSize: 40, + fontWeight: '800', + lineHeight: 46, + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/theme.ts b/dapps/appkit-wagmi/src/screens/GoodWallet/theme.ts new file mode 100644 index 00000000..7e2145dd --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/theme.ts @@ -0,0 +1,18 @@ +/** + * GoodWallet skin — a deliberately DISTINCT look from the Omen host (which is dark + violet). This + * is a light, clean "trustworthy wallet" palette (emerald accent) so the deposit-confirm modal + * reads as a separate wallet app that opened over Omen, not an Omen sub-screen. + */ +export const GOODWALLET_COLORS = { + bg: '#F4F6F8', + surface: '#FFFFFF', + surfaceAlt: '#EEF1F4', + textPrimary: '#0B0F14', + textSecondary: '#5B6673', + textMuted: '#8A94A3', + accent: '#16A34A', + accentSoft: '#22C55E', + accentTint: 'rgba(22, 163, 74, 0.10)', + border: '#E3E8EE', + danger: '#DC2626', +} as const; diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts b/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts new file mode 100644 index 00000000..3d7eb902 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts @@ -0,0 +1,77 @@ +/** + * Mock wallet holdings for the GoodWallet deposit demo — a wallet that natively knows which tokens + * you hold, per network. Static (no network / keys); mirrors how OmenStore seeds fake state. + * `priceUsd` converts a token amount into the USD value we credit the Omen account with (stables ≈ 1, + * native tokens carry realistic mock prices). + */ +export interface WalletToken { + symbol: string; + name: string; + /** Mock balance held, in whole token units. */ + balance: number; + /** Mock USD price used to convert amounts to a USD credit. */ + priceUsd: number; + /** Brand color for the token badge. */ + color: string; +} + +export interface WalletNetwork { + /** Display name — must match the BX network names so a pre-selection maps across. */ + name: string; + /** CAIP-2 id, for the network logo (WalletConnect assets CDN). */ + caip2: string; + tokens: WalletToken[]; +} + +const usdc = (balance: number): WalletToken => ({ + symbol: 'USDC', + name: 'USD Coin', + balance, + priceUsd: 1, + color: '#2775CA', +}); +const usdt = (balance: number): WalletToken => ({ + symbol: 'USDT', + name: 'Tether', + balance, + priceUsd: 1, + color: '#26A17B', +}); +const dai = (balance: number): WalletToken => ({ + symbol: 'DAI', + name: 'Dai', + balance, + priceUsd: 1, + color: '#F5AC37', +}); +const eth = (balance: number): WalletToken => ({ + symbol: 'ETH', + name: 'Ethereum', + balance, + priceUsd: 3550, + color: '#627EEA', +}); +const pol = (balance: number): WalletToken => ({ + symbol: 'POL', + name: 'Polygon', + balance, + priceUsd: 0.52, + color: '#8247E5', +}); + +export const WALLET_NETWORKS: WalletNetwork[] = [ + {name: 'Base', caip2: 'eip155:8453', tokens: [usdc(1240.55), eth(0.842), usdt(300)]}, + {name: 'Ethereum', caip2: 'eip155:1', tokens: [usdc(860.2), eth(1.35), dai(410)]}, + {name: 'Polygon', caip2: 'eip155:137', tokens: [usdc(540), pol(1250)]}, + {name: 'Arbitrum', caip2: 'eip155:42161', tokens: [usdc(920.75), eth(0.21)]}, + {name: 'Optimism', caip2: 'eip155:10', tokens: [usdc(305.2), eth(0.16)]}, +]; + +export function usdValue(token: WalletToken, amount: number): number { + return Math.round(token.priceUsd * amount * 100) / 100; +} + +/** Network logo from the WalletConnect assets CDN (same source BX uses). */ +export function networkImageUrl(caip2: string): string { + return `https://api.walletconnect.com/assets/v1/image/network/${encodeURIComponent(caip2)}/md`; +} diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx new file mode 100644 index 00000000..f694879a --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -0,0 +1,314 @@ +import React, {useCallback, useEffect, useState} from 'react'; +import {ActivityIndicator, Linking, StyleSheet, View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; +import {WebView, WebViewMessageEvent} from 'react-native-webview'; +import type { + ShouldStartLoadRequest, + WebViewErrorEvent, + WebViewHttpErrorEvent, + WebViewOpenWindowEvent, +} from 'react-native-webview/lib/WebViewTypes'; + +import {RootStackScreenProps} from '@/utils/TypesUtil'; +import {ToastUtils} from '@/utils/ToastUtils'; +import OmenStore from '@/stores/OmenStore'; +import {PaySuccessView} from '@/screens/PayWebView/PaySuccessView'; + +import {useWebViewDebugLog} from './useWebViewDebugLog'; +import WebViewDebugOverlay from './WebViewDebugOverlay'; +import {OMEN_COLORS} from './theme'; + +// On-screen debug overlay for the deposit WebView. We have no JS console on-device, so this +// surfaces the navigation / handoff / forwarded BX-console trail. Flip to false to strip it. +const DEBUG = true; + +// The BX /deposit page posts these over the RN WebView bridge (window.ReactNativeWebView) — see +// buyer-experience src/lib/webview-bridge.ts. This is the deposit contract, distinct from the +// PAY_SUCCESS/PAY_FAILURE contract PayWebView handles, so this screen owns its own onMessage. +type DepositMessage = { + type?: 'DEPOSIT_COMPLETE' | 'DEPOSIT_CANCELLED' | 'DEPOSIT_OPEN_WALLET'; + source?: 'wallet' | 'exchange' | 'direct'; + txHash?: string; + amount?: number; + // DEPOSIT_OPEN_WALLET only: hand off to our in-app GoodWallet screen. + to?: string; + app?: string; + network?: string; + token?: string; +}; + +// A debug line forwarded from inside the BX page by the injected console/error hook below. Marked +// with `__omenDebug` so it stays disjoint from the DEPOSIT_* deposit contract. +type DebugMessage = { + __omenDebug: true; + level: 'console' | 'error' | 'open'; + text: string; +}; + +// Injected into the BX page BEFORE its content loads: mirror console.*, window errors, unhandled +// rejections, and window.open() back over the bridge so they land in the on-screen debug overlay. +const DEBUG_FORWARDER = ` +(function () { + if (window.__omenDebugInstalled) return; + window.__omenDebugInstalled = true; + function post(level, text) { + try { + window.ReactNativeWebView.postMessage( + JSON.stringify({ __omenDebug: true, level: level, text: String(text).slice(0, 2000) }) + ); + } catch (e) {} + } + function fmt(args) { + return Array.prototype.map + .call(args, function (a) { + try { + if (a instanceof Error) return a.name + ': ' + a.message; + if (typeof a === 'object') return JSON.stringify(a); + return String(a); + } catch (e) { + return String(a); + } + }) + .join(' '); + } + ['log', 'info', 'warn', 'error'].forEach(function (k) { + var orig = console[k] ? console[k].bind(console) : function () {}; + console[k] = function () { + post(k === 'error' ? 'error' : 'console', k.toUpperCase() + ': ' + fmt(arguments)); + orig.apply(console, arguments); + }; + }); + window.addEventListener('error', function (e) { + post('error', 'window.onerror: ' + (e.message || '') + (e.filename ? ' @ ' + e.filename + ':' + e.lineno : '')); + }); + window.addEventListener('unhandledrejection', function (e) { + var r = e && e.reason; + post('error', 'unhandledrejection: ' + (r && r.message ? r.message : JSON.stringify(r))); + }); + var _open = window.open; + window.open = function (u, n, f) { + post('open', 'window.open(' + u + ')'); + try { + return _open ? _open.call(window, u, n, f) : null; + } catch (err) { + post('error', 'window.open threw: ' + err); + return null; + } + }; + post('console', 'debug forwarder installed @ ' + location.href); +})(); +true; +`; + +// A wallet-connect deeplink carries the WC pairing URI in its `uri` query param. Kept local +// (mirrors PayWebView) so the merged PayWebView stays untouched. +function isWalletDeeplink(url: string): boolean { + try { + const wcUri = new URL(url).searchParams.get('uri'); + return !!wcUri && wcUri.startsWith('wc:'); + } catch { + return false; + } +} + +function formatUsd(n: number): string { + return `$${n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/, ',')}`; +} + +function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepositWebView'>) { + const {url} = route.params; + const debug = useWebViewDebugLog(); + const log = debug.log; + + // Once the deposit page reports completion we swap the WebView for the success result screen + // (reuses PayWebView's Lottie checkmark view). + const [successMessage, setSuccessMessage] = useState(null); + // Dark loading cover over the WebView until the page paints — kills the white flash on open. + const [isPageLoading, setIsPageLoading] = useState(true); + + // After a successful deposit, show the confirmation briefly then close the deposit flow and + // return to the Omen home — which now reflects the new balance + activity row. + useEffect(() => { + if (successMessage === null) { + return; + } + const timer = setTimeout(() => navigation.goBack(), 2000); + + return () => clearTimeout(timer); + }, [successMessage, navigation]); + + const openWallet = useCallback( + async (target: string) => { + log('open', 'openWallet →', target); + try { + const can = await Linking.canOpenURL(target); + log('open', `canOpenURL = ${can}`); + } catch (err) { + log('error', 'canOpenURL threw', String(err)); + } + Linking.openURL(target) + .then(() => log('open', 'openURL resolved')) + .catch(err => { + log('error', 'openURL rejected', String(err)); + ToastUtils.showErrorToast( + "Couldn't open wallet", + 'The wallet app may not be installed.', + ); + }); + }, + [log], + ); + + // In-frame navigations to a wallet deeplink are handed to the OS (opens the wallet); everything + // else loads in the WebView. + const onShouldStartLoadWithRequest = useCallback( + (request: ShouldStartLoadRequest): boolean => { + const wallet = isWalletDeeplink(request.url); + log('nav', `shouldStart (wallet=${wallet}) →`, request.url); + if (wallet) { + openWallet(request.url); + return false; + } + return true; + }, + [log, openWallet], + ); + + // window.open() targets route here instead. Only hand wallet deeplinks off to the OS so a page + // can't drive Linking.openURL to arbitrary native schemes (tel:/sms:/intent:...). + const onOpenWindow = useCallback( + (event: WebViewOpenWindowEvent) => { + const {targetUrl} = event.nativeEvent; + const wallet = isWalletDeeplink(targetUrl); + log('open', `openWindow (wallet=${wallet}) →`, targetUrl); + if (wallet) { + openWallet(targetUrl); + } + }, + [log, openWallet], + ); + + const onMessage = useCallback( + (event: WebViewMessageEvent) => { + const raw = event.nativeEvent.data; + + let parsed: DepositMessage | DebugMessage; + try { + parsed = JSON.parse(raw); + } catch { + // Non-JSON message — ignore. + return; + } + + // Forwarded BX console/error line → overlay only, never the deposit contract. + if ((parsed as DebugMessage).__omenDebug) { + const d = parsed as DebugMessage; + log(d.level, d.text); + return; + } + + const message = parsed as DepositMessage; + log('msg', `onMessage ${message.type ?? '(no type)'}`, raw.slice(0, 500)); + + if (message.type === 'DEPOSIT_OPEN_WALLET') { + // Open our in-app "GoodWallet" — the demo of the ideal native-deposit UX. + navigation.navigate('GoodDepositConfirm', { + to: message.to, + app: message.app, + amount: message.amount, + network: message.network, + token: message.token, + }); + return; + } + + if (message.type === 'DEPOSIT_COMPLETE') { + const credited = OmenStore.credit({ + amount: message.amount, + source: message.source, + txHash: message.txHash, + }); + setSuccessMessage(`${formatUsd(credited)} added to your account`); + } else if (message.type === 'DEPOSIT_CANCELLED') { + navigation.goBack(); + } + }, + [log, navigation], + ); + + const onError = useCallback( + (event: WebViewErrorEvent) => { + const {code, description, url: failedUrl} = event.nativeEvent; + log('error', `onError ${code}`, `${description} @ ${failedUrl}`); + ToastUtils.showErrorToast( + 'Failed to load deposit page', + 'Check your connection and try again.', + ); + navigation.goBack(); + }, + [log, navigation], + ); + + const onHttpError = useCallback( + (event: WebViewHttpErrorEvent) => { + const {statusCode, description, url: failedUrl} = event.nativeEvent; + log('error', `onHttpError ${statusCode}`, `${description ?? ''} @ ${failedUrl}`); + }, + [log], + ); + + if (successMessage !== null) { + return ( + navigation.goBack()} + /> + ); + } + + return ( + + setIsPageLoading(false)} + /> + {isPageLoading ? ( + + + + ) : null} + {DEBUG ? : null} + + ); +} + +export default OmenDepositWebView; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: OMEN_COLORS.bg, + }, + webview: { + flex: 1, + backgroundColor: OMEN_COLORS.bg, + }, + loading: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: OMEN_COLORS.bg, + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenLogo.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenLogo.tsx new file mode 100644 index 00000000..097c06fc --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenLogo.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import {StyleSheet, Text, View} from 'react-native'; + +import {OMEN_COLORS} from './theme'; + +/** + * Omen brand mark — drawn entirely in code (no image asset) so the demo stays self-contained. + * A circular accent badge with a crescent glyph next to the "Omen" wordmark, mirroring the web + * host (apps/deposit-demo). Kept tiny and presentational. + */ +function OmenLogo() { + return ( + + + + + Omen + + ); +} + +export default OmenLogo; + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + badge: { + width: 26, + height: 26, + borderRadius: 13, + backgroundColor: OMEN_COLORS.accent, + alignItems: 'center', + justifyContent: 'center', + }, + glyph: { + color: OMEN_COLORS.textPrimary, + fontSize: 14, + lineHeight: 18, + }, + wordmark: { + color: OMEN_COLORS.textPrimary, + fontSize: 18, + fontWeight: '600', + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/Omen/WebViewDebugOverlay.tsx b/dapps/appkit-wagmi/src/screens/Omen/WebViewDebugOverlay.tsx new file mode 100644 index 00000000..6bd42c0d --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/WebViewDebugOverlay.tsx @@ -0,0 +1,194 @@ +import React, {useMemo, useState} from 'react'; +import {ScrollView, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; + +import {ToastUtils} from '@/utils/ToastUtils'; + +import type {DebugEntry, DebugLevel} from './useWebViewDebugLog'; + +const LEVEL_COLOR: Record = { + nav: '#8b5cf6', + open: '#38bdf8', + msg: '#a1a1aa', + console: '#e4e4e7', + error: '#f87171', +}; + +function timeOf(ts: number): string { + const d = new Date(ts); + const pad = (n: number, len = 2) => String(n).padStart(len, '0'); + return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad( + d.getMilliseconds(), + 3, + )}`; +} + +interface Props { + entries: DebugEntry[]; + onClear: () => void; +} + +/** + * Developer-facing debug overlay for the deposit WebView. A floating pill (bottom-right) that + * expands into a scrollable log of navigations / handler verdicts / forwarded BX console lines, + * so we can root-cause the wallet-open handoff on-device without a JS console. Copy dumps the + * whole log to the clipboard. + */ +function WebViewDebugOverlay({entries, onClear}: Props) { + const [open, setOpen] = useState(false); + + const asText = useMemo( + () => + entries + .map(e => `${timeOf(e.ts)} [${e.level}] ${e.label}${e.detail ? `\n ${e.detail}` : ''}`) + .join('\n'), + [entries], + ); + + const onCopy = () => { + Clipboard.setString(asText); + ToastUtils.showInfoToast('Debug log copied', `${entries.length} entries`); + }; + + if (!open) { + return ( + setOpen(true)} + activeOpacity={0.8} + testID="omen-debug-fab"> + 🐞 {entries.length} + + ); + } + + return ( + + + + Deposit debug · {entries.length} + + + Copy + + + Clear + + setOpen(false)} style={styles.btn}> + Hide + + + + + {entries.length === 0 ? ( + No events yet — tap a wallet. + ) : ( + entries.map(e => ( + + + {timeOf(e.ts)} + [{e.level}] + {e.label} + + {e.detail ? {e.detail} : null} + + )) + )} + + + + ); +} + +export default WebViewDebugOverlay; + +const styles = StyleSheet.create({ + fab: { + position: 'absolute', + right: 12, + bottom: 24, + backgroundColor: 'rgba(24,24,27,0.92)', + borderRadius: 999, + paddingHorizontal: 14, + paddingVertical: 8, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#3f3f46', + }, + fabText: { + color: '#f4f4f5', + fontSize: 13, + fontWeight: '600', + }, + panel: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + height: '55%', + }, + panelInner: { + flex: 1, + backgroundColor: 'rgba(9,9,11,0.96)', + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: '#3f3f46', + }, + toolbar: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 12, + paddingVertical: 10, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#27272a', + }, + title: { + color: '#f4f4f5', + fontSize: 13, + fontWeight: '600', + }, + toolbarActions: { + flexDirection: 'row', + gap: 8, + }, + btn: { + backgroundColor: '#27272a', + borderRadius: 6, + paddingHorizontal: 10, + paddingVertical: 5, + }, + btnText: { + color: '#e4e4e7', + fontSize: 12, + fontWeight: '600', + }, + list: { + flex: 1, + }, + listContent: { + padding: 12, + gap: 8, + }, + empty: { + color: '#71717a', + fontSize: 12, + }, + entry: { + gap: 2, + }, + entryHead: { + fontSize: 11, + }, + time: { + color: '#52525b', + }, + label: { + color: '#f4f4f5', + fontWeight: '600', + }, + detail: { + color: '#a1a1aa', + fontSize: 11, + fontFamily: 'Courier', + paddingLeft: 8, + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts new file mode 100644 index 00000000..43d29594 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts @@ -0,0 +1,34 @@ +import {getMetadata} from '@/utils/misc'; + +/** + * BX deposit-flow route (branch preview Worker). This is a standalone "add money" surface that + * posts DEPOSIT_COMPLETE / DEPOSIT_CANCELLED back over the RN WebView bridge — never a redirect. + */ +const BX_DEPOSIT_URL = + 'https://tomiir-bx-deposit-flow-poc-wc-pay-buyer-experience-dev.walletconnect-v1-bridge.workers.dev/deposit'; + +/** Omen's deposit destination (demo treasury). */ +const OMEN_TREASURY = '0x13302Eb0aD9Af2F847119dC4Ac632fFe196d0B0f'; + +/** + * Build the deposit URL for the in-app WebView. Mirrors PasteUrlButton.buildPayUrl: + * - `returnUrl` = our AppKit native deeplink, so wallets reopen this app after signing + * (BX maps it to the WC session redirect metadata). + * - `preferUniversalLinks` = open wallets via universal links. + * Plus the deposit-specific `to` (destination) and `app` (branding) params, and an optional + * `amount` prefill. + */ +export function buildOmenDepositUrl(opts: {amount?: number} = {}): string { + const url = new URL(BX_DEPOSIT_URL); + url.searchParams.set('to', OMEN_TREASURY); + url.searchParams.set('app', 'Omen'); + url.searchParams.set('returnUrl', getMetadata().redirect.native); + url.searchParams.set('preferUniversalLinks', '1'); + // Render BX in dark mode so the deposit surface matches the Omen host bg (no light flash). + url.searchParams.set('theme', 'dark'); + if (opts.amount && opts.amount > 0) { + url.searchParams.set('amount', String(opts.amount)); + } + + return url.toString(); +} diff --git a/dapps/appkit-wagmi/src/screens/Omen/index.tsx b/dapps/appkit-wagmi/src/screens/Omen/index.tsx new file mode 100644 index 00000000..db6227d7 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/index.tsx @@ -0,0 +1,271 @@ +import React from 'react'; +import {Linking, ScrollView, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import {useNavigation} from '@react-navigation/native'; +import type {NativeStackNavigationProp} from '@react-navigation/native-stack'; +import {useSnapshot} from 'valtio'; + +import {RootStackParamList} from '@/utils/TypesUtil'; +import OmenStore from '@/stores/OmenStore'; + +import {buildOmenDepositUrl} from './depositUrl'; +import OmenLogo from './OmenLogo'; +import {OMEN_COLORS} from './theme'; + +function formatUsd(n: number): string { + const sign = n < 0 ? '-' : ''; + return `${sign}$${Math.abs(n) + .toFixed(2) + .replace(/\B(?=(\d{3})+(?!\d))/, ',')}`; +} + +function formatDate(ts: number): string { + return new Date(ts).toLocaleDateString('en-US', {month: 'short', day: 'numeric'}); +} + +/** + * Omen — a mock "funded account" host screen wearing the Omen brand skin (dark, violet accent), + * mirroring the web demo (apps/deposit-demo). Shows a balance + activity and opens the + * WalletConnect Pay deposit flow in an in-app WebView (OmenDepositWebView), which credits the + * balance from the result posted back over the RN bridge. No context switch, no redirect. + */ +function OmenScreen() { + const {balance, activity} = useSnapshot(OmenStore.state); + const navigation = useNavigation>(); + + const onDeposit = () => { + navigation.navigate('OmenDepositWebView', {url: buildOmenDepositUrl()}); + }; + + return ( + + + navigation.goBack()} + hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}> + + + + Sign out + + + + Welcome Offer: Deposit $5, get $200 + + + + + Account balance + {formatUsd(balance)} + + + + + + Deposit crypto + + + Withdraw + + + + + Activity + {activity.map(tx => { + const isCredit = tx.amount > 0; + return ( + + + + {isCredit ? '↓' : '↑'} + + + + {tx.label} + + {formatDate(tx.ts)} + {tx.txHash ? ( + <> + · + + Linking.openURL(`https://basescan.org/tx/${tx.txHash}`) + }> + View on explorer + + + ) : null} + + + + {isCredit ? '+' : ''} + {formatUsd(tx.amount)} + + + ); + })} + + + Deposits powered by WalletConnect + + + ); +} + +export default OmenScreen; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: OMEN_COLORS.bg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingTop: 56, + paddingBottom: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: OMEN_COLORS.border, + }, + chevron: { + color: OMEN_COLORS.textSecondary, + fontSize: 30, + lineHeight: 30, + width: 24, + }, + signOut: { + color: OMEN_COLORS.textSecondary, + fontSize: 14, + }, + banner: { + backgroundColor: OMEN_COLORS.accent, + paddingVertical: 10, + alignItems: 'center', + }, + bannerText: { + color: OMEN_COLORS.textPrimary, + fontSize: 14, + fontWeight: '500', + }, + content: { + paddingHorizontal: 24, + paddingVertical: 32, + gap: 32, + }, + balanceBlock: { + alignItems: 'center', + gap: 4, + paddingVertical: 8, + }, + balanceLabel: { + color: OMEN_COLORS.textSecondary, + fontSize: 14, + }, + balance: { + color: OMEN_COLORS.textPrimary, + fontSize: 44, + fontWeight: '600', + letterSpacing: -1, + }, + actions: { + flexDirection: 'row', + justifyContent: 'center', + gap: 12, + }, + pill: { + borderRadius: 999, + paddingHorizontal: 24, + paddingVertical: 13, + alignItems: 'center', + justifyContent: 'center', + }, + pillPrimary: { + backgroundColor: OMEN_COLORS.accent, + }, + pillPrimaryText: { + color: OMEN_COLORS.textPrimary, + fontSize: 15, + fontWeight: '600', + }, + pillDisabled: { + backgroundColor: OMEN_COLORS.surfaceRaised, + }, + pillDisabledText: { + color: OMEN_COLORS.textMuted, + fontSize: 15, + fontWeight: '600', + }, + activity: { + gap: 4, + }, + sectionLabel: { + color: OMEN_COLORS.textSecondary, + fontSize: 14, + fontWeight: '500', + paddingHorizontal: 4, + marginBottom: 4, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: OMEN_COLORS.surface, + }, + rowBadge: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + }, + rowBadgeGlyph: { + fontSize: 16, + }, + rowText: { + flex: 1, + gap: 2, + }, + rowLabel: { + color: OMEN_COLORS.textPrimary, + fontSize: 14, + }, + rowMeta: { + flexDirection: 'row', + alignItems: 'center', + }, + rowDate: { + color: OMEN_COLORS.textMuted, + fontSize: 12, + }, + rowLink: { + color: OMEN_COLORS.accentSoft, + fontSize: 12, + }, + rowAmount: { + fontSize: 14, + fontWeight: '600', + }, + footer: { + color: OMEN_COLORS.textFaint, + fontSize: 12, + textAlign: 'center', + }, +}); diff --git a/dapps/appkit-wagmi/src/screens/Omen/theme.ts b/dapps/appkit-wagmi/src/screens/Omen/theme.ts new file mode 100644 index 00000000..58f80141 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/theme.ts @@ -0,0 +1,19 @@ +/** + * Omen host skin — a branded, intentionally dark palette (NOT AppKit's `useTheme()` tokens). + * Mirrors the web demo (apps/deposit-demo/src/index.css): zinc surfaces + a violet accent + * (oklch(0.55 0.24 288) ≈ #7c3aed) and an emerald credit color. Single source for the Omen screen. + */ +export const OMEN_COLORS = { + bg: '#09090b', + surface: '#18181b', + surfaceRaised: '#27272a', + textPrimary: '#f4f4f5', + textSecondary: '#a1a1aa', + textMuted: '#71717a', + textFaint: '#52525b', + accent: '#7c3aed', + accentSoft: '#8b5cf6', + credit: '#34d399', + creditTint: 'rgba(52, 211, 153, 0.15)', + border: '#27272a', +} as const; diff --git a/dapps/appkit-wagmi/src/screens/Omen/useWebViewDebugLog.ts b/dapps/appkit-wagmi/src/screens/Omen/useWebViewDebugLog.ts new file mode 100644 index 00000000..d3ed9507 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/useWebViewDebugLog.ts @@ -0,0 +1,48 @@ +import {useCallback, useRef, useState} from 'react'; + +/** + * Tiny in-memory debug log for the deposit WebView. We have no console access inside the remote BX + * page on-device, so every navigation/handler/console line is funneled here and rendered by + * WebViewDebugOverlay. Capped ring buffer — plain React state, no external store. + */ +export type DebugLevel = 'nav' | 'open' | 'msg' | 'console' | 'error'; + +export interface DebugEntry { + id: string; + ts: number; + level: DebugLevel; + label: string; + detail?: string; +} + +const MAX_ENTRIES = 200; + +export interface WebViewDebugLog { + entries: DebugEntry[]; + log: (level: DebugLevel, label: string, detail?: string) => void; + clear: () => void; +} + +export function useWebViewDebugLog(): WebViewDebugLog { + const [entries, setEntries] = useState([]); + const seq = useRef(0); + + const log = useCallback((level: DebugLevel, label: string, detail?: string) => { + seq.current += 1; + const entry: DebugEntry = { + id: `${Date.now().toString(36)}_${seq.current}`, + ts: Date.now(), + level, + label, + detail, + }; + setEntries(prev => { + const next = [entry, ...prev]; + return next.length > MAX_ENTRIES ? next.slice(0, MAX_ENTRIES) : next; + }); + }, []); + + const clear = useCallback(() => setEntries([]), []); + + return {entries, log, clear}; +} diff --git a/dapps/appkit-wagmi/src/stores/OmenStore.ts b/dapps/appkit-wagmi/src/stores/OmenStore.ts new file mode 100644 index 00000000..8ed102c8 --- /dev/null +++ b/dapps/appkit-wagmi/src/stores/OmenStore.ts @@ -0,0 +1,66 @@ +import {proxy} from 'valtio'; + +/** + * Mock "funded account" state for the Omen deposit demo. + * + * Omen is a stand-in host app (think a trading/prediction app) whose only job here is to show a + * balance + activity and open the WalletConnect Pay deposit flow in a WebView. The deposit result + * arrives over the RN WebView bridge (see OmenDepositWebView) and is credited via `credit()`. + * In-memory only — resets on reload (mirrors the web demo's seed). + */ + +export type DepositSource = 'wallet' | 'exchange' | 'direct'; + +export interface OmenActivity { + id: string; + label: string; + /** Positive = credit, negative = debit. */ + amount: number; + ts: number; + /** On-chain tx hash — present only for the real wallet path. */ + txHash?: string; +} + +interface State { + balance: number; + activity: OmenActivity[]; +} + +const state = proxy({ + balance: 1182.85, + activity: [ + {id: 't3', label: 'Evaluation fee', amount: -299.99, ts: Date.now() - 86_400_000}, + {id: 't2', label: 'Payout — June', amount: 1250, ts: Date.now() - 3 * 86_400_000}, + {id: 't1', label: 'Deposit — Coinbase', amount: 62.35, ts: Date.now() - 5 * 86_400_000}, + ], +}); + +const OmenStore = { + state, + + /** + * Credit a completed deposit and prepend an activity row. The amount may be chosen inside the + * wallet and not reported back, so we fall back to a nominal demo amount (mirrors the web demo). + * Returns the credited amount so the caller can show it in the success view. + */ + credit({amount, source, txHash}: {amount?: number; source?: DepositSource; txHash?: string}): number { + const credited = amount && amount > 0 ? amount : 25; + const sourceLabel = source ? source.charAt(0).toUpperCase() + source.slice(1) : 'Crypto'; + + state.balance = Math.round((state.balance + credited) * 100) / 100; + state.activity = [ + { + id: `dep_${Date.now().toString(36)}`, + label: `Deposit — ${sourceLabel}`, + amount: credited, + ts: Date.now(), + txHash, + }, + ...state.activity, + ]; + + return credited; + }, +}; + +export default OmenStore; diff --git a/dapps/appkit-wagmi/src/utils/TypesUtil.ts b/dapps/appkit-wagmi/src/utils/TypesUtil.ts index 47f00804..32fd92d2 100644 --- a/dapps/appkit-wagmi/src/utils/TypesUtil.ts +++ b/dapps/appkit-wagmi/src/utils/TypesUtil.ts @@ -71,6 +71,15 @@ export type RootStackParamList = { AppKitLogs: undefined; NetworkSettings: undefined; PayWebView: {url: string}; + Omen: undefined; + OmenDepositWebView: {url: string}; + GoodDepositConfirm: { + to?: string; + app?: string; + amount?: number; + network?: string; + token?: string; + }; }; export type HomeTabParamList = {