From f5d9c0167274c900dc1dbac41a3381758ed3162d Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Mon, 30 Mar 2026 18:02:37 -0300 Subject: [PATCH 001/110] Fix: use the adjusted min for swap pills --- src/navigation/services/swap-crypto/components/BottomAmount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigation/services/swap-crypto/components/BottomAmount.tsx b/src/navigation/services/swap-crypto/components/BottomAmount.tsx index c153118cc4..7f22a34f42 100644 --- a/src/navigation/services/swap-crypto/components/BottomAmount.tsx +++ b/src/navigation/services/swap-crypto/components/BottomAmount.tsx @@ -489,7 +489,7 @@ const BottomAmount: React.FC = ({ const minAmount = _minAmount.toString(); if (primaryIsFiat && rate) { const minAmountFiat = ( - limitsOpts.limits.minAmount * rate + _minAmount * rate ).toFixed(2); curValRef.current = minAmountFiat; updateAmountRef.current(minAmountFiat, true); From 64fda609d98b6d449c95be0a975e1c96eed53419 Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Wed, 1 Apr 2026 17:05:00 -0300 Subject: [PATCH 002/110] Fix: use touchable and min size on bottom notif actions --- .../bottom-notification/BottomNotification.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/modal/bottom-notification/BottomNotification.tsx b/src/components/modal/bottom-notification/BottomNotification.tsx index 62025ce345..c92e450406 100644 --- a/src/components/modal/bottom-notification/BottomNotification.tsx +++ b/src/components/modal/bottom-notification/BottomNotification.tsx @@ -28,6 +28,7 @@ import {Theme, useNavigation, useTheme} from '@react-navigation/native'; import Markdown from 'react-native-markdown-display'; import {resetBottomNotificationModalConfig} from '../../../store/app/app.actions'; import {HEIGHT} from '../../styled/Containers'; +import {TouchableOpacity} from '../../base/TouchableOpacity'; export interface BottomNotificationConfig { type: 'success' | 'info' | 'warning' | 'error' | 'question' | 'wait'; @@ -182,13 +183,16 @@ const BottomNotification = React.memo(() => { }; return ( - - {text.toUpperCase()} - + + {text.toUpperCase()} + + ); }), [actions, dispatch, rootState], From 14c3a494439a287b9ffd7e6d0077f98bd2d5787f Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Fri, 3 Apr 2026 11:08:30 -0300 Subject: [PATCH 003/110] SendTo: Fix - Email validation regex causing stack overflow --- src/navigation/wallet/screens/send/SendTo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/navigation/wallet/screens/send/SendTo.tsx b/src/navigation/wallet/screens/send/SendTo.tsx index ded35fe656..d1ac27b38f 100644 --- a/src/navigation/wallet/screens/send/SendTo.tsx +++ b/src/navigation/wallet/screens/send/SendTo.tsx @@ -160,8 +160,8 @@ const isEmailAddress = (text: string) => { if (!text.includes('@')) { return false; } - const reg = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w\w+)+$/; - return reg.test(text); + + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text); }; export const BuildKeyAccountRow = ( From 7e6af6de443fc51edd083f9ff297674ec69bf358 Mon Sep 17 00:00:00 2001 From: Marty Alcala Date: Fri, 3 Apr 2026 12:54:51 -0400 Subject: [PATCH 004/110] fix: prevent unhandled rejected dangling otp toggle promise --- src/navigation/bitpay-id/screens/EnableTwoFactor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/navigation/bitpay-id/screens/EnableTwoFactor.tsx b/src/navigation/bitpay-id/screens/EnableTwoFactor.tsx index 6e21acae4e..ff44e77307 100644 --- a/src/navigation/bitpay-id/screens/EnableTwoFactor.tsx +++ b/src/navigation/bitpay-id/screens/EnableTwoFactor.tsx @@ -133,11 +133,11 @@ const EnableTwoFactor = ({navigation}: EnableTwoFactorProps) => { mode: 'onChange', }); const onSubmit = async (twoFactorCode: string) => { - toggleTwoFactor(twoFactorCode); + return toggleTwoFactor(twoFactorCode); }; const submitForm = async (code: string) => { Keyboard.dismiss(); - await onSubmit(code); + await onSubmit(code).catch(() => {}); }; const onFormSubmit = handleSubmit(async ({code}) => submitForm(code)); @@ -240,7 +240,7 @@ const EnableTwoFactor = ({navigation}: EnableTwoFactorProps) => { onPress={() => { navigator.navigate(WalletScreens.PAY_PRO_CONFIRM_TWO_FACTOR, { onSubmit: async (twoFactorCode: string) => { - toggleTwoFactor(twoFactorCode); + return toggleTwoFactor(twoFactorCode); }, twoFactorCodeLength: 6, }); From 3b35196f1f58487b120488ce7c0e4ecabd3c75a9 Mon Sep 17 00:00:00 2001 From: johnathan White Date: Mon, 30 Mar 2026 23:45:40 -0400 Subject: [PATCH 005/110] added build phase --- index.js | 2 -- ios/BitPayApp.xcodeproj/project.pbxproj | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 8f31c929db..ddfd3b48e1 100644 --- a/index.js +++ b/index.js @@ -46,8 +46,6 @@ Sentry.init({ dsn: SENTRY_DSN, enabled: !__DEV__, environment: __DEV__ ? 'development' : 'production', - release: APP_VERSION, - dist: GIT_COMMIT_HASH, sendDefaultPii: false, enableLogs: false, enableAutoNativeBreadcrumbs: false, diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj index 06539dbf2f..76300ea093 100644 --- a/ios/BitPayApp.xcodeproj/project.pbxproj +++ b/ios/BitPayApp.xcodeproj/project.pbxproj @@ -188,6 +188,7 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + EBB510F948BF432C893398ED /* Upload Debug Symbols to Sentry */, C3CF03A4A13B8DA5B5BB02BA /* [CP] Embed Pods Frameworks */, A4F9FF5781B0ADEE84C32EAD /* [CP] Copy Pods Resources */, ); @@ -265,7 +266,21 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n"; + }; + EBB510F948BF432C893398ED /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=\"$SRCROOT/../sentry.properties\"\n/bin/sh \"$SRCROOT/../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh\"\n"; }; 3C1070649A3322DC1391852F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; From 7cb32927a5d24f16cd10ae90c1ce400c8fcd264b Mon Sep 17 00:00:00 2001 From: johnathan White Date: Fri, 3 Apr 2026 15:57:52 -0400 Subject: [PATCH 006/110] bump 14.41.0 --- android/app/build.gradle | 4 ++-- ios/BitPayApp/Info.plist | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index c4325766f8..f4cf843dfe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -104,8 +104,8 @@ android { applicationId "com.bitpay.wallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 91212412 - versionName "14.40.5" + versionCode 91212414 + versionName "14.41.0" missingDimensionStrategy 'react-native-camera', 'mlkit' ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" diff --git a/ios/BitPayApp/Info.plist b/ios/BitPayApp/Info.plist index e9499fbbd6..6520043088 100644 --- a/ios/BitPayApp/Info.plist +++ b/ios/BitPayApp/Info.plist @@ -26,7 +26,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 14.40.5 + 14.41.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/package.json b/package.json index b133dbde1f..eb09662e04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitpay", - "version": "14.40.5", + "version": "14.41.0", "private": true, "engines": { "node": ">=20" From 886b0e90026aaf9baa14a9ff114e962bf9408b04 Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Wed, 18 Mar 2026 17:46:11 -0300 Subject: [PATCH 007/110] Create/Import: Enhancement - reduce duplicate wallet creation during create and import key --- src/Root.tsx | 2 + .../onboarding/screens/CreateKey.tsx | 77 ++++----- src/navigation/wallet/screens/KeyOverview.tsx | 1 + src/store/wallet/effects/create/create.ts | 150 ++++++++++++------ src/store/wallet/effects/import/import.ts | 99 ++++++++---- src/utils/helper-methods.ts | 108 +++++++++---- 6 files changed, 299 insertions(+), 138 deletions(-) diff --git a/src/Root.tsx b/src/Root.tsx index b1a45f5e5d..3412ac71ff 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -756,6 +756,7 @@ export default () => { accountsArray, key.methods as KeyMethods, getBaseEVMAccountCreationCoinsAndTokens(), + key.wallets, ); key.wallets.push(...wallets); dispatch(successAddWallet({key})); @@ -799,6 +800,7 @@ export default () => { [0], key.methods as KeyMethods, getBaseSVMAccountCreationCoinsAndTokens(), + key.wallets, ); key.wallets.push(...wallets); logManager.info( diff --git a/src/navigation/onboarding/screens/CreateKey.tsx b/src/navigation/onboarding/screens/CreateKey.tsx index 00d12e42a0..c467162ab8 100644 --- a/src/navigation/onboarding/screens/CreateKey.tsx +++ b/src/navigation/onboarding/screens/CreateKey.tsx @@ -1,5 +1,5 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'; -import React, {useEffect, useLayoutEffect, useRef} from 'react'; +import React, {useEffect, useLayoutEffect, useRef, useCallback} from 'react'; import {ScrollView} from 'react-native'; import {useAndroidBackHandler} from 'react-navigation-backhandler'; import styled from 'styled-components/native'; @@ -109,6 +109,45 @@ const CreateOrImportKey = ({ ); }; + const onCreateKeyPress = useCallback(async () => { + try { + const context = 'onboarding'; + + showOngoingProcess('CREATING_KEY'); + await sleep(500); + + const createdKey = await dispatch( + startCreateKey(getBaseKeyCreationCoinsAndTokens(), context), + ); + + dispatch(setHomeCarouselConfig({id: createdKey.id, show: true})); + hideOngoingProcess(); + + askForTrackingThenNavigate(() => { + dispatch(Analytics.track('Clicked Create New Key', {context})); + navigation.navigate('BackupKey', { + context, + key: createdKey, + }); + }); + } catch (err: any) { + const errstring = + err instanceof Error ? err.message : JSON.stringify(err); + + logManager.error(`Error creating key: ${errstring}`); + hideOngoingProcess(); + await sleep(500); + showErrorModal(errstring); + } + }, [ + dispatch, + navigation, + showOngoingProcess, + hideOngoingProcess, + askForTrackingThenNavigate, + showErrorModal, + ]); + useLayoutEffect(() => { navigation.setOptions({ gestureEnabled: false, @@ -147,41 +186,7 @@ const CreateOrImportKey = ({ diff --git a/src/navigation/wallet/screens/KeyOverview.tsx b/src/navigation/wallet/screens/KeyOverview.tsx index 1620e44ff5..d50c312eea 100644 --- a/src/navigation/wallet/screens/KeyOverview.tsx +++ b/src/navigation/wallet/screens/KeyOverview.tsx @@ -888,6 +888,7 @@ const KeyOverview = () => { accountsArray, key.methods as KeyMethods, getBaseEVMAccountCreationCoinsAndTokens(), + key.wallets, password, ); diff --git a/src/store/wallet/effects/create/create.ts b/src/store/wallet/effects/create/create.ts index 922649276d..3536b5a9ff 100644 --- a/src/store/wallet/effects/create/create.ts +++ b/src/store/wallet/effects/create/create.ts @@ -61,6 +61,35 @@ import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; import {Analytics} from '../../../analytics/analytics.effects'; +const runWithConcurrency = async ( + items: T[], + limit: number, + worker: (item: T, index: number) => Promise, +): Promise => { + const results: R[] = new Array(items.length); + let nextIndex = 0; + + const runners = Array.from( + {length: Math.min(limit, items.length)}, + async () => { + while (true) { + const currentIndex = nextIndex++; + if (currentIndex >= items.length) { + return; + } + results[currentIndex] = await worker(items[currentIndex], currentIndex); + } + }, + ); + + await Promise.all(runners); + return results; +}; + +const defer = (fn: () => void) => { + setTimeout(fn, 0); +}; + export interface CreateOptions { network?: Network; account?: number; @@ -121,6 +150,7 @@ export const startCreateKey = ); const key = buildKeyObj({key: _key, wallets}); + if (context !== 'onboarding') { dispatch(Analytics.track('Created Key')); } @@ -129,6 +159,7 @@ export const startCreateKey = key, }), ); + resolve(key); } catch (err) { reject(err); @@ -407,6 +438,7 @@ export const createMultipleWallets = defaultLanguage, }, } = getState(); + const {tokenOptionsByAddress} = tokenManager.getTokenOptions(); const tokenOpts = { @@ -414,10 +446,12 @@ export const createMultipleWallets = ...tokenOptionsByAddress, ...WALLET.customTokenOptionsByAddress, }; + const wallets: API[] = []; const tokens = currencies.filter(({isToken}) => isToken); const coins = currencies.filter(({isToken}) => !isToken); - for (const coin of coins) { + + const baseWalletResults = await runWithConcurrency(coins, 3, async coin => { try { const wallet = (await dispatch( createWallet({ @@ -431,51 +465,48 @@ export const createMultipleWallets = }), )) as Wallet; - const receiveAddress = (await dispatch( + const receiveAddress = (await dispatch( createWalletAddress({wallet, newAddress: true}), )) as string; + wallet.receiveAddress = receiveAddress; - wallets.push(wallet); - for (const token of tokens) { - if (token.chain === coin.chain) { - const tokenWallet = await dispatch( - createTokenWallet( - wallet, - token.currencyAbbreviation.toLowerCase(), - token.tokenAddress!, - tokenOpts, - ), - ); - if (tokenWallet) { - wallets.push(tokenWallet); - } - } - } + + const chainTokens = tokens.filter(token => token.chain === coin.chain); + + return {coin, wallet, chainTokens}; } catch (err) { const errMsg = err instanceof Error ? err.message : JSON.stringify(err); logManager.debug(`Error creating wallet - continue anyway: ${errMsg}`); + return null; } - } + }); - // build out app specific props - return wallets.map(wallet => { - // subscribe new wallet to push notifications - if (notificationsAccepted) { - dispatch(subscribePushNotifications(wallet, brazeEid!)); - } - // subscribe new wallet to email notifications - if ( - emailNotifications && - emailNotifications.accepted && - emailNotifications.email - ) { - const prefs = { - email: emailNotifications.email, - language: defaultLanguage, - unit: 'btc', // deprecated - }; - dispatch(subscribeEmailNotifications(wallet, prefs)); + const validBaseWallets = baseWalletResults.filter(Boolean) as Array<{ + coin: (typeof coins)[number]; + wallet: Wallet; + chainTokens: typeof tokens; + }>; + + for (const result of validBaseWallets) { + wallets.push(result.wallet); + + for (const token of result.chainTokens) { + const tokenWallet = await dispatch( + createTokenWallet( + result.wallet, + token.currencyAbbreviation.toLowerCase(), + token.tokenAddress!, + tokenOpts, + ), + ); + + if (tokenWallet) { + wallets.push(tokenWallet); + } } + } + + const result = wallets.map(wallet => { const {currencyAbbreviation, currencyName} = dispatch( mapAbbreviationAndName( wallet.credentials.coin, @@ -483,6 +514,7 @@ export const createMultipleWallets = wallet.credentials?.token?.address, ), ); + return merge( wallet, buildWalletObj( @@ -491,6 +523,29 @@ export const createMultipleWallets = ), ); }); + + defer(() => { + result.forEach(wallet => { + if (notificationsAccepted) { + dispatch(subscribePushNotifications(wallet, brazeEid!)); + } + + if ( + emailNotifications && + emailNotifications.accepted && + emailNotifications.email + ) { + const prefs = { + email: emailNotifications.email, + language: defaultLanguage, + unit: 'btc', + }; + dispatch(subscribeEmailNotifications(wallet, prefs)); + } + }); + }); + + return result; }; ///////////////////////////////////////////////////////////// @@ -527,16 +582,16 @@ const createWallet = ...options, }; - bwcClient.fromString( - key.createCredentials(password, { - coin, - chain, - network, - account, - n: 1, - m: 1, - }), - ); + const credentials = key.createCredentials(password, { + coin, + chain, + network, + account, + n: 1, + m: 1, + }); + + bwcClient.fromString(credentials); const name = isL2NoSideChainNetwork(chain) && coin === chain @@ -576,6 +631,7 @@ const createWallet = ), ); } + return resolve( dispatch( createWallet({ diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index e1e25acf72..a190f8363f 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -108,6 +108,27 @@ import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; import * as Sentry from '@sentry/react-native'; +const getMissingVmAccounts = ( + wallets: Wallet[], + accountsArray: number[], + expectedChainCoins: Array<{chain: string; currencyAbbreviation: string}>, +) => { + const existing = new Set( + wallets.map(wallet => { + const account = wallet.credentials.account || 0; + const chain = wallet.credentials.chain; + const coin = wallet.credentials.coin; + return `${account}:${chain}:${coin}`; + }), + ); + + return accountsArray.filter(account => { + return expectedChainCoins.some(({chain, currencyAbbreviation}) => { + return !existing.has(`${account}:${chain}:${currencyAbbreviation}`); + }); + }); +}; + const BWC = BwcProvider.getInstance(); const BwcConstants = BWC.getConstants(); @@ -869,24 +890,41 @@ export const startImportMnemonic = ...WALLET.customTokenOptionsByAddress, }; const {words, xPrivKey} = importData; + opts.words = normalizeMnemonic(words); opts.xPrivKey = xPrivKey; const data = await serverAssistedImport(opts); + // we need to ensure that each evm/svm account has all supported wallets attached. const vmWallets = getEvmGasWallets(data.wallets); const accountsArray = [ ...new Set(vmWallets.map(wallet => wallet.credentials.account)), ]; - const _wallets = await createWalletsForAccounts( - dispatch, + + const expectedVmWallets = getBaseVMAccountCreationCoinsAndTokens(); + + const missingAccounts = getMissingVmAccounts( + data.wallets, accountsArray, - data.key as KeyMethods, - getBaseVMAccountCreationCoinsAndTokens(), + expectedVmWallets, ); + + const _wallets = + missingAccounts.length > 0 + ? await createWalletsForAccounts( + dispatch, + accountsArray, + data.key as KeyMethods, + getBaseVMAccountCreationCoinsAndTokens(), + data.wallets, + ) + : []; + if (_wallets.length > 0) { data.wallets.push(..._wallets); } + // To Avoid Duplicate wallet import const { key: _key, @@ -904,10 +942,36 @@ export const startImportMnemonic = dispatch(deleteKey({keyId: opts.keyId})); } + const resultWallets = wallets.map(wallet => { + const {currencyAbbreviation, currencyName} = dispatch( + mapAbbreviationAndName( + wallet.credentials.coin, + wallet.credentials.chain, + wallet.credentials.token?.address, + ), + ); + return merge( + wallet, + buildWalletObj( + {...wallet.credentials, currencyAbbreviation, currencyName}, + tokenOptsByAddress, + ), + ); + }); const key = buildKeyObj({ key: _key, keyName, - wallets: wallets.map(wallet => { + wallets: resultWallets, + backupComplete: true, + }); + + dispatch( + successImport({ + key, + }), + ); + setTimeout(() => { + resultWallets.forEach(wallet => { // subscribe new wallet to push notifications if (notificationsAccepted) { dispatch(subscribePushNotifications(wallet, brazeEid!)); @@ -925,29 +989,8 @@ export const startImportMnemonic = }; dispatch(subscribeEmailNotifications(wallet, prefs)); } - const {currencyAbbreviation, currencyName} = dispatch( - mapAbbreviationAndName( - wallet.credentials.coin, - wallet.credentials.chain, - wallet.credentials.token?.address, - ), - ); - return merge( - wallet, - buildWalletObj( - {...wallet.credentials, currencyAbbreviation, currencyName}, - tokenOptsByAddress, - ), - ); - }), - backupComplete: true, - }); - - dispatch( - successImport({ - key, - }), - ); + }); + }, 0); resolve(key); } catch (e) { dispatch(failedImport()); diff --git a/src/utils/helper-methods.ts b/src/utils/helper-methods.ts index 81339036bf..d09a647f36 100644 --- a/src/utils/helper-methods.ts +++ b/src/utils/helper-methods.ts @@ -769,6 +769,39 @@ export const fixWalletAddresses = async ({ ); }; +const normalizeWalletKeyCoin = (coin: string) => { + return coin === 'pol' ? 'matic' : coin; +}; + +const buildExistingWalletKeySet = (wallets: Wallet[]) => { + return new Set( + wallets.map(wallet => { + const account = wallet.credentials.account || 0; + const chain = wallet.credentials.chain; + const coin = normalizeWalletKeyCoin(wallet.credentials.coin); + return `${account}:${chain}:${coin}`; + }), + ); +}; + +const getMissingCurrenciesForAccount = ( + account: number, + currencies: { + chain: string; + currencyAbbreviation: string; + isToken: boolean; + tokenAddress?: string; + }[], + existingWalletKeySet: Set, +) => { + return currencies.filter(currency => { + const chain = currency.chain; + const coin = normalizeWalletKeyCoin(currency.currencyAbbreviation); + const walletKey = `${account}:${chain}:${coin}`; + return !existingWalletKeySet.has(walletKey); + }); +}; + export const createWalletsForAccounts = async ( dispatch: any, accountsArray: number[], @@ -779,36 +812,57 @@ export const createWalletsForAccounts = async ( isToken: boolean; tokenAddress?: string; }[], + existingWallets: Wallet[], password?: string, ) => { - return ( - await Promise.all( - accountsArray.flatMap(async account => { - try { - const newWallets = (await dispatch( - createMultipleWallets({ - key: key as KeyMethods, - currencies, - options: { - password, - account, - customAccount: true, - }, - }), - )) as Wallet[]; - return newWallets; - } catch (err) { - const errMsg = - err instanceof Error ? err.message : JSON.stringify(err); - logManager.debug( - `Error creating wallet - continue anyway: ${errMsg}`, + const existingWalletKeySet = buildExistingWalletKeySet(existingWallets); + + const results = await Promise.all( + accountsArray.map(async account => { + const missingCurrencies = getMissingCurrenciesForAccount( + account, + currencies, + existingWalletKeySet, + ); + + if (missingCurrencies.length === 0) { + return []; + } + + try { + const newWallets = (await dispatch( + createMultipleWallets({ + key: key as KeyMethods, + currencies: missingCurrencies, + options: { + password, + account, + customAccount: true, + }, + }), + )) as Wallet[]; + + newWallets.forEach(wallet => { + const walletAccount = wallet.credentials.account || 0; + const walletChain = wallet.credentials.chain; + const walletCoin = normalizeWalletKeyCoin(wallet.credentials.coin); + existingWalletKeySet.add( + `${walletAccount}:${walletChain}:${walletCoin}`, ); - } - }), - ) - ) - .flat() - .filter(Boolean) as Wallet[]; + }); + + return newWallets; + } catch (err) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logManager.debug( + `Error creating wallet for account ${account} - continue anyway: ${errMsg}`, + ); + return []; + } + }), + ); + + return results.flat().filter(Boolean) as Wallet[]; }; export const getVMGasWallets = (wallets: Wallet[]) => { From 6b01d9dad4185fac30aae2b713b36ab2daa58de1 Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Thu, 2 Apr 2026 11:23:50 -0300 Subject: [PATCH 008/110] KeySettings: Fix - remove old LogAction by logManager --- src/navigation/wallet/screens/KeySettings.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/navigation/wallet/screens/KeySettings.tsx b/src/navigation/wallet/screens/KeySettings.tsx index 7296d4752c..e5a89bd407 100644 --- a/src/navigation/wallet/screens/KeySettings.tsx +++ b/src/navigation/wallet/screens/KeySettings.tsx @@ -53,9 +53,7 @@ import { import { buildAccountList, buildWalletObj, - checkEncryptPassword, checkPrivateKeyEncrypted, - generateKeyExportCode, mapAbbreviationAndName, } from '../../../store/wallet/utils/wallet'; import {Key} from '../../../store/wallet/wallet.models'; @@ -76,7 +74,7 @@ import {useTheme} from 'styled-components/native'; import {IsSVMChain, IsVMChain} from '../../../store/wallet/utils/currency'; import {useOngoingProcess, useTokenContext} from '../../../contexts'; import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; -import {LogActions} from '../../utils/logging'; +import {logManager} from '../../../managers/LogManager'; const WalletSettingsContainer = styled.SafeAreaView` flex: 1; @@ -184,7 +182,7 @@ const KeySettings = () => { cta(decryptedKey); } catch (e: any) { const errStr = e instanceof Error ? e.message : JSON.stringify(e); - Logger.error('[KeySettings] Decrypt Error', errStr); + logManager.error('[KeySettings] Decrypt Error', errStr); await dispatch(AppActions.dismissDecryptPasswordModal()); await sleep(500); // Wait to close Decrypt Password modal dispatch(showBottomNotificationModal(WrongPasswordError())); From 9363e6e20e4437b24fb9bf3911334b454c87725d Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Thu, 2 Apr 2026 11:27:39 -0300 Subject: [PATCH 009/110] Exclude mocks folder to tsconfig --- tsconfig.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index b1a4aa48e8..0a200828b3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -61,6 +61,14 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, "exclude": [ - "node_modules", "babel.config.js", "metro.config.js", "jest.config.js", "android", "ios", "**/node_modules", "**/Pods" + "node_modules", + "babel.config.js", + "metro.config.js", + "jest.config.js", + "android", + "ios", + "**/node_modules", + "**/Pods", + "__mocks__" ] } From 0982dccac3da3febae50da58a715ce5e69b297cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Edgardo=20Baz=C3=A1n?= Date: Thu, 2 Apr 2026 11:54:03 -0300 Subject: [PATCH 010/110] Validate: Fix - general fixes for validate script (#10) --- declarations.d.ts | 2 ++ .../modal/bottom-notification/BottomNotification.tsx | 4 ++-- src/constants/config.ts | 1 - src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts | 3 ++- .../tabs/settings/about/screens/PortfolioWalletDebug.tsx | 2 +- src/store/card/card.effects.ts | 4 ++-- tsconfig.json | 1 + 7 files changed, 10 insertions(+), 7 deletions(-) diff --git a/declarations.d.ts b/declarations.d.ts index dd3b8e16ee..0ccacc1d42 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -30,4 +30,6 @@ declare module '@env' { export const BASESCAN_API_KEY: string; export const TEST_MODE_NETWORK: string; export const SENTRY_DSN: string; + export const REGTEST_BASE_BITPAY_URL: string; + export const STATIC_CONTENT_CARDS_ENABLED: string; } diff --git a/src/components/modal/bottom-notification/BottomNotification.tsx b/src/components/modal/bottom-notification/BottomNotification.tsx index c92e450406..b57bfb0957 100644 --- a/src/components/modal/bottom-notification/BottomNotification.tsx +++ b/src/components/modal/bottom-notification/BottomNotification.tsx @@ -1,4 +1,4 @@ -import React, {ReactChild, useEffect, useMemo, useCallback} from 'react'; +import React, {ReactNode, useEffect, useMemo, useCallback} from 'react'; import {Platform} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import SheetModal from '../base/sheet/SheetModal'; @@ -41,7 +41,7 @@ export interface BottomNotificationConfig { action: (rootState: RootState) => any; }>; code?: string; - message2?: ReactChild; + message2?: ReactNode; enableBackdropDismiss: boolean; onBackdropDismiss?: () => void; } diff --git a/src/constants/config.ts b/src/constants/config.ts index 48dcd190ef..3dfa1f8652 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -1,4 +1,3 @@ -// @ts-ignore import { REGTEST_BASE_BITPAY_URL, STATIC_CONTENT_CARDS_ENABLED as STATIC_CONTENT_CARDS_ENABLED_ENV, diff --git a/src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts b/src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts index 1925b0bbbb..be90040019 100644 --- a/src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts +++ b/src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts @@ -123,7 +123,8 @@ const usePortfolioAssetRows = ({gainLossMode, keyId}: Args): Result => { return getDisplayAssetRowItems(items); }, [items]); - const populateLoadingByKeyPrevRef = useRef>(); + const populateLoadingByKeyPrevRef = + useRef>(undefined); const isPopulateLoadingByKey = useMemo(() => { if (!isPopulateInProgress || !walletIdsByAssetKey) { return undefined; diff --git a/src/navigation/tabs/settings/about/screens/PortfolioWalletDebug.tsx b/src/navigation/tabs/settings/about/screens/PortfolioWalletDebug.tsx index de5befc2e0..cb237cbfd2 100644 --- a/src/navigation/tabs/settings/about/screens/PortfolioWalletDebug.tsx +++ b/src/navigation/tabs/settings/about/screens/PortfolioWalletDebug.tsx @@ -557,7 +557,7 @@ const PortfolioWalletDebug = ({ 'WALLET', ], }); - portfolioRaw = decrypt.out(parsed as any, 'PORTFOLIO'); + portfolioRaw = decrypt.out(parsed as any, 'PORTFOLIO', undefined); } catch (e) { portfolioRaw = null; } diff --git a/src/store/card/card.effects.ts b/src/store/card/card.effects.ts index 99f16331ba..cd4988c16d 100644 --- a/src/store/card/card.effects.ts +++ b/src/store/card/card.effects.ts @@ -599,8 +599,8 @@ export const startAddToGooglePay = const errMsg = err instanceof Error ? err.message : JSON.stringify(err); logManager.error(`googlePay - completePushProvisionError - ${errMsg}`); - if (e instanceof Error) { - if (['CANCELED'].includes(e.message)) { + if (err instanceof Error) { + if (['CANCELED'].includes(err.message)) { return; } } diff --git a/tsconfig.json b/tsconfig.json index 0a200828b3..57a2636a88 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,6 +47,7 @@ // "types": [], /* Type declaration files to be included in compilation. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "resolveJsonModule": true, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ "skipLibCheck": false /* Skip type checking of declaration files. */ From 9fcb6d64311edbbcafb56d151981639b4d1f4ba5 Mon Sep 17 00:00:00 2001 From: Gustavo Cortez Date: Tue, 31 Mar 2026 15:19:41 -0300 Subject: [PATCH 011/110] AppsFlyer: Chore - Upgrade react-native-appsflyer --- ios/BitPayApp.xcodeproj/project.pbxproj | 20 ++++++++++++++++++++ ios/Podfile.lock | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj index 76300ea093..1a694d0948 100644 --- a/ios/BitPayApp.xcodeproj/project.pbxproj +++ b/ios/BitPayApp.xcodeproj/project.pbxproj @@ -189,6 +189,7 @@ 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, EBB510F948BF432C893398ED /* Upload Debug Symbols to Sentry */, + AF19107D42FF74BA8F0049CF /* Inject Bundle Hash */, C3CF03A4A13B8DA5B5BB02BA /* [CP] Embed Pods Frameworks */, A4F9FF5781B0ADEE84C32EAD /* [CP] Copy Pods Resources */, ); @@ -321,6 +322,25 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources.sh\"\n"; showEnvVarsInLog = 0; }; + AF19107D42FF74BA8F0049CF /* Inject Bundle Hash */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Inject Bundle Hash"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/scripts/inject-bundle-hash.sh\""; + showEnvVarsInLog = 0; + }; C3CF03A4A13B8DA5B5BB02BA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7ec1f69f56..9e34bbb735 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - AppsFlyerFramework (6.16.2): - - AppsFlyerFramework/Main (= 6.16.2) - - AppsFlyerFramework/Main (6.16.2) + - AppsFlyerFramework (6.17.9): + - AppsFlyerFramework/Main (= 6.17.9) + - AppsFlyerFramework/Main (6.17.9) - boost (1.84.0) - braze-react-native-sdk (16.1.0): - boost @@ -1931,8 +1931,8 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-appsflyer (6.16.2): - - AppsFlyerFramework (= 6.16.2) + - react-native-appsflyer (6.17.9): + - AppsFlyerFramework (= 6.17.9) - React - react-native-biometrics (3.0.1): - React-Core @@ -3920,7 +3920,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - AppsFlyerFramework: fe5303bffcdfd941d5f570c2d21eaaea982e7bdc + AppsFlyerFramework: 4f9a8237d40e9093941aef760cd77d606238aedd boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 braze-react-native-sdk: b3bd4594310a390c12beb06b02bf0fbbfcbca7e0 BrazeKit: dc74d3969d42e3f890461fe842e9422480269774 @@ -3975,7 +3975,7 @@ SPEC CHECKSUMS: React-logger: 2021eb67660b673cc654635832136fbbf2c79103 React-Mapbuffer: 9bda44c983f9c683047546a338ebe9a21020babd React-microtasksnativemodule: 9b52faf56750d7e3c67d9cf96b650f14c31524c2 - react-native-appsflyer: d52a08a5e687e4c2aca78519b0e258e1a82fad0a + react-native-appsflyer: 29efeef14176da773b44bbe09531ae4fdd55793f react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc react-native-ble-plx: 37a065e8604e03af88b725d1c4e8cb4c9a48c651 react-native-blur: ba0e9ad6274783c8d45f42da82acae02e25784ad diff --git a/package.json b/package.json index eb09662e04..c76ad9baa6 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "react-hook-form": "7.19.5", "react-i18next": "12.2.0", "react-native": "0.82.0", - "react-native-appsflyer": "6.16.2", + "react-native-appsflyer": "6.17.9", "react-native-biometrics": "3.0.1", "react-native-ble-plx": "3.1.2", "react-native-bootsplash": "6.3.7", diff --git a/yarn.lock b/yarn.lock index c20635c136..ec76e2994b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14266,10 +14266,10 @@ react-native-animatable@1.4.0: dependencies: prop-types "^15.8.1" -react-native-appsflyer@6.16.2: - version "6.16.2" - resolved "https://registry.yarnpkg.com/react-native-appsflyer/-/react-native-appsflyer-6.16.2.tgz#074707a789571673ffa5fafc6518902661023d93" - integrity sha512-ggMED0/R2Ypj5TLZNZ/HS9SsUorfsTMguRuH0x6inH+KSZqUghVjOAtQYxPPw3ZDgJBEZLhW9CaLTGd8dECtPQ== +react-native-appsflyer@6.17.9: + version "6.17.9" + resolved "https://registry.yarnpkg.com/react-native-appsflyer/-/react-native-appsflyer-6.17.9.tgz#18c520b774166db491de7e26e3c89536840f2e85" + integrity sha512-oEddwSsVL8D3ki8ayWZV34GyORAxvL1BXq3mL1xB8Hdfg+xxLyjAXSvWbj0t3E3NJ2KrgLRf/hbTlsPltfo/Uw== react-native-biometrics@3.0.1: version "3.0.1" From a7fd279dbfa91fa46dbfe74dcc2d20ba5ec3eb4e Mon Sep 17 00:00:00 2001 From: lyambo Date: Thu, 19 Mar 2026 12:03:14 -0400 Subject: [PATCH 012/110] Support deferred nonce EVM txps by assigning fresh nonce before signing --- src/store/wallet/effects/send/send.ts | 12 +++++++++--- src/store/wallet/wallet.models.ts | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index 4753dadf9c..ae123be1f0 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -1398,7 +1398,13 @@ export const publishAndSign = logManager.debug('success publish [publishAndSign]'); } - const txpToSign = publishedTx || txp; + let txpToSign = publishedTx || txp; + + // For deferred-nonce EVM txps, assign fresh nonce from BWS before signing + if (txpToSign.deferNonce && IsEVMChain(txpToSign.chain)) { + txpToSign = await wallet.assignNonce({ txp: txpToSign }); + logManager.info(`Deferred nonce: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`); + } if (key.isReadOnly && !key.hardwareSource) { // read only wallet @@ -1596,7 +1602,7 @@ export const publishAndSignMultipleProposals = | void | Error )[] = []; - const evmTxsWithNonce = txps.filter(txp => txp.nonce !== undefined); + const evmTxsWithNonce = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce); evmTxsWithNonce.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); for (const txp of evmTxsWithNonce) { try { @@ -1620,7 +1626,7 @@ export const publishAndSignMultipleProposals = } // Process transactions without a nonce concurrently - const withoutNonce = txps.filter(txp => txp.nonce === undefined); + const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce); const promisesWithoutNonce: Promise< Partial | void | Error >[] = withoutNonce.map(txp => diff --git a/src/store/wallet/wallet.models.ts b/src/store/wallet/wallet.models.ts index 2373342b91..aeb3647a94 100644 --- a/src/store/wallet/wallet.models.ts +++ b/src/store/wallet/wallet.models.ts @@ -399,6 +399,7 @@ export interface TransactionProposal { multisigGnosisContractAddress?: string; network: Network; nonce?: number; + deferNonce?: boolean; note?: { body?: string; }; From 91dbf03f99fdbdb2b63793be1aeb0ee245e44d4f Mon Sep 17 00:00:00 2001 From: lyambo Date: Wed, 25 Mar 2026 14:20:00 -0400 Subject: [PATCH 013/110] Use prepareTx and handle null nonce for EVM txps Call prepareTx instead of assignNonce. Also check for null nonce as a fallback so the app is ready for when BWS always defers nonce assignment for EVM chains. --- src/store/wallet/effects/send/send.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index ae123be1f0..d0ef62d28c 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -1400,10 +1400,11 @@ export const publishAndSign = let txpToSign = publishedTx || txp; - // For deferred-nonce EVM txps, assign fresh nonce from BWS before signing - if (txpToSign.deferNonce && IsEVMChain(txpToSign.chain)) { - txpToSign = await wallet.assignNonce({ txp: txpToSign }); - logManager.info(`Deferred nonce: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`); + // EVM txps without a nonce need one assigned before signing. + // TODO: remove deferNonce check once BWS always defers nonce for EVM chains + if ((txpToSign.deferNonce || txpToSign.nonce == null) && IsEVMChain(txpToSign.chain)) { + txpToSign = await wallet.prepareTx({ txp: txpToSign }); + logManager.info(`prepareTx: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`); } if (key.isReadOnly && !key.hardwareSource) { @@ -1602,7 +1603,7 @@ export const publishAndSignMultipleProposals = | void | Error )[] = []; - const evmTxsWithNonce = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce); + const evmTxsWithNonce = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce || (txp.nonce == null && IsEVMChain(txp.chain))); evmTxsWithNonce.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); for (const txp of evmTxsWithNonce) { try { @@ -1626,7 +1627,7 @@ export const publishAndSignMultipleProposals = } // Process transactions without a nonce concurrently - const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce); + const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce && !IsEVMChain(txp.chain)); const promisesWithoutNonce: Promise< Partial | void | Error >[] = withoutNonce.map(txp => From 9403fc73f299f60e2a4713d7a19f40ce77b17bd2 Mon Sep 17 00:00:00 2001 From: lyambo Date: Mon, 6 Apr 2026 15:48:31 -0400 Subject: [PATCH 014/110] Extend deferred nonce support to XRP Add IsNonceChain helper that covers EVM + XRP and use it in the prepareTx guard and multi-proposal signing filters so XRP txps get the same sequential nonce handling as EVM. --- src/store/wallet/effects/send/send.ts | 17 +++++++++-------- src/store/wallet/utils/currency.ts | 4 ++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index d0ef62d28c..8f49874aa5 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -91,6 +91,7 @@ import { IsSVMChain, IsUtxoChain, IsEVMChain, + IsNonceChain, } from '../../utils/currency'; import {CommonActions, NavigationProp} from '@react-navigation/native'; import {BwcProvider} from '../../../../lib/bwc'; @@ -1400,10 +1401,10 @@ export const publishAndSign = let txpToSign = publishedTx || txp; - // EVM txps without a nonce need one assigned before signing. - // TODO: remove deferNonce check once BWS always defers nonce for EVM chains - if ((txpToSign.deferNonce || txpToSign.nonce == null) && IsEVMChain(txpToSign.chain)) { - txpToSign = await wallet.prepareTx({ txp: txpToSign }); + // Nonce-based chains (EVM, XRP) without a nonce need one assigned before signing. + // TODO: remove deferNonce check once BWS always defers nonce for nonce chains + if ((txpToSign.deferNonce || txpToSign.nonce == null) && IsNonceChain(txpToSign.chain)) { + txpToSign = (await wallet.prepareTx({ txp: txpToSign })) as TransactionProposal; logManager.info(`prepareTx: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`); } @@ -1603,9 +1604,9 @@ export const publishAndSignMultipleProposals = | void | Error )[] = []; - const evmTxsWithNonce = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce || (txp.nonce == null && IsEVMChain(txp.chain))); - evmTxsWithNonce.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); - for (const txp of evmTxsWithNonce) { + const nonceTxs = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce || (txp.nonce == null && IsNonceChain(txp.chain))); + nonceTxs.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); + for (const txp of nonceTxs) { try { const result = await dispatch( publishAndSign({ @@ -1627,7 +1628,7 @@ export const publishAndSignMultipleProposals = } // Process transactions without a nonce concurrently - const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce && !IsEVMChain(txp.chain)); + const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce && !IsNonceChain(txp.chain)); const promisesWithoutNonce: Promise< Partial | void | Error >[] = withoutNonce.map(txp => diff --git a/src/store/wallet/utils/currency.ts b/src/store/wallet/utils/currency.ts index 72d587dd99..3fa653137c 100644 --- a/src/store/wallet/utils/currency.ts +++ b/src/store/wallet/utils/currency.ts @@ -93,6 +93,10 @@ export const IsEVMChain = (chain: string): boolean => { return Object.keys(BitpaySupportedEvmCoins).includes(_chain); }; +export const IsNonceChain = (chain: string): boolean => { + return IsEVMChain(chain) || chain.toLowerCase() === 'xrp'; +}; + export const IsSVMChain = (chain: string): boolean => { const _chain = cloneDeep(chain).toLowerCase(); From 40eb99a61d1433d6e75ead9e7a84aa04f0968dff Mon Sep 17 00:00:00 2001 From: lyambo Date: Tue, 7 Apr 2026 16:03:18 -0400 Subject: [PATCH 015/110] Update nonce txp filter logic --- src/store/wallet/effects/send/send.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index 8f49874aa5..235dad66ab 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -1604,7 +1604,12 @@ export const publishAndSignMultipleProposals = | void | Error )[] = []; - const nonceTxs = txps.filter(txp => txp.nonce !== undefined || txp.deferNonce || (txp.nonce == null && IsNonceChain(txp.chain))); + const nonceTxs = txps.filter(txp => { + const hasAssignedNonce = txp.nonce !== undefined; // BWS already set a nonce (normal flow) + const hasDeferredNonce = txp.deferNonce; // flagged txp — app assigns nonce via prepareTx + const isMissingNonceOnNonceChain = txp.nonce == null && IsNonceChain(txp.chain); // handles when BWS always defers nonce by default (deferNonce flag won't be needed) + return hasAssignedNonce || hasDeferredNonce || isMissingNonceOnNonceChain; + }); nonceTxs.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); for (const txp of nonceTxs) { try { From 4904d651261a5761ee123f2331da5b3b180e81f2 Mon Sep 17 00:00:00 2001 From: johnathan White Date: Tue, 7 Apr 2026 21:50:58 -0400 Subject: [PATCH 016/110] updated testIDs and accessibility labels --- src/components/checkbox/Checkbox.tsx | 1 + src/components/list/ChainSelectionRow.tsx | 5 ++- src/components/list/CurrencySelectionRow.tsx | 3 +- src/components/list/KeyWalletsRow.tsx | 4 ++ src/components/list/WalletRow.tsx | 2 + .../BottomNotification.tsx | 2 + .../components/ViaTransportButton.tsx | 4 ++ .../import-account/AddByDerivationPath.tsx | 11 ++++- .../import-account/SelectLedgerCurrency.tsx | 4 ++ .../import-account/SelectWalletsToImport.tsx | 14 ++++++- src/components/modal/pin/PinModal.tsx | 2 +- .../pagination-dots/PaginationDots.tsx | 3 +- src/navigation/auth/screens/CreateAccount.tsx | 32 +++++++++----- .../auth/screens/ForgotPassword.tsx | 8 ++-- src/navigation/auth/screens/Login.tsx | 24 +++++++---- src/navigation/auth/screens/SecureAccount.tsx | 7 ++-- src/navigation/auth/screens/VerifyEmail.tsx | 13 +++--- .../bitpay-id/screens/ProfileSettings.tsx | 3 +- .../components/OnboardingFinishModal.tsx | 2 +- .../onboarding/screens/CreateKey.tsx | 10 +++-- .../onboarding/screens/Notifications.tsx | 13 +++--- .../onboarding/screens/OnboardingStart.tsx | 19 +++++---- src/navigation/onboarding/screens/Pin.tsx | 13 +++--- .../onboarding/screens/TermsOfUse.tsx | 9 ++-- .../swap-crypto/screens/SwapCryptoRoot.tsx | 16 +++++++ .../tabs/contacts/screens/ContactsAdd.tsx | 10 ++++- .../tabs/contacts/screens/ContactsDetails.tsx | 12 +++++- .../tabs/contacts/screens/ContactsRoot.tsx | 9 +++- .../tabs/home/components/AssetRow.tsx | 2 + .../tabs/home/components/AssetsSearchPill.tsx | 2 + .../tabs/home/components/AssetsSection.tsx | 2 + .../tabs/home/components/Crypto.tsx | 4 ++ .../tabs/home/components/FeedbackCard.tsx | 20 +++++++-- .../home/components/HeaderProfileButton.tsx | 2 + .../tabs/home/components/HeaderScanButton.tsx | 2 + .../tabs/home/components/LinkingButtons.tsx | 2 + .../home/components/MarketingCarousel.tsx | 6 ++- .../tabs/home/components/PortfolioBalance.tsx | 4 ++ .../home/components/SecurePasskeyBanner.tsx | 2 + .../tabs/home/components/Wallet.tsx | 2 + .../tabs/home/components/cards/LinkCard.tsx | 2 + .../exchange-rates/ExchangeRateItem.tsx | 6 ++- .../tabs/home/components/offers/OfferCard.tsx | 4 +- .../tabs/home/screens/AllAssets.tsx | 2 + src/navigation/tabs/settings/SettingsRoot.tsx | 4 ++ .../tabs/settings/components/Connections.tsx | 14 ++++++- .../settings/components/Notifications.tsx | 4 ++ .../screens/BanxaDetails.tsx | 16 +++++++ .../screens/ChangellyDetails.tsx | 16 +++++++ .../screens/MoonpayDetails.tsx | 12 ++++++ .../screens/MoonpaySellDetails.tsx | 18 ++++++++ .../external-services/screens/RampDetails.tsx | 8 ++++ .../screens/RampSellDetails.tsx | 14 +++++++ .../screens/SardineDetails.tsx | 12 ++++++ .../screens/SimplexDetails.tsx | 10 +++++ .../screens/SimplexSellDetails.tsx | 10 +++++ .../screens/ThorswapDetails.tsx | 14 +++++++ .../screens/TransakDetails.tsx | 12 ++++++ .../general/screens/AltCurrencySettings.tsx | 2 + .../general/screens/LanguageSettings.tsx | 5 ++- .../components/DecryptEnterPasswordModal.tsx | 3 +- .../wallet/components/FileOrText.tsx | 9 ++-- .../wallet/components/RecoveryPhrase.tsx | 42 ++++++++++++------- .../wallet/screens/AccountDetails.tsx | 3 +- .../wallet/screens/AddCustomToken.tsx | 3 +- src/navigation/wallet/screens/AddWallet.tsx | 16 ++++++- src/navigation/wallet/screens/Backup.tsx | 8 ++-- .../wallet/screens/BackupOnboarding.tsx | 8 ++-- .../wallet/screens/BackupSharedKey.tsx | 8 ++-- .../wallet/screens/CreationOptions.tsx | 2 + .../wallet/screens/CurrencySelection.tsx | 8 ++-- src/navigation/wallet/screens/Import.tsx | 2 +- .../wallet/screens/InviteCosigners.tsx | 3 +- .../wallet/screens/RecoveryPhrase.tsx | 10 +++-- .../wallet/screens/TransactionDetails.tsx | 8 +++- .../screens/TransactionProposalDetails.tsx | 8 ++++ .../wallet/screens/VerifyPhrase.tsx | 11 ++--- src/navigation/wallet/screens/send/SendTo.tsx | 6 +++ .../screens/send/confirm/BillConfirm.tsx | 3 +- .../wallet/screens/send/confirm/Confirm.tsx | 2 + 80 files changed, 522 insertions(+), 131 deletions(-) diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx index 4d9b3fb11a..81ffc1d058 100644 --- a/src/components/checkbox/Checkbox.tsx +++ b/src/components/checkbox/Checkbox.tsx @@ -76,6 +76,7 @@ const Checkbox: React.FC = ({ }} // @ts-ignore --> testing testID="checkbox" + accessibilityLabel="Checkbox" outerStyle={{ ...baseStyles, borderColor: 'transparent', diff --git a/src/components/list/ChainSelectionRow.tsx b/src/components/list/ChainSelectionRow.tsx index f8f69672e2..65bb102b50 100644 --- a/src/components/list/ChainSelectionRow.tsx +++ b/src/components/list/ChainSelectionRow.tsx @@ -46,9 +46,10 @@ export const ChainSelectionRow: React.FC = memo( const {coin: currencyAbbreviation, chain, img, name} = chainObj; return ( - + onToggle(currencyAbbreviation, chain)}> diff --git a/src/components/list/CurrencySelectionRow.tsx b/src/components/list/CurrencySelectionRow.tsx index e960cc7b27..f86af36a67 100644 --- a/src/components/list/CurrencySelectionRow.tsx +++ b/src/components/list/CurrencySelectionRow.tsx @@ -99,7 +99,8 @@ const CurrencySelectionRow: React.FC = ({ return ( diff --git a/src/components/list/KeyWalletsRow.tsx b/src/components/list/KeyWalletsRow.tsx index d7da5dbd5f..646b3eb539 100644 --- a/src/components/list/KeyWalletsRow.tsx +++ b/src/components/list/KeyWalletsRow.tsx @@ -222,6 +222,8 @@ const KeyWalletsRow = ({ isLast={key?.mergedUtxoAndEvmAccounts.length === index + 1}> onHide(evmAccount?.receiveAddress)}> @@ -334,6 +336,8 @@ const KeyWalletsRow = ({ (prev.chain !== wallet.chain && ( wallet?.chain && onHide(`${wallet.chain}-${key.key}`) }> diff --git a/src/components/list/WalletRow.tsx b/src/components/list/WalletRow.tsx index ca7622cd74..21c571be5a 100644 --- a/src/components/list/WalletRow.tsx +++ b/src/components/list/WalletRow.tsx @@ -211,6 +211,8 @@ const WalletRow = ({ return ( { = props => { return ( @@ -66,6 +68,8 @@ export const ViaUsbButton: React.FC = props => { return ( diff --git a/src/components/modal/import-ledger-wallet/import-account/AddByDerivationPath.tsx b/src/components/modal/import-ledger-wallet/import-account/AddByDerivationPath.tsx index 8a75fca495..2a2b8fbf54 100644 --- a/src/components/modal/import-ledger-wallet/import-account/AddByDerivationPath.tsx +++ b/src/components/modal/import-ledger-wallet/import-account/AddByDerivationPath.tsx @@ -448,7 +448,8 @@ export const AddByDerivationPath: React.FC = props => { = props => { - diff --git a/src/components/modal/import-ledger-wallet/import-account/SelectLedgerCurrency.tsx b/src/components/modal/import-ledger-wallet/import-account/SelectLedgerCurrency.tsx index 41c88cdf40..4eda6073d4 100644 --- a/src/components/modal/import-ledger-wallet/import-account/SelectLedgerCurrency.tsx +++ b/src/components/modal/import-ledger-wallet/import-account/SelectLedgerCurrency.tsx @@ -988,6 +988,8 @@ export const SelectLedgerCurrency: React.FC = props => { {CHAINS.map((c, index) => ( onContinue(c.chain)} + testID={`ledger-select-currency-${c.chain}-button`} + accessibilityLabel={`${c.label} currency`} key={index}> @@ -1015,6 +1017,8 @@ export const SelectLedgerCurrency: React.FC = props => { diff --git a/src/components/modal/import-ledger-wallet/import-account/SelectWalletsToImport.tsx b/src/components/modal/import-ledger-wallet/import-account/SelectWalletsToImport.tsx index a1699be804..3938718cac 100644 --- a/src/components/modal/import-ledger-wallet/import-account/SelectWalletsToImport.tsx +++ b/src/components/modal/import-ledger-wallet/import-account/SelectWalletsToImport.tsx @@ -160,6 +160,8 @@ export const SelectWalletsToImport: React.FC = props => { {uiFormattedWallets.map((uiFormattedWallet, index) => ( onPress(uiFormattedWallet)}> = props => { {selectedWallets ? ( {selectedWallets > 1 ? ( - ) : ( - )} @@ -197,6 +205,8 @@ export const SelectWalletsToImport: React.FC = props => { diff --git a/src/components/modal/pin/PinModal.tsx b/src/components/modal/pin/PinModal.tsx index 72828255e8..a0a485ec98 100644 --- a/src/components/modal/pin/PinModal.tsx +++ b/src/components/modal/pin/PinModal.tsx @@ -395,7 +395,7 @@ const Pin = gestureHandlerRootHOC( - + = ({ }; return ( - - + + ( = ({ control={control} render={({field: {onChange, onBlur, value}}) => ( = ({ control={control} render={({field: {onChange, onBlur, value}}) => ( = ({ control={control} render={({field: {onChange, onBlur, value}}) => ( = ({ control={control} render={({field}) => ( <> - + setValue('agreedToTOSandPP', !field.value, { shouldValidate: true, @@ -400,8 +406,10 @@ const CreateAccountScreen: React.FC = ({ control={control} render={({field}) => ( <> - + setValue('agreedToMarketingCommunications', !field.value) } @@ -421,10 +429,11 @@ const CreateAccountScreen: React.FC = ({ /> - + @@ -434,7 +443,8 @@ const CreateAccountScreen: React.FC = ({ {t('Already have an account?')}{' '} { navigation.navigate('Login'); }}> diff --git a/src/navigation/auth/screens/ForgotPassword.tsx b/src/navigation/auth/screens/ForgotPassword.tsx index 6265c2ec2f..ca9b2702bf 100644 --- a/src/navigation/auth/screens/ForgotPassword.tsx +++ b/src/navigation/auth/screens/ForgotPassword.tsx @@ -123,14 +123,15 @@ const ForgotPasswordScreen: React.FC< setCaptchaModalVisible(false); }; return ( - + ( diff --git a/src/navigation/auth/screens/Login.tsx b/src/navigation/auth/screens/Login.tsx index 8b4c5d4b66..f4ea98d5d9 100644 --- a/src/navigation/auth/screens/Login.tsx +++ b/src/navigation/auth/screens/Login.tsx @@ -251,14 +251,15 @@ const LoginScreen: React.FC = ({navigation, route}) => { }; return ( - - + + ( = ({navigation, route}) => { control={control} render={({field: {onChange, onBlur, value}}) => ( = ({navigation, route}) => { /> - + ) : ( ) : ( diff --git a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx index e1d7f8a823..fad0452ddf 100644 --- a/src/navigation/tabs/contacts/screens/ContactsDetails.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsDetails.tsx @@ -337,7 +337,11 @@ const ContactsDetails = ({
{t('Address')} - + {copied ? : null} @@ -398,7 +402,11 @@ const ContactsDetails = ({ onBackdropPress={() => setShowIconOptions(false)}> {contactOptions.map(({img, title: optionTitle, onPress}, index) => ( - + {img} {optionTitle} diff --git a/src/navigation/tabs/contacts/screens/ContactsRoot.tsx b/src/navigation/tabs/contacts/screens/ContactsRoot.tsx index 0db810725b..460be68080 100644 --- a/src/navigation/tabs/contacts/screens/ContactsRoot.tsx +++ b/src/navigation/tabs/contacts/screens/ContactsRoot.tsx @@ -215,6 +215,8 @@ const ContactsRoot = ({}: NativeStackScreenProps< {theme.dark ? : } @@ -259,7 +261,12 @@ const ContactsRoot = ({}: NativeStackScreenProps< {t('Get started by adding your first one.')} - diff --git a/src/navigation/tabs/home/components/Crypto.tsx b/src/navigation/tabs/home/components/Crypto.tsx index ab58a212bf..6a0c745424 100644 --- a/src/navigation/tabs/home/components/Crypto.tsx +++ b/src/navigation/tabs/home/components/Crypto.tsx @@ -552,6 +552,8 @@ const Crypto = () => { { haptic('soft'); navigation.navigate('CreationOptions'); @@ -560,6 +562,8 @@ const Crypto = () => { { haptic('soft'); // Apply SettingsDetails config so that the custom header is used diff --git a/src/navigation/tabs/home/components/FeedbackCard.tsx b/src/navigation/tabs/home/components/FeedbackCard.tsx index 245dc772f0..00525e4b4b 100644 --- a/src/navigation/tabs/home/components/FeedbackCard.tsx +++ b/src/navigation/tabs/home/components/FeedbackCard.tsx @@ -102,7 +102,10 @@ const FeedbackCard: React.FC = () => { {t('Feedback')} - rateApp('default')}> + rateApp('default')}> @@ -111,13 +114,22 @@ const FeedbackCard: React.FC = () => { - rateApp('disappointed')}> + rateApp('disappointed')}> - rateApp('ok')}> + rateApp('ok')}> - rateApp('love')}> + rateApp('love')}> diff --git a/src/navigation/tabs/home/components/HeaderProfileButton.tsx b/src/navigation/tabs/home/components/HeaderProfileButton.tsx index 08186c015e..1ec4fa2818 100644 --- a/src/navigation/tabs/home/components/HeaderProfileButton.tsx +++ b/src/navigation/tabs/home/components/HeaderProfileButton.tsx @@ -16,6 +16,8 @@ const ProfileButton: React.FC = () => { return ( { user ? navigation.navigate('BitPayIdProfile') diff --git a/src/navigation/tabs/home/components/HeaderScanButton.tsx b/src/navigation/tabs/home/components/HeaderScanButton.tsx index 70a0a9dac1..ab9fe1e10b 100644 --- a/src/navigation/tabs/home/components/HeaderScanButton.tsx +++ b/src/navigation/tabs/home/components/HeaderScanButton.tsx @@ -38,6 +38,8 @@ const ScanButton: React.FC = () => { return ( { dispatch( Analytics.track('Open Scanner', { diff --git a/src/navigation/tabs/home/components/LinkingButtons.tsx b/src/navigation/tabs/home/components/LinkingButtons.tsx index f69d2c8d20..4cfc4f68df 100644 --- a/src/navigation/tabs/home/components/LinkingButtons.tsx +++ b/src/navigation/tabs/home/components/LinkingButtons.tsx @@ -219,6 +219,8 @@ const LinkingButtons = ({buy, sell, receive, send, swap, maxWidth}: Props) => { = ({ { skipNextCardPressRef.current = true; }} @@ -340,8 +341,9 @@ const MarketingCarousel: React.FC = ({ { if (index !== activeIndex) { carouselRef.current?.scrollTo({index, animated: true}); diff --git a/src/navigation/tabs/home/components/PortfolioBalance.tsx b/src/navigation/tabs/home/components/PortfolioBalance.tsx index 3421aa45df..647e884acf 100644 --- a/src/navigation/tabs/home/components/PortfolioBalance.tsx +++ b/src/navigation/tabs/home/components/PortfolioBalance.tsx @@ -272,11 +272,15 @@ const PortfolioBalance = () => { {t('Portfolio Balance')} { dispatch(toggleHideAllBalances()); }}> diff --git a/src/navigation/tabs/home/components/SecurePasskeyBanner.tsx b/src/navigation/tabs/home/components/SecurePasskeyBanner.tsx index 1165b6cc0a..a4b39b6a35 100644 --- a/src/navigation/tabs/home/components/SecurePasskeyBanner.tsx +++ b/src/navigation/tabs/home/components/SecurePasskeyBanner.tsx @@ -59,6 +59,8 @@ const SecurePasskeyBanner: React.FC = () => { return ( { navigation.dispatch( CommonActions.reset({ diff --git a/src/navigation/tabs/home/components/Wallet.tsx b/src/navigation/tabs/home/components/Wallet.tsx index 3d9d8c2f46..97a5add51b 100644 --- a/src/navigation/tabs/home/components/Wallet.tsx +++ b/src/navigation/tabs/home/components/Wallet.tsx @@ -235,6 +235,8 @@ const WalletCardComponent: React.FC = ({ return ( diff --git a/src/navigation/tabs/home/components/cards/LinkCard.tsx b/src/navigation/tabs/home/components/cards/LinkCard.tsx index 314026c4ef..892b230076 100644 --- a/src/navigation/tabs/home/components/cards/LinkCard.tsx +++ b/src/navigation/tabs/home/components/cards/LinkCard.tsx @@ -46,6 +46,8 @@ const LinkCard: React.FC = ({image, description, onPress}) => { return ( { haptic('soft'); onPress(); diff --git a/src/navigation/tabs/home/components/exchange-rates/ExchangeRateItem.tsx b/src/navigation/tabs/home/components/exchange-rates/ExchangeRateItem.tsx index dd43fc5b5b..18b23cc3d1 100644 --- a/src/navigation/tabs/home/components/exchange-rates/ExchangeRateItem.tsx +++ b/src/navigation/tabs/home/components/exchange-rates/ExchangeRateItem.tsx @@ -122,7 +122,11 @@ const ExchangeRateItem = ({ ); return ( - + diff --git a/src/navigation/tabs/home/components/offers/OfferCard.tsx b/src/navigation/tabs/home/components/offers/OfferCard.tsx index 9a0d8a195d..08cb2d8529 100644 --- a/src/navigation/tabs/home/components/offers/OfferCard.tsx +++ b/src/navigation/tabs/home/components/offers/OfferCard.tsx @@ -117,7 +117,9 @@ const OfferCard: React.FC = props => { + accessibilityRole="button" + testID={`home-offer-card-button-${contentCard.id}`} + accessibilityLabel={title ? `${title} offer` : 'View offer'}> {coverImageSource ? ( = ({navigation, route}) => { autoCapitalize="none" autoCorrect={false} onChangeText={setQuery} + testID="all-assets-search-input" + accessibilityLabel="Search assets" /> diff --git a/src/navigation/tabs/settings/SettingsRoot.tsx b/src/navigation/tabs/settings/SettingsRoot.tsx index dcdcb74724..18ee5f4f17 100644 --- a/src/navigation/tabs/settings/SettingsRoot.tsx +++ b/src/navigation/tabs/settings/SettingsRoot.tsx @@ -139,6 +139,8 @@ const SettingsHome: React.FC = ({route, navigation}) => { { navigation.navigate('SettingsDetails', { initialRoute: item.id, @@ -158,6 +160,8 @@ const SettingsHome: React.FC = ({route, navigation}) => { const ListHeaderComponent = () => ( { if (user) { navigation.navigate('BitPayIdProfile'); diff --git a/src/navigation/tabs/settings/components/Connections.tsx b/src/navigation/tabs/settings/components/Connections.tsx index d80046b877..6d9c1613df 100644 --- a/src/navigation/tabs/settings/components/Connections.tsx +++ b/src/navigation/tabs/settings/components/Connections.tsx @@ -99,7 +99,10 @@ const Connections = () => { return ( - goToCoinbase()}> + goToCoinbase()}> @@ -109,7 +112,10 @@ const Connections = () => {
- goToMethod()}> + goToMethod()}> @@ -120,6 +126,8 @@ const Connections = () => {
{ haptic('impactLight'); goToWalletConnect(); @@ -134,6 +142,8 @@ const Connections = () => {
{ haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/components/Notifications.tsx b/src/navigation/tabs/settings/components/Notifications.tsx index b4d5ddcdc1..10c3c9471c 100644 --- a/src/navigation/tabs/settings/components/Notifications.tsx +++ b/src/navigation/tabs/settings/components/Notifications.tsx @@ -76,6 +76,8 @@ const Notifications = () => { navigation.navigate('PushNotifications')}> {t('Push Notifications')} @@ -86,6 +88,8 @@ const Notifications = () => { navigation.navigate('EmailNotifications')}> {t('Email Notifications')} diff --git a/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx b/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx index 160b5e21f9..3967fa38d8 100644 --- a/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx @@ -392,6 +392,8 @@ const BanxaDetails: React.FC = () => { {t('Having problems with Banxa?')} { haptic('impactLight'); dispatch( @@ -430,6 +432,8 @@ const BanxaDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -444,6 +448,8 @@ const BanxaDetails: React.FC = () => {
{ haptic('impactLight'); dispatch( @@ -461,6 +467,8 @@ const BanxaDetails: React.FC = () => { { copyText(paymentRequest.address); setCopiedDepositAddress(true); @@ -480,6 +488,8 @@ const BanxaDetails: React.FC = () => { {!!paymentRequest.order_id && ( { copyText(paymentRequest.order_id!); setCopiedOrderId(true); @@ -500,6 +510,8 @@ const BanxaDetails: React.FC = () => { {!!paymentRequest.ref && ( { copyText(paymentRequest.ref?.toString()!); setCopiedReferenceId(true); @@ -520,6 +532,8 @@ const BanxaDetails: React.FC = () => { {!!paymentRequest.transaction_id && ( { copyText(paymentRequest.transaction_id!); setCopiedTransactionId(true); @@ -538,6 +552,8 @@ const BanxaDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx b/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx index 06814be4ac..2451bc65f0 100644 --- a/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx @@ -196,6 +196,8 @@ const ChangellyDetails: React.FC = () => { { copyText(swapTx.addressTo); setCopiedDepositAddress(true); @@ -266,6 +268,8 @@ const ChangellyDetails: React.FC = () => {
{ copyText('security@changelly.com'); @@ -288,6 +292,8 @@ const ChangellyDetails: React.FC = () => {
{ copyText(swapTx.exchangeTxId); @@ -313,6 +319,8 @@ const ChangellyDetails: React.FC = () => { { copyText(swapTx.payinAddress); setCopiedPayinAddress(true); @@ -332,6 +340,8 @@ const ChangellyDetails: React.FC = () => { {swapTx.payinExtraId ? ( { copyText(swapTx.payinExtraId!); setCopiedPayinExtraId(true); @@ -351,6 +361,8 @@ const ChangellyDetails: React.FC = () => { { copyText(swapTx.refundAddress); setCopiedRefundAddress(true); @@ -369,6 +381,8 @@ const ChangellyDetails: React.FC = () => { { copyText(swapTx.exchangeTxId); setCopiedExchangeTxId(true); @@ -386,6 +400,8 @@ const ChangellyDetails: React.FC = () => { { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx b/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx index a6b8f08a08..b262bc4e96 100644 --- a/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx @@ -245,6 +245,8 @@ const MoonpayDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -270,6 +272,8 @@ const MoonpayDetails: React.FC = () => { {t('Having problems with Moonpay?')}{' '} { haptic('impactLight'); dispatch( @@ -289,6 +293,8 @@ const MoonpayDetails: React.FC = () => { { copyText(paymentRequest.address); setCopiedDepositAddress(true); @@ -308,6 +314,8 @@ const MoonpayDetails: React.FC = () => { {!!paymentRequest.transaction_id && ( { copyText(paymentRequest.transaction_id!); setCopiedTransactionId(true); @@ -327,6 +335,8 @@ const MoonpayDetails: React.FC = () => { { copyText(paymentRequest.external_id); setCopiedExternalId(true); @@ -344,6 +354,8 @@ const MoonpayDetails: React.FC = () => { { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx b/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx index 3c2f1ad116..7788d1aebc 100644 --- a/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/MoonpaySellDetails.tsx @@ -349,6 +349,8 @@ const MoonpaySellDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -382,6 +384,8 @@ const MoonpaySellDetails: React.FC = () => { {t('Having problems with Moonpay?')}{' '} { haptic('impactLight'); dispatch( @@ -402,6 +406,8 @@ const MoonpaySellDetails: React.FC = () => { {!!sellOrder.address_to && ( { if (sellOrder.address_to) { copyText(sellOrder.address_to); @@ -425,6 +431,8 @@ const MoonpaySellDetails: React.FC = () => { ['failed'].includes(sellOrder.status) ? ( { copyText(sellOrder.refund_address); setCopiedRefundAddress(true); @@ -445,6 +453,8 @@ const MoonpaySellDetails: React.FC = () => { {!!sellOrder.tx_sent_id && ( { copyText(sellOrder.tx_sent_id!); setCopiedTransactionSentId(true); @@ -465,6 +475,8 @@ const MoonpaySellDetails: React.FC = () => { {!!sellOrder.transaction_id && ( { copyText(sellOrder.transaction_id!); setCopiedTransactionId(true); @@ -486,6 +498,8 @@ const MoonpaySellDetails: React.FC = () => { { copyText(sellOrder.external_id); setCopiedExternalId(true); @@ -506,6 +520,8 @@ const MoonpaySellDetails: React.FC = () => { {['bitpayPending', 'waitingForDeposit'].includes(sellOrder.status) && sourceWallet ? ( { haptic('impactLight'); dispatch( @@ -596,6 +612,8 @@ const MoonpaySellDetails: React.FC = () => { (['bitpayPending', 'waitingForDeposit'].includes(sellOrder.status) && !sourceWallet) ? ( { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx b/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx index 7d57c3b409..5ae01bcfd5 100644 --- a/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx @@ -155,6 +155,8 @@ const RampDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -180,6 +182,8 @@ const RampDetails: React.FC = () => { {t('What is the status of my payment?')}{' '} { haptic('impactLight'); dispatch( @@ -197,6 +201,8 @@ const RampDetails: React.FC = () => { { copyText(paymentRequest.address); setCopiedDepositAddress(true); @@ -214,6 +220,8 @@ const RampDetails: React.FC = () => { { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/RampSellDetails.tsx b/src/navigation/tabs/settings/external-services/screens/RampSellDetails.tsx index fc6a932c10..66f71a306f 100644 --- a/src/navigation/tabs/settings/external-services/screens/RampSellDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/RampSellDetails.tsx @@ -176,6 +176,8 @@ const RampSellDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -207,6 +209,8 @@ const RampSellDetails: React.FC = () => { {t('Having problems with Ramp?')}{' '} { haptic('impactLight'); dispatch( @@ -230,6 +234,8 @@ const RampSellDetails: React.FC = () => { {t('What is the status of my crypto sale?')}{' '} { haptic('impactLight'); dispatch( @@ -247,6 +253,8 @@ const RampSellDetails: React.FC = () => { { copyText(sellOrder.address_to); setCopiedDepositAddress(true); @@ -266,6 +274,8 @@ const RampSellDetails: React.FC = () => { {sellOrder.quote_id ? ( { copyText(sellOrder.quote_id!); setCopiedPaymentId(true); @@ -286,6 +296,8 @@ const RampSellDetails: React.FC = () => { {!!sellOrder.tx_sent_id && ( { copyText(sellOrder.tx_sent_id!); setCopiedTransactionSentId(true); @@ -304,6 +316,8 @@ const RampSellDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx b/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx index 810acc2c92..8f75fbca53 100644 --- a/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx @@ -291,6 +291,8 @@ const SardineDetails: React.FC = () => { {t('Having problems with Sardine?')} { haptic('impactLight'); dispatch( @@ -310,6 +312,8 @@ const SardineDetails: React.FC = () => { { copyText(paymentRequest.address); setCopiedDepositAddress(true); @@ -329,6 +333,8 @@ const SardineDetails: React.FC = () => { {!!paymentRequest.order_id && ( { copyText(paymentRequest.order_id!); setCopiedOrderId(true); @@ -349,6 +355,8 @@ const SardineDetails: React.FC = () => { {!!paymentRequest.external_id && ( { copyText(paymentRequest.external_id!); setCopiedReferenceId(true); @@ -369,6 +377,8 @@ const SardineDetails: React.FC = () => { {!!paymentRequest.transaction_id && ( { copyText(paymentRequest.transaction_id!); setCopiedTransactionId(true); @@ -387,6 +397,8 @@ const SardineDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/SimplexDetails.tsx b/src/navigation/tabs/settings/external-services/screens/SimplexDetails.tsx index 32bedf3a16..651e85599f 100644 --- a/src/navigation/tabs/settings/external-services/screens/SimplexDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/SimplexDetails.tsx @@ -158,6 +158,8 @@ const SimplexDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( @@ -174,6 +176,8 @@ const SimplexDetails: React.FC = () => { { copyText(paymentRequest.address || ''); setCopiedDepositAddress(true); @@ -192,6 +196,8 @@ const SimplexDetails: React.FC = () => { { copyText(paymentRequest.payment_id); setCopiedPaymentId(true); @@ -210,6 +216,8 @@ const SimplexDetails: React.FC = () => { { copyText(paymentRequest.order_id); setCopiedOrderId(true); @@ -227,6 +235,8 @@ const SimplexDetails: React.FC = () => { { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/SimplexSellDetails.tsx b/src/navigation/tabs/settings/external-services/screens/SimplexSellDetails.tsx index 38515fdf3c..835fc87d35 100644 --- a/src/navigation/tabs/settings/external-services/screens/SimplexSellDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/SimplexSellDetails.tsx @@ -192,6 +192,8 @@ const SimplexSellDetails: React.FC = () => { {t('Having problems with Simplex?')}{' '} { haptic('impactLight'); dispatch( @@ -211,6 +213,8 @@ const SimplexSellDetails: React.FC = () => { { copyText(sellOrder.address_to); setCopiedDepositAddress(true); @@ -230,6 +234,8 @@ const SimplexSellDetails: React.FC = () => { {sellOrder.quote_id ? ( { copyText(sellOrder.quote_id!); setCopiedPaymentId(true); @@ -250,6 +256,8 @@ const SimplexSellDetails: React.FC = () => { {!!sellOrder.tx_sent_id && ( { copyText(sellOrder.tx_sent_id!); setCopiedTransactionSentId(true); @@ -268,6 +276,8 @@ const SimplexSellDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx b/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx index e8681e7cde..8100546069 100644 --- a/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx @@ -301,6 +301,8 @@ const ThorswapDetails: React.FC = () => { { copyText(swapTx.addressTo); setCopiedDepositAddress(true); @@ -371,6 +373,8 @@ const ThorswapDetails: React.FC = () => {
{ copyText('security@thorswap.com'); @@ -393,6 +397,8 @@ const ThorswapDetails: React.FC = () => {
{ copyText(swapTx.txHash); @@ -416,6 +422,8 @@ const ThorswapDetails: React.FC = () => { { copyText(swapTx.payinAddress); setCopiedPayinAddress(true); @@ -435,6 +443,8 @@ const ThorswapDetails: React.FC = () => { {swapTx.payinExtraId ? ( { copyText(swapTx.payinExtraId!); setCopiedPayinExtraId(true); @@ -472,6 +482,8 @@ const ThorswapDetails: React.FC = () => { { copyText(swapTx.txHash); setCopiedTxHash(true); @@ -489,6 +501,8 @@ const ThorswapDetails: React.FC = () => { { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx b/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx index 9276382361..5ed7f5aef5 100644 --- a/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx @@ -465,6 +465,8 @@ const TransakDetails: React.FC = () => { {t('Having problems with Transak?')} { haptic('impactLight'); dispatch( @@ -484,6 +486,8 @@ const TransakDetails: React.FC = () => { { copyText(paymentRequest.address); setCopiedDepositAddress(true); @@ -503,6 +507,8 @@ const TransakDetails: React.FC = () => { {!!paymentRequest.order_id && ( { copyText(paymentRequest.order_id!); setCopiedOrderId(true); @@ -523,6 +529,8 @@ const TransakDetails: React.FC = () => { {!!paymentRequest.external_id && ( { copyText(paymentRequest.external_id!); setCopiedReferenceId(true); @@ -543,6 +551,8 @@ const TransakDetails: React.FC = () => { {!!paymentRequest.transaction_id && ( { copyText(paymentRequest.transaction_id!); setCopiedTransactionId(true); @@ -561,6 +571,8 @@ const TransakDetails: React.FC = () => { )} { haptic('impactLight'); dispatch( diff --git a/src/navigation/tabs/settings/general/screens/AltCurrencySettings.tsx b/src/navigation/tabs/settings/general/screens/AltCurrencySettings.tsx index 0a997a46d1..0a2ae5a809 100644 --- a/src/navigation/tabs/settings/general/screens/AltCurrencySettings.tsx +++ b/src/navigation/tabs/settings/general/screens/AltCurrencySettings.tsx @@ -270,6 +270,8 @@ const AltCurrencySettings = () => { { updateSearchResults(text); }} diff --git a/src/navigation/tabs/settings/general/screens/LanguageSettings.tsx b/src/navigation/tabs/settings/general/screens/LanguageSettings.tsx index 57b601d19e..cb757d329a 100644 --- a/src/navigation/tabs/settings/general/screens/LanguageSettings.tsx +++ b/src/navigation/tabs/settings/general/screens/LanguageSettings.tsx @@ -53,7 +53,10 @@ const LanguageSettings: React.FC = () => { {LanguageList.map(({name, isoCode}) => { return ( - setSelected(isoCode)}> + setSelected(isoCode)}> {name} {loading && selected === isoCode ? ( { return ( { return ( @@ -589,7 +590,8 @@ const FileOrText = () => { control={control} render={({field: {onChange, onBlur, value}}) => ( { diff --git a/src/navigation/wallet/screens/TransactionProposalDetails.tsx b/src/navigation/wallet/screens/TransactionProposalDetails.tsx index 38cbfd2f09..95a5b83a56 100644 --- a/src/navigation/wallet/screens/TransactionProposalDetails.tsx +++ b/src/navigation/wallet/screens/TransactionProposalDetails.tsx @@ -695,6 +695,8 @@ const TransactionProposalDetails = () => { description={t('Payment accepted, but not yet broadcasted.')} /> - )} + txp.canBeRemoved) && + !key.isReadOnly && ( + + )} ) : null} @@ -776,9 +776,9 @@ const TransactionProposalDetails = () => {
{t('DETAILS')}
- {!isTSSWallet &&
} + {!isTSSWallet(wallet) &&
} - {isTSSWallet && transaction && ( + {isTSSWallet(wallet) && transaction && ( { (payProDetails && !payproIsLoading && !paymentExpired)) ? ( { return null; }, [selectingProposalsWalletId, wallets]); - const currentKey = useMemo(() => { - if (currentWallet) { - return keys[currentWallet.keyId]; - } - return null; - }, [currentWallet, keys]); - - const isTSSWallet = useMemo(() => { - return currentKey ? isTSSKey(currentKey) : false; - }, [currentKey]); - const tssCallbacks = useTSSCallbacks({ setTssStatus, setTssProgress, @@ -408,8 +397,7 @@ const TransactionProposalNotifications = () => { } const wallet = findWalletById(wallets, walletId) as Wallet; - const key = keys[wallet.keyId]; - const isTSS = isTSSKey(key); + const isTSS = isTSSWallet(wallet); if (_.indexOf(txpsToSign, txp) >= 0) { _.remove(txpsToSign, txpToSign => { @@ -459,8 +447,7 @@ const TransactionProposalNotifications = () => { tssMetadata, } = fullWalletObj; - const key = keys[fullWalletObj.keyId]; - const isTSS = isTSSKey(key); + const isTSS = isTSSWallet(fullWalletObj); return ( <> @@ -639,7 +626,7 @@ const TransactionProposalNotifications = () => { return ( - {isTSSWallet && currentWallet ? ( + {currentWallet && isTSSWallet(currentWallet) ? ( { ) as Wallet; const key = keys[wallet.keyId]; - if (isTSSKey(key)) { + if (isTSSWallet(wallet)) { const txp = txpsToSign[0]; await dispatch( joinTSSSigningSession({ diff --git a/src/navigation/wallet/screens/WalletDetails.tsx b/src/navigation/wallet/screens/WalletDetails.tsx index c319340f55..10092ebf32 100644 --- a/src/navigation/wallet/screens/WalletDetails.tsx +++ b/src/navigation/wallet/screens/WalletDetails.tsx @@ -140,7 +140,7 @@ import {BillPayAccount} from '../../../store/shop/shop.models'; import debounce from 'lodash.debounce'; import ArchaxFooter from '../../../components/archax/archax-footer'; import {ExternalServicesScreens} from '../../services/ExternalServicesGroup'; -import {isTSSKey} from '../../../store/wallet/effects/tss-send/tss-send'; +import {isTSSWallet} from '../../../store/wallet/effects/tss-send/tss-send'; import {logManager} from '../../../managers/LogManager'; export type WalletDetailsScreenParamList = { @@ -1139,7 +1139,8 @@ const WalletDetails: React.FC = ({route}) => { {fullWalletObj.credentials.n} - ) : isTSSKey(key) && fullWalletObj.tssMetadata ? ( + ) : isTSSWallet(fullWalletObj) && + fullWalletObj.tssMetadata ? ( Threshold {fullWalletObj.tssMetadata.m}/ diff --git a/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx b/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx index 8e7a9a611e..f236007b43 100644 --- a/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx @@ -88,7 +88,7 @@ import TransportHID from '@ledgerhq/react-native-hid'; import {LISTEN_TIMEOUT, OPEN_TIMEOUT} from '../../../../../constants/config'; import {BitpaySupportedCoins} from '../../../../../constants/currencies'; import {useOngoingProcess, usePaymentSent} from '../../../../../contexts'; -import {isTSSKey} from '../../../../../store/wallet/effects/tss-send/tss-send'; +import {isTSSWallet} from '../../../../../store/wallet/effects/tss-send/tss-send'; import TSSProgressTracker from '../../../components/TSSProgressTracker'; import {useTSSCallbacks} from '../../../../../utils/hooks/useTSSCalbacks'; @@ -151,7 +151,6 @@ const BillConfirm: React.FC< const {showPaymentSent, hidePaymentSent} = usePaymentSent(); const {showOngoingProcess, hideOngoingProcess} = useOngoingProcess(); - const isTSSWallet = key ? isTSSKey(key) : false; const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ currentRound: 0, @@ -438,8 +437,8 @@ const BillConfirm: React.FC< key, wallet, recipient, - ...(isTSSWallet && {tssCallbacks}), - ...(isTSSWallet && {setShowTSSProgressModal}), + ...(isTSSWallet(wallet) && {tssCallbacks}), + ...(isTSSWallet(wallet) && {setShowTSSProgressModal}), }), ) : await dispatch( @@ -640,7 +639,7 @@ const BillConfirm: React.FC< <>
Summary
- {isTSSWallet && wallet && ( + {wallet && isTSSWallet(wallet) && ( { [dispatch], ); - const isTSSWallet = isTSSKey(key); const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ currentRound: 0, @@ -477,8 +476,8 @@ const Confirm = () => { wallet, recipient, transport, - ...(isTSSWallet && {tssCallbacks}), - ...(isTSSWallet && {setShowTSSProgressModal}), + ...(isTSSWallet(wallet) && {tssCallbacks}), + ...(isTSSWallet(wallet) && {setShowTSSProgressModal}), }), ); } @@ -705,7 +704,7 @@ const Confirm = () => {
{t('Summary')}
- {isTSSWallet && ( + {wallet && isTSSWallet(wallet) && ( { useState(null); const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); - const isTSSWallet = key ? isTSSKey(key) : false; const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ currentRound: 0, @@ -541,8 +540,8 @@ const Confirm = () => { key, wallet, recipient, - ...(isTSSWallet && {tssCallbacks}), - ...(isTSSWallet && {setShowTSSProgressModal}), + ...(isTSSWallet(wallet) && {tssCallbacks}), + ...(isTSSWallet(wallet) && {setShowTSSProgressModal}), }), ) : await dispatch( @@ -742,7 +741,7 @@ const Confirm = () => { {wallet || coinbaseAccount ? ( <>
Summary
- {isTSSWallet && wallet && ( + {wallet && isTSSWallet(wallet) && ( { useState(null); const [showTSSProgressModal, setShowTSSProgressModal] = useState(false); - const isTSSWallet = key ? isTSSKey(key) : false; const [tssStatus, setTssStatus] = useState('initializing'); const [tssProgress, setTssProgress] = useState({ currentRound: 0, @@ -402,8 +401,8 @@ const PayProConfirm = () => { key, wallet, recipient, - ...(isTSSWallet && {tssCallbacks}), - ...(isTSSWallet && {setShowTSSProgressModal}), + ...(isTSSWallet(wallet) && {tssCallbacks}), + ...(isTSSWallet(wallet) && {setShowTSSProgressModal}), }), ) : await dispatch( @@ -433,7 +432,9 @@ const PayProConfirm = () => { showPaymentSent({ onCloseModal, title: - wallet?.credentials.n > 1 ? t('Proposal created') : t('Payment Sent'), + wallet && wallet?.credentials.n > 1 + ? t('Proposal created') + : t('Payment Sent'), }); await sleep(1000); @@ -656,7 +657,7 @@ const PayProConfirm = () => { keyboardShouldPersistTaps={'handled'}>
Summary
- {isTSSWallet && wallet && ( + {wallet && isTSSWallet(wallet) && ( void; } +export const isTSSWallet = (wallet: Wallet): boolean => { + return !!wallet.tssKeyId; +}; + export const isTSSKey = (key: Key): boolean => { - return !!( - key?.properties?.keychain?.privateKeyShare || - key?.properties?.keychain?.reducedPrivateKeyShare || - key?.properties?.keychain?.commonKeyChain || - key?.wallets?.some(wallet => wallet.pendingTssSession) - ); + return !!key?.wallets?.some(wallet => wallet.tssKeyId) && !key.isReadOnly; }; export const requiresTSSSigning = (wallet: Wallet, key: Key): boolean => { @@ -483,19 +482,10 @@ export const startTSSSigning = txp: TransactionProposal; callbacks: TSSSigningCallbacks; timeout?: number; - isCreator?: boolean; password?: string | undefined; }): Effect> => async (dispatch, getState): Promise => { - const { - key, - wallet, - txp, - callbacks, - timeout = 600000, - isCreator, - password, - } = opts; + const {key, wallet, txp, callbacks, timeout = 600000, password} = opts; return new Promise(async (resolve, reject) => { try { @@ -588,8 +578,12 @@ export const startTSSSigning = `[TSS Sign] All ${signatures.length} signature(s) collected, pushing to BWS`, ); + // All signing participants push signatures — not just the creator. + // This handles the case where a read-only wallet creates the txp + // and the actual signers (who are not the creator) must push. + // ignore errors here let signedTXP: TransactionProposal | null = null; - if (isCreator) { + try { signedTXP = await new Promise( (resolvePush, rejectPush) => { wallet.pushSignatures( @@ -606,6 +600,12 @@ export const startTSSSigning = ); }, ); + } catch (error) { + logManager.error( + `[TSS Sign] Error pushing signatures: ${ + error instanceof Error ? error.message : JSON.stringify(error) + }`, + ); } logManager.debug('[TSS Sign] TSS signing completed successfully'); @@ -620,7 +620,7 @@ export const startTSSSigning = }); }; -const pollTxpUntilBroadcast = async ( +export const pollTxpUntilBroadcast = async ( wallet: Wallet, txpId: string, ): Promise => { @@ -692,8 +692,6 @@ export const joinTSSSigningSession = await toggleTSSModal(setShowTSSProgressModal, true); } // Both creator and joiner use startTSSSigning. - // Creator calls pushSignatures then broadcasts; joiner polls until broadcast. - const isCreator = txp.creatorId === wallet.credentials.copayerId; try { const signedTx = await dispatch( startTSSSigning({ @@ -701,19 +699,19 @@ export const joinTSSSigningSession = wallet, txp, callbacks, - isCreator, password, }), ); - if (isCreator) { - if (signedTx.status === 'accepted') { - const broadcastedTx = await broadcastTx(wallet, signedTx); - logManager.debug('[TSS Join] Broadcast complete'); - await callbacks.onStatusChange('complete'); - return broadcastedTx as TransactionProposal; - } - return signedTx; - } else { + try { + const broadcastedTx = await broadcastTx(wallet, signedTx); + logManager.debug('[TSS Join] Broadcast complete'); + await callbacks.onStatusChange('complete'); + return broadcastedTx as TransactionProposal; + } catch (_) { + // Another participant may have broadcasted already — poll for it. + logManager.debug( + '[TSS Join] Broadcast failed, polling for broadcasted status', + ); const broadcastedTxp = await pollTxpUntilBroadcast(wallet, txp.id); await callbacks.onStatusChange('complete'); return broadcastedTxp; diff --git a/src/store/wallet/utils/wallet.ts b/src/store/wallet/utils/wallet.ts index 47c7a841bc..f124094803 100644 --- a/src/store/wallet/utils/wallet.ts +++ b/src/store/wallet/utils/wallet.ts @@ -246,7 +246,7 @@ export const buildTssKeyObj = ({ keyName: keyName || `TSS Key (${tssKey.metadata.m}-of-${tssKey.metadata.n})`, hideKeyBalance: false, - isReadOnly: false, + isReadOnly: !tssKey, }; }; From 61dce3c869fc7b8079b498f1d0a8d07e360cc5d6 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Tue, 7 Apr 2026 13:31:56 -0300 Subject: [PATCH 024/110] TSS: Enhancement - add 'how to find Session ID' info text --- .../wallet/components/TSSProgressTracker.tsx | 6 +- .../wallet/screens/InviteCosigners.tsx | 142 +++++++++++++++++- 2 files changed, 144 insertions(+), 4 deletions(-) diff --git a/src/navigation/wallet/components/TSSProgressTracker.tsx b/src/navigation/wallet/components/TSSProgressTracker.tsx index f071431daf..f2a4595cd3 100644 --- a/src/navigation/wallet/components/TSSProgressTracker.tsx +++ b/src/navigation/wallet/components/TSSProgressTracker.tsx @@ -10,6 +10,8 @@ import { Success25, Action, LightBlue, + BitPay, + Midnight, } from '../../../styles/colors'; import {useTranslation} from 'react-i18next'; import { @@ -153,13 +155,13 @@ const HelpBanner = styled.View` margin-top: 8px; padding: 10px 12px; border-radius: 8px; - background-color: ${({theme: {dark}}) => (dark ? '#1E2A3A' : '#EAF2FF')}; + background-color: ${({theme: {dark}}) => (dark ? Midnight : LightBlue)}; `; const HelpBannerText = styled(BaseText)` font-size: 13px; line-height: 19px; - color: ${({theme: {dark}}) => (dark ? '#90B4D4' : '#2C5282')}; + color: ${({theme: {dark}}) => (dark ? White : BitPay)}; `; const TimeAgo: React.FC<{date: Date}> = ({date}) => { diff --git a/src/navigation/wallet/screens/InviteCosigners.tsx b/src/navigation/wallet/screens/InviteCosigners.tsx index fcde43270b..b948b17dce 100644 --- a/src/navigation/wallet/screens/InviteCosigners.tsx +++ b/src/navigation/wallet/screens/InviteCosigners.tsx @@ -1,5 +1,5 @@ import React, {useState, useEffect} from 'react'; -import {ScrollView, Modal, Share} from 'react-native'; +import {ScrollView, Modal, Share, useWindowDimensions} from 'react-native'; import styled from 'styled-components/native'; import {NativeStackScreenProps} from '@react-navigation/native-stack'; import {useTranslation} from 'react-i18next'; @@ -19,8 +19,11 @@ import { Slate30, Success25, Black, + LightBlue, + BitPay, + Midnight, } from '../../../styles/colors'; -import {Paragraph, BaseText} from '../../../components/styled/Text'; +import {BaseText} from '../../../components/styled/Text'; import { HeaderRightContainer, ScreenGutter, @@ -245,6 +248,51 @@ const ButtonWrapper = styled.View` padding: 0 16px; margin-top: 20px; `; + +const SessionIdHelpBanner = styled.View` + margin-top: 8px; + margin-bottom: 16px; + padding: 10px 12px; + border-radius: 8px; + background-color: ${({theme: {dark}}) => (dark ? Midnight : LightBlue)}; +`; + +const SessionIdHelpTitle = styled(BaseText)` + font-size: 13px; + line-height: 19px; + font-weight: 600; + color: ${({theme: {dark}}) => (dark ? White : BitPay)}; +`; + +const HelpStepRow = styled.View` + flex-direction: row; + align-items: flex-start; + margin-bottom: 10px; +`; + +const HelpStepBubble = styled.View` + width: 18px; + height: 18px; + border-radius: 9px; + align-items: center; + justify-content: center; + margin-right: 8px; + margin-top: 1px; + background-color: ${({theme: {dark}}) => (dark ? SlateDark : BitPay)}; +`; + +const HelpStepBubbleText = styled(BaseText)` + font-size: 11px; + font-weight: 700; + color: ${White}; +`; + +const HelpStepText = styled(BaseText)` + flex: 1; + font-size: 13px; + line-height: 19px; + color: ${({theme: {dark}}) => (dark ? White : BitPay)}; +`; export interface InviteCoSignersParamsList { keyId: string; } @@ -265,11 +313,15 @@ const InviteCosigners: React.FC = ({route}) => { const SuccessIcon = theme.dark ? SuccessDarkIcon : SuccessLightIcon; const QrCodeSvg = theme.dark ? QrCodeSvgGrey : QrCodeSvgBlack; + const {height: screenHeight} = useWindowDimensions(); + const isSmallScreen = screenHeight < 700; + const {keyId} = route.params; const key = useAppSelector(({WALLET}) => WALLET.keys[keyId]); const tssSession = key?.tssSession; const [isModalVisible, setIsModalVisible] = useState(false); + const [showSessionIdHelp, setShowSessionIdHelp] = useState(true); const [selectedCopayer, setSelectedCopayer] = useState( null, ); @@ -331,6 +383,7 @@ const InviteCosigners: React.FC = ({route}) => { setPendingJoinCode(null); setIsInviteShared(false); setAddCoSignerError(null); + setShowSessionIdHelp(true); }; const handleAlreadyShared = () => { @@ -458,6 +511,9 @@ const InviteCosigners: React.FC = ({route}) => { onChangeText={text => { setSessionId(text); setAddCoSignerError(null); + if (text.trim()) { + setShowSessionIdHelp(false); + } }} placeholder="" placeholderTextColor={LuckySevens} @@ -469,6 +525,88 @@ const InviteCosigners: React.FC = ({route}) => { {addCoSignerError && {addCoSignerError}} + + setShowSessionIdHelp(v => !v)}> + + {t('How to find the Session ID?')} + + + {showSessionIdHelp && ( + <> + {[ + t( + 'Ask the co-signer who is joining the wallet to open the app.', + ), + <> + {t('Tap the ')} + + + + + {t(' icon next to ')} + + {t('Your Crypto')} + + {'.'} + , + <> + {t('Select ')} + + {t('Join Shared Wallet')} + + {'.'} + , + <> + {t('Choose ')} + + {t('Threshold Signature Wallet')} + + {'.'} + , + t( + 'After the co-signer enters their name, the Session ID will be displayed and ready to share.', + ), + ].map((text, i, arr) => ( + + + + {i + 1} + + + + {text} + + + ))} + + )} + From c7ce5effe02692abb8577dc1e093ab2242a8b3bb Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 8 Apr 2026 17:01:42 -0300 Subject: [PATCH 027/110] TSS: Enhancement - use temporary directory for keyshare file and delete after share --- .../screens/wallet-settings/ExportTSSWallet.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx index 3c45406983..33c0fd277f 100644 --- a/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx +++ b/src/navigation/wallet/screens/wallet-settings/ExportTSSWallet.tsx @@ -252,6 +252,7 @@ const ExportTSSWallet = () => { }; const shareKeyshareFile = async ({password}: {password: string}) => { + let filePath: string | undefined; try { setShareButtonState('loading'); await sleep(500); @@ -272,12 +273,9 @@ const ExportTSSWallet = () => { : walletName; const filename = `${APP_NAME_UPPERCASE}-Keyshare-${displayName}.txt`; - const rootPath = - Platform.OS === 'ios' - ? RNFS.LibraryDirectoryPath - : RNFS.TemporaryDirectoryPath; + const rootPath = RNFS.TemporaryDirectoryPath; - const filePath = `${rootPath}/${filename}`; + filePath = `${rootPath}/${filename}`; const txt = t( 'Here is the encrypted keyshare backup for wallet: {{name}}\n\n{{keyshare}}\n\nTo import this backup, copy all text between {...}, including the symbols {}', @@ -295,6 +293,8 @@ const ExportTSSWallet = () => { await Share.open(opts); + RNFS.unlink(filePath).catch(() => {}); + setShareButtonState('success'); await sleep(500); setShareButtonState(undefined); @@ -303,6 +303,9 @@ const ExportTSSWallet = () => { dispatch(WalletActions.setBackupComplete(keyId)); } catch (err: any) { logManager.debug(`[shareKeyshareFile]: ${err.message}`); + if (filePath) { + RNFS.unlink(filePath).catch(() => {}); + } if (err && err.message === 'User did not share') { setShareButtonState('success'); setBackupCompleted(true); From e17dd7e582067980aa0282f1a062fc3bc271bf27 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Thu, 9 Apr 2026 09:53:47 -0300 Subject: [PATCH 028/110] E2E: Fix - move misplaced accessibilityLabel from JSX children to props --- .../wallet/components/RecoveryPhrase.tsx | 25 ++++++++++------- src/navigation/wallet/screens/AddWallet.tsx | 4 +-- .../wallet/screens/TransactionDetails.tsx | 4 +-- src/store/wallet/effects/send/send.ts | 27 ++++++++++++++----- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/navigation/wallet/components/RecoveryPhrase.tsx b/src/navigation/wallet/components/RecoveryPhrase.tsx index 75c03d1016..7ee37eee8c 100644 --- a/src/navigation/wallet/components/RecoveryPhrase.tsx +++ b/src/navigation/wallet/components/RecoveryPhrase.tsx @@ -682,8 +682,9 @@ const RecoveryPhrase = () => { {t('This process may take a few minutes to complete.')} - - accessibilityLabel="Advanced options container" + { {t('Include Testnet Wallets')} - - accessibilityLabel="Include testnet wallets" + { @@ -739,8 +741,9 @@ const RecoveryPhrase = () => { {t('Include Legacy Wallets')} - - accessibilityLabel="Include legacy wallets" + { @@ -761,8 +764,9 @@ const RecoveryPhrase = () => { {t('Specify Derivation Path')} - - accessibilityLabel="Specify derivation path" + { @@ -860,8 +864,9 @@ const RecoveryPhrase = () => { {t('Shared Wallet')} - - accessibilityLabel="Shared wallet" + { diff --git a/src/navigation/wallet/screens/AddWallet.tsx b/src/navigation/wallet/screens/AddWallet.tsx index ca645317ca..89eca4bc2b 100644 --- a/src/navigation/wallet/screens/AddWallet.tsx +++ b/src/navigation/wallet/screens/AddWallet.tsx @@ -405,8 +405,8 @@ const AddWallet = ({ toggleUseNativeSegwit()}> accessibilityLabel="Use native SegWit" + onPress={() => toggleUseNativeSegwit()}> Segwit @@ -424,8 +424,8 @@ const AddWallet = ({ toggleUseTaproot()}> accessibilityLabel="Use Taproot" + onPress={() => toggleUseTaproot()}> Taproot diff --git a/src/navigation/wallet/screens/TransactionDetails.tsx b/src/navigation/wallet/screens/TransactionDetails.tsx index 2eba471e78..ad96845bec 100644 --- a/src/navigation/wallet/screens/TransactionDetails.tsx +++ b/src/navigation/wallet/screens/TransactionDetails.tsx @@ -690,8 +690,8 @@ const TransactionDetails = () => { copyText(txs.txid!)}> accessibilityLabel="Transaction details copy txid button" + onPress={() => copyText(txs.txid!)}> {copied ? : null} @@ -728,9 +728,9 @@ const TransactionDetails = () => { diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index cc18514bd5..7dd73e54aa 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -1404,9 +1404,16 @@ export const publishAndSign = // Nonce-based chains (EVM, XRP) without a nonce need one assigned before signing. // TODO: remove deferNonce check once BWS always defers nonce for nonce chains - if ((txpToSign.deferNonce || txpToSign.nonce == null) && IsNonceChain(txpToSign.chain)) { - txpToSign = (await wallet.prepareTx({ txp: txpToSign })) as TransactionProposal; - logManager.info(`prepareTx: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`); + if ( + (txpToSign.deferNonce || txpToSign.nonce == null) && + IsNonceChain(txpToSign.chain) + ) { + txpToSign = (await wallet.prepareTx({ + txp: txpToSign, + })) as TransactionProposal; + logManager.info( + `prepareTx: BWS assigned nonce ${txpToSign.nonce} to txp ${txpToSign.id}`, + ); } if (key.isReadOnly && !key.hardwareSource) { @@ -1610,8 +1617,11 @@ export const publishAndSignMultipleProposals = const nonceTxs = txps.filter(txp => { const hasAssignedNonce = txp.nonce !== undefined; // BWS already set a nonce (normal flow) const hasDeferredNonce = txp.deferNonce; // flagged txp — app assigns nonce via prepareTx - const isMissingNonceOnNonceChain = txp.nonce == null && IsNonceChain(txp.chain); // handles when BWS always defers nonce by default (deferNonce flag won't be needed) - return hasAssignedNonce || hasDeferredNonce || isMissingNonceOnNonceChain; + const isMissingNonceOnNonceChain = + txp.nonce == null && IsNonceChain(txp.chain); // handles when BWS always defers nonce by default (deferNonce flag won't be needed) + return ( + hasAssignedNonce || hasDeferredNonce || isMissingNonceOnNonceChain + ); }); nonceTxs.sort((a, b) => (a.nonce || 0) - (b.nonce || 0)); for (const txp of nonceTxs) { @@ -1636,7 +1646,12 @@ export const publishAndSignMultipleProposals = } // Process transactions without a nonce concurrently - const withoutNonce = txps.filter(txp => txp.nonce === undefined && !txp.deferNonce && !IsNonceChain(txp.chain)); + const withoutNonce = txps.filter( + txp => + txp.nonce === undefined && + !txp.deferNonce && + !IsNonceChain(txp.chain), + ); const promisesWithoutNonce: Promise< Partial | void | Error >[] = withoutNonce.map(txp => From 89fe1733fb7edf357a30606f5b150bc13d9277b8 Mon Sep 17 00:00:00 2001 From: Gabriel Masclef Date: Thu, 9 Apr 2026 15:52:38 -0300 Subject: [PATCH 029/110] Fix: send payment_method prop for banxa CreateOrder --- .../services/buy-crypto/utils/banxa-utils.ts | 2 +- .../externalServicesOfferSelector.tsx | 52 ++++++++++--------- .../services/screens/BuyAndSellRoot.tsx | 1 + .../services/utils/external-services-utils.ts | 2 + src/store/buy-crypto/buy-crypto.models.ts | 2 + 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/navigation/services/buy-crypto/utils/banxa-utils.ts b/src/navigation/services/buy-crypto/utils/banxa-utils.ts index 4c3eb21f42..b70545bb93 100644 --- a/src/navigation/services/buy-crypto/utils/banxa-utils.ts +++ b/src/navigation/services/buy-crypto/utils/banxa-utils.ts @@ -243,7 +243,7 @@ export const getBanxaSelectedPaymentMethodData = ( ); break; case 'other': - selectedBanxaPMData = banxaPaymentMethods[0]; + selectedBanxaPMData = undefined; break; default: selectedBanxaPMData = undefined; diff --git a/src/navigation/services/components/externalServicesOfferSelector.tsx b/src/navigation/services/components/externalServicesOfferSelector.tsx index 9a40dfa3a3..c54e4b027f 100644 --- a/src/navigation/services/components/externalServicesOfferSelector.tsx +++ b/src/navigation/services/components/externalServicesOfferSelector.tsx @@ -746,7 +746,7 @@ const ExternalServicesOfferSelector: React.FC< paymentMethod, ); - if (!banxaSelectedPaymentMethodData) { + if (paymentMethod.method !== 'other' && !banxaSelectedPaymentMethodData) { const msg = t( 'Banxa currently does not support operations with the selected combination crypto(coin)-fiat(fiatCurrency)-paymentMethod(paymentMethod).', { @@ -762,32 +762,34 @@ const ExternalServicesOfferSelector: React.FC< return; } - offers.banxa.paymentMethodId = banxaSelectedPaymentMethodData.id; + offers.banxa.paymentMethodId = banxaSelectedPaymentMethodData?.id; - try { - const banxaCurrencyLimitsData = - banxaSelectedPaymentMethodData.transaction_limits.find( - tx_limit => - tx_limit.fiat_code.toUpperCase() === - offers.banxa.fiatCurrency.toUpperCase(), - ); + if (banxaSelectedPaymentMethodData?.transaction_limits) { + try { + const banxaCurrencyLimitsData = + banxaSelectedPaymentMethodData.transaction_limits.find( + tx_limit => + tx_limit.fiat_code.toUpperCase() === + offers.banxa.fiatCurrency.toUpperCase(), + ); - offers.banxa.amountLimits = { - min: banxaCurrencyLimitsData - ? Number(banxaCurrencyLimitsData.min) - : getBanxaFiatAmountLimits().min, - max: banxaCurrencyLimitsData - ? Number(banxaCurrencyLimitsData.max) - : getBanxaFiatAmountLimits().max, - }; - } catch (err) { - const errMsg = err instanceof Error ? err.message : JSON.stringify(err); - logger.debug( - `Error getting Banxa transaction limits. Setting default values. Error: ${errMsg}`, - ); - offers.banxa.amountLimits = dispatch( - getBuyCryptoFiatLimits('banxa', offers.banxa.fiatCurrency), - ); + offers.banxa.amountLimits = { + min: banxaCurrencyLimitsData + ? Number(banxaCurrencyLimitsData.min) + : getBanxaFiatAmountLimits().min, + max: banxaCurrencyLimitsData + ? Number(banxaCurrencyLimitsData.max) + : getBanxaFiatAmountLimits().max, + }; + } catch (err) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logger.debug( + `Error getting Banxa transaction limits. Setting default values. Error: ${errMsg}`, + ); + offers.banxa.amountLimits = dispatch( + getBuyCryptoFiatLimits('banxa', offers.banxa.fiatCurrency), + ); + } } if ( diff --git a/src/navigation/services/screens/BuyAndSellRoot.tsx b/src/navigation/services/screens/BuyAndSellRoot.tsx index b3e3bf52c8..18d66b3a6b 100644 --- a/src/navigation/services/screens/BuyAndSellRoot.tsx +++ b/src/navigation/services/screens/BuyAndSellRoot.tsx @@ -2312,6 +2312,7 @@ const BuyAndSellRoot = ({ target: getBanxaCoinFormat(coin), wallet_address: address, blockchain: getBanxaChainFormat(selectedWallet.chain), + payment_method: paymentMethod.method, return_url_on_success: `${APP_DEEPLINK_PREFIX}banxa?externalId=${banxaExternalId}&status=pending`, return_url_on_cancelled: `${APP_DEEPLINK_PREFIX}banxaCancelled?externalId=${banxaExternalId}&status=cancelled`, return_url_on_failure: `${APP_DEEPLINK_PREFIX}banxaFailed?externalId=${banxaExternalId}&status=failed`, diff --git a/src/navigation/services/utils/external-services-utils.ts b/src/navigation/services/utils/external-services-utils.ts index 2700a965f2..0a69f8e9e2 100644 --- a/src/navigation/services/utils/external-services-utils.ts +++ b/src/navigation/services/utils/external-services-utils.ts @@ -83,6 +83,8 @@ export const getErrorMessage = (err: any): string => { } else if (err.error.message) { msg = err.error.message; } + } else if (err?.response?.data?.message) { + msg = err.response.data.message; } else if (err.message) { msg = err.message; } diff --git a/src/store/buy-crypto/buy-crypto.models.ts b/src/store/buy-crypto/buy-crypto.models.ts index eae34e327b..41ebb037c4 100644 --- a/src/store/buy-crypto/buy-crypto.models.ts +++ b/src/store/buy-crypto/buy-crypto.models.ts @@ -189,6 +189,8 @@ export interface BanxaCreateOrderRequestData { email?: string; // Customer's mobile number. This will pre-populate the customers' mobile number field when they are redirected to Banxa checkout mobile?: string; + // Bitpay's side selected paymentMethodKey + payment_method?: PaymentMethodKey; } export interface BanxaOrderData { From e22b74f313748a4ea207ede6402e70a60bbaf4c3 Mon Sep 17 00:00:00 2001 From: johnathan White Date: Sat, 11 Apr 2026 01:31:03 -0400 Subject: [PATCH 030/110] bump 14.41.1 --- android/app/build.gradle | 4 ++-- ios/BitPayApp/Info.plist | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f4cf843dfe..616b93eb2f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -104,8 +104,8 @@ android { applicationId "com.bitpay.wallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 91212414 - versionName "14.41.0" + versionCode 91212416 + versionName "14.41.1" missingDimensionStrategy 'react-native-camera', 'mlkit' ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" diff --git a/ios/BitPayApp/Info.plist b/ios/BitPayApp/Info.plist index 6520043088..80396db1f9 100644 --- a/ios/BitPayApp/Info.plist +++ b/ios/BitPayApp/Info.plist @@ -26,7 +26,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 14.41.0 + 14.41.1 CFBundleSignature ???? CFBundleURLTypes diff --git a/package.json b/package.json index fd7b5406e4..8faf43065e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitpay", - "version": "14.41.0", + "version": "14.41.1", "private": true, "engines": { "node": ">=20" From 5df59da163842f4502d5b2bb80afc8fa5d76f454 Mon Sep 17 00:00:00 2001 From: vinotestlio Date: Mon, 4 May 2026 12:10:47 +0530 Subject: [PATCH 031/110] uitests pages and tests added --- .github/workflows/ios-xcuitest.yml | 183 +++++++++++++++ ios/BitPayApp.xcodeproj/project.pbxproj | 216 ++++++++++++++++-- .../xcshareddata/xcschemes/BitPayApp.xcscheme | 11 + .../tests/test594_BasicSwapBTC_.swift | 81 +++++++ .../tests/test595_BasicSellBTC_.swift | 74 ++++++ .../tests/test596_BasicBuyBTC_.swift | 82 +++++++ .../AllureReport/AllureXCTestSupport.swift | 75 ++++++ ios/UITests/BitPayApp.xctestplan | 33 +++ ios/UITests/pages/AddCryptoOptionPage.swift | 34 +++ ios/UITests/pages/BuyPage.swift | 37 +++ ios/UITests/pages/ConfirmPaymentPage.swift | 45 ++++ ios/UITests/pages/EnterAmountPage.swift | 57 +++++ ios/UITests/pages/HomePage.swift | 152 ++++++++++++ .../pages/ImportRecoveryPhrasePage.swift | 39 ++++ ios/UITests/pages/MykeyPage.swift | 45 ++++ ios/UITests/pages/NewRecoveryPhrasePage.swift | 89 ++++++++ ios/UITests/pages/OnboardingPage.swift | 174 ++++++++++++++ ios/UITests/pages/PortfolioBalancePage.swift | 82 +++++++ ios/UITests/pages/SelectCurrencyPage.swift | 65 ++++++ .../pages/SelectKeyToDepositPage.swift | 37 +++ ios/UITests/pages/SellPage.swift | 37 +++ ios/UITests/pages/SwapPage.swift | 113 +++++++++ ios/UITests/pages/VerifyYourPhrasePage.swift | 100 ++++++++ .../test591_OnboardingCreateWallet.swift | 96 ++++++++ .../test592_ImportWalletRecoveryPhrase.swift | 83 +++++++ .../test593_BTCConfirmPaymentScreen.swift | 78 +++++++ ios/UITests/tests/test594_BasicSwapBTC_.swift | 81 +++++++ ios/UITests/tests/test595_BasicSellBTC_.swift | 74 ++++++ ios/UITests/tests/test596_BasicBuyBTC_.swift | 82 +++++++ testlio-cli/project-config.json | 10 + testlio-cli/test-config.json | 4 + 31 files changed, 2347 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ios-xcuitest.yml create mode 100644 ios/BitPayAppUITests/tests/test594_BasicSwapBTC_.swift create mode 100644 ios/BitPayAppUITests/tests/test595_BasicSellBTC_.swift create mode 100644 ios/BitPayAppUITests/tests/test596_BasicBuyBTC_.swift create mode 100644 ios/UITests/AllureReport/AllureXCTestSupport.swift create mode 100644 ios/UITests/BitPayApp.xctestplan create mode 100644 ios/UITests/pages/AddCryptoOptionPage.swift create mode 100644 ios/UITests/pages/BuyPage.swift create mode 100644 ios/UITests/pages/ConfirmPaymentPage.swift create mode 100644 ios/UITests/pages/EnterAmountPage.swift create mode 100644 ios/UITests/pages/HomePage.swift create mode 100644 ios/UITests/pages/ImportRecoveryPhrasePage.swift create mode 100644 ios/UITests/pages/MykeyPage.swift create mode 100644 ios/UITests/pages/NewRecoveryPhrasePage.swift create mode 100644 ios/UITests/pages/OnboardingPage.swift create mode 100644 ios/UITests/pages/PortfolioBalancePage.swift create mode 100644 ios/UITests/pages/SelectCurrencyPage.swift create mode 100644 ios/UITests/pages/SelectKeyToDepositPage.swift create mode 100644 ios/UITests/pages/SellPage.swift create mode 100644 ios/UITests/pages/SwapPage.swift create mode 100644 ios/UITests/pages/VerifyYourPhrasePage.swift create mode 100644 ios/UITests/tests/test591_OnboardingCreateWallet.swift create mode 100644 ios/UITests/tests/test592_ImportWalletRecoveryPhrase.swift create mode 100644 ios/UITests/tests/test593_BTCConfirmPaymentScreen.swift create mode 100644 ios/UITests/tests/test594_BasicSwapBTC_.swift create mode 100644 ios/UITests/tests/test595_BasicSellBTC_.swift create mode 100644 ios/UITests/tests/test596_BasicBuyBTC_.swift create mode 100644 testlio-cli/project-config.json create mode 100644 testlio-cli/test-config.json diff --git a/.github/workflows/ios-xcuitest.yml b/.github/workflows/ios-xcuitest.yml new file mode 100644 index 0000000000..66cba343d5 --- /dev/null +++ b/.github/workflows/ios-xcuitest.yml @@ -0,0 +1,183 @@ +name: iOS XCUITest + +on: + workflow_dispatch: + push: + branches: [main, develop] + +jobs: + ios-ui-tests: + runs-on: macos-latest + timeout-minutes: 180 + + steps: + # Checkout Repo + - name: Checkout Repo + uses: actions/checkout@v4 + + # Select Xcode + - name: Select Xcode 26.0.1 + run: sudo xcode-select -switch /Applications/Xcode_26.0.1.app + + # Setup Node + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: yarn + + # Install JS dependencies + - name: Install JS dependencies + run: yarn install --frozen-lockfile + + # Setup Ruby + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: false + + # Install Gems + - name: Install Gems + run: | + bundle config set deployment false + bundle install + + # Disable Sentry upload + - name: Disable Sentry upload + run: | + echo "SENTRY_DISABLE_AUTO_UPLOAD=true" >> $GITHUB_ENV + + # Install Pods + - name: Install Pods + run: | + cd ios + bundle exec pod install + + # Generate React Native Bundle + - name: Generate React Native Bundle + run: | + npx react-native bundle \ + --entry-file index.js \ + --platform ios \ + --dev false \ + --bundle-output ios/main.jsbundle \ + --assets-dest ios + + # Boot Simulator + - name: Boot Simulator + run: | + xcrun simctl boot "iPhone 17" || true + xcrun simctl bootstatus "iPhone 17" -b + + # Build for testing + - name: Build for testing + run: | + set -o pipefail + xcodebuild \ + -workspace ios/BitPayApp.xcworkspace \ + -scheme BitPayApp \ + -configuration Release \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + -derivedDataPath Build \ + -only-testing:BitPayAppUITests \ + build-for-testing + + # Run XCUITest + - name: Run XCUITest + run: | + set -o pipefail + xcodebuild \ + -workspace ios/BitPayApp.xcworkspace \ + -scheme BitPayApp \ + -configuration Release \ + -destination 'platform=iOS Simulator,name=iPhone 17' \ + -derivedDataPath Build \ + -resultBundlePath TestResults.xcresult \ + -only-testing:BitPayAppUITests \ + test-without-building + + # Upload XCResult + - name: Upload XCUITest Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: xcuitest-results + path: TestResults.xcresult + + # Install Allure CLI + - name: Install Allure CLI + if: always() + run: brew install allure || true + + # Install allure-xcresult + - name: Install allure-xcresult + if: always() + run: | + if ! command -v allure-xcresult &> /dev/null; then + curl -L -o allure-xcresult https://github.com/kvld/allure-xcresult/releases/download/v1.5.0/allure-xcresult + chmod +x allure-xcresult + sudo mv allure-xcresult /usr/local/bin/ + fi + + # Convert to Allure + - name: Convert XCResult to Allure Results + if: always() + run: | + allure-xcresult \ + --input TestResults.xcresult \ + --output allure-results + + # Zip Allure + - name: Zip Allure Results + if: always() + run: | + zip -r allure-results.zip allure-results + + # Upload Allure Results + - name: Upload Allure Results Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-results + path: allure-results.zip + + # Generate HTML + - name: Generate Allure HTML Report + if: always() + run: | + allure generate allure-results --clean -o allure-report + + # Upload HTML + - name: Upload Allure HTML Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-html-report + path: allure-report + + # Install Testlio CLI + - name: Install Testlio CLI + if: always() + run: npm install -g @testlio/cli + + # Upload to Testlio + - name: Upload Results to Testlio + if: always() + run: | + export RUN_API_TOKEN="${{ secrets.TESTLIO_RUN_TOKEN }}" + + testlio create-run \ + --testConfig testlio-cli/test-config.json \ + --projectConfig testlio-cli/project-config.json \ + --externalResults true \ + --resultProvider local \ + --automatedDeviceIds 363a5ce7-6cef-436c-8531-6c07195c8fe8 + + testlio parse-run-results \ + --projectConfig testlio-cli/project-config.json \ + --path allure-results.zip \ + --automatedDeviceId 363a5ce7-6cef-436c-8531-6c07195c8fe8 + + testlio finalize-results \ + --projectConfig testlio-cli/project-config.json diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj index 1a694d0948..cd2f56b0a9 100644 --- a/ios/BitPayApp.xcodeproj/project.pbxproj +++ b/ios/BitPayApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -14,7 +14,7 @@ 256B92C82AC0ED0D00C1DF97 /* bitpay-recovery-phrase-template.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 256B92C72AC0ED0D00C1DF97 /* bitpay-recovery-phrase-template.pdf */; }; 4054F0E729AEFAF0009BBC9C /* AllowedUrlPrefixProtocol.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4054F0E529AEFAEF009BBC9C /* AllowedUrlPrefixProtocol.mm */; }; 523EFC4FD62CB0CD2FBD02FC /* libPods-BitPayApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B1AA7DD4226DBC3478258808 /* libPods-BitPayApp.a */; }; - 52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */; }; + 52C9714B9004BCE190EA469D /* BitPayApp/BundleHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */; }; 5B31AAFE2786387B002765E1 /* RCTDosh.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B31AAFD2786387B002765E1 /* RCTDosh.mm */; }; 5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B31AB5E2786515A002765E1 /* Dosh.swift */; }; 5B5631782838854000C395A8 /* PassKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5631772838854000C395A8 /* PassKit.framework */; }; @@ -32,6 +32,16 @@ F64C0BF216CE4402A51A5ACC /* Archivo-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + CD12DFFA2FA86D5D004E0E3B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = BitPayApp; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 13B07F961A680F5B00A75B9A /* BitPayApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BitPayApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = BitPayApp/Images.xcassets; sourceTree = ""; }; @@ -52,7 +62,7 @@ 5BCF1C342740B8B2006D872E /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = BitPayApp/BootSplash.storyboard; sourceTree = ""; }; 5BEA4BEE2838400700AB2121 /* RCTPaymentPass.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTPaymentPass.h; sourceTree = ""; }; 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTPaymentPass.mm; sourceTree = ""; }; - 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; 6D615C4729725FD70063DB8E /* RCTTimer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTimer.mm; sourceTree = ""; }; 6D615C4829725FD70063DB8E /* RCTTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTimer.h; sourceTree = ""; }; 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Light.ttf"; path = "../assets/fonts/Archivo-Light.ttf"; sourceTree = ""; }; @@ -61,7 +71,8 @@ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = BitPayApp/AppDelegate.swift; sourceTree = ""; }; B1AA7DD4226DBC3478258808 /* libPods-BitPayApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BitPayApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; BCDAB70057C84109A9201936 /* Archivo-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Regular.ttf"; path = "../assets/fonts/Archivo-Regular.ttf"; sourceTree = ""; }; - E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BitPayApp/BundleHash.swift; sourceTree = ""; }; + CD12DFF42FA86D5D004E0E3B /* BitPayAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BitPayAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BitPayApp/BundleHash.swift; sourceTree = ""; }; E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BitPayApp.release.xcconfig"; path = "Target Support Files/Pods-BitPayApp/Pods-BitPayApp.release.xcconfig"; sourceTree = ""; }; EC0FDD692CC92E90009E1B35 /* InAppMessageModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppMessageModule.mm; sourceTree = ""; }; ECCB92762860B25D00F9CF8B /* SilentPushEvent.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SilentPushEvent.mm; sourceTree = ""; }; @@ -71,6 +82,10 @@ F4435EB64D2448EC885B3036 /* Archivo-Black.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Black.ttf"; path = "../assets/fonts/Archivo-Black.ttf"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + CD12E04A2FA87645004E0E3B /* UITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = UITests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -81,6 +96,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CD12DFF12FA86D5D004E0E3B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -107,7 +129,7 @@ 5BEA4BEE2838400700AB2121 /* RCTPaymentPass.h */, 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */, 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */, - E30DDDCF29E5E048D75CCF1A /* BundleHash.swift */, + E30DDDCF29E5E048D75CCF1A /* BitPayApp/BundleHash.swift */, ); name = BitPayApp; sourceTree = ""; @@ -156,12 +178,14 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + CD12E04A2FA87645004E0E3B /* UITests */, 13B07FAE1A68108700A75B9A /* BitPayApp */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 7950FCCDC90E6306EDD77BCD /* Pods */, 3A6F88D6BF7C4F7A8D93264A /* Resources */, + CD12DFEF2FA86C56004E0E3B /* Recovered References */, ); indentWidth = 2; sourceTree = ""; @@ -172,10 +196,19 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* BitPayApp.app */, + CD12DFF42FA86D5D004E0E3B /* BitPayAppUITests.xctest */, ); name = Products; sourceTree = ""; }; + CD12DFEF2FA86C56004E0E3B /* Recovered References */ = { + isa = PBXGroup; + children = ( + 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */, + ); + name = "Recovered References"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -202,17 +235,45 @@ productReference = 13B07F961A680F5B00A75B9A /* BitPayApp.app */; productType = "com.apple.product-type.application"; }; + CD12DFF32FA86D5D004E0E3B /* BitPayAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CD12DFFE2FA86D5D004E0E3B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */; + buildPhases = ( + CD12DFF02FA86D5D004E0E3B /* Sources */, + CD12DFF12FA86D5D004E0E3B /* Frameworks */, + CD12DFF22FA86D5D004E0E3B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CD12DFFB2FA86D5D004E0E3B /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + CD12E04A2FA87645004E0E3B /* UITests */, + ); + name = BitPayAppUITests; + packageProductDependencies = ( + ); + productName = BitPayAppUITests; + productReference = CD12DFF42FA86D5D004E0E3B /* BitPayAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1210; TargetAttributes = { 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1240; }; + CD12DFF32FA86D5D004E0E3B = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BitPayApp" */; @@ -229,6 +290,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* BitPayApp */, + CD12DFF32FA86D5D004E0E3B /* BitPayAppUITests */, ); }; /* End PBXProject section */ @@ -252,6 +314,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CD12DFF22FA86D5D004E0E3B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -269,20 +338,6 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nSENTRY_XCODE=\"../node_modules/@sentry/react-native/scripts/sentry-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $SENTRY_XCODE\"\n"; }; - EBB510F948BF432C893398ED /* Upload Debug Symbols to Sentry */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Upload Debug Symbols to Sentry"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export SENTRY_PROPERTIES=\"$SRCROOT/../sentry.properties\"\n/bin/sh \"$SRCROOT/../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh\"\n"; - }; 3C1070649A3322DC1391852F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -313,10 +368,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-resources.sh\"\n"; @@ -349,15 +408,33 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BitPayApp/Pods-BitPayApp-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + EBB510F948BF432C893398ED /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export SENTRY_PROPERTIES=\"$SRCROOT/../sentry.properties\"\n/bin/sh \"$SRCROOT/../node_modules/@sentry/react-native/scripts/sentry-xcode-debug-files.sh\"\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -374,19 +451,33 @@ 6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */, ECCB92782860B25D00F9CF8B /* SilentPushEvent.mm in Sources */, 5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */, - 52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */, + 52C9714B9004BCE190EA469D /* BitPayApp/BundleHash.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CD12DFF02FA86D5D004E0E3B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + CD12DFFB2FA86D5D004E0E3B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* BitPayApp */; + targetProxy = CD12DFFA2FA86D5D004E0E3B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4221E54904CA4996330CEB54 /* Pods-BitPayApp.debug.xcconfig */; buildSettings = { ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME"; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; @@ -430,7 +521,6 @@ baseConfigurationReference = E639E0380D05420CD0D64119 /* Pods-BitPayApp.release.xcconfig */; buildSettings = { ALLOWED_URL_PREFIXES = "ALLOWED-URL-PREFIXES-REPLACE-ME"; - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ENABLE_MODULES = YES; @@ -645,6 +735,79 @@ }; name = Release; }; + CD12DFFC2FA86D5D004E0E3B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = BitPayApp; + }; + name = Debug; + }; + CD12DFFD2FA86D5D004E0E3B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.test.appium.BitPayAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = BitPayApp; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -666,6 +829,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CD12DFFE2FA86D5D004E0E3B /* Build configuration list for PBXNativeTarget "BitPayAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD12DFFC2FA86D5D004E0E3B /* Debug */, + CD12DFFD2FA86D5D004E0E3B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; diff --git a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme index 3874813c99..5798fabddf 100644 --- a/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme +++ b/ios/BitPayApp.xcodeproj/xcshareddata/xcschemes/BitPayApp.xcscheme @@ -38,6 +38,17 @@ ReferencedContainer = "container:BitPayApp.xcodeproj"> + + + + :` convention. + static func addLabel(_ name: String, value: String) { + XCTContext.runActivity(named: "allure.label.\(name):\(value)") { _ in } + } + + /// Sets a custom test name visible in the report. + static func setDisplayName(_ name: String) { + XCTContext.runActivity(named: "allure.name:\(name)") { _ in } + } + + /// Adds a link to an issue tracker entry. + static func addIssue(_ title: String, url: String) { + XCTContext.runActivity(named: "allure.issue.\(title):\(url)") { _ in } + } + + /// Adds a generic hyperlink label. + static func addLink(_ title: String, url: String) { + XCTContext.runActivity(named: "allure.link.\(title):\(url)") { _ in } + } + + /// Adds a plain description to the test. + static func addDescription(_ description: String) { + XCTContext.runActivity(named: "allure.description:\(description)") { _ in } + } + + /// Wraps a code block in an activity that appears as a step in Allure. + @discardableResult + static func step(_ name: String, block: () throws -> T) rethrows -> T { + try XCTContext.runActivity(named: name) { _ in try block() } + } +} + +extension XCTestCase { + /// Attaches a full-screen capture to the current test. Lifetime is `.keepAlways` + /// so it remains available in the `.xcresult` bundle for Allure to render. + func allureAttachScreenshot(name: String = "Screenshot") { + let screenshot = XCUIScreen.main.screenshot() + let attachment = XCTAttachment(screenshot: screenshot) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + /// Attaches a screenshot of the supplied application. + func allureAttachAppScreenshot(app: XCUIApplication, name: String = "App Screenshot") { + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + /// Captures failure-time artifacts if the current test did not succeed. + func allureCaptureFailureArtifacts(app: XCUIApplication?, name: String = "Failure Screenshot") { + guard let app else { return } + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } +} + diff --git a/ios/UITests/BitPayApp.xctestplan b/ios/UITests/BitPayApp.xctestplan new file mode 100644 index 0000000000..fa2bba8979 --- /dev/null +++ b/ios/UITests/BitPayApp.xctestplan @@ -0,0 +1,33 @@ +{ + "configurations" : [ + { + "id" : "B276FD7D-F0C0-4086-A649-A4A594C8C330", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "performanceAntipatternCheckerEnabled" : true, + "targetForVariableExpansion" : { + "containerPath" : "container:BitPayApp.xcodeproj", + "identifier" : "13B07F861A680F5B00A75B9A", + "name" : "BitPayApp" + }, + "uiTestingScreenshotsLifetime" : "keepAlways", + "userAttachmentLifetime" : "keepAlways" + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:BitPayApp.xcodeproj", + "identifier" : "CD620BE32F90C491005CB04D", + "name" : "BitPayAppUITests" + } + } + ], + "version" : 1 +} diff --git a/ios/UITests/pages/AddCryptoOptionPage.swift b/ios/UITests/pages/AddCryptoOptionPage.swift new file mode 100644 index 0000000000..4b26128fee --- /dev/null +++ b/ios/UITests/pages/AddCryptoOptionPage.swift @@ -0,0 +1,34 @@ +import XCTest + +class AddCryptoOptionPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + var selectAnOptionText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Select an Option'") + ).firstMatch + } + + var importKey: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Import Key'") + ).firstMatch + } + + // MARK: - Actions + + func isSelectAnOptionTitleDisplayed(timeout: TimeInterval = 30) -> Bool { + return selectAnOptionText.waitForExistence(timeout: timeout) + } + + func tapImportKey() { + importKey.tap() + } + +} diff --git a/ios/UITests/pages/BuyPage.swift b/ios/UITests/pages/BuyPage.swift new file mode 100644 index 0000000000..127302d882 --- /dev/null +++ b/ios/UITests/pages/BuyPage.swift @@ -0,0 +1,37 @@ +import XCTest + +class BuyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var buyTitle: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Buy'") + ).firstMatch + } + + var bitcoin: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Bitcoin'") + ).firstMatch + } + + + // MARK: - Validations + + func isBuyPageTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return buyTitle.waitForExistence(timeout: timeout) + } + + func tapBitcoin() { + bitcoin.tap() + } + + +} diff --git a/ios/UITests/pages/ConfirmPaymentPage.swift b/ios/UITests/pages/ConfirmPaymentPage.swift new file mode 100644 index 0000000000..fd82fc0aac --- /dev/null +++ b/ios/UITests/pages/ConfirmPaymentPage.swift @@ -0,0 +1,45 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class ConfirmPaymentPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var confirmPaymentText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Confirm Payment'") + ).firstMatch + } + + var summaryText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'SUMMARY'") + ).firstMatch + } + + + // MARK: - Actions + + func isConfirmPaymentTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return confirmPaymentText.waitForExistence(timeout: timeout) + } + + func isSummaryTextDisplayed(timeout: TimeInterval = 5) -> Bool { + return summaryText.waitForExistence(timeout: timeout) + } + + +} diff --git a/ios/UITests/pages/EnterAmountPage.swift b/ios/UITests/pages/EnterAmountPage.swift new file mode 100644 index 0000000000..1c226e614b --- /dev/null +++ b/ios/UITests/pages/EnterAmountPage.swift @@ -0,0 +1,57 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class EnterAmountPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var amountField: XCUIElement { + app.textFields.firstMatch + } + + var continueButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label CONTAINS 'Continue'") + ).firstMatch + } + + + // MARK: - Actions + + func enterAmount(amount: String = "0") { + let element = amountField + + element.tap() + + if let currentValue = element.value as? String, + !currentValue.isEmpty { + + let deleteString = String( + repeating: XCUIKeyboardKey.delete.rawValue, + count: currentValue.count + ) + element.typeText(deleteString) + } + + element.typeText(amount) + } + + func tapContinue() { + continueButton.tap() + } + + +} diff --git a/ios/UITests/pages/HomePage.swift b/ios/UITests/pages/HomePage.swift new file mode 100644 index 0000000000..1c6a9139bf --- /dev/null +++ b/ios/UITests/pages/HomePage.swift @@ -0,0 +1,152 @@ +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class HomePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var continueWithoutAnAccountButton: XCUIElement { + app.descendants(matching: .any) + .matching( + NSPredicate(format: "label == 'continue-without-an-account-button'") + ) + .firstMatch + } + + var skipButton: XCUIElement { + app.otherElements["skip-button"].firstMatch + } + + var createKeyButton: XCUIElement { + app.otherElements["create-a-key-button"].firstMatch + } + + var backupKeyLabel: XCUIElement { + app.staticTexts["Would you like to backup your key?"] + } + + var laterButton: XCUIElement { + app.staticTexts["LATER"].firstMatch + } + + var gotItButton: XCUIElement { + app.staticTexts["GOT IT"] + } + + var checkbox1: XCUIElement { + app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 0) + } + + var checkbox2: XCUIElement { + app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 1) + } + + var checkbox3: XCUIElement { + app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 2) + } + + var agreeAndContinueButton: XCUIElement { + app.otherElements["agree-and-continue-button"].firstMatch + } + + var yourPortfolioBalanceText: XCUIElement { + app.otherElements["Portfolio Balance"].firstMatch + } + + var onboardingScrollView: XCUIElement { + app.scrollViews.firstMatch + } + + var buyButton: XCUIElement { + app.buttons.containing(NSPredicate(format: "label CONTAINS 'Buy'")) + .firstMatch + } + + var backUpKey: XCUIElement { + app.staticTexts["BACK UP KEY"] + } + + var addCryptoButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Add crypto wallet'") + ).firstMatch + } + + // MARK: - Actions + + func handleTrackingPermissionIfDisplayed() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let askAppNotToTrackButton = springboard.buttons["Ask App Not to Track"] + + if askAppNotToTrackButton.waitForExistence(timeout: 30) { + askAppNotToTrackButton.tap() + } + } + + func tapContinuewithoutAnAccount() { + continueWithoutAnAccountButton.tap() + } + + func skipOnboarding() { + skipButton.tap() + } + + func createWallet() { + createKeyButton.tap() + } + + func isBackupKeyLabelDisplayed(timeout: TimeInterval = 25) -> Bool { + return backupKeyLabel.waitForExistence(timeout: timeout) + } + + func tapLater() { + laterButton.tap() + } + + func acceptTerms() { + checkbox1.tap() + checkbox2.tap() + checkbox3.tap() + } + + func tapAgreeAndContinueButton() { + agreeAndContinueButton.tap() + } + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + + func swipeOnboarding() { + onboardingScrollView.swipeRight() + } + + func tapGotIt() { + gotItButton.tap() + } + + func tapBuyOption() { + buyButton.tap() + } + + func tapBackUpKey() { + backUpKey.tap() + } + + func tapAddCryptoButton() { + addCryptoButton.tap() + } + +} diff --git a/ios/UITests/pages/ImportRecoveryPhrasePage.swift b/ios/UITests/pages/ImportRecoveryPhrasePage.swift new file mode 100644 index 0000000000..1ea89287a5 --- /dev/null +++ b/ios/UITests/pages/ImportRecoveryPhrasePage.swift @@ -0,0 +1,39 @@ +import XCTest + +class ImportRecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var inputRecoveryPhrase: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Enter recovery phrase'")) + .firstMatch + } + + var importWalletButton: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Import wallet'")) + .firstMatch + } + + // MARK: - Actions + + func isImportScreenDisplayed(timeout: TimeInterval = 5) -> Bool { + return inputRecoveryPhrase.waitForExistence(timeout: timeout) + } + + func enterRecoveryPhrase(_ phrase: String) { + inputRecoveryPhrase.tap() + inputRecoveryPhrase.typeText(phrase) + } + + func tapImportWallet() { + importWalletButton.tap() + } +} diff --git a/ios/UITests/pages/MykeyPage.swift b/ios/UITests/pages/MykeyPage.swift new file mode 100644 index 0000000000..f4e12d2f50 --- /dev/null +++ b/ios/UITests/pages/MykeyPage.swift @@ -0,0 +1,45 @@ +import XCTest + +class MyKeyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var myKeyTab: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key'") + ).firstMatch + } + + var myWalletsText: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label == 'My Wallets'") + ).firstMatch + } + + var bitcoinWallet: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'Bitcoin'") + ).firstMatch + } + + // MARK: - Validations + + func isMyKeyDisplayed(timeout: TimeInterval = 180) -> Bool { + return myKeyTab.waitForExistence(timeout: timeout) + } + + func isMyWalletsDisplayed(timeout: TimeInterval = 5) -> Bool { + return myWalletsText.waitForExistence(timeout: timeout) + } + + func isBitcoinBTCWalletDisplayed(timeout: TimeInterval = 5) -> Bool { + return bitcoinWallet.waitForExistence(timeout: timeout) + } + +} diff --git a/ios/UITests/pages/NewRecoveryPhrasePage.swift b/ios/UITests/pages/NewRecoveryPhrasePage.swift new file mode 100644 index 0000000000..37ded1f5ba --- /dev/null +++ b/ios/UITests/pages/NewRecoveryPhrasePage.swift @@ -0,0 +1,89 @@ +import XCTest + +class NewRecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var instructionText: XCUIElement { + app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'recovery phrase'") + ).firstMatch + } + + var textFields: [XCUIElement] { + app.textFields.allElementsBoundByIndex + } + + var confirmButton: XCUIElement { + app.otherElements["Confirm"].firstMatch + } + + var okButton: XCUIElement { + app.staticTexts["OK"].firstMatch + } + + // MARK: - Actions + + func isVerifyScreenDisplayed(timeout: TimeInterval = 10) -> Bool { + return instructionText.waitForExistence(timeout: timeout) + } + + func tapConfirm() { + XCTAssertTrue(confirmButton.waitForExistence(timeout: 5)) + confirmButton.tap() + } + + func tapOK() { + XCTAssertTrue(okButton.waitForExistence(timeout: 5)) + okButton.tap() + } + + // MARK: - Business Logic + + /// Extract indexes dynamically (e.g. 11, 9, 12) + func getRequiredIndexes() -> [Int] { + + XCTAssertTrue(instructionText.waitForExistence(timeout: 5)) + + let instruction = instructionText.label + + return extractNumbers(from: instruction).compactMap { Int($0) } + } + + /// Enter words dynamically + func enterRecoveryPhrase(words: [String]) { + + let indexes = getRequiredIndexes() + + for (i, index) in indexes.enumerated() { + let word = words[index - 1] + + let field = textFields[i] + XCTAssertTrue(field.waitForExistence(timeout: 5)) + + field.tap() + field.typeText(word) + } + + NSLog("Entered words for indexes \(indexes)") + } + + func extractNumbers(from text: String) -> [String] { + do { + let regex = try NSRegularExpression(pattern: "\\d+") + let results = regex.matches( + in: text, + range: NSRange(text.startIndex..., in: text) + ) + return results.map { String(text[Range($0.range, in: text)!]) } + } catch { + return [] + } + } +} diff --git a/ios/UITests/pages/OnboardingPage.swift b/ios/UITests/pages/OnboardingPage.swift new file mode 100644 index 0000000000..51d5d2a8f6 --- /dev/null +++ b/ios/UITests/pages/OnboardingPage.swift @@ -0,0 +1,174 @@ +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 10/03/26. +// + +import XCTest + +class OnboardingPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var continueWithoutAnAccountButton: XCUIElement { + app.descendants(matching: .any) + .matching(NSPredicate(format: "label == 'Continue without an account'")) + .firstMatch + } + + var skipButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Skip'") + ).firstMatch + } + + var skipBackupButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Skip backup'") + ).firstMatch + } + + var createKeyButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Create a key'") + ).firstMatch + } + + var backupKeyLabel: XCUIElement { + app.staticTexts["Would you like to backup your key?"] + } + + var laterButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'LATER'") + ).firstMatch + } + + var gotItButton: XCUIElement { + app.staticTexts["GOT IT"] + } + + // var checkbox1: XCUIElement { + // app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 0) + // } + + var checkbox1: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, My funds are held and controlled on this device'" + ) + ).firstMatch + } + + var checkbox2: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, BitPay can never recover my funds for me'" + ) + ).firstMatch + } + + var checkbox3: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate( + format: + "label CONTAINS 'Checkbox, I have read, understood and accepted the Wallet Terms of Use'" + ) + ).firstMatch + } + + // var checkbox2: XCUIElement { + // app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 1) + // } + // + // var checkbox3: XCUIElement { + // app.otherElements.matching(identifier: "checkboxBorder").element(boundBy: 2) + // } + + var agreeAndContinueButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Agree and continue'") + ).firstMatch + } + + var yourPortfolioBalanceText: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Portfolio balance info'") + ).firstMatch + } + + var onboardingScrollView: XCUIElement { + app.scrollViews.firstMatch + } + + // MARK: - Actions + + func handleTrackingPermissionIfDisplayed() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let askAppNotToTrackButton = springboard.buttons["Ask App Not to Track"] + + if askAppNotToTrackButton.waitForExistence(timeout: 30) { + askAppNotToTrackButton.tap() + } + } + + func tapContinuewithoutAnAccount() { + continueWithoutAnAccountButton.tap() + } + + func skipOnboarding() { + skipButton.tap() + } + + func skipBackup() { + skipBackupButton.tap() + } + + func createWallet() { + createKeyButton.tap() + } + + func isBackupKeyLabelDisplayed(timeout: TimeInterval = 120) -> Bool { + return backupKeyLabel.waitForExistence(timeout: timeout) + } + + func tapLater() { + laterButton.tap() + } + + func acceptTerms() { + XCTAssertTrue(checkbox1.waitForExistence(timeout: 10)) + checkbox1.tap() + checkbox2.tap() + + let coordinate = checkbox3.coordinate( + withNormalizedOffset: CGVector(dx: 0.1, dy: 0.5) + ) + coordinate.tap() + } + + func tapAgreeAndContinueButton() { + agreeAndContinueButton.tap() + } + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + + func swipeOnboarding() { + onboardingScrollView.swipeRight() + } + + func tapGotIt() { + gotItButton.tap() + } +} diff --git a/ios/UITests/pages/PortfolioBalancePage.swift b/ios/UITests/pages/PortfolioBalancePage.swift new file mode 100644 index 0000000000..673a9a3a64 --- /dev/null +++ b/ios/UITests/pages/PortfolioBalancePage.swift @@ -0,0 +1,82 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class PortfolioBalancePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var yourPortfolioBalanceText: XCUIElement { + app.otherElements["Portfolio Balance"].firstMatch + } + + + var addCryptoButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Add crypto wallet'") + ).firstMatch + } + + var buyButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Buy'") + ).firstMatch + } + + var sellButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Sell'") + ).firstMatch + } + + var sendButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Send'") + ).firstMatch + } + + var swapButton: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Swap'") + ).firstMatch + } + + // MARK: - Actions + + func isYourPortfolioBalanceTextDisplayed(timeout: TimeInterval = 25) -> Bool { + return yourPortfolioBalanceText.waitForExistence(timeout: timeout) + } + + func tapBuyButton() { + buyButton.tap() + } + + func tapSellButton() { + sellButton.tap() + } + + func tapAddCryptoButton() { + addCryptoButton.tap() + } + + func tapSendButton() { + sendButton.tap() + } + + func tapSwapButton() { + swapButton.tap() + } + +} diff --git a/ios/UITests/pages/SelectCurrencyPage.swift b/ios/UITests/pages/SelectCurrencyPage.swift new file mode 100644 index 0000000000..690a327e82 --- /dev/null +++ b/ios/UITests/pages/SelectCurrencyPage.swift @@ -0,0 +1,65 @@ + +// +// OnboardingPage.swift +// BitPayApp +// +// Created by vinoth vasu on 16/03/26. +// + +import XCTest + +class SelectCurrencyPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var selectCurrencyTitle: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Select a Currency'") + ).firstMatch + } + + var bitcoinCurrency: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label CONTAINS 'Bitcoin, BTC'") + ).firstMatch + } + + var sendToTitle: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Send To'") + ).firstMatch + } + + var recipientAddress: XCUIElement { + app.descendants(matching: .any).element( + matching: NSPredicate(format: "label == 'Recipient address'") + ).firstMatch + } + + + // MARK: - Actions + + func isSelectCurrencyTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return selectCurrencyTitle.waitForExistence(timeout: timeout) + } + + func tapBitcoinCurrency() { + bitcoinCurrency.tap() + } + + func isSendToTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return sendToTitle.waitForExistence(timeout: timeout) + } + + func enterRecipientAddress(address: String) { + recipientAddress.tap() + recipientAddress.typeText(address) + } + +} diff --git a/ios/UITests/pages/SelectKeyToDepositPage.swift b/ios/UITests/pages/SelectKeyToDepositPage.swift new file mode 100644 index 0000000000..9f37acdcb4 --- /dev/null +++ b/ios/UITests/pages/SelectKeyToDepositPage.swift @@ -0,0 +1,37 @@ +import XCTest + +class SelectKeyToDepositPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var selectKeyToDeposit: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select Key to Deposit to'") + ).firstMatch + } + + var myKeyWallet: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key wallet'") + ).firstMatch + } + + + // MARK: - Validations + + func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool { + return selectKeyToDeposit.waitForExistence(timeout: timeout) + } + + func tapMyKeyWallet() { + myKeyWallet.tap() + } + + +} diff --git a/ios/UITests/pages/SellPage.swift b/ios/UITests/pages/SellPage.swift new file mode 100644 index 0000000000..7b691165a5 --- /dev/null +++ b/ios/UITests/pages/SellPage.swift @@ -0,0 +1,37 @@ +import XCTest + +class SellPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var sellTitle: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Sell'") + ).firstMatch + } + + var chooseCrypto: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Choose Crypto'") + ).firstMatch + } + + + // MARK: - Validations + + func isSellPageTitleDisplayed(timeout: TimeInterval = 180) -> Bool { + return sellTitle.waitForExistence(timeout: timeout) + } + + func tapChooseCrypto() { + chooseCrypto.tap() + } + + +} diff --git a/ios/UITests/pages/SwapPage.swift b/ios/UITests/pages/SwapPage.swift new file mode 100644 index 0000000000..b642827b34 --- /dev/null +++ b/ios/UITests/pages/SwapPage.swift @@ -0,0 +1,113 @@ +import XCTest + +class SwapPage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var swapTitle: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Swap'") + ).firstMatch + } + + var selectWalletFrom: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select wallet to swap from'") + ).firstMatch + } + + var cryptoToSwapPage: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Crypto to Swap'") + ).firstMatch + } + + var bitcoin: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Bitcoin, BTC'") + ).firstMatch + } + + var selectWalletTo: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select wallet to swap to'") + ).firstMatch + } + + var swapTo: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Swap To'") + ).firstMatch + } + + var ethereum: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Ethereum, ETH'") + ).firstMatch + } + + var selectKeyToDeposit: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'Select Key to Deposit to'") + ).firstMatch + } + + var myKeyWallet: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'My Key wallet'") + ).firstMatch + } + + var evmAccount: XCUIElement { + app.otherElements.matching( + NSPredicate(format: "label == 'EVM Account'") + ).firstMatch + } + + + // MARK: - Validations + + func isSwapPageTitleDisplayed(timeout: TimeInterval = 5) -> Bool { + return swapTitle.waitForExistence(timeout: timeout) + } + + func tapSelectWalletFrom() { + selectWalletFrom.tap() + } + + func isCryptoToSwapPageDisplayed(timeout: TimeInterval = 5) -> Bool { + return cryptoToSwapPage.waitForExistence(timeout: timeout) + } + + func tapBitcoin() { + bitcoin.tap() + } + + func tapSelectWalletTo() { + selectWalletTo.tap() + } + + func isSwapToPageDisplayed(timeout: TimeInterval = 5) -> Bool { + return swapTo.waitForExistence(timeout: timeout) + } + + func tapEthereum() { + ethereum.tap() + } + + func isSelectKeyToDepositToDisplayed(timeout: TimeInterval = 5) -> Bool { + return selectKeyToDeposit.waitForExistence(timeout: timeout) + } + + func tapMyKeyWallet() { + myKeyWallet.tap() + } + + +} diff --git a/ios/UITests/pages/VerifyYourPhrasePage.swift b/ios/UITests/pages/VerifyYourPhrasePage.swift new file mode 100644 index 0000000000..dde1166147 --- /dev/null +++ b/ios/UITests/pages/VerifyYourPhrasePage.swift @@ -0,0 +1,100 @@ +import XCTest + +class RecoveryPhrasePage { + + let app: XCUIApplication + + init(app: XCUIApplication) { + self.app = app + } + + // MARK: - Elements + + var recoveryPhraseTitle: XCUIElement { + app.staticTexts["Recovery Phrase"] + } + + var nextButton: XCUIElement { + app.otherElements["next-button"].firstMatch + } + + var allStaticTexts: XCUIElementQuery { + app.staticTexts + } + + var textFields: [XCUIElement] { + app.textFields.allElementsBoundByIndex + } + + // MARK: - Actions + + func isRecoveryPhraseDisplayed(timeout: TimeInterval = 10) -> Bool { + return recoveryPhraseTitle.waitForExistence(timeout: timeout) + } + + func tapNext() { + XCTAssertTrue(nextButton.waitForExistence(timeout: 5)) + nextButton.tap() + } + + // MARK: - Business Logic + + /// Extract 12 recovery words dynamically + func getRecoveryWords() -> [String] { + return allStaticTexts.allElementsBoundByIndex.compactMap { element in + let text = element.label.trimmingCharacters(in: .whitespaces) + + // Only lowercase words (filters headers, numbers) + let isWord = + text.range(of: "^[a-z]+$", options: .regularExpression) != nil + return isWord ? text : nil + } + .prefix(12) + .map { $0 } + } + + func extractNumbers(from text: String) -> [String] { + do { + let regex = try NSRegularExpression(pattern: "\\d+") + let results = regex.matches( + in: text, + range: NSRange(text.startIndex..., in: text) + ) + return results.map { String(text[Range($0.range, in: text)!]) } + } catch { + return [] + } + } + + /// Extract indexes like 2nd, 11th, 9th dynamically + func getRequiredIndexes() -> [Int] { + + let instructionElement = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS 'recovery phrase correctly'") + ).firstMatch + + XCTAssertTrue(instructionElement.waitForExistence(timeout: 5)) + + let instruction = instructionElement.label + + return extractNumbers(from: instruction).compactMap { Int($0) } + } + + /// Enter recovery phrase based on required indexes + func enterRecoveryPhrase(words: [String]) { + + let indexes = getRequiredIndexes() + + for (i, index) in indexes.enumerated() { + let word = words[index - 1] + + let field = textFields[i] + XCTAssertTrue(field.waitForExistence(timeout: 5)) + + field.tap() + field.typeText(word) + } + + NSLog("Entered words for indexes \(indexes)") + } +} diff --git a/ios/UITests/tests/test591_OnboardingCreateWallet.swift b/ios/UITests/tests/test591_OnboardingCreateWallet.swift new file mode 100644 index 0000000000..7bb1afe676 --- /dev/null +++ b/ios/UITests/tests/test591_OnboardingCreateWallet.swift @@ -0,0 +1,96 @@ +import XCTest + +final class OnboardingCreateWallet: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + onboardingPage.handleTrackingPermissionIfDisplayed() + } + + @MainActor + func testOnboardingCreateWallet() throws { + + AllureXCTestSupport.setDisplayName( + "Onboarding Create Wallet (Continue Without Account)" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "003e6686-1128-4e66-b020-aa03f3b6ec67" + ) + AllureXCTestSupport.addDescription( + "Onboarding Create Wallet without creating a new" + ) + + let onboardingPage = OnboardingPage(app: app) + + AllureXCTestSupport.step( + "(4) On the onboarding screen “Seamlessly buy & swap”, tap Continue without an account." + ) { + onboardingPage.tapContinuewithoutAnAccount() + } + + AllureXCTestSupport.step( + "(5) On the “Turn on notifications” screen, tap Skip." + ) { + onboardingPage.skipOnboarding() + } + + AllureXCTestSupport.step( + "(6) On the “Protect your wallet” screen, tap Skip" + ) { + onboardingPage.skipOnboarding() //To skip protect wallet + } + + AllureXCTestSupport.step( + "(7) On the “Create a key or import an existing key” screen, tap Create a Key." + ) { + onboardingPage.createWallet() + } + + AllureXCTestSupport.step( + "(8) Wait until wallet creation finishes and the “Would you like to backup your key?” screen is displayed." + ) { + XCTAssertTrue( + onboardingPage.isBackupKeyLabelDisplayed(), + "Backup key page did not appear after tapping 'Create a Key'" + ) + } + + AllureXCTestSupport.step("(9) Tap Skip on the backup prompt screen.") { + onboardingPage.skipBackup() //To skip backup key + } + + AllureXCTestSupport.step( + "(10) On the “Are you sure?” confirmation modal, tap Later. " + ) { + onboardingPage.tapLater() + } + + AllureXCTestSupport.step( + "(11, 12 & 13) On the “Important” screen, select all the three agreements checkbox. " + ) { + onboardingPage.acceptTerms() + } + + AllureXCTestSupport.step("(14) Tap Agree and Continue. ") { + onboardingPage.tapAgreeAndContinueButton() + } + + AllureXCTestSupport.step( + "(15) Wait until the Home screen finishes loading. " + ) { + XCTAssertTrue( + onboardingPage.isYourPortfolioBalanceTextDisplayed(), + "Home Page - Your Portfolio Balance text is not displayed" + ) + } + } + +} diff --git a/ios/UITests/tests/test592_ImportWalletRecoveryPhrase.swift b/ios/UITests/tests/test592_ImportWalletRecoveryPhrase.swift new file mode 100644 index 0000000000..af38ef6f71 --- /dev/null +++ b/ios/UITests/tests/test592_ImportWalletRecoveryPhrase.swift @@ -0,0 +1,83 @@ +import XCTest + +final class ImportWalletRecoveryPhrase: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + let onboardingPage = OnboardingPage(app: app) + // onboardingPage.handleTrackingPermissionIfDisplayed() + } + + @MainActor + func testImportWalletRecoveryPhrase() throws { + + AllureXCTestSupport.setDisplayName( + "Import Wallet via Recovery Phrase (BTC)" + ) + AllureXCTestSupport.addLabel( + "testlioManualTestID", + value: "1ca663da-1909-451f-9908-25ba9cdc0396" + ) + AllureXCTestSupport.addDescription( + "Import Wallet via Recovery Phrase (BTC)" + ) + + let portfolioBalancePage = PortfolioBalancePage(app: app) + let selectAnOptionPage = AddCryptoOptionPage(app: app) + let importRecoveryPhasePage = ImportRecoveryPhrasePage(app: app) + let myKeyPage = MyKeyPage(app: app) + + AllureXCTestSupport.step( + "(1 to 3) On the Home screen, tap the Plus (+) control near the “Your Crypto” section to open wallet/key options." + ) { + portfolioBalancePage.tapAddCryptoButton() + } + + AllureXCTestSupport.step( + "(4) Wait until the “Select an Option” screen is displayed." + ) { + XCTAssertTrue( + selectAnOptionPage.isSelectAnOptionTitleDisplayed(), + "Select an option page is not displayed" + ) + } + + AllureXCTestSupport.step("(5) Tap Import Key") { + selectAnOptionPage.tapImportKey() + } + + AllureXCTestSupport.step( + "(6 to 7) On the “Import” screen, tap the Recovery Phrase input field & Enter a valid Recovery Phrase. " + ) { + importRecoveryPhasePage.enterRecoveryPhrase( + "hobby short divert lady spare quit act settle body town license alone" + ) + } + + AllureXCTestSupport.step("(8) Tap Import Wallet") { + importRecoveryPhasePage.tapImportWallet() + } + + AllureXCTestSupport.step( + "(9) Wait until the import finishes and the My Key screen is displayed (use an extended wait to account for slower imports). " + ) { + XCTAssertTrue(myKeyPage.isMyKeyDisplayed(), "Is My Key not displayed") + XCTAssertTrue( + myKeyPage.isMyWalletsDisplayed(), + "My Wallets not displayed" + ) + XCTAssertTrue( + myKeyPage.isBitcoinBTCWalletDisplayed(), + "Bitcoin BTC Wallet not displayed" + ) + } + + } + +} diff --git a/ios/UITests/tests/test593_BTCConfirmPaymentScreen.swift b/ios/UITests/tests/test593_BTCConfirmPaymentScreen.swift new file mode 100644 index 0000000000..c7bcdbc8df --- /dev/null +++ b/ios/UITests/tests/test593_BTCConfirmPaymentScreen.swift @@ -0,0 +1,78 @@ +//import XCTest +// +//final class BTCConfirmPayment: XCTestCase { +// +// var app: XCUIApplication! +// +// override func setUp() { +// continueAfterFailure = false +// +// app = XCUIApplication() +// app.launch() +// +// let onboardingPage = OnboardingPage(app: app) +// onboardingPage.handleTrackingPermissionIfDisplayed() +// } +// +// @MainActor +// func testBTCConfirmPayment() throws { +// +// AllureXCTestSupport.setDisplayName( +// "Basic Send (BTC) - Confirm Payment Screen" +// ) +// AllureXCTestSupport.addLabel( +// "testlioManualTestID", +// value: "be4a044a-2c3a-4cad-aa5c-9398f415b483" +// ) +// AllureXCTestSupport.addDescription( +// "Basic Send (BTC) - Confirm Payment Screen" +// ) +// +// let portfolioBalancePage = PortfolioBalancePage(app: app) +// let selectCurrencyPage = SelectCurrencyPage(app: app) +// let enterAmountPage = EnterAmountPage(app: app) +// let confirmPaymentPage = ConfirmPaymentPage(app: app) +// +// AllureXCTestSupport.step("(1 to 4) On the Home screen, tap Send in the top menu.") { +// portfolioBalancePage.tapSendButton() +// } +// +// AllureXCTestSupport.step("(5) Wait until the Select a Currency screen is displayed. ") { +// XCTAssertTrue( +// selectCurrencyPage.isSelectCurrencyTitleDisplayed(), +// "Select Currency page not displayed" +// ) +// } +// +// AllureXCTestSupport.step("(6) Tap Bitcoin.") { +// selectCurrencyPage.tapBitcoinCurrency() +// } +// +// AllureXCTestSupport.step("(7) Wait until the Send To screen is displayed.") { +// selectCurrencyPage.isSendToTitleDisplayed() +// } +// +// AllureXCTestSupport.step("(8 & 9) Tap the Search contact or enter address field & Paste the receiver address.") { +// selectCurrencyPage.enterRecipientAddress(address: "bc1q0effzahtsn685tyjppgukpvfhv37hrlm4g67ws") +// } +// +// AllureXCTestSupport.step("(10 & 11) Wait until the Amount screen is displayed & Enter 0.00005 BTC.") { +// enterAmountPage.enterAmount(amount: "0.00005") +// } +// +// AllureXCTestSupport.step("(12) Tap Continue and Verify") { +// enterAmountPage.tapContinue() +// +// XCTAssertTrue( +// confirmPaymentPage.isConfirmPaymentTitleDisplayed(), +// "Confirm payment page not displayed" +// ) +// +// XCTAssertTrue( +// confirmPaymentPage.isSummaryTextDisplayed(), +// "Confirm payment - Summary text not displayed" +// ) +// +// } +// } +//} diff --git a/ios/UITests/tests/test594_BasicSwapBTC_.swift b/ios/UITests/tests/test594_BasicSwapBTC_.swift new file mode 100644 index 0000000000..d7c2707aaa --- /dev/null +++ b/ios/UITests/tests/test594_BasicSwapBTC_.swift @@ -0,0 +1,81 @@ +//import XCTest +// +//final class BasicSwapBTC: XCTestCase { +// +// var app: XCUIApplication! +// +// override func setUp() { +// continueAfterFailure = false +// +// app = XCUIApplication() +// app.launch() +// +// let onboardingPage = OnboardingPage(app: app) +// onboardingPage.handleTrackingPermissionIfDisplayed() +// } +// +// @MainActor +// func testBasicSwapBTC() throws { +// +// AllureXCTestSupport.setDisplayName( +// "Basic Swap BTC to ETH" +// ) +// AllureXCTestSupport.addLabel( +// "testlioManualTestID", +// value: "947f685f-e106-4708-922f-43419f9f8f50" +// ) +// AllureXCTestSupport.addDescription( +// "Basic Swap BTC to ETH (View Offers)" +// ) +// +// let portfolioBalancePage = PortfolioBalancePage(app: app) +// let swapPage = SwapPage(app: app) +// let selectKeyToDepositPage = SelectKeyToDepositPage(app: app) +// let enterAmountPage = EnterAmountPage(app: app) +// let selectCurrencyPage = SelectCurrencyPage(app: app) +// +// AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Swap in the top menu. ") { +// portfolioBalancePage.tapSwapButton() +// } +// +// AllureXCTestSupport.step("(5) Wait until the Swap Crypto screen is displayed.") { +// XCTAssertTrue(swapPage.isSwapPageTitleDisplayed(), "Swap page not displayed") +// } +// +// AllureXCTestSupport.step("(6) In the Swap From section, tap Select Wallet") { +// swapPage.tapSelectWalletFrom() +// } +// +// AllureXCTestSupport.step("(7) On the Crypto to Swap screen, tap Bitcoin (BTC)") { +// swapPage.tapBitcoin() +// } +// +// AllureXCTestSupport.step("(8) Back on the Swap Crypto screen, in the Swap To section, tap Select Crypto.") { +// swapPage.tapSelectWalletTo() +// } +// +// AllureXCTestSupport.step("(9) On the Swap To list screen, tap Ethereum (ETH).") { +// swapPage.tapEthereum() +// } +// +// AllureXCTestSupport.step("(10) On the Select Key to Deposit to screen, tap My Key (choose the entry that has funds available if multiple are shown).") { +// XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed") +// selectKeyToDepositPage.tapMyKeyWallet() +// } +// +// AllureXCTestSupport.step("(11) On the Swap Crypto screen, tap Enter Amount.") { +// } +// +// AllureXCTestSupport.step("(12) On the Swap Amount bottom sheet, enter 0.0006 BTC.") { +// } +// +// AllureXCTestSupport.step("(13) Tap Continue.") { +// } +// +// AllureXCTestSupport.step("(14) Tap View Offers.") { +// } +// +// AllureXCTestSupport.step("(15) Wait until the Offers screen is displayed. ") { +// } +// } +//} diff --git a/ios/UITests/tests/test595_BasicSellBTC_.swift b/ios/UITests/tests/test595_BasicSellBTC_.swift new file mode 100644 index 0000000000..3612bc7f7a --- /dev/null +++ b/ios/UITests/tests/test595_BasicSellBTC_.swift @@ -0,0 +1,74 @@ +//import XCTest +// +//final class BasicSellBTC: XCTestCase { +// +// var app: XCUIApplication! +// +// override func setUp() { +// continueAfterFailure = false +// +// app = XCUIApplication() +// app.launch() +// +// let onboardingPage = OnboardingPage(app: app) +// onboardingPage.handleTrackingPermissionIfDisplayed() +// } +// +// @MainActor +// func testBasicSellBTC() throws { +// +// AllureXCTestSupport.setDisplayName( +// "Basic Sell BTC" +// ) +// AllureXCTestSupport.addLabel( +// "testlioManualTestID", +// value: "dfc4301a-fad9-4d7f-a13a-91ca1f1d54a7" +// ) +// AllureXCTestSupport.addDescription( +// "Basic Sell BTC (Continue With Opens Browser)" +// ) +// +// let portfolioBalancePage = PortfolioBalancePage(app: app) +// let sellPage = SellPage(app: app) +// let enterAmountPage = EnterAmountPage(app: app) +// let selectCurrencyPage = SelectCurrencyPage(app: app) +// +// AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Sell.") { +// portfolioBalancePage.tapSellButton() +// } +// +// AllureXCTestSupport.step("(5) Wait until the Sell screen is displayed.") { +// XCTAssertTrue(sellPage.isSellPageTitleDisplayed(), "Sell page not displayed") +// } +// +// AllureXCTestSupport.step("(6) Enter $30 as the sell amount.") { +// enterAmountPage.enterAmount(amount: "30") +// } +// +// AllureXCTestSupport.step("(7) Tap Choose Crypto.") { +// sellPage.tapChooseCrypto() +// } +// +// AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") { +// selectCurrencyPage.tapBitcoinCurrency() +// } +// +// AllureXCTestSupport.step("(9) Tap Continue with.") { +// enterAmountPage.tapContinue() +// } +// +// AllureXCTestSupport.step("(10) Wait for the browser / in-app webview to open. ") { +// // +// // XCTAssertTrue( +// // confirmPaymentPage.isConfirmPaymentTitleDisplayed(), +// // "Confirm payment page not displayed" +// // ) +// // +// // XCTAssertTrue( +// // confirmPaymentPage.isSummaryTextDisplayed(), +// // "Confirm payment - Summary text not displayed" +// // ) +// +// } +// } +//} diff --git a/ios/UITests/tests/test596_BasicBuyBTC_.swift b/ios/UITests/tests/test596_BasicBuyBTC_.swift new file mode 100644 index 0000000000..8ce96f218d --- /dev/null +++ b/ios/UITests/tests/test596_BasicBuyBTC_.swift @@ -0,0 +1,82 @@ +//import XCTest +// +//final class BasicBuyBTC: XCTestCase { +// +// var app: XCUIApplication! +// +// override func setUp() { +// continueAfterFailure = false +// +// app = XCUIApplication() +// app.launch() +// +// let onboardingPage = OnboardingPage(app: app) +// onboardingPage.handleTrackingPermissionIfDisplayed() +// } +// +// @MainActor +// func testBasicBuyBTC() throws { +// +// AllureXCTestSupport.setDisplayName( +// "Basic Buy BTC" +// ) +// AllureXCTestSupport.addLabel( +// "testlioManualTestID", +// value: "3c9c0271-76ba-4da3-88ad-8a5d6ea60211" +// ) +// AllureXCTestSupport.addDescription( +// "Basic Buy BTC (Best Offer → Continue With Opens Browser)" +// ) +// +// let portfolioBalancePage = PortfolioBalancePage(app: app) +// let buyPage = BuyPage(app: app) +// let selectKeyToDepositPage = SelectKeyToDepositPage(app: app) +// let enterAmountPage = EnterAmountPage(app: app) +// let selectCurrencyPage = SelectCurrencyPage(app: app) +// +// AllureXCTestSupport.step("(1 to 4) From the Home screen, tap Buy.") { +// portfolioBalancePage.tapBuyButton() +// } +// +// AllureXCTestSupport.step("(5) Wait until the Buy screen is displayed. ") { +// XCTAssertTrue(buyPage.isBuyPageTitleDisplayed(), "Buy page not displayed") +// } +// +// AllureXCTestSupport.step("(6) Enter $30 as the buy amount.") { +// enterAmountPage.enterAmount(amount: "30") +// } +// +// AllureXCTestSupport.step("(7) Tap Choose Crypto.") { +// buyPage.tapBitcoin() +// } +// +// AllureXCTestSupport.step("(8) On the Select Crypto screen, tap Bitcoin (BTC). ") { +// selectCurrencyPage.tapBitcoinCurrency() +// } +// +// AllureXCTestSupport.step("(9) On the Select Key to Deposit to screen, tap My Key with funds available.") { +// XCTAssertTrue(selectKeyToDepositPage.isSelectKeyToDepositToDisplayed(), "Select Key to Deposit to dialog not displayed") +// selectKeyToDepositPage.tapMyKeyWallet() +// } +// +// AllureXCTestSupport.step("(10) Wait until best-offer search completes and at least one offer is displayed.") { +// // +// // XCTAssertTrue( +// // confirmPaymentPage.isConfirmPaymentTitleDisplayed(), +// // "Confirm payment page not displayed" +// // ) +// // +// // XCTAssertTrue( +// // confirmPaymentPage.isSummaryTextDisplayed(), +// // "Confirm payment - Summary text not displayed" +// // ) +// +// } +// +// AllureXCTestSupport.step("(11) Tap Continue with {ProviderName}.") { +// } +// +// AllureXCTestSupport.step("(12) Wait for the browser / in-app webview to open.") { +// } +// } +//} diff --git a/testlio-cli/project-config.json b/testlio-cli/project-config.json new file mode 100644 index 0000000000..1ea5da9269 --- /dev/null +++ b/testlio-cli/project-config.json @@ -0,0 +1,10 @@ +{ + "baseURI": "https://api.testlio.com/", + "platformURI": "https://app.testlio.com/tmt/project/", + "projectId": "3307", + "automatedRunCollectionGuid": "2df42fa4-225b-430e-882d-e5fc3ec31de7", + "testRunCollectionGuid": "a77e695c-5061-4451-98fe-4f93b8a26d9b", + "resultCollectionGuid": "3688212d-e132-40a3-a4bd-aae8b664a2d8", + "workspaceName": "bitpay---sandbox" +} + diff --git a/testlio-cli/test-config.json b/testlio-cli/test-config.json new file mode 100644 index 0000000000..a22b7fcc48 --- /dev/null +++ b/testlio-cli/test-config.json @@ -0,0 +1,4 @@ +{ + "automatedTestNamePrefix": "Automation Run: iOS __BUILD_VERSION__" +} + From 0538771f1aa13f5891f3ae8a23f9f51bdd54b493 Mon Sep 17 00:00:00 2001 From: vinotestlio Date: Mon, 4 May 2026 16:40:30 +0530 Subject: [PATCH 032/110] master branch updated --- .github/workflows/ios-xcuitest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ios-xcuitest.yml b/.github/workflows/ios-xcuitest.yml index 66cba343d5..fa91dae7cf 100644 --- a/.github/workflows/ios-xcuitest.yml +++ b/.github/workflows/ios-xcuitest.yml @@ -3,7 +3,7 @@ name: iOS XCUITest on: workflow_dispatch: push: - branches: [main, develop] + branches: [master, develop] jobs: ios-ui-tests: From 656b12f4dbe2df4bfaee09fa86328ae76173a537 Mon Sep 17 00:00:00 2001 From: vinotestlio Date: Mon, 4 May 2026 16:49:56 +0530 Subject: [PATCH 033/110] pull request codn added --- .github/workflows/ios-xcuitest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ios-xcuitest.yml b/.github/workflows/ios-xcuitest.yml index fa91dae7cf..77875e387a 100644 --- a/.github/workflows/ios-xcuitest.yml +++ b/.github/workflows/ios-xcuitest.yml @@ -4,6 +4,8 @@ on: workflow_dispatch: push: branches: [master, develop] + pull_request: + branches: [master, develop] jobs: ios-ui-tests: From 8c918507ddb6fdd0cdd9ce5432f8d1d4fca366dc Mon Sep 17 00:00:00 2001 From: vinotestlio Date: Thu, 7 May 2026 19:52:42 +0530 Subject: [PATCH 034/110] master src merge --- .../onboarding/screens/TermsOfUse.tsx | 16 - .../ExternalServicesLoadingWalletSkeleton.tsx | 38 ++ .../tabs/home/components/AssetRow.tsx | 5 - .../tabs/home/hooks/usePortfolioAssetRows.ts | 394 ------------------ src/navigation/wallet/screens/send/SendTo.tsx | 4 +- src/portfolio/core/pnl/types.ts | 43 ++ src/store/wallet/effects/import/import.ts | 21 - src/store/wallet/effects/status/status.ts | 16 - 8 files changed, 83 insertions(+), 454 deletions(-) create mode 100644 src/navigation/services/components/ExternalServicesLoadingWalletSkeleton.tsx delete mode 100644 src/navigation/tabs/home/hooks/usePortfolioAssetRows.ts create mode 100644 src/portfolio/core/pnl/types.ts diff --git a/src/navigation/onboarding/screens/TermsOfUse.tsx b/src/navigation/onboarding/screens/TermsOfUse.tsx index 44e0c73eed..1b2a900025 100644 --- a/src/navigation/onboarding/screens/TermsOfUse.tsx +++ b/src/navigation/onboarding/screens/TermsOfUse.tsx @@ -175,21 +175,6 @@ const TermsOfUse: React.FC = ({route, navigation}) => {