From b21d2dd4c58f57bdb19e29acde98f306903045d6 Mon Sep 17 00:00:00 2001 From: tomiir Date: Wed, 15 Jul 2026 18:51:08 +0200 Subject: [PATCH 1/9] feat(appkit-wagmi): Omen deposit-flow demo in WebView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an "Omen" mock funded-account screen that opens the WalletConnect Pay deposit flow (buyer-experience /deposit) in an in-app WebView — no context switch, no redirect. Reuses the #570 WebView scaffolding (wc: deeplink hand-off, returnUrl + preferUniversalLinks) with a deposit-specific onMessage that credits a mock balance from DEPOSIT_COMPLETE/DEPOSIT_CANCELLED. - OmenStore (valtio): mock balance + activity + credit() - Omen screen: balance, "Deposit crypto", activity list - OmenDepositWebView: WebView + deeplink interception + DEPOSIT_* bridge - Entry button on the Connections screen + navigation wiring Co-Authored-By: Claude Opus 4.8 --- .../src/navigators/RootStackNavigator.tsx | 22 +++ .../Connections/components/OmenDemoButton.tsx | 19 +++ .../src/screens/Connections/index.tsx | 2 + .../src/screens/Omen/OmenDepositWebView.tsx | 140 +++++++++++++++++ .../src/screens/Omen/depositUrl.ts | 32 ++++ dapps/appkit-wagmi/src/screens/Omen/index.tsx | 145 ++++++++++++++++++ dapps/appkit-wagmi/src/stores/OmenStore.ts | 66 ++++++++ dapps/appkit-wagmi/src/utils/TypesUtil.ts | 2 + 8 files changed, 428 insertions(+) create mode 100644 dapps/appkit-wagmi/src/screens/Connections/components/OmenDemoButton.tsx create mode 100644 dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx create mode 100644 dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts create mode 100644 dapps/appkit-wagmi/src/screens/Omen/index.tsx create mode 100644 dapps/appkit-wagmi/src/stores/OmenStore.ts diff --git a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx index 773494d8..221701ca 100644 --- a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx +++ b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx @@ -9,6 +9,8 @@ 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'; const StackNavigator = createNativeStackNavigator(); @@ -76,6 +78,26 @@ 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/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx new file mode 100644 index 00000000..b34dcb89 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -0,0 +1,140 @@ +import React, {useCallback, useState} from 'react'; +import {Linking, StyleSheet} from 'react-native'; +import {WebView, WebViewMessageEvent} from 'react-native-webview'; +import type { + ShouldStartLoadRequest, + 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'; + +// 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'; + source?: 'wallet' | 'exchange' | 'direct'; + txHash?: string; + amount?: number; +}; + +// 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; + + // 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); + + const openWallet = useCallback((target: string) => { + Linking.openURL(target).catch(() => { + ToastUtils.showErrorToast("Couldn't open wallet", 'The wallet app may not be installed.'); + }); + }, []); + + // 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 => { + if (isWalletDeeplink(request.url)) { + openWallet(request.url); + return false; + } + return true; + }, + [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; + if (isWalletDeeplink(targetUrl)) { + openWallet(targetUrl); + } + }, + [openWallet], + ); + + const onMessage = useCallback( + (event: WebViewMessageEvent) => { + let message: DepositMessage; + try { + message = JSON.parse(event.nativeEvent.data); + } catch { + // Non-JSON message — ignore. + 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(); + } + }, + [navigation], + ); + + const onLoadError = useCallback(() => { + ToastUtils.showErrorToast( + 'Failed to load deposit page', + 'Check your connection and try again.', + ); + navigation.goBack(); + }, [navigation]); + + if (successMessage !== null) { + return ( + navigation.goBack()} + /> + ); + } + + return ( + + ); +} + +export default OmenDepositWebView; + +const styles = StyleSheet.create({ + webview: { + flex: 1, + }, +}); 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..8a64f74d --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts @@ -0,0 +1,32 @@ +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 mock treasury (deposit destination). Burn address for the POC — no real funds implied. */ +const OMEN_TREASURY = '0x000000000000000000000000000000000000dEaD'; + +/** + * 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'); + 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..e1ff8247 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/Omen/index.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import {Linking, ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'; +import {Button, FlexView, Text} from '@reown/appkit-ui-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 {useTheme} from '@/hooks/useTheme'; +import OmenStore from '@/stores/OmenStore'; + +import {buildOmenDepositUrl} from './depositUrl'; + +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. 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 Theme = useTheme(); + const {balance, activity} = useSnapshot(OmenStore.state); + const navigation = useNavigation>(); + + const onDeposit = () => { + navigation.navigate('OmenDepositWebView', {url: buildOmenDepositUrl()}); + }; + + return ( + + + + Account balance + + + {formatUsd(balance)} + + + + + + + + + Activity + + + + {activity.map(tx => ( + + + + {tx.amount > 0 ? '↓' : '↑'} + + + {tx.label} + + {formatDate(tx.ts)} + + {tx.txHash ? ( + Linking.openURL(`https://basescan.org/tx/${tx.txHash}`)}> + + View on explorer + + + ) : null} + + + 0 ? Theme['success-100'] : Theme['fg-100']}}> + {tx.amount > 0 ? '+' : ''} + {formatUsd(tx.amount)} + + + ))} + + + + Deposits powered by WalletConnect + + + ); +} + +export default OmenScreen; + +const styles = StyleSheet.create({ + content: { + padding: 20, + gap: 24, + }, + balanceBlock: { + gap: 6, + paddingVertical: 16, + }, + balance: { + fontSize: 42, + }, + actions: { + alignItems: 'center', + }, + sectionLabel: { + marginBottom: -8, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + rowLeft: { + flex: 1, + gap: 12, + }, + rowIcon: { + fontSize: 18, + width: 24, + textAlign: 'center', + }, + rowText: { + flex: 1, + }, + footer: { + marginTop: 8, + }, +}); 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..45b584f6 100644 --- a/dapps/appkit-wagmi/src/utils/TypesUtil.ts +++ b/dapps/appkit-wagmi/src/utils/TypesUtil.ts @@ -71,6 +71,8 @@ export type RootStackParamList = { AppKitLogs: undefined; NetworkSettings: undefined; PayWebView: {url: string}; + Omen: undefined; + OmenDepositWebView: {url: string}; }; export type HomeTabParamList = { From df529e7e2f4120f7684fbc8788de03092e1b7bdd Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 10:28:29 +0200 Subject: [PATCH 2/9] feat(omen): apply Omen brand skin to native deposit demo screen Restyle the native Omen screen to match the web host (apps/deposit-demo): dark zinc surfaces, violet accent, code-drawn logo + wordmark header with Sign out, purple welcome-offer banner, styled balance/deposit pill/activity. Hide the stack header for Omen so the in-content header is the only one. No image assets added. Co-Authored-By: Claude Opus 4.8 --- .../src/navigators/RootStackNavigator.tsx | 5 +- .../src/screens/Omen/OmenLogo.tsx | 48 +++ dapps/appkit-wagmi/src/screens/Omen/index.tsx | 274 +++++++++++++----- dapps/appkit-wagmi/src/screens/Omen/theme.ts | 19 ++ 4 files changed, 268 insertions(+), 78 deletions(-) create mode 100644 dapps/appkit-wagmi/src/screens/Omen/OmenLogo.tsx create mode 100644 dapps/appkit-wagmi/src/screens/Omen/theme.ts diff --git a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx index 221701ca..767d9d2a 100644 --- a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx +++ b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx @@ -82,10 +82,7 @@ export function RootStackNavigator() { name="Omen" component={OmenScreen} options={{ - headerShown: true, - headerBackButtonDisplayMode: 'minimal', - title: 'Omen', - headerTintColor: Theme['fg-100'], + headerShown: false, }} /> + + + + 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/index.tsx b/dapps/appkit-wagmi/src/screens/Omen/index.tsx index e1ff8247..db6227d7 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/index.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/index.tsx @@ -1,15 +1,15 @@ import React from 'react'; -import {Linking, ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'; -import {Button, FlexView, Text} from '@reown/appkit-ui-react-native'; +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 {useTheme} from '@/hooks/useTheme'; 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 ? '-' : ''; @@ -23,12 +23,12 @@ function formatDate(ts: number): string { } /** - * Omen — a mock "funded account" host screen. Shows a balance + activity and opens the + * 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 Theme = useTheme(); const {balance, activity} = useSnapshot(OmenStore.state); const navigation = useNavigation>(); @@ -37,109 +37,235 @@ function OmenScreen() { }; return ( - - - - Account balance - - - {formatUsd(balance)} - - + + + navigation.goBack()} + hitSlop={{top: 8, bottom: 8, left: 8, right: 8}}> + + + + Sign out + - - + + Welcome Offer: Deposit $5, get $200 - - Activity - + + + Account balance + {formatUsd(balance)} + - - {activity.map(tx => ( - - - - {tx.amount > 0 ? '↓' : '↑'} - - - {tx.label} - - {formatDate(tx.ts)} - - {tx.txHash ? ( - Linking.openURL(`https://basescan.org/tx/${tx.txHash}`)}> - - View on explorer - - - ) : null} - - - 0 ? Theme['success-100'] : Theme['fg-100']}}> - {tx.amount > 0 ? '+' : ''} - {formatUsd(tx.amount)} - + + + + Deposit crypto + + + Withdraw - ))} - + - - Deposits powered by WalletConnect - - + + 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: { - padding: 20, - gap: 24, + paddingHorizontal: 24, + paddingVertical: 32, + gap: 32, }, balanceBlock: { - gap: 6, - paddingVertical: 16, + alignItems: 'center', + gap: 4, + paddingVertical: 8, + }, + balanceLabel: { + color: OMEN_COLORS.textSecondary, + fontSize: 14, }, balance: { - fontSize: 42, + 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: { - marginBottom: -8, + color: OMEN_COLORS.textSecondary, + fontSize: 14, + fontWeight: '500', + paddingHorizontal: 4, + marginBottom: 4, }, row: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', + gap: 12, paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: OMEN_COLORS.surface, }, - rowLeft: { - flex: 1, - gap: 12, + rowBadge: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', }, - rowIcon: { - fontSize: 18, - width: 24, - textAlign: '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: { - marginTop: 8, + 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; From 0be3924dc105b8087c043e6fe9580db2bd61fc76 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 11:05:06 +0200 Subject: [PATCH 3/9] feat(omen): on-screen debug overlay for deposit WebView We have no JS console on-device, so tapping a wallet failing silently was un-diagnosable. Add a floating debug overlay that captures every navigation request + isWalletDeeplink verdict, window.open targets, the Linking canOpenURL/openURL handoff result, load/HTTP errors, and BX's own console/window-error/unhandledrejection lines (forwarded via injected JS over the RN bridge, marked __omenDebug so the DEPOSIT_* contract is untouched). Copy/Clear + capped ring buffer. Gated behind a DEBUG const. Co-Authored-By: Claude Opus 4.8 --- .../src/screens/Omen/OmenDepositWebView.tsx | 191 ++++++++++++++--- .../src/screens/Omen/WebViewDebugOverlay.tsx | 194 ++++++++++++++++++ .../src/screens/Omen/useWebViewDebugLog.ts | 48 +++++ 3 files changed, 401 insertions(+), 32 deletions(-) create mode 100644 dapps/appkit-wagmi/src/screens/Omen/WebViewDebugOverlay.tsx create mode 100644 dapps/appkit-wagmi/src/screens/Omen/useWebViewDebugLog.ts diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index b34dcb89..807ff4a9 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -1,8 +1,10 @@ import React, {useCallback, useState} from 'react'; -import {Linking, StyleSheet} from 'react-native'; +import {Linking, StyleSheet, View} from 'react-native'; import {WebView, WebViewMessageEvent} from 'react-native-webview'; import type { ShouldStartLoadRequest, + WebViewErrorEvent, + WebViewHttpErrorEvent, WebViewOpenWindowEvent, } from 'react-native-webview/lib/WebViewTypes'; @@ -11,6 +13,13 @@ import {ToastUtils} from '@/utils/ToastUtils'; import OmenStore from '@/stores/OmenStore'; import {PaySuccessView} from '@/screens/PayWebView/PaySuccessView'; +import {useWebViewDebugLog} from './useWebViewDebugLog'; +import WebViewDebugOverlay from './WebViewDebugOverlay'; + +// 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. @@ -21,6 +30,69 @@ type DepositMessage = { amount?: number; }; +// 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 { @@ -38,28 +110,48 @@ function formatUsd(n: number): string { 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); - const openWallet = useCallback((target: string) => { - Linking.openURL(target).catch(() => { - ToastUtils.showErrorToast("Couldn't open wallet", 'The wallet app may not be installed.'); - }); - }, []); + 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 => { - if (isWalletDeeplink(request.url)) { + const wallet = isWalletDeeplink(request.url); + log('nav', `shouldStart (wallet=${wallet}) →`, request.url); + if (wallet) { openWallet(request.url); return false; } return true; }, - [openWallet], + [log, openWallet], ); // window.open() targets route here instead. Only hand wallet deeplinks off to the OS so a page @@ -67,23 +159,37 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos const onOpenWindow = useCallback( (event: WebViewOpenWindowEvent) => { const {targetUrl} = event.nativeEvent; - if (isWalletDeeplink(targetUrl)) { + const wallet = isWalletDeeplink(targetUrl); + log('open', `openWindow (wallet=${wallet}) →`, targetUrl); + if (wallet) { openWallet(targetUrl); } }, - [openWallet], + [log, openWallet], ); const onMessage = useCallback( (event: WebViewMessageEvent) => { - let message: DepositMessage; + const raw = event.nativeEvent.data; + + let parsed: DepositMessage | DebugMessage; try { - message = JSON.parse(event.nativeEvent.data); + 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_COMPLETE') { const credited = OmenStore.credit({ amount: message.amount, @@ -95,16 +201,29 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos navigation.goBack(); } }, - [navigation], + [log, navigation], ); - const onLoadError = useCallback(() => { - ToastUtils.showErrorToast( - 'Failed to load deposit page', - 'Check your connection and try again.', - ); - navigation.goBack(); - }, [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 ( @@ -116,24 +235,32 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos } return ( - + + + {DEBUG ? : null} + ); } export default OmenDepositWebView; const styles = StyleSheet.create({ + container: { + flex: 1, + }, webview: { flex: 1, }, 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/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}; +} From a8bf4f4e2d522be86e3095db61e5865e9c81f6e5 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 11:56:57 +0200 Subject: [PATCH 4/9] feat(omen): seamless dark deposit webview (no white flash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open BX with ?theme=dark, paint the WebView + a dark loading cover in the Omen bg until onLoadEnd (kills the white flash on open), wrap in a dark SafeAreaView, and hide the redundant native header so BX's own "Add money" chrome is the only one — the deposit reads as part of the app. Co-Authored-By: Claude Opus 4.8 --- .../src/navigators/RootStackNavigator.tsx | 8 +++--- .../src/screens/Omen/OmenDepositWebView.tsx | 27 ++++++++++++++++--- .../src/screens/Omen/depositUrl.ts | 2 ++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx index 767d9d2a..b5eb0297 100644 --- a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx +++ b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx @@ -89,10 +89,10 @@ export function RootStackNavigator() { name="OmenDepositWebView" component={OmenDepositWebView} options={{ - headerShown: true, - headerBackButtonDisplayMode: 'minimal', - title: 'Add money', - headerTintColor: Theme['fg-100'], + // BX renders its own "Add money" header (+ close) inside the webview, and the screen + // wraps it in a dark SafeAreaView — so the native header would just be a redundant + // light bar. Hide it for a seamless "inside the app" surface. + headerShown: false, }} /> diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index 807ff4a9..55bcbcb0 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useState} from 'react'; -import {Linking, StyleSheet, View} from 'react-native'; +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, @@ -15,6 +16,7 @@ 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. @@ -116,6 +118,8 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos // 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); const openWallet = useCallback( async (target: string) => { @@ -235,11 +239,12 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos } return ( - + setIsPageLoading(false)} /> + {isPageLoading ? ( + + + + ) : null} {DEBUG ? : null} - + ); } @@ -260,8 +271,16 @@ 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/depositUrl.ts b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts index 8a64f74d..93f21f26 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts +++ b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts @@ -24,6 +24,8 @@ export function buildOmenDepositUrl(opts: {amount?: number} = {}): string { 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)); } From 623fd733323bd493b20c9c5515d46079b8feaa25 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 12:22:42 +0200 Subject: [PATCH 5/9] feat(omen): demo deposit address + auto-return home on success Deposit to 0x13302Eb0aD9Af2F847119dC4Ac632fFe196d0B0f, and after a successful deposit show the confirmation briefly then close the webview and return to the Omen home (now showing the credited balance + new activity row). Co-Authored-By: Claude Opus 4.8 --- .../src/screens/Omen/OmenDepositWebView.tsx | 13 ++++++++++++- dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index 55bcbcb0..ba0b877c 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +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'; @@ -121,6 +121,17 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos // 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); diff --git a/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts index 93f21f26..43d29594 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts +++ b/dapps/appkit-wagmi/src/screens/Omen/depositUrl.ts @@ -7,8 +7,8 @@ import {getMetadata} from '@/utils/misc'; const BX_DEPOSIT_URL = 'https://tomiir-bx-deposit-flow-poc-wc-pay-buyer-experience-dev.walletconnect-v1-bridge.workers.dev/deposit'; -/** Omen's mock treasury (deposit destination). Burn address for the POC — no real funds implied. */ -const OMEN_TREASURY = '0x000000000000000000000000000000000000dEaD'; +/** Omen's deposit destination (demo treasury). */ +const OMEN_TREASURY = '0x13302Eb0aD9Af2F847119dC4Ac632fFe196d0B0f'; /** * Build the deposit URL for the in-app WebView. Mirrors PasteUrlButton.buildPayUrl: From d2a11b861b035682e0089c7f611269a60781e031 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 13:42:09 +0200 Subject: [PATCH 6/9] feat(gooddeposit): in-app GoodWallet native-deposit screen Intercept the gooddeposit:// handoff from the BX deposit page in OmenDepositWebView and open a full-screen "GoodWallet" modal (its own light skin + brand, slid up so it reads as a separate wallet). The screen lists mock token holdings, lets you pick a token + amount, shows the requesting app + destination, and on Confirm mock-sends, credits OmenStore, and returns to Omen. Co-Authored-By: Claude Opus 4.8 --- .../src/navigators/RootStackNavigator.tsx | 12 + .../src/screens/GoodWallet/GoodWalletLogo.tsx | 48 +++ .../src/screens/GoodWallet/index.tsx | 391 ++++++++++++++++++ .../src/screens/GoodWallet/theme.ts | 18 + .../src/screens/GoodWallet/tokens.ts | 26 ++ .../src/screens/Omen/OmenDepositWebView.tsx | 36 +- dapps/appkit-wagmi/src/utils/TypesUtil.ts | 1 + 7 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 dapps/appkit-wagmi/src/screens/GoodWallet/GoodWalletLogo.tsx create mode 100644 dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx create mode 100644 dapps/appkit-wagmi/src/screens/GoodWallet/theme.ts create mode 100644 dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts diff --git a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx index b5eb0297..3435bffc 100644 --- a/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx +++ b/dapps/appkit-wagmi/src/navigators/RootStackNavigator.tsx @@ -11,6 +11,7 @@ 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(); @@ -95,6 +96,17 @@ export function RootStackNavigator() { headerShown: false, }} /> + ); } 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..06316b46 --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx @@ -0,0 +1,391 @@ +import React, {useMemo, useState} from 'react'; +import { + ActivityIndicator, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import {SafeAreaView} 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 {TOKENS, Token, 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')}`; +} + +/** + * GoodWallet — a mock wallet's native "deposit intent" screen. Reached when the BX deposit page + * hands off via `gooddeposit://` (intercepted in OmenDepositWebView) and presented as a full-screen + * modal so it reads as a separate wallet. Lists the tokens you hold, lets you pick one + set the + * amount, shows the destination + requesting app, and on Confirm "sends" and returns to Omen with + * the balance credited. All mocked — no real tx. + */ +function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepositConfirm'>) { + const {to, app, amount: amountParam} = route.params; + const appName = app || 'the app'; + + const [selected, setSelected] = useState(TOKENS[0]); + 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 <= selected.balance; + const credited = useMemo(() => usdValue(selected, amount), [selected, amount]); + + function handleConfirm() { + if (!isValid) { + return; + } + setPhase('sending'); + // Mock the on-chain send: brief pending, then credit Omen and bounce back to it. + 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)} {selected.symbol} to {appName} + + + ) : ( + <> + + + + {fmtUsd(credited)} sent + Returning to {appName}… + + )} + + + ); + } + + return ( + + + + navigation.goBack()} hitSlop={hitSlop}> + Cancel + + + + + + Deposit request + {appName} + {to ? To {shortAddr(to)} : null} + + + Pay with + + {TOKENS.map(token => { + const isSel = token.symbol === selected.symbol; + return ( + setSelected(token)} + style={[styles.tokenRow, isSel && styles.tokenRowSelected]}> + + {token.symbol.slice(0, 1)} + + + {token.name} + + {fmtToken(token.balance)} {token.symbol} + + + + {isSel ? : null} + + + ); + })} + + + Amount + + setAmountStr(t.replace(/[^0-9.]/g, ''))} + keyboardType="decimal-pad" + placeholder="0" + placeholderTextColor={C.textMuted} + style={styles.amountInput} + /> + {selected.symbol} + setAmountStr(String(selected.balance))} + style={styles.maxBtn}> + Max + + + + {amount > 0 ? `≈ ${fmtUsd(credited)}` : `Balance ${fmtToken(selected.balance)} ${selected.symbol}`} + + + + + + Confirm deposit + + + + ); +} + +export default GoodDepositConfirm; + +const hitSlop = {top: 10, bottom: 10, left: 10, right: 10}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + backgroundColor: C.bg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 20, + paddingVertical: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: C.border, + backgroundColor: C.surface, + }, + cancel: { + color: C.textSecondary, + fontSize: 15, + fontWeight: '500', + }, + content: { + padding: 20, + gap: 10, + }, + requestCard: { + backgroundColor: C.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: C.border, + padding: 16, + gap: 2, + marginBottom: 8, + }, + requestLabel: { + color: C.textMuted, + fontSize: 12, + fontWeight: '600', + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + requestApp: { + color: C.textPrimary, + fontSize: 20, + fontWeight: '700', + }, + requestAddr: { + color: C.textSecondary, + fontSize: 13, + }, + sectionLabel: { + color: C.textSecondary, + fontSize: 13, + fontWeight: '600', + marginTop: 6, + }, + tokenList: { + backgroundColor: C.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: C.border, + overflow: 'hidden', + }, + tokenRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + paddingHorizontal: 14, + paddingVertical: 13, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: C.border, + }, + tokenRowSelected: { + backgroundColor: C.accentTint, + }, + tokenBadge: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + }, + tokenBadgeText: { + color: '#FFFFFF', + fontSize: 15, + fontWeight: '700', + }, + tokenText: { + flex: 1, + }, + tokenName: { + color: C.textPrimary, + fontSize: 15, + fontWeight: '600', + }, + tokenBal: { + color: C.textSecondary, + fontSize: 13, + }, + 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, + }, + amountRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + backgroundColor: C.surface, + borderRadius: 16, + borderWidth: 1, + borderColor: C.border, + paddingHorizontal: 16, + paddingVertical: 12, + }, + amountInput: { + flex: 1, + color: C.textPrimary, + fontSize: 28, + fontWeight: '700', + padding: 0, + }, + amountSymbol: { + color: C.textSecondary, + fontSize: 16, + fontWeight: '600', + }, + maxBtn: { + backgroundColor: C.accentTint, + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 6, + }, + maxText: { + color: C.accent, + fontSize: 13, + fontWeight: '700', + }, + amountHint: { + color: C.textMuted, + fontSize: 13, + paddingLeft: 4, + }, + footer: { + padding: 20, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: C.border, + backgroundColor: C.surface, + }, + confirm: { + backgroundColor: C.accent, + borderRadius: 14, + paddingVertical: 16, + alignItems: 'center', + }, + confirmDisabled: { + backgroundColor: C.textMuted, + opacity: 0.5, + }, + confirmText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '700', + }, + centered: { + flex: 1, + 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..f296c33f --- /dev/null +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts @@ -0,0 +1,26 @@ +/** + * Mock token holdings for the GoodWallet deposit demo — a wallet that natively knows which tokens + * you hold. 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. + */ +export interface Token { + symbol: string; + name: string; + /** Mock balance held, in whole token units. */ + balance: number; + /** Mock USD price used to convert the deposit amount into a USD credit. */ + priceUsd: number; + /** Brand color for the token badge. */ + color: string; +} + +export const TOKENS: Token[] = [ + {symbol: 'USDC', name: 'USD Coin', balance: 1240.55, priceUsd: 1, color: '#2775CA'}, + {symbol: 'ETH', name: 'Ethereum', balance: 0.842, priceUsd: 3550, color: '#627EEA'}, + {symbol: 'USDT', name: 'Tether', balance: 620, priceUsd: 1, color: '#26A17B'}, + {symbol: 'DAI', name: 'Dai', balance: 305.2, priceUsd: 1, color: '#F5AC37'}, +]; + +export function usdValue(token: Token, amount: number): number { + return Math.round(token.priceUsd * amount * 100) / 100; +} diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index ba0b877c..a05bfdd9 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -106,6 +106,27 @@ function isWalletDeeplink(url: string): boolean { } } +// Custom scheme the BX deposit page uses to hand off to our in-app "GoodWallet" demo wallet. It +// never reaches the OS — we intercept it in the WebView and navigate to the GoodDepositConfirm +// modal, so the deposit stays inside this app (no context switch, no native scheme registration). +const GOOD_DEPOSIT_SCHEME = 'gooddeposit://'; + +function parseGoodDeposit(url: string): {to?: string; app?: string; amount?: number} { + try { + const u = new URL(url); + const amountRaw = u.searchParams.get('amount'); + const amount = amountRaw ? Number(amountRaw) : undefined; + + return { + to: u.searchParams.get('to') ?? undefined, + app: u.searchParams.get('app') ?? undefined, + amount: amount && amount > 0 ? amount : undefined, + }; + } catch { + return {}; + } +} + function formatUsd(n: number): string { return `$${n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/, ',')}`; } @@ -158,6 +179,12 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos // else loads in the WebView. const onShouldStartLoadWithRequest = useCallback( (request: ShouldStartLoadRequest): boolean => { + // In-app GoodWallet handoff — navigate to the confirm modal, never leave the app. + if (request.url.startsWith(GOOD_DEPOSIT_SCHEME)) { + log('nav', 'gooddeposit handoff →', request.url); + navigation.navigate('GoodDepositConfirm', parseGoodDeposit(request.url)); + return false; + } const wallet = isWalletDeeplink(request.url); log('nav', `shouldStart (wallet=${wallet}) →`, request.url); if (wallet) { @@ -166,7 +193,7 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos } return true; }, - [log, openWallet], + [log, openWallet, navigation], ); // window.open() targets route here instead. Only hand wallet deeplinks off to the OS so a page @@ -174,13 +201,18 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos const onOpenWindow = useCallback( (event: WebViewOpenWindowEvent) => { const {targetUrl} = event.nativeEvent; + if (targetUrl.startsWith(GOOD_DEPOSIT_SCHEME)) { + log('open', 'gooddeposit handoff (window.open) →', targetUrl); + navigation.navigate('GoodDepositConfirm', parseGoodDeposit(targetUrl)); + return; + } const wallet = isWalletDeeplink(targetUrl); log('open', `openWindow (wallet=${wallet}) →`, targetUrl); if (wallet) { openWallet(targetUrl); } }, - [log, openWallet], + [log, openWallet, navigation], ); const onMessage = useCallback( diff --git a/dapps/appkit-wagmi/src/utils/TypesUtil.ts b/dapps/appkit-wagmi/src/utils/TypesUtil.ts index 45b584f6..2a82ae56 100644 --- a/dapps/appkit-wagmi/src/utils/TypesUtil.ts +++ b/dapps/appkit-wagmi/src/utils/TypesUtil.ts @@ -73,6 +73,7 @@ export type RootStackParamList = { PayWebView: {url: string}; Omen: undefined; OmenDepositWebView: {url: string}; + GoodDepositConfirm: {to?: string; app?: string; amount?: number}; }; export type HomeTabParamList = { From c65a08473f22f32c30da06548f9be94533cd921f Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 14:09:18 +0200 Subject: [PATCH 7/9] fix(gooddeposit): open GoodWallet via DEPOSIT_OPEN_WALLET bridge message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle the DEPOSIT_OPEN_WALLET postMessage in onMessage → navigate to the GoodDepositConfirm modal. Drop the gooddeposit:// scheme interception: iOS WKWebView never surfaced the custom-scheme navigation, so the deeplink path was dead. The postMessage bridge is the reliable channel. Co-Authored-By: Claude Opus 4.8 --- .../src/screens/Omen/OmenDepositWebView.tsx | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index a05bfdd9..c66ba648 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -26,10 +26,13 @@ const DEBUG = true; // 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'; + 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; }; // A debug line forwarded from inside the BX page by the injected console/error hook below. Marked @@ -106,27 +109,6 @@ function isWalletDeeplink(url: string): boolean { } } -// Custom scheme the BX deposit page uses to hand off to our in-app "GoodWallet" demo wallet. It -// never reaches the OS — we intercept it in the WebView and navigate to the GoodDepositConfirm -// modal, so the deposit stays inside this app (no context switch, no native scheme registration). -const GOOD_DEPOSIT_SCHEME = 'gooddeposit://'; - -function parseGoodDeposit(url: string): {to?: string; app?: string; amount?: number} { - try { - const u = new URL(url); - const amountRaw = u.searchParams.get('amount'); - const amount = amountRaw ? Number(amountRaw) : undefined; - - return { - to: u.searchParams.get('to') ?? undefined, - app: u.searchParams.get('app') ?? undefined, - amount: amount && amount > 0 ? amount : undefined, - }; - } catch { - return {}; - } -} - function formatUsd(n: number): string { return `$${n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/, ',')}`; } @@ -179,12 +161,6 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos // else loads in the WebView. const onShouldStartLoadWithRequest = useCallback( (request: ShouldStartLoadRequest): boolean => { - // In-app GoodWallet handoff — navigate to the confirm modal, never leave the app. - if (request.url.startsWith(GOOD_DEPOSIT_SCHEME)) { - log('nav', 'gooddeposit handoff →', request.url); - navigation.navigate('GoodDepositConfirm', parseGoodDeposit(request.url)); - return false; - } const wallet = isWalletDeeplink(request.url); log('nav', `shouldStart (wallet=${wallet}) →`, request.url); if (wallet) { @@ -193,7 +169,7 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos } return true; }, - [log, openWallet, navigation], + [log, openWallet], ); // window.open() targets route here instead. Only hand wallet deeplinks off to the OS so a page @@ -201,18 +177,13 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos const onOpenWindow = useCallback( (event: WebViewOpenWindowEvent) => { const {targetUrl} = event.nativeEvent; - if (targetUrl.startsWith(GOOD_DEPOSIT_SCHEME)) { - log('open', 'gooddeposit handoff (window.open) →', targetUrl); - navigation.navigate('GoodDepositConfirm', parseGoodDeposit(targetUrl)); - return; - } const wallet = isWalletDeeplink(targetUrl); log('open', `openWindow (wallet=${wallet}) →`, targetUrl); if (wallet) { openWallet(targetUrl); } }, - [log, openWallet, navigation], + [log, openWallet], ); const onMessage = useCallback( @@ -237,6 +208,16 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos 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, + }); + return; + } + if (message.type === 'DEPOSIT_COMPLETE') { const credited = OmenStore.credit({ amount: message.amount, From 229263722366a263d1997916e79c7749a5381600 Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 16:22:43 +0200 Subject: [PATCH 8/9] feat(gooddeposit): preselect network/token from the deposit handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry network + token through the DEPOSIT_OPEN_WALLET bridge message into the GoodWallet screen: preselect the matching token (still changeable) and show the chosen network on the request card — the "ideal wallet: preselected but changeable" behavior. Co-Authored-By: Claude Opus 4.8 --- .../src/screens/GoodWallet/index.tsx | 23 +++++++++++-------- .../src/screens/Omen/OmenDepositWebView.tsx | 4 ++++ dapps/appkit-wagmi/src/utils/TypesUtil.ts | 8 ++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx index 06316b46..fd66afda 100644 --- a/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx @@ -46,10 +46,12 @@ function mockTxHash(): string { * the balance credited. All mocked — no real tx. */ function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepositConfirm'>) { - const {to, app, amount: amountParam} = route.params; + const {to, app, amount: amountParam, network, token} = route.params; const appName = app || 'the app'; - const [selected, setSelected] = useState(TOKENS[0]); + // Preselect the token the host pre-picked (still changeable), else the first holding. + const preselected = TOKENS.find(t => t.symbol === token) ?? TOKENS[0]; + const [selected, setSelected] = useState(preselected); const [amountStr, setAmountStr] = useState( amountParam && amountParam > 0 ? String(amountParam) : '', ); @@ -111,26 +113,27 @@ function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepos Deposit request {appName} + {network ? Network · {network} : null} {to ? To {shortAddr(to)} : null} Pay with - {TOKENS.map(token => { - const isSel = token.symbol === selected.symbol; + {TOKENS.map(tok => { + const isSel = tok.symbol === selected.symbol; return ( setSelected(token)} + onPress={() => setSelected(tok)} style={[styles.tokenRow, isSel && styles.tokenRowSelected]}> - - {token.symbol.slice(0, 1)} + + {tok.symbol.slice(0, 1)} - {token.name} + {tok.name} - {fmtToken(token.balance)} {token.symbol} + {fmtToken(tok.balance)} {tok.symbol} diff --git a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx index c66ba648..f694879a 100644 --- a/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx +++ b/dapps/appkit-wagmi/src/screens/Omen/OmenDepositWebView.tsx @@ -33,6 +33,8 @@ type DepositMessage = { // 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 @@ -214,6 +216,8 @@ function OmenDepositWebView({route, navigation}: RootStackScreenProps<'OmenDepos to: message.to, app: message.app, amount: message.amount, + network: message.network, + token: message.token, }); return; } diff --git a/dapps/appkit-wagmi/src/utils/TypesUtil.ts b/dapps/appkit-wagmi/src/utils/TypesUtil.ts index 2a82ae56..32fd92d2 100644 --- a/dapps/appkit-wagmi/src/utils/TypesUtil.ts +++ b/dapps/appkit-wagmi/src/utils/TypesUtil.ts @@ -73,7 +73,13 @@ export type RootStackParamList = { PayWebView: {url: string}; Omen: undefined; OmenDepositWebView: {url: string}; - GoodDepositConfirm: {to?: string; app?: string; amount?: number}; + GoodDepositConfirm: { + to?: string; + app?: string; + amount?: number; + network?: string; + token?: string; + }; }; export type HomeTabParamList = { From d8c3988e33493fb42b8a7011461cac4d473dc74f Mon Sep 17 00:00:00 2001 From: tomiir Date: Thu, 16 Jul 2026 16:51:08 +0200 Subject: [PATCH 9/9] feat(gooddeposit): polish GoodWallet + network/token picker + prices - Redesign the screen: app-avatar request card, horizontal network chips (with logos), a token list showing balances + USD value, a cleaner amount card and a "Deposit $X" CTA. - Add an in-wallet network + token selector (mirrors the BX concept); the BX pre-selection still preselects both, and they stay changeable. - Model mock holdings per network with realistic prices for non-stables (ETH 3550, POL 0.52; stables ~1) and show per-token USD value. - Fix the close control not being tappable on iOS: drop the modal SafeAreaView for useSafeAreaInsets + a padded Pressable close button below the status bar. Co-Authored-By: Claude Opus 4.8 --- .../src/screens/GoodWallet/index.tsx | 368 ++++++++++++------ .../src/screens/GoodWallet/tokens.ts | 73 +++- 2 files changed, 321 insertions(+), 120 deletions(-) diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx index fd66afda..bc3d413a 100644 --- a/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/index.tsx @@ -1,6 +1,8 @@ import React, {useMemo, useState} from 'react'; import { ActivityIndicator, + Image, + Pressable, ScrollView, StyleSheet, Text, @@ -8,14 +10,20 @@ import { TouchableOpacity, View, } from 'react-native'; -import {SafeAreaView} from 'react-native-safe-area-context'; +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 {TOKENS, Token, usdValue} from './tokens'; +import { + WALLET_NETWORKS, + WalletNetwork, + WalletToken, + networkImageUrl, + usdValue, +} from './tokens'; type Phase = 'select' | 'sending' | 'success'; @@ -38,35 +46,62 @@ 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 via `gooddeposit://` (intercepted in OmenDepositWebView) and presented as a full-screen - * modal so it reads as a separate wallet. Lists the tokens you hold, lets you pick one + set the - * amount, shows the destination + requesting app, and on Confirm "sends" and returns to Omen with - * the balance credited. All mocked — no real tx. + * 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'; - // Preselect the token the host pre-picked (still changeable), else the first holding. - const preselected = TOKENS.find(t => t.symbol === token) ?? TOKENS[0]; - const [selected, setSelected] = useState(preselected); + 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 <= selected.balance; - const credited = useMemo(() => usdValue(selected, amount), [selected, amount]); + 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'); - // Mock the on-chain send: brief pending, then credit Omen and bounce back to it. setTimeout(() => { OmenStore.credit({amount: credited, source: 'wallet', txHash: mockTxHash()}); setPhase('success'); @@ -76,68 +111,105 @@ function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepos if (phase !== 'select') { return ( - - - {phase === 'sending' ? ( - <> - - Sending… - - {fmtToken(amount)} {selected.symbol} to {appName} - - - ) : ( - <> - - - - {fmtUsd(credited)} sent - Returning to {appName}… - - )} - - + + {phase === 'sending' ? ( + <> + + Sending… + + {fmtToken(amount)} {selectedToken.symbol} on {selectedNetwork.name} + + + ) : ( + <> + + + + {fmtUsd(credited)} sent + Returning to {appName}… + + )} + ); } return ( - - + + - navigation.goBack()} hitSlop={hitSlop}> - Cancel - + navigation.goBack()} + hitSlop={12} + style={({pressed}) => [styles.closeBtn, pressed && styles.closeBtnPressed]}> + + - + - Deposit request - {appName} - {network ? Network · {network} : null} - {to ? To {shortAddr(to)} : null} + + {appName.slice(0, 1).toUpperCase()} + + + Deposit to + {appName} + {to ? {shortAddr(to)} : null} + - Pay with + Network + + {WALLET_NETWORKS.map(n => { + const isSel = n.name === selectedNetwork.name; + return ( + pickNetwork(n)} + style={[styles.chip, isSel && styles.chipSelected]}> + + {n.name} + + ); + })} + + + Asset - {TOKENS.map(tok => { - const isSel = tok.symbol === selected.symbol; + {selectedNetwork.tokens.map((tok, i) => { + const isSel = tok.symbol === selectedToken.symbol; return ( setSelected(tok)} - style={[styles.tokenRow, isSel && styles.tokenRowSelected]}> + onPress={() => { + 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} - - {isSel ? : null} + + {fmtUsd(tok.balance * tok.priceUsd)} + + {isSel ? : null} + ); @@ -145,44 +217,48 @@ function GoodDepositConfirm({route, navigation}: RootStackScreenProps<'GoodDepos Amount - - setAmountStr(t.replace(/[^0-9.]/g, ''))} - keyboardType="decimal-pad" - placeholder="0" - placeholderTextColor={C.textMuted} - style={styles.amountInput} - /> - {selected.symbol} - setAmountStr(String(selected.balance))} - style={styles.maxBtn}> - Max - + + + 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 ? `≈ ${fmtUsd(credited)}` : `Balance ${fmtToken(selected.balance)} ${selected.symbol}`} - - + - Confirm deposit + + {amount > 0 ? `Deposit ${fmtUsd(credited)}` : 'Enter an amount'} + - + ); } export default GoodDepositConfirm; -const hitSlop = {top: 10, bottom: 10, left: 10, right: 10}; - const styles = StyleSheet.create({ root: { flex: 1, @@ -192,29 +268,59 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingHorizontal: 20, - paddingVertical: 14, + paddingHorizontal: 16, + paddingBottom: 14, + backgroundColor: C.surface, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: C.border, - backgroundColor: C.surface, }, - cancel: { + closeBtn: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: C.surfaceAlt, + alignItems: 'center', + justifyContent: 'center', + }, + closeBtnPressed: { + opacity: 0.6, + }, + closeGlyph: { color: C.textSecondary, - fontSize: 15, - fontWeight: '500', + fontSize: 16, + fontWeight: '600', + lineHeight: 18, }, content: { - padding: 20, - gap: 10, + padding: 16, + gap: 8, }, requestCard: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, backgroundColor: C.surface, - borderRadius: 16, + borderRadius: 18, borderWidth: 1, borderColor: C.border, padding: 16, - gap: 2, - marginBottom: 8, + 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, @@ -225,22 +331,52 @@ const styles = StyleSheet.create({ }, requestApp: { color: C.textPrimary, - fontSize: 20, + fontSize: 18, fontWeight: '700', }, requestAddr: { color: C.textSecondary, fontSize: 13, + marginTop: 1, }, sectionLabel: { color: C.textSecondary, fontSize: 13, fontWeight: '600', - marginTop: 6, + 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: 16, + borderRadius: 18, borderWidth: 1, borderColor: C.border, overflow: 'hidden', @@ -251,22 +387,24 @@ const styles = StyleSheet.create({ gap: 12, paddingHorizontal: 14, paddingVertical: 13, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: C.border, + }, + tokenRowDivider: { + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: C.border, }, tokenRowSelected: { backgroundColor: C.accentTint, }, tokenBadge: { - width: 36, - height: 36, - borderRadius: 18, + width: 38, + height: 38, + borderRadius: 19, alignItems: 'center', justifyContent: 'center', }, tokenBadgeText: { color: '#FFFFFF', - fontSize: 15, + fontSize: 16, fontWeight: '700', }, tokenText: { @@ -277,10 +415,20 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: '600', }, - tokenBal: { + 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, @@ -299,28 +447,31 @@ const styles = StyleSheet.create({ borderRadius: 5, backgroundColor: C.accent, }, - amountRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, + amountCard: { backgroundColor: C.surface, - borderRadius: 16, + borderRadius: 18, borderWidth: 1, borderColor: C.border, paddingHorizontal: 16, - paddingVertical: 12, + paddingVertical: 14, + gap: 6, + }, + amountRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 10, }, amountInput: { flex: 1, color: C.textPrimary, - fontSize: 28, + fontSize: 30, fontWeight: '700', padding: 0, }, amountSymbol: { color: C.textSecondary, fontSize: 16, - fontWeight: '600', + fontWeight: '700', }, maxBtn: { backgroundColor: C.accentTint, @@ -336,23 +487,23 @@ const styles = StyleSheet.create({ amountHint: { color: C.textMuted, fontSize: 13, - paddingLeft: 4, }, footer: { - padding: 20, + paddingHorizontal: 16, + paddingTop: 12, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: C.border, backgroundColor: C.surface, }, confirm: { backgroundColor: C.accent, - borderRadius: 14, - paddingVertical: 16, + borderRadius: 16, + paddingVertical: 17, alignItems: 'center', }, confirmDisabled: { backgroundColor: C.textMuted, - opacity: 0.5, + opacity: 0.4, }, confirmText: { color: '#FFFFFF', @@ -360,7 +511,6 @@ const styles = StyleSheet.create({ fontWeight: '700', }, centered: { - flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, diff --git a/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts b/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts index f296c33f..3d7eb902 100644 --- a/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts +++ b/dapps/appkit-wagmi/src/screens/GoodWallet/tokens.ts @@ -1,26 +1,77 @@ /** - * Mock token holdings for the GoodWallet deposit demo — a wallet that natively knows which tokens - * you hold. 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. + * 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 Token { +export interface WalletToken { symbol: string; name: string; /** Mock balance held, in whole token units. */ balance: number; - /** Mock USD price used to convert the deposit amount into a USD credit. */ + /** Mock USD price used to convert amounts to a USD credit. */ priceUsd: number; /** Brand color for the token badge. */ color: string; } -export const TOKENS: Token[] = [ - {symbol: 'USDC', name: 'USD Coin', balance: 1240.55, priceUsd: 1, color: '#2775CA'}, - {symbol: 'ETH', name: 'Ethereum', balance: 0.842, priceUsd: 3550, color: '#627EEA'}, - {symbol: 'USDT', name: 'Tether', balance: 620, priceUsd: 1, color: '#26A17B'}, - {symbol: 'DAI', name: 'Dai', balance: 305.2, priceUsd: 1, color: '#F5AC37'}, +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: Token, amount: number): number { +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`; +}