diff --git a/.github/workflows/release-android-base.yaml b/.github/workflows/release-android-base.yaml index 41bf64e2..f0f6b58b 100644 --- a/.github/workflows/release-android-base.yaml +++ b/.github/workflows/release-android-base.yaml @@ -57,6 +57,8 @@ on: relay-url: description: 'Relay URL' required: false + pay-api-key: + required: false sentry-dsn: required: false sentry-file: @@ -114,7 +116,7 @@ jobs: echo "$ENV_FILE_CONTENT" > "$ENV_FILE_PATH" else touch ${{ inputs.root-path }}/.env.${{ inputs.release-type }} - echo -e "ENV_PROJECT_ID=${{ secrets.project-id }}\nENV_RELAY_URL=${{ secrets.relay-url }}\nENV_SENTRY_DSN=${{ secrets.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} + echo -e "ENV_PROJECT_ID=${{ secrets.project-id }}\nENV_RELAY_URL=${{ secrets.relay-url }}\nENV_SENTRY_DSN=${{ secrets.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}\nENV_PAY_API_KEY=${{ secrets.pay-api-key }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} fi - name: Add Sentry file diff --git a/.github/workflows/release-dapp-ios-internal.yaml b/.github/workflows/release-dapp-ios-internal.yaml index 866f07df..102ba7c2 100644 --- a/.github/workflows/release-dapp-ios-internal.yaml +++ b/.github/workflows/release-dapp-ios-internal.yaml @@ -31,6 +31,7 @@ jobs: relay-url: ${{ secrets.ENV_RELAY_URL }} sentry-dsn: ${{ secrets.W3M_WAGMI_SENTRY_DSN }} sentry-file: ${{ secrets.W3M_WAGMI_SENTRY_FILE }} + pay-api-key: ${{ secrets.ENV_PAY_API_KEY }} apple-username: ${{ secrets.APPLE_USERNAME }} apple-key-id: ${{ secrets.APPLE_KEY_ID }} apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }} diff --git a/.github/workflows/release-ios-base.yaml b/.github/workflows/release-ios-base.yaml index 3edbf5a5..abca1ea0 100644 --- a/.github/workflows/release-ios-base.yaml +++ b/.github/workflows/release-ios-base.yaml @@ -71,6 +71,8 @@ on: required: false sentry-dsn: required: false + pay-api-key: + required: false sentry-file: required: true apple-username: @@ -174,7 +176,7 @@ jobs: echo "$ENV_FILE_CONTENT" > "$ENV_FILE_PATH" else touch ${{ inputs.root-path }}/.env.${{ inputs.release-type }} - echo -e "ENV_PROJECT_ID=${{ secrets.project-id }}\nENV_RELAY_URL=${{ secrets.relay-url }}\nENV_SENTRY_DSN=${{ secrets.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} + echo -e "ENV_PROJECT_ID=${{ secrets.project-id }}\nENV_RELAY_URL=${{ secrets.relay-url }}\nENV_SENTRY_DSN=${{ secrets.sentry-dsn }}\nENV_SENTRY_TAG=${{ inputs.release-type }}\nENV_PAY_API_KEY=${{ secrets.pay-api-key }}" >> ${{ inputs.root-path }}/.env.${{ inputs.release-type }} fi # Create sentry file diff --git a/.github/workflows/release-wallet-android-internal.yaml b/.github/workflows/release-wallet-android-internal.yaml index d6b4952a..f3567975 100644 --- a/.github/workflows/release-wallet-android-internal.yaml +++ b/.github/workflows/release-wallet-android-internal.yaml @@ -21,6 +21,7 @@ jobs: relay-url: ${{ secrets.ENV_RELAY_URL }} sentry-dsn: ${{ secrets.W3W_SENTRY_DSN }} sentry-file: ${{ secrets.W3W_SENTRY_FILE }} + pay-api-key: ${{ secrets.ENV_PAY_API_KEY }} secrets-file: ${{ secrets.ANDROID_SECRETS_FILE }} firebase-app-id: ${{ secrets.W3W_ANDROID_INTERNAL_FIREBASE_APP_ID }} gsa-key: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }} diff --git a/wallets/rn_cli_wallet/.env.example b/wallets/rn_cli_wallet/.env.example index 95d6ec3a..d03bab7e 100644 --- a/wallets/rn_cli_wallet/.env.example +++ b/wallets/rn_cli_wallet/.env.example @@ -10,3 +10,4 @@ SENTRY_DISABLE_AUTO_UPLOAD=true # debug, internal, production ENV_SENTRY_TAG='debug' +ENV_PAY_API_KEY='' diff --git a/wallets/rn_cli_wallet/android/settings.gradle b/wallets/rn_cli_wallet/android/settings.gradle index ca54a56b..fe2fe455 100644 --- a/wallets/rn_cli_wallet/android/settings.gradle +++ b/wallets/rn_cli_wallet/android/settings.gradle @@ -1,6 +1,8 @@ pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } plugins { id("com.facebook.react.settings") } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } + + rootProject.name = 'RNWeb3Wallet' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/wallets/rn_cli_wallet/babel.config.js b/wallets/rn_cli_wallet/babel.config.js index 9836c78f..929d21c8 100644 --- a/wallets/rn_cli_wallet/babel.config.js +++ b/wallets/rn_cli_wallet/babel.config.js @@ -20,6 +20,7 @@ module.exports = { '@/utils': './src/utils', '@/provider': './src/provider', '@/store': './src/store', + '@/lib': './src/lib', }, }, ], diff --git a/wallets/rn_cli_wallet/declarations.d.ts b/wallets/rn_cli_wallet/declarations.d.ts index 4b7d4297..0e89b3f4 100644 --- a/wallets/rn_cli_wallet/declarations.d.ts +++ b/wallets/rn_cli_wallet/declarations.d.ts @@ -4,6 +4,7 @@ declare module '*.webp'; declare module 'react-native-config' { export interface NativeConfig { ENV_PROJECT_ID: string; + ENV_PAY_API_KEY: string; ENV_RELAY_URL: string; ENV_SENTRY_DSN: string; ENV_SENTRY_TAG: string; diff --git a/wallets/rn_cli_wallet/package.json b/wallets/rn_cli_wallet/package.json index 6e2aabc3..d4ca647f 100644 --- a/wallets/rn_cli_wallet/package.json +++ b/wallets/rn_cli_wallet/package.json @@ -37,7 +37,8 @@ "@ton/core": "0.62.0", "@ton/crypto": "3.3.0", "@ton/ton": "15.4.0", - "@walletconnect/react-native-compat": "2.23.0", + "@walletconnect/pay": "0.0.0-canary.1", + "@walletconnect/react-native-compat": "2.23.1-canary.1", "bip39": "3.1.0", "dayjs": "1.11.11", "ed25519-hd-key": "^1.3.0", diff --git a/wallets/rn_cli_wallet/src/components/CopyURIDialog.tsx b/wallets/rn_cli_wallet/src/components/CopyURIDialog.tsx index 89af643f..1fbb12ff 100644 --- a/wallets/rn_cli_wallet/src/components/CopyURIDialog.tsx +++ b/wallets/rn_cli_wallet/src/components/CopyURIDialog.tsx @@ -1,9 +1,9 @@ -import {useState} from 'react'; -import {Dimensions, StyleSheet, TextInput, View} from 'react-native'; +import { useState } from 'react'; +import { Dimensions, StyleSheet, TextInput, View } from 'react-native'; import Dialog from 'react-native-dialog'; -import {useTheme} from '@/hooks/useTheme'; -import {ConnectButton} from './ConnectButton'; +import { useTheme } from '@/hooks/useTheme'; +import { ConnectButton } from './ConnectButton'; interface copyURIDialogProps { visible: boolean; @@ -41,16 +41,17 @@ export function CopyURIDialog({ useNativeDriver contentStyle={[ styles.mainContainer, - {maxWidth: windowWidth * 0.9, backgroundColor: Theme['bg-175']}, - ]}> + { maxWidth: windowWidth * 0.9, backgroundColor: Theme['bg-175'] }, + ]} + > - - Enter a WalletConnect URI + + Paste URI or Payment Link - To get the URI press the copy to clipboard button from your dapp's - WalletConnect interface. + style={[styles.descriptionText, { color: Theme['fg-150'] }]} + > + Paste a WalletConnect URI or a WalletConnect Pay link to continue. @@ -66,7 +67,7 @@ export function CopyURIDialog({ ]} placeholderTextColor={Theme['fg-300']} onChangeText={setUri} - placeholder="wc://a13aef..." + placeholder="wc://... or https://pay.walletconnect.com/..." clearButtonMode="always" enablesReturnKeyAutomatically autoCapitalize="none" @@ -80,7 +81,7 @@ export function CopyURIDialog({ /> diff --git a/wallets/rn_cli_wallet/src/components/Modal.tsx b/wallets/rn_cli_wallet/src/components/Modal.tsx index fc2d568f..9cb56dca 100644 --- a/wallets/rn_cli_wallet/src/components/Modal.tsx +++ b/wallets/rn_cli_wallet/src/components/Modal.tsx @@ -1,6 +1,6 @@ -import {useSnapshot} from 'valtio'; -import {useCallback, useMemo} from 'react'; -import {StyleSheet, View} from 'react-native'; +import { useSnapshot } from 'valtio'; +import { useCallback, useMemo } from 'react'; +import { StyleSheet, View } from 'react-native'; import RNModal from 'react-native-modal'; import ModalStore from '@/store/ModalStore'; @@ -8,7 +8,7 @@ import SessionProposalModal from '@/modals/SessionProposalModal'; import SessionSignModal from '@/modals/SessionSignModal'; import SessionSendTransactionModal from '@/modals/SessionSendTransactionModal'; import SessionSignTypedDataModal from '@/modals/SessionSignTypedDataModal'; -import {LoadingModal} from '@/modals/LoadingModal'; +import { LoadingModal } from '@/modals/LoadingModal'; import SessionAuthenticateModal from '@/modals/SessionAuthenticateModal'; import SessionSignSuiPersonalMessageModal from '@/modals/SessionSuiSignPersonalMessageModal'; import SessionSignSuiTransactionModal from '@/modals/SessionSuiSignTransactionModal'; @@ -16,9 +16,11 @@ import SessionSignAndExecuteSuiTransactionModal from '@/modals/SessionSuiSignAnd import SessionTonSendMessageModal from '@/modals/SessionTonSendMessageModal'; import SessionTonSignDataModal from '@/modals/SessionTonSignDataModal'; import SessionSignTronModal from '@/modals/SessionSignTronModal'; +import PaymentOptionsModal from '@/modals/PaymentOptionsModal'; +import ImportWalletModal from '@/modals/ImportWalletModal'; export default function Modal() { - const {open, view} = useSnapshot(ModalStore.state); + const { open, view } = useSnapshot(ModalStore.state); // handle the modal being closed by click outside const onClose = useCallback(() => { if (open) { @@ -47,11 +49,15 @@ export default function Modal() { case 'SessionSuiSignAndExecuteTransactionModal': return ; case 'SessionTonSendMessageModal': - return + return ; case 'SessionTonSignDataModal': - return + return ; case 'SessionSignTronModal': - return + return ; + case 'PaymentOptionsModal': + return ; + case 'ImportWalletModal': + return ; default: return ; } @@ -67,7 +73,8 @@ export default function Modal() { onBackdropPress={onClose} onModalHide={onClose} style={styles.modal} - isVisible={open}> + isVisible={open} + > {componentView} ); diff --git a/wallets/rn_cli_wallet/src/hooks/useInitializePaySDK.ts b/wallets/rn_cli_wallet/src/hooks/useInitializePaySDK.ts new file mode 100644 index 00000000..eef3cf86 --- /dev/null +++ b/wallets/rn_cli_wallet/src/hooks/useInitializePaySDK.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; +import Config from 'react-native-config'; + +import PayStore from '@/store/PayStore'; + +const PAY_CONFIG = { + projectId: Config.ENV_PROJECT_ID || '', + apiKey: Config.ENV_PAY_API_KEY || '', + metadata: { + name: 'RN Web3Wallet', + bundleId: 'com.walletconnect.web3wallet.rnsample', + }, +}; + +export default function useInitializePaySDK() { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + if (initialized) { + return; + } + + PayStore.initialize(PAY_CONFIG); + setInitialized(PayStore.isAvailable()); + }, [initialized]); + + return initialized; +} diff --git a/wallets/rn_cli_wallet/src/lib/EIP155Lib.ts b/wallets/rn_cli_wallet/src/lib/EIP155Lib.ts index 45b00920..f5b2e884 100644 --- a/wallets/rn_cli_wallet/src/lib/EIP155Lib.ts +++ b/wallets/rn_cli_wallet/src/lib/EIP155Lib.ts @@ -1,10 +1,11 @@ -import {providers, Wallet} from 'ethers'; +import { providers, Wallet } from 'ethers'; /** * Types */ interface IInitArgs { mnemonic?: string; + privateKey?: string; } /** @@ -17,16 +18,30 @@ export default class EIP155Lib { this.wallet = wallet; } - static init({mnemonic}: IInitArgs) { - const wallet = mnemonic - ? Wallet.fromMnemonic(mnemonic) - : Wallet.createRandom(); + static init({ mnemonic, privateKey }: IInitArgs) { + let wallet: Wallet; + + if (privateKey) { + wallet = new Wallet(privateKey); + } else if (mnemonic) { + wallet = Wallet.fromMnemonic(mnemonic); + } else { + wallet = Wallet.createRandom(); + } return new EIP155Lib(wallet); } getMnemonic() { - return this.wallet.mnemonic.phrase; + return this.wallet.mnemonic?.phrase ?? ''; + } + + getPrivateKey() { + return this.wallet.privateKey; + } + + hasMnemonic() { + return !!this.wallet.mnemonic?.phrase; } getAddress() { diff --git a/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx new file mode 100644 index 00000000..4d292b4e --- /dev/null +++ b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx @@ -0,0 +1,147 @@ +import { useState } from 'react'; +import { + StyleSheet, + View, + TextInput, + TouchableOpacity, + Alert, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { Text } from '@reown/appkit-ui-react-native'; + +import { useTheme } from '@/hooks/useTheme'; +import ModalStore from '@/store/ModalStore'; +import { loadEIP155Wallet } from '@/utils/EIP155WalletUtil'; + +export default function ImportWalletModal() { + const Theme = useTheme(); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleImport = async () => { + if (!input.trim()) { + Alert.alert('Error', 'Please enter a mnemonic or private key'); + return; + } + + setIsLoading(true); + try { + const { address } = loadEIP155Wallet(input); + Alert.alert('Success', `Wallet imported!\n\nNew address: ${address}`); + ModalStore.close(); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : 'Invalid mnemonic or private key'; + Alert.alert('Error', message); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + Import EVM Wallet + + + + Enter a mnemonic phrase or private key to import an existing wallet. + + + + + + ModalStore.close()} + > + + Cancel + + + + + + {isLoading ? 'Importing...' : 'Import'} + + + + + + ); +} + +const styles = StyleSheet.create({ + keyboardView: { + width: '100%', + }, + container: { + width: '100%', + padding: 20, + borderTopLeftRadius: 34, + borderTopRightRadius: 34, + rowGap: 16, + }, + description: { + textAlign: 'center', + }, + input: { + borderWidth: 1, + borderRadius: 12, + padding: 16, + minHeight: 100, + textAlignVertical: 'top', + fontSize: 14, + }, + buttonContainer: { + flexDirection: 'row', + gap: 12, + }, + button: { + flex: 1, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + }, + cancelButton: {}, + importButton: { + backgroundColor: '#3396FF', + }, +}); diff --git a/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal.tsx b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal.tsx new file mode 100644 index 00000000..f0b06bcb --- /dev/null +++ b/wallets/rn_cli_wallet/src/modals/PaymentOptionsModal.tsx @@ -0,0 +1,842 @@ +import { useCallback, useState } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + Image, + ActivityIndicator, + TextInput, +} from 'react-native'; +import { useSnapshot } from 'valtio'; + +import ModalStore from '@/store/ModalStore'; +import { useTheme } from '@/hooks/useTheme'; +import { ActionButton } from '@/components/ActionButton'; +import PayStore from '@/store/PayStore'; +import { eip155Wallets } from '@/utils/EIP155WalletUtil'; +import SettingsStore from '@/store/SettingsStore'; +import type { + PaymentOptionsResponse, + PaymentOption, + CollectDataFieldResult, +} from '@walletconnect/pay'; + +// Format date input as user types (YYYY-MM-DD) +function formatDateInput(value: string): string { + // Remove any non-numeric characters except dashes + const cleaned = value.replace(/[^0-9]/g, ''); + + // Format as YYYY-MM-DD + if (cleaned.length <= 4) { + return cleaned; + } else if (cleaned.length <= 6) { + return `${cleaned.slice(0, 4)}-${cleaned.slice(4)}`; + } else { + return `${cleaned.slice(0, 4)}-${cleaned.slice(4, 6)}-${cleaned.slice( + 6, + 8, + )}`; + } +} + +// Validate date format (YYYY-MM-DD) and check if it's a valid past date +function isValidDateOfBirth(dateStr: string): boolean { + if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + return false; + } + + const [year, month, day] = dateStr.split('-').map(Number); + const date = new Date(year, month - 1, day); + const now = new Date(); + + // Check if date is valid and in the past (and reasonable - not before 1900) + return ( + date.getFullYear() === year && + date.getMonth() === month - 1 && + date.getDate() === day && + date < now && + year >= 1900 + ); +} + +// Format amount with decimals +function formatAmount( + value: string, + decimals: number, + minDecimals: number = 0, +): string { + const num = BigInt(value); + const divisor = BigInt(10 ** decimals); + const integerPart = num / divisor; + const fractionalPart = num % divisor; + + if (fractionalPart === BigInt(0)) { + if (minDecimals > 0) { + return `${integerPart}.${'0'.repeat(minDecimals)}`; + } + return integerPart.toString(); + } + + const fractionalStr = fractionalPart.toString().padStart(decimals, '0'); + // Remove trailing zeros, but keep at least minDecimals + let trimmedFractional = fractionalStr.replace(/0+$/, ''); + if (trimmedFractional.length < minDecimals) { + trimmedFractional = trimmedFractional.padEnd(minDecimals, '0'); + } + return `${integerPart}.${trimmedFractional}`; +} + +export default function PaymentOptionsModal() { + const Theme = useTheme(); + const { data } = useSnapshot(ModalStore.state); + const paymentData = data?.paymentOptions as + | PaymentOptionsResponse + | undefined; + const isLoading = + data?.loadingMessage !== undefined && !data?.errorMessage && !paymentData; + const errorMessage = data?.errorMessage; + + const [selectedOption, setSelectedOption] = useState( + null, + ); + const [paymentActions, setPaymentActions] = useState(null); + const [isLoadingActions, setIsLoadingActions] = useState(false); + const [isSigningPayment, setIsSigningPayment] = useState(false); + const [actionsError, setActionsError] = useState(null); + const [collectedData, setCollectedData] = useState>( + {}, + ); + const [collectDataCompleted, setCollectDataCompleted] = useState(false); + const [collectDataError, setCollectDataError] = useState(null); + + const onClose = useCallback(() => { + setSelectedOption(null); + setPaymentActions(null); + setActionsError(null); + setCollectedData({}); + setCollectDataCompleted(false); + setCollectDataError(null); + ModalStore.close(); + }, []); + + const onBack = useCallback(() => { + // From signing view, go back to payment options + if (selectedOption) { + setSelectedOption(null); + setPaymentActions(null); + setActionsError(null); + } + }, [selectedOption]); + + const updateCollectedField = useCallback( + (fieldId: string, value: string, fieldType?: string) => { + // Format date fields as user types + const formattedValue = + fieldType === 'date' ? formatDateInput(value) : value; + setCollectedData(prev => ({ ...prev, [fieldId]: formattedValue })); + setCollectDataError(null); + }, + [], + ); + + const fetchPaymentActions = useCallback( + async (option: PaymentOption) => { + const payClient = PayStore.getClient(); + if (!payClient || !paymentData) { + setActionsError('Pay SDK not initialized'); + return; + } + + setIsLoadingActions(true); + setActionsError(null); + + try { + console.log( + '[Pay] Getting required payment actions for option:', + option.id, + ); + const actions = await payClient.getRequiredPaymentActions({ + paymentId: paymentData.paymentId, + optionId: option.id, + }); + console.log('[Pay] Required actions:', actions); + setPaymentActions(actions); + } catch (error: any) { + console.error('[Pay] Error getting payment actions:', error); + setActionsError(error?.message || 'Failed to get payment actions'); + } finally { + setIsLoadingActions(false); + } + }, + [paymentData], + ); + + const onSaveCollectData = useCallback(() => { + if (!paymentData?.collectData) { + setCollectDataCompleted(true); + return; + } + + // Validate required fields + const missingFields = paymentData.collectData.fields + .filter(field => field.required && !collectedData[field.id]?.trim()) + .map(field => field.name); + + if (missingFields.length > 0) { + setCollectDataError(`Please fill in: ${missingFields.join(', ')}`); + return; + } + + // Validate date fields format + const invalidDateFields = paymentData.collectData.fields + .filter( + field => + field.fieldType === 'date' && + collectedData[field.id]?.trim() && + !isValidDateOfBirth(collectedData[field.id]), + ) + .map(field => field.name); + + if (invalidDateFields.length > 0) { + setCollectDataError( + `Invalid date format for: ${invalidDateFields.join( + ', ', + )}. Use YYYY-MM-DD (e.g., 1990-01-15)`, + ); + return; + } + + setCollectDataError(null); + setCollectDataCompleted(true); + // Now show payment options - user will select one + }, [paymentData, collectedData]); + + const onSelectOption = useCallback( + (option: PaymentOption) => { + setSelectedOption(option); + // collectData is already handled, fetch actions immediately + fetchPaymentActions(option); + }, + [fetchPaymentActions], + ); + + const onApprovePayment = useCallback(async () => { + if ( + !paymentActions || + paymentActions.length === 0 || + !selectedOption || + !paymentData + ) { + return; + } + + // Validate required fields + if (paymentData.collectData) { + const missingFields = paymentData.collectData.fields + .filter(field => field.required && !collectedData[field.id]?.trim()) + .map(field => field.name); + + if (missingFields.length > 0) { + setActionsError( + `Please fill in required fields: ${missingFields.join(', ')}`, + ); + return; + } + } + + setIsSigningPayment(true); + setActionsError(null); + + try { + const payClient = PayStore.getClient(); + const wallet = eip155Wallets[SettingsStore.state.eip155Address]; + const signatures: string[] = []; + + for (const action of paymentActions) { + if (action.walletRpc) { + const { method, params } = action.walletRpc; + const parsedParams = JSON.parse(params); + + console.log('[Pay] Signing action:', method, parsedParams); + + if ( + method === 'eth_signTypedData_v4' || + method === 'eth_signTypedData_v3' || + method === 'eth_signTypedData' + ) { + const typedData = JSON.parse(parsedParams[1]); + const { domain, types, message: messageData } = typedData; + // Remove EIP712Domain from types as ethers handles it + delete types.EIP712Domain; + const signature = await wallet._signTypedData( + domain, + types, + messageData, + ); + console.log('[Pay] Signature:', signature); + signatures.push(signature); + } + } + } + + // Prepare collected data for submission + const collectedDataResults: CollectDataFieldResult[] = + paymentData.collectData + ? paymentData.collectData.fields + .filter(field => collectedData[field.id]?.trim()) + .map(field => ({ + id: field.id, + value: collectedData[field.id].trim(), + })) + : []; + + // Submit results back to Pay SDK + if (payClient) { + console.log('[Pay] Confirming payment with signatures:', signatures); + console.log('[Pay] Collected data:', collectedDataResults); + + const confirmResult = await payClient.confirmPayment({ + paymentId: paymentData.paymentId, + optionId: selectedOption.id, + signatures, + collectedData: + collectedDataResults.length > 0 ? collectedDataResults : undefined, + }); + + console.log('[Pay] Payment confirmed:', confirmResult); + } + + ModalStore.close(); + } catch (error: any) { + console.error('[Pay] Error signing payment:', error); + setActionsError(error?.message || 'Failed to sign payment'); + } finally { + setIsSigningPayment(false); + } + }, [paymentActions, selectedOption, paymentData, collectedData]); + + if (isLoading) { + return ( + + + + + {data?.loadingMessage || 'Loading payment options...'} + + + + ); + } + + if (errorMessage) { + return ( + + + Payment Error + + + + {errorMessage} + + + + + Close + + + + ); + } + + if (!paymentData) { + return null; + } + + const { info, options } = paymentData; + + // Determine if we need to show collect data form + const hasCollectData = + paymentData?.collectData && paymentData.collectData.fields.length > 0; + + // Show collect data form FIRST if it exists and not completed (before payment options) + const showCollectDataForm = hasCollectData && !collectDataCompleted; + // Show signing view when option is selected (collectData already completed or doesn't exist) + const showSigningView = + selectedOption && (!hasCollectData || collectDataCompleted); + + // Show Collect Data Form FIRST (before payment options) + if (showCollectDataForm) { + return ( + + + Additional Information + + + {/* Payment Info */} + {info?.merchant && ( + + {info.merchant.iconUrl && ( + + )} + + {info.merchant.name} + + + )} + + {info?.amount && ( + + + $ + {formatAmount(info.amount.value, info.amount.display.decimals, 2)} + + + )} + + {/* Collect Data Form */} + + {paymentData.collectData!.fields.map(field => ( + + + {field.name} + {field.required && ( + * + )} + + + updateCollectedField(field.id, value, field.fieldType) + } + placeholder={ + field.fieldType === 'date' + ? 'YYYY-MM-DD (e.g., 1990-01-15)' + : `Enter ${field.name}` + } + placeholderTextColor={Theme['fg-300']} + autoCapitalize="none" + keyboardType={ + field.fieldType === 'date' ? 'number-pad' : 'default' + } + maxLength={field.fieldType === 'date' ? 10 : undefined} + /> + + ))} + + + {/* Error */} + {collectDataError && ( + + + {collectDataError} + + + )} + + {/* Action Buttons */} + + + Continue to Payment + + + Cancel + + + + ); + } + + // Show Signing Confirmation when option is selected + if (showSigningView) { + return ( + + + Confirm Payment + + + {/* Selected Option Info */} + + {selectedOption.amount.display.iconUrl && ( + + )} + + {formatAmount( + selectedOption.amount.value, + selectedOption.amount.display.decimals, + 2, + )}{' '} + {selectedOption.amount.display.assetSymbol} + + + on {selectedOption.amount.display.networkName || 'Unknown Network'} + + + + {/* Loading Actions */} + {isLoadingActions && ( + + + + Preparing payment... + + + )} + + {/* Actions Error */} + {actionsError && ( + + + {actionsError} + + + )} + + {/* Actions Ready */} + {paymentActions && !isLoadingActions && ( + + + This will sign a transfer authorization for this payment. + + + )} + + {/* Action Buttons */} + + {paymentActions && !isLoadingActions && ( + + {isSigningPayment ? 'Signing...' : 'Approve & Sign'} + + )} + + Back + + + + ); + } + + return ( + + + Payment Request + + + {/* Merchant Info */} + {info?.merchant && ( + + {info.merchant.iconUrl && ( + + )} + + {info.merchant.name} + + + )} + + {/* Amount */} + {info?.amount && ( + + + ${formatAmount(info.amount.value, info.amount.display.decimals, 2)} + + + {info.amount.display.assetName} + + + )} + + {/* Payment Options */} + + Select Payment Method + + + + {options.map(option => ( + onSelectOption(option)} + > + + {option.amount.display.iconUrl && ( + + )} + + + {formatAmount( + option.amount.value, + option.amount.display.decimals, + 2, + )}{' '} + {option.amount.display.assetSymbol} + + + {option.amount.display.networkName || 'Unknown Network'} + + + + + ~{Math.round(option.etaS / 60)} min + + + ))} + + + + + Cancel + + + + ); +} + +const styles = StyleSheet.create({ + container: { + borderTopLeftRadius: 34, + borderTopRightRadius: 34, + paddingTop: 20, + paddingBottom: 20, + maxHeight: '80%', + }, + headerTitle: { + fontSize: 22, + fontWeight: '700', + textAlign: 'center', + marginBottom: 8, + }, + loadingContainer: { + padding: 40, + alignItems: 'center', + justifyContent: 'center', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + }, + errorContainer: { + padding: 20, + alignItems: 'center', + }, + errorText: { + fontSize: 14, + textAlign: 'center', + }, + merchantContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 12, + paddingHorizontal: 20, + }, + merchantIcon: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + }, + merchantName: { + fontSize: 18, + fontWeight: '600', + }, + amountContainer: { + alignItems: 'center', + paddingVertical: 16, + }, + amountValue: { + fontSize: 32, + fontWeight: '700', + }, + amountLabel: { + fontSize: 14, + marginTop: 4, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: 12, + }, + optionsContainer: { + paddingHorizontal: 20, + maxHeight: 300, + }, + optionCard: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: 16, + borderRadius: 16, + marginBottom: 8, + }, + optionInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + optionIcon: { + width: 32, + height: 32, + borderRadius: 16, + marginRight: 12, + }, + optionDetails: { + flex: 1, + }, + optionAmount: { + fontSize: 16, + fontWeight: '600', + }, + optionNetwork: { + fontSize: 12, + marginTop: 2, + }, + optionEta: { + fontSize: 12, + }, + footerContainer: { + paddingHorizontal: 20, + paddingTop: 16, + alignItems: 'center', + }, + closeButton: { + width: '100%', + height: 48, + borderRadius: 100, + }, + closeButtonText: { + fontWeight: '600', + fontSize: 16, + }, + confirmationContainer: { + alignItems: 'center', + paddingVertical: 24, + paddingHorizontal: 20, + }, + confirmIcon: { + width: 48, + height: 48, + borderRadius: 24, + marginBottom: 12, + }, + confirmAmount: { + fontSize: 28, + fontWeight: '700', + }, + confirmNetwork: { + fontSize: 14, + marginTop: 4, + }, + actionsContainer: { + paddingHorizontal: 20, + paddingVertical: 16, + }, + actionsLabel: { + fontSize: 14, + textAlign: 'center', + }, + confirmButtonsContainer: { + paddingHorizontal: 20, + paddingTop: 16, + gap: 8, + }, + approveButton: { + width: '100%', + height: 48, + borderRadius: 100, + }, + approveButtonText: { + fontWeight: '600', + fontSize: 16, + }, + collectDataContainer: { + paddingHorizontal: 20, + paddingVertical: 16, + }, + collectDataTitle: { + fontSize: 16, + fontWeight: '600', + marginBottom: 12, + }, + collectDataScrollView: { + flexShrink: 1, + }, + collectDataScrollContent: { + paddingHorizontal: 20, + paddingBottom: 16, + }, + fieldContainer: { + marginBottom: 16, + }, + fieldLabel: { + fontSize: 14, + marginBottom: 6, + }, + fieldInput: { + height: 44, + borderRadius: 12, + paddingHorizontal: 12, + borderWidth: 1, + fontSize: 16, + }, +}); diff --git a/wallets/rn_cli_wallet/src/screens/App.tsx b/wallets/rn_cli_wallet/src/screens/App.tsx index aa6e5bdd..1f96b06f 100644 --- a/wallets/rn_cli_wallet/src/screens/App.tsx +++ b/wallets/rn_cli_wallet/src/screens/App.tsx @@ -1,16 +1,17 @@ -import {useCallback, useEffect} from 'react'; +import { useCallback, useEffect } from 'react'; import Config from 'react-native-config'; -import {Linking, Platform, StatusBar, useColorScheme} from 'react-native'; -import {NavigationContainer} from '@react-navigation/native'; +import { Linking, Platform, StatusBar, useColorScheme } from 'react-native'; +import { NavigationContainer } from '@react-navigation/native'; import * as Sentry from '@sentry/react-native'; import BootSplash from 'react-native-bootsplash'; import Toast from 'react-native-toast-message'; -import {RELAYER_EVENTS} from '@walletconnect/core'; +import { RELAYER_EVENTS } from '@walletconnect/core'; -import {RootStackNavigator} from '@/navigators/RootStackNavigator'; +import { RootStackNavigator } from '@/navigators/RootStackNavigator'; import useInitializeWalletKit from '@/hooks/useInitializeWalletKit'; +import useInitializePaySDK from '@/hooks/useInitializePaySDK'; import useWalletKitEventsManager from '@/hooks/useWalletKitEventsManager'; -import {walletKit} from '@/utils/WalletKitUtil'; +import { walletKit } from '@/utils/WalletKitUtil'; import SettingsStore from '@/store/SettingsStore'; import ModalStore from '@/store/ModalStore'; @@ -46,9 +47,12 @@ const App = () => { // Step 2 - Once initialized, set up wallet connect event manager useWalletKitEventsManager(initialized); + // Step 3 - Initialize WalletConnect Pay SDK + useInitializePaySDK(); + useEffect(() => { if (initialized) { - BootSplash.hide({fade: true}); + BootSplash.hide({ fade: true }); walletKit.core.relayer.on(RELAYER_EVENTS.connect, () => { Toast.show({ @@ -76,10 +80,10 @@ const App = () => { const pair = useCallback(async (uri: string) => { try { - ModalStore.open('LoadingModal', {loadingMessage: 'Pairing...'}); + ModalStore.open('LoadingModal', { loadingMessage: 'Pairing...' }); await SettingsStore.state.initPromise; - await walletKit.pair({uri}); + await walletKit.pair({ uri }); } catch (error: any) { ModalStore.open('LoadingModal', { errorMessage: error?.message || 'There was an error pairing', @@ -88,7 +92,7 @@ const App = () => { }, []); const deeplinkHandler = useCallback( - ({url}: {url: string}) => { + ({ url }: { url: string }) => { const isLinkMode = url.includes('wc_ev'); SettingsStore.setIsLinkModeRequest(isLinkMode); @@ -100,7 +104,9 @@ const App = () => { } else if (url.includes('wc:')) { pair(url); } else if (url.includes('wc?')) { - ModalStore.open('LoadingModal', {loadingMessage: 'Loading request...'}); + ModalStore.open('LoadingModal', { + loadingMessage: 'Loading request...', + }); } }, [pair], @@ -124,7 +130,7 @@ const App = () => { return; } - deeplinkHandler({url: initialUrl}); + deeplinkHandler({ url: initialUrl }); } const sub = Linking.addEventListener('url', deeplinkHandler); @@ -141,7 +147,7 @@ const App = () => { ); diff --git a/wallets/rn_cli_wallet/src/screens/Connections/index.tsx b/wallets/rn_cli_wallet/src/screens/Connections/index.tsx index 7a7726d6..0f6b435b 100644 --- a/wallets/rn_cli_wallet/src/screens/Connections/index.tsx +++ b/wallets/rn_cli_wallet/src/screens/Connections/index.tsx @@ -1,31 +1,91 @@ -import {useEffect, useState} from 'react'; +import { useEffect, useState, useCallback } from 'react'; -import {walletKit} from '@/utils/WalletKitUtil'; +import { walletKit } from '@/utils/WalletKitUtil'; import Sessions from '@/screens/Connections/components/Sessions'; import ActionButtons from '@/screens/Connections/components/ActionButtons'; -import {CopyURIDialog} from '@/components/CopyURIDialog'; -import {ConnectionsStackScreenProps} from '@/utils/TypesUtil'; +import { CopyURIDialog } from '@/components/CopyURIDialog'; +import { ConnectionsStackScreenProps } from '@/utils/TypesUtil'; import ModalStore from '@/store/ModalStore'; import SettingsStore from '@/store/SettingsStore'; +import PayStore from '@/store/PayStore'; type Props = ConnectionsStackScreenProps<'Connections'>; -export default function Connections({route}: Props) { +// Check if a URI is a WalletConnect Pay payment link +function isPaymentLink(uri: string): boolean { + return uri.includes('pay.walletconnect.com') && uri.includes('pid='); +} + +export default function Connections({ route }: Props) { const [copyDialogVisible, setCopyDialogVisible] = useState(false); - const onDialogConnect = (uri: string) => { - setCopyDialogVisible(false); - setTimeout(() => { - pair(uri); - }, 1000); - }; + const handlePaymentLink = useCallback(async (paymentLink: string) => { + const payClient = PayStore.getClient(); + console.log( + '[Pay] PayClient available:', + PayStore.isAvailable(), + payClient, + ); + if (!payClient) { + console.error('[Pay] PayClient not initialized'); + ModalStore.open('LoadingModal', { + errorMessage: 'Pay SDK not initialized. Please restart the app.', + }); + return; + } + + // Show loading modal + ModalStore.open('PaymentOptionsModal', { + loadingMessage: 'Fetching payment options...', + }); + + try { + // Get wallet accounts - Base only for testing + const eip155Address = SettingsStore.state.eip155Address; + const accounts = eip155Address ? [`eip155:8453:${eip155Address}`] : []; + + console.log('[Pay] Fetching payment options for:', paymentLink); + console.log('[Pay] Accounts:', accounts); + + const paymentOptions = await payClient.getPaymentOptions({ + paymentLink, + accounts, + includePaymentInfo: true, + }); + + console.log('[Pay] Payment options received:', paymentOptions); + + // Show payment options modal + ModalStore.open('PaymentOptionsModal', { paymentOptions }); + } catch (error: any) { + console.error('[Pay] Error fetching payment options:', error); + ModalStore.open('PaymentOptionsModal', { + errorMessage: error?.message || 'Failed to fetch payment options', + }); + } + }, []); + + const onDialogConnect = useCallback( + (uri: string) => { + setCopyDialogVisible(false); + setTimeout(() => { + // Check if it's a payment link + if (isPaymentLink(uri)) { + handlePaymentLink(uri); + } else { + pair(uri); + } + }, 500); + }, + [handlePaymentLink], + ); const onDialogCancel = () => { setCopyDialogVisible(false); }; async function pair(uri: string) { - ModalStore.open('LoadingModal', {loadingMessage: 'Pairing...'}); + ModalStore.open('LoadingModal', { loadingMessage: 'Pairing...' }); /** * Wait for settings walletKit to be initialized before calling pair @@ -34,7 +94,7 @@ export default function Connections({route}: Props) { try { setCopyDialogVisible(false); - await walletKit.pair({uri}); + await walletKit.pair({ uri }); } catch (error: any) { ModalStore.open('LoadingModal', { errorMessage: error?.message || 'There was an error pairing', @@ -45,9 +105,14 @@ export default function Connections({route}: Props) { useEffect(() => { // URI received from QR code scanner if (route.params?.uri) { - pair(route.params.uri); + const uri = route.params.uri; + if (isPaymentLink(uri)) { + handlePaymentLink(uri); + } else { + pair(uri); + } } - }, [route.params?.uri]); + }, [route.params?.uri, handlePaymentLink]); return ( <> diff --git a/wallets/rn_cli_wallet/src/screens/Settings/index.tsx b/wallets/rn_cli_wallet/src/screens/Settings/index.tsx index 069a9ed4..ca0f8efe 100644 --- a/wallets/rn_cli_wallet/src/screens/Settings/index.tsx +++ b/wallets/rn_cli_wallet/src/screens/Settings/index.tsx @@ -1,22 +1,24 @@ -import {useSnapshot} from 'valtio'; -import {useEffect, useState} from 'react'; -import {Text, View, Alert, ScrollView} from 'react-native'; +import { useSnapshot } from 'valtio'; +import { useEffect, useState } from 'react'; +import { Text, View, Alert, ScrollView } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard'; -import {getVersion, getBuildNumber} from 'react-native-device-info'; +import { getVersion, getBuildNumber } from 'react-native-device-info'; -import {eip155Wallets} from '@/utils/EIP155WalletUtil'; +import { eip155Wallets } from '@/utils/EIP155WalletUtil'; import SettingsStore from '@/store/SettingsStore'; -import {Card} from '@/components/Card'; -import {useTheme} from '@/hooks/useTheme'; +import ModalStore from '@/store/ModalStore'; +import { Card } from '@/components/Card'; +import { useTheme } from '@/hooks/useTheme'; import styles from './styles'; -import {SettingsStackScreenProps} from '@/utils/TypesUtil'; +import { SettingsStackScreenProps } from '@/utils/TypesUtil'; import { storage } from '@/utils/storage'; type Props = SettingsStackScreenProps<'Settings'>; -export default function Settings({navigation}: Props) { +export default function Settings({ navigation }: Props) { const Theme = useTheme(); - const {eip155Address, suiAddress, tonAddress, tronAddress, socketStatus} = useSnapshot(SettingsStore.state); + const { eip155Address, suiAddress, tonAddress, tronAddress, socketStatus } = + useSnapshot(SettingsStore.state); const [clientId, setClientId] = useState(''); useEffect(() => { @@ -38,8 +40,9 @@ export default function Settings({navigation}: Props) { - Account + contentInsetAdjustmentBehavior="automatic" + > + Account + ModalStore.open('ImportWalletModal', {})} + icon="chevronRight" + /> - Device + Device ; + +// Store client outside of valtio proxy since class instances don't work well with proxies +let clientInstance: PayClientInstance | null = null; + +const PayStore = { + initialize(options: PayClientOptions) { + // Check availability at call time, not import time + if (!PayClient.isAvailable()) { + console.warn('[PayStore] Native provider not available'); + return; + } + + try { + clientInstance = new PayClient(options); + console.log( + '[PayStore] PayClient initialized with projectId:', + clientInstance.projectId, + ); + } catch (error) { + console.error('[PayStore] Failed to initialize PayClient:', error); + clientInstance = null; + } + }, + + getClient(): PayClientInstance | null { + return clientInstance; + }, + + isAvailable(): boolean { + return clientInstance !== null; + }, +}; + +export default PayStore; diff --git a/wallets/rn_cli_wallet/src/utils/EIP155WalletUtil.ts b/wallets/rn_cli_wallet/src/utils/EIP155WalletUtil.ts index 0454751a..d3a55168 100644 --- a/wallets/rn_cli_wallet/src/utils/EIP155WalletUtil.ts +++ b/wallets/rn_cli_wallet/src/utils/EIP155WalletUtil.ts @@ -1,5 +1,6 @@ import EIP155Lib from '../lib/EIP155Lib'; import { storage } from './storage'; +import SettingsStore from '@/store/SettingsStore'; export let wallet1: EIP155Lib; export let wallet2: EIP155Lib; @@ -9,18 +10,16 @@ export let eip155Addresses: string[]; let address1: string; // let address2: string; -/** - * Utilities - */ export async function createOrRestoreEIP155Wallet() { const mnemonic1 = await storage.getItem('EIP155_MNEMONIC_1'); + const privateKey1 = await storage.getItem('EIP155_PRIVATE_KEY_1'); if (mnemonic1) { - wallet1 = EIP155Lib.init({mnemonic: mnemonic1}); + wallet1 = EIP155Lib.init({ mnemonic: mnemonic1 }); + } else if (privateKey1) { + wallet1 = EIP155Lib.init({ privateKey: privateKey1 }); } else { wallet1 = EIP155Lib.init({}); - - // Don't store mnemonic in local storage in a production project! storage.setItem('EIP155_MNEMONIC_1', wallet1.getMnemonic()); } @@ -36,3 +35,39 @@ export async function createOrRestoreEIP155Wallet() { eip155Addresses, }; } + +export function loadEIP155Wallet(input: string): { + address: string; + wallet: EIP155Lib; +} { + const trimmedInput = input.trim(); + const isPrivateKey = + trimmedInput.startsWith('0x') && trimmedInput.length === 66; + + const newWallet = isPrivateKey + ? EIP155Lib.init({ privateKey: trimmedInput }) + : EIP155Lib.init({ mnemonic: trimmedInput }); + + const newAddress = newWallet.getAddress(); + + // Update module-level exports + wallet1 = newWallet; + address1 = newAddress; + eip155Wallets = { [newAddress]: newWallet }; + eip155Addresses = [newAddress]; + + // Persist to storage + if (newWallet.hasMnemonic()) { + storage.setItem('EIP155_MNEMONIC_1', newWallet.getMnemonic()); + storage.removeItem('EIP155_PRIVATE_KEY_1'); + } else { + storage.setItem('EIP155_PRIVATE_KEY_1', newWallet.getPrivateKey()); + storage.removeItem('EIP155_MNEMONIC_1'); + } + + // Update store + SettingsStore.setEIP155Address(newAddress); + SettingsStore.setWallet(newWallet); + + return { address: newAddress, wallet: newWallet }; +} diff --git a/wallets/rn_cli_wallet/tsconfig.json b/wallets/rn_cli_wallet/tsconfig.json index ff86eb33..9d33f2e9 100644 --- a/wallets/rn_cli_wallet/tsconfig.json +++ b/wallets/rn_cli_wallet/tsconfig.json @@ -17,7 +17,8 @@ "@/modals/*": ["./src/modals/*"], "@/utils/*": ["./src/utils/*"], "@/provider/*": ["./src/provider/*"], - "@/store/*": ["./src/store/*"] + "@/store/*": ["./src/store/*"], + "@/lib/*": ["./src/lib/*"] } } } diff --git a/wallets/rn_cli_wallet/yarn.lock b/wallets/rn_cli_wallet/yarn.lock index 31340897..d2dd7563 100644 --- a/wallets/rn_cli_wallet/yarn.lock +++ b/wallets/rn_cli_wallet/yarn.lock @@ -4133,9 +4133,35 @@ __metadata: languageName: node linkType: hard -"@walletconnect/react-native-compat@npm:2.23.0": - version: 2.23.0 - resolution: "@walletconnect/react-native-compat@npm:2.23.0" +"@walletconnect/logger@npm:3.0.1": + version: 3.0.1 + resolution: "@walletconnect/logger@npm:3.0.1" + dependencies: + "@walletconnect/safe-json": ^1.0.2 + pino: 10.0.0 + checksum: c83e03fa76f40a6d81e3a7fb9f2c5c6f73ef011327bb3266b02ecac6d9b5f3972069a9a63d93d8df9dce1b90ae0ea310ae6be3e253967c3a1e79322890353fa6 + languageName: node + linkType: hard + +"@walletconnect/pay@npm:0.0.0-canary.1": + version: 0.0.0-canary.1 + resolution: "@walletconnect/pay@npm:0.0.0-canary.1" + dependencies: + "@walletconnect/logger": 3.0.1 + "@walletconnect/types": 2.23.1 + "@walletconnect/utils": 2.23.1 + peerDependencies: + react-native: ">=0.64.0" + peerDependenciesMeta: + react-native: + optional: true + checksum: 2b479b02f46212cffd175ebc076512461b92b7d72c342eb1f60692ea68accc14cfca85ef64d71ac3287cb5d9a1f5e75995f9402be34be29855552923ea7647a8 + languageName: node + linkType: hard + +"@walletconnect/react-native-compat@npm:2.23.1-canary.1": + version: 2.23.1-canary.1 + resolution: "@walletconnect/react-native-compat@npm:2.23.1-canary.1" dependencies: events: 3.3.0 fast-text-encoding: 1.0.6 @@ -4149,7 +4175,7 @@ __metadata: peerDependenciesMeta: expo-application: optional: true - checksum: 6dee8efd1be29e4c2f4d9ea58c9b24e6ffeabb6253b9a68f3608398add6e2d8a9adb7af87f13325b2b93c47e4cc53bf6cdcbe25afe3b86f1c6f3e14eb24a5489 + checksum: ed6bfd844e375d664b5c298660d50ee3067c2e095b5d3eb7c072abb1ed51ef29aa233db7e9657c1568420cba037502daa75e32c2b4649a9162e8416f490535c5 languageName: node linkType: hard @@ -4224,6 +4250,20 @@ __metadata: languageName: node linkType: hard +"@walletconnect/types@npm:2.23.1": + version: 2.23.1 + resolution: "@walletconnect/types@npm:2.23.1" + dependencies: + "@walletconnect/events": 1.0.1 + "@walletconnect/heartbeat": 1.2.2 + "@walletconnect/jsonrpc-types": 1.0.4 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 3.0.1 + events: 3.3.0 + checksum: 9ba16b197308b7f28add993381385dbe0851bd00d910aa1709fa16ce5ce4ffb865b0cf234b1fdf066f079ad57e4ec7c8c9082776ef2594c1c54ab3553a6b6e3c + languageName: node + linkType: hard + "@walletconnect/utils@npm:2.23.0": version: 2.23.0 resolution: "@walletconnect/utils@npm:2.23.0" @@ -4252,6 +4292,34 @@ __metadata: languageName: node linkType: hard +"@walletconnect/utils@npm:2.23.1": + version: 2.23.1 + resolution: "@walletconnect/utils@npm:2.23.1" + dependencies: + "@msgpack/msgpack": 3.1.2 + "@noble/ciphers": 1.3.0 + "@noble/curves": 1.9.7 + "@noble/hashes": 1.8.0 + "@scure/base": 1.2.6 + "@walletconnect/jsonrpc-utils": 1.0.8 + "@walletconnect/keyvaluestorage": 1.1.1 + "@walletconnect/logger": 3.0.1 + "@walletconnect/relay-api": 1.0.11 + "@walletconnect/relay-auth": 1.1.0 + "@walletconnect/safe-json": 1.0.2 + "@walletconnect/time": 1.0.2 + "@walletconnect/types": 2.23.1 + "@walletconnect/window-getters": 1.0.1 + "@walletconnect/window-metadata": 1.0.1 + blakejs: 1.2.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + ox: 0.9.3 + uint8arrays: 3.1.1 + checksum: 5dadeb03a3d8fae39f1150363dd3cab9501884e2a78cc43530aad1278e50f6c99afd46c0af9057018eb33e049251d3a2e352c69380b6a34a7b13e72d6739c2bd + languageName: node + linkType: hard + "@walletconnect/window-getters@npm:1.0.1, @walletconnect/window-getters@npm:^1.0.1": version: 1.0.1 resolution: "@walletconnect/window-getters@npm:1.0.1" @@ -4320,7 +4388,8 @@ __metadata: "@types/lodash.clonedeep": ^4.5.9 "@types/react": ^19.1.1 "@types/react-test-renderer": ^19.1.0 - "@walletconnect/react-native-compat": 2.23.0 + "@walletconnect/pay": 0.0.0-canary.1 + "@walletconnect/react-native-compat": 2.23.1-canary.1 babel-plugin-module-resolver: ^5.0.0 bip39: 3.1.0 dayjs: 1.11.11