diff --git a/.detoxrc.js b/.detoxrc.js index 0b5b837895f..dae464e8324 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -4,6 +4,8 @@ module.exports = { args: { config: 'e2e/jest.e2e.config.js', _: ['e2e'], + bail: true, + forceExit: true, }, }, devices: { diff --git a/.eslintrc.js b/.eslintrc.js index 6df1e6e1e52..f8cb2150123 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,5 +39,6 @@ module.exports = { ], 'jest/expect-expect': 'off', 'jest/no-disabled-tests': 'off', + 'no-nested-ternary': 'off', }, }; diff --git a/.github/workflows/macstadium-tests.yml b/.github/workflows/macstadium-tests.yml index b4dfe0d643e..4748f7b8df8 100644 --- a/.github/workflows/macstadium-tests.yml +++ b/.github/workflows/macstadium-tests.yml @@ -154,7 +154,7 @@ jobs: run: | rm -rf /Users/administrator/.cocoapods/repos/cocoapods/.git/index.lock yarn install-bundle && yarn install-pods - + - uses: irgaly/xcode-cache@v1 with: key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} @@ -164,11 +164,12 @@ jobs: run: | sed -i'' -e "s/IS_TESTING=false/IS_TESTING=true/" .env && rm -f .env-e yarn detox build --configuration ios.sim.release + - name: Detox iOS e2e tests serial + run: | + ./scripts/run-serial-e2e.sh 3 - name: Detox iOS e2e tests parallel run: | - ./scripts/run-parallel-e2e.sh 3 + ./scripts/run-parallel-e2e.sh - - name: Detox iOS e2e tests serial - run: | - ./scripts/run-serial-e2e.sh 3 + diff --git a/e2e/Disabled/hardhatTransactionFlowSendAndWC.disable.js b/e2e/Disabled/anvilTransactionFlowSendAndWC.disable.js similarity index 97% rename from e2e/Disabled/hardhatTransactionFlowSendAndWC.disable.js rename to e2e/Disabled/anvilTransactionFlowSendAndWC.disable.js index 078e7ef09d2..d31a2bd1c3d 100644 --- a/e2e/Disabled/hardhatTransactionFlowSendAndWC.disable.js +++ b/e2e/Disabled/anvilTransactionFlowSendAndWC.disable.js @@ -40,11 +40,11 @@ const getOnchainBalance = async (address, tokenContractAddress) => { }; beforeAll(async () => { - await Helpers.startHardhat(); + await Helpers.startAnvil(); await Helpers.startIosSimulator(); }); -describe.skip('Hardhat Transaction Flow', () => { +describe.skip('Anvil Transaction Flow', () => { it('Should show the welcome screen', async () => { await Helpers.checkIfVisible('welcome-screen'); }); @@ -85,9 +85,9 @@ describe.skip('Hardhat Transaction Flow', () => { await Helpers.sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await Helpers.waitAndTap('dev-button-hardhat'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await Helpers.waitAndTap('dev-button-anvil'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); }); it('Should open send sheet after tapping send fab', async () => { @@ -392,6 +392,6 @@ describe.skip('Hardhat Transaction Flow', () => { await connector?.killSession(); connector = null; await device.clearKeychain(); - await Helpers.killHardhat(); + await Helpers.killAnvil(); }); }); diff --git a/e2e/Disabled/hardhatTransactionFlowSwaps.disabled.js b/e2e/Disabled/anvilTransactionFlowSwaps.disabled.js similarity index 98% rename from e2e/Disabled/hardhatTransactionFlowSwaps.disabled.js rename to e2e/Disabled/anvilTransactionFlowSwaps.disabled.js index 1ff90c981af..7564b4a96ca 100644 --- a/e2e/Disabled/hardhatTransactionFlowSwaps.disabled.js +++ b/e2e/Disabled/anvilTransactionFlowSwaps.disabled.js @@ -6,7 +6,7 @@ const ios = device.getPlatform() === 'ios'; const android = device.getPlatform() === 'android'; beforeAll(async () => { - await Helpers.startHardhat(); + await Helpers.startAnvil(); await Helpers.startIosSimulator(); }); @@ -29,11 +29,11 @@ const checkIfSwapCompleted = async (assetName, amount) => { }; // FIXME: Mainnet DAI doesn't show up in the swap search results -// This might be related to @Jin's latest work on changes to hardhat as +// This might be related to @Jin's latest work on changes to anvil as // part of the addy's REST API migration // // marking the test as SKIP for now -describe.skip('Hardhat Transaction Flow', () => { +describe.skip('Anvil Transaction Flow', () => { it('Should show the welcome screen', async () => { await Helpers.checkIfVisible('welcome-screen'); }); @@ -74,9 +74,9 @@ describe.skip('Hardhat Transaction Flow', () => { await Helpers.sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await Helpers.waitAndTap('dev-button-hardhat'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await Helpers.waitAndTap('dev-button-anvil'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); }); it.skip('Should deposit DAI (via Compound)', async () => { @@ -414,6 +414,6 @@ describe.skip('Hardhat Transaction Flow', () => { await connector?.killSession(); connector = null; await device.clearKeychain(); - await Helpers.killHardhat(); + await Helpers.killAnvil(); }); }); diff --git a/e2e/Disabled/registerENSFlow.disabled.js b/e2e/Disabled/registerENSFlow.disabled.js index a177ab85c65..a6677f74b85 100644 --- a/e2e/Disabled/registerENSFlow.disabled.js +++ b/e2e/Disabled/registerENSFlow.disabled.js @@ -110,7 +110,7 @@ const validatePrimaryName = async name => { }; beforeAll(async () => { - await Helpers.startHardhat(); + await Helpers.startAnvil(); await Helpers.startIosSimulator(); }); @@ -155,9 +155,9 @@ describe.skip('Register ENS Flow', () => { await Helpers.sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await Helpers.waitAndTap('dev-button-hardhat'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await Helpers.waitAndTap('dev-button-anvil'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); }); it('Should navigate to the Discover sheet screen after swiping left', async () => { @@ -637,6 +637,6 @@ describe.skip('Register ENS Flow', () => { afterAll(async () => { // Reset the app state await device.clearKeychain(); - await Helpers.killHardhat(); + await Helpers.killAnvil(); }); }); diff --git a/e2e/Disabled/sendSheetFlow.disabled.ts b/e2e/Disabled/sendSheetFlow.disabled.ts index 93b50f7091a..8c18386033e 100644 --- a/e2e/Disabled/sendSheetFlow.disabled.ts +++ b/e2e/Disabled/sendSheetFlow.disabled.ts @@ -1,7 +1,7 @@ import { device } from 'detox'; import { - startHardhat, - killHardhat, + startAnvil, + killAnvil, importWalletFlow, sendETHtoTestWallet, waitAndTap, @@ -16,11 +16,11 @@ import { describe.skip('Send Sheet Interaction Flow', () => { beforeAll(async () => { await device.reloadReactNative(); - await startHardhat(); + await startAnvil(); }); afterAll(async () => { await device.clearKeychain(); - await killHardhat(); + await killAnvil(); }); it('Import a wallet and go to welcome', async () => { @@ -31,9 +31,9 @@ describe.skip('Send Sheet Interaction Flow', () => { await sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await waitAndTap('dev-button-hardhat'); - await checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await waitAndTap('dev-button-anvil'); + await checkIfVisible('testnet-toast-Anvil'); }); it('Should open send sheet after tapping send button', async () => { diff --git a/e2e/Disabled/swapSheetFlow1.disabled.js b/e2e/Disabled/swapSheetFlow1.disabled.js index a7ed282b8dc..f9e77663162 100644 --- a/e2e/Disabled/swapSheetFlow1.disabled.js +++ b/e2e/Disabled/swapSheetFlow1.disabled.js @@ -9,11 +9,11 @@ const android = device.getPlatform() === 'android'; describe.skip('Swap Sheet Interaction Flow', () => { beforeAll(async () => { - await Helpers.startHardhat(); + await Helpers.startAnvil(); }); afterAll(async () => { await device.clearKeychain(); - await Helpers.killHardhat(); + await Helpers.killAnvil(); }); it('Import a wallet and go to welcome', async () => { @@ -24,14 +24,14 @@ describe.skip('Swap Sheet Interaction Flow', () => { await Helpers.sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { await Helpers.delayTime('very-long'); - await Helpers.waitAndTap('dev-button-hardhat'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + await Helpers.waitAndTap('dev-button-anvil'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); }); // FIXME: Mainnet DAI doesn't show up in the swap search results - // This might be related to @Jin's latest work on changes to hardhat as + // This might be related to @Jin's latest work on changes to anvil as // part of the addy's REST API migration // // marking the test as SKIP for now diff --git a/e2e/Disabled/swapSheetFlow2.disabled.js b/e2e/Disabled/swapSheetFlow2.disabled.js index 7c91840a432..97b13a821d8 100644 --- a/e2e/Disabled/swapSheetFlow2.disabled.js +++ b/e2e/Disabled/swapSheetFlow2.disabled.js @@ -2,14 +2,14 @@ import * as Helpers from '../helpers'; import { device } from 'detox'; beforeAll(async () => { - await Helpers.startHardhat(); + await Helpers.startAnvil(); }); const ios = device.getPlatform() === 'ios'; const android = device.getPlatform() === 'android'; // FIXME: Mainnet DAI doesn't show up in the swap search results -// This might be related to @Jin's latest work on changes to hardhat as +// This might be related to @Jin's latest work on changes to anvil as // part of the addy's REST API migration // // marking the test as SKIP for now @@ -54,14 +54,14 @@ describe.skip('Swap Sheet Interaction Flow', () => { await Helpers.sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { // need to wait for balances to be fetched await Helpers.delay(10000); - await Helpers.waitAndTap('dev-button-hardhat'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + await Helpers.waitAndTap('dev-button-anvil'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); }); - it('Should connect to hardhat', async () => { + it('Should connect to anvil', async () => { await Helpers.swipe('wallet-screen', 'right', 'slow'); await Helpers.checkIfVisible('profile-screen'); await Helpers.waitAndTap('settings-button'); @@ -69,8 +69,8 @@ describe.skip('Swap Sheet Interaction Flow', () => { await Helpers.scrollTo('settings-menu-container', 'bottom'); await Helpers.waitAndTap('developer-section'); await Helpers.swipeUntilVisible('alert-section', 'developer-settings-sheet', 'up'); - await Helpers.waitAndTap('hardhat-section'); - await Helpers.checkIfVisible('testnet-toast-Hardhat'); + await Helpers.waitAndTap('anvil-section'); + await Helpers.checkIfVisible('testnet-toast-Anvil'); await Helpers.swipe('profile-screen', 'left', 'slow'); }); @@ -474,6 +474,6 @@ describe.skip('Swap Sheet Interaction Flow', () => { afterAll(async () => { // Reset the app state await device.clearKeychain(); - await Helpers.killHardhat(); + await Helpers.killAnvil(); }); }); diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 441a9d0144d..2b0a39e880a 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -4,7 +4,7 @@ import { exec } from 'child_process'; import { JsonRpcProvider } from '@ethersproject/providers'; import { Wallet } from '@ethersproject/wallet'; import { expect, device, element, by, waitFor } from 'detox'; -import { parseEther } from '@ethersproject/units'; +import { formatEther, parseEther } from '@ethersproject/units'; import { IosElementAttributes, AndroidElementAttributes } from 'detox/detox'; const TESTING_WALLET = '0x3637f053D542E6D00Eee42D656dD7C59Fa33a62F'; @@ -20,12 +20,12 @@ interface ProviderFunction { _instance?: JsonRpcProvider; } -export async function startHardhat() { +export async function startAnvil() { await delayTime('short'); - exec('yarn hardhat'); + exec('yarn anvil'); } -export async function killHardhat() { +export async function killAnvil() { await delayTime('short'); exec('kill $(lsof -t -i:8545)'); } @@ -53,14 +53,14 @@ export async function importWalletFlow(customSeed?: string) { await checkIfVisible('wallet-screen'); } -export async function beforeAllcleanApp({ hardhat }: { hardhat?: boolean }) { +export async function beforeAllcleanApp({ anvil }: { anvil?: boolean }) { jest.resetAllMocks(); - hardhat && (await startHardhat()); + anvil && (await startAnvil()); } -export async function afterAllcleanApp({ hardhat }: { hardhat?: boolean }) { +export async function afterAllcleanApp({ anvil }: { anvil?: boolean }) { await device.clearKeychain(); - hardhat && (await killHardhat()); + anvil && (await killAnvil()); } export async function tap(elementId: string | RegExp) { @@ -467,16 +467,23 @@ export const getProvider: ProviderFunction = () => { }; export async function sendETHtoTestWallet() { + console.log('getting provider'); const provider = getProvider(); - // Hardhat account 0 that has 10000 ETH + console.log('got provider', provider); + // anvil account 0 that has 10000 ETH const wallet = new Wallet('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', provider); + console.log('got wallet', wallet); // Sending 20 ETH so we have enough to pay the tx fees even when the gas is too high + console.log('sending eth'); await wallet.sendTransaction({ to: TESTING_WALLET, value: parseEther('20'), }); + console.log('sent eth'); await delayTime('long'); + console.log('checking balance'); const balance = await provider.getBalance(TESTING_WALLET); + console.log('got balance', formatEther(balance)); if (balance.lt(parseEther('20'))) { throw Error('Error sending ETH to test wallet'); } diff --git a/e2e/init.js b/e2e/init.js index a8567e849b3..86d68eb47ad 100644 --- a/e2e/init.js +++ b/e2e/init.js @@ -8,7 +8,7 @@ beforeAll(async () => { if (device.getPlatform() === 'android') { // connecting to metro await device.reverseTcpPort(8081); - // connecting to hardhat + // connecting to anvil await device.reverseTcpPort(8545); // TODO: WIP for android connecting in dev // make sure we don't have gesture navigation what might cause collisions diff --git a/e2e/parallel/1_importAndWatchWalletsFlow.spec.ts b/e2e/parallel/1_importAndWatchWalletsFlow.spec.ts index 0e9b30cdaab..2259062c196 100644 --- a/e2e/parallel/1_importAndWatchWalletsFlow.spec.ts +++ b/e2e/parallel/1_importAndWatchWalletsFlow.spec.ts @@ -16,10 +16,10 @@ const android = device.getPlatform() === 'android'; describe('Import from private key flow', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('with 0x - Should show the welcome screen', async () => { await checkIfVisible('welcome-screen'); diff --git a/e2e/parallel/2_newWalletFlow.spec.ts b/e2e/parallel/2_newWalletFlow.spec.ts index 241dcb4e5d1..5db07d964f6 100644 --- a/e2e/parallel/2_newWalletFlow.spec.ts +++ b/e2e/parallel/2_newWalletFlow.spec.ts @@ -5,10 +5,10 @@ const android = device.getPlatform() === 'android'; describe('New Wallet flow', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('should show the welcome screen', async () => { diff --git a/e2e/parallel/3_homeScreen.spec.ts b/e2e/parallel/3_homeScreen.spec.ts index 1e32821ace8..8f78e63cfb5 100644 --- a/e2e/parallel/3_homeScreen.spec.ts +++ b/e2e/parallel/3_homeScreen.spec.ts @@ -14,10 +14,10 @@ const RAINBOW_TEST_WALLET = 'rainbowtestwallet.eth'; describe('Home Screen', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('imports wallet', async () => { diff --git a/e2e/parallel/4_discoverSheetFlow.spec.ts b/e2e/parallel/4_discoverSheetFlow.spec.ts index 0e2f9d632a3..5af4b8abd7c 100644 --- a/e2e/parallel/4_discoverSheetFlow.spec.ts +++ b/e2e/parallel/4_discoverSheetFlow.spec.ts @@ -17,10 +17,10 @@ const ios = device.getPlatform() === 'ios'; describe('Discover Screen Flow', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('Should import wallet and go to wallet screen', async () => { await importWalletFlow(); diff --git a/e2e/parallel/5_watchedWalletCollectionActionsFlow.spec.ts b/e2e/parallel/5_watchedWalletCollectionActionsFlow.spec.ts index 03fa4a5ab3e..b2c56b8656f 100644 --- a/e2e/parallel/5_watchedWalletCollectionActionsFlow.spec.ts +++ b/e2e/parallel/5_watchedWalletCollectionActionsFlow.spec.ts @@ -13,10 +13,10 @@ import { describe('Watched showcase and hidden actions flow', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('watches a wallet and loads wallet screen', async () => { diff --git a/e2e/parallel/6_maliciousDappConnection.spec.ts b/e2e/parallel/6_maliciousDappConnection.spec.ts index ad13c226403..2a3e1b890b9 100644 --- a/e2e/parallel/6_maliciousDappConnection.spec.ts +++ b/e2e/parallel/6_maliciousDappConnection.spec.ts @@ -15,11 +15,11 @@ import { WALLET_VARS } from '../testVariables'; describe('Check malicious dapp warning', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('Should be able to watch a wallet and load the wallet screen', async () => { diff --git a/e2e/parallel/7_manualBackup.spec.ts b/e2e/parallel/7_manualBackup.spec.ts index 680e042fc4d..9fd08830bca 100644 --- a/e2e/parallel/7_manualBackup.spec.ts +++ b/e2e/parallel/7_manualBackup.spec.ts @@ -14,10 +14,10 @@ import { describe('Backups', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: false }); + await beforeAllcleanApp({ anvil: false }); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: false }); + await afterAllcleanApp({ anvil: false }); }); it('Imports wallet', async () => { diff --git a/e2e/serial/1_sendSheetFlowContacts.spec.ts b/e2e/serial/1_sendSheetFlowContacts.spec.ts index ece0ee36771..005173d66a3 100644 --- a/e2e/serial/1_sendSheetFlowContacts.spec.ts +++ b/e2e/serial/1_sendSheetFlowContacts.spec.ts @@ -18,10 +18,10 @@ const android = device.getPlatform() === 'android'; describe('Send Sheet Interaction Flow Contacts', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: true }); + await beforeAllcleanApp({}); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: true }); + await afterAllcleanApp({}); }); it('Import a wallet and go to welcome', async () => { @@ -32,9 +32,9 @@ describe('Send Sheet Interaction Flow Contacts', () => { await sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await waitAndTap('dev-button-hardhat'); - await checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await waitAndTap('dev-button-anvil'); + await checkIfVisible('testnet-toast-Anvil'); }); it('Should open send sheet after tapping send fab', async () => { diff --git a/e2e/serial/2_swaps.spec.ts b/e2e/serial/2_swaps.spec.ts index 9a4e3ed972c..f18f2c87fab 100644 --- a/e2e/serial/2_swaps.spec.ts +++ b/e2e/serial/2_swaps.spec.ts @@ -30,10 +30,10 @@ import { WALLET_VARS } from '../testVariables'; describe('Swap Sheet Interaction Flow', () => { beforeAll(async () => { - await beforeAllcleanApp({ hardhat: true }); + await beforeAllcleanApp({}); }); afterAll(async () => { - await afterAllcleanApp({ hardhat: true }); + await afterAllcleanApp({}); }); it('Import a wallet and go to welcome', async () => { @@ -45,9 +45,9 @@ describe('Swap Sheet Interaction Flow', () => { await sendETHtoTestWallet(); }); - it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { - await tap('dev-button-hardhat'); - await checkIfVisible('testnet-toast-Hardhat'); + it('Should show Anvil Toast after pressing Connect To Anvil', async () => { + await tap('dev-button-anvil'); + await checkIfVisible('testnet-toast-Anvil'); // doesn't work atm // validate it has the expected funds of 20 eth diff --git a/globals.d.ts b/globals.d.ts index bd6bee49caa..3c401e51846 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -49,8 +49,8 @@ declare module 'react-native-dotenv' { export const NFT_API_KEY: string; export const NFT_API_URL: string; export const ETHERSCAN_API_KEY: string; - export const HARDHAT_URL_ANDROID: string; - export const HARDHAT_URL_IOS: string; + export const ANVIL_URL_ANDROID: string; + export const ANVIL_URL_IOS: string; export const RAINBOW_MASTER_KEY: string; export const SECURE_WALLET_HASH_KEY: string; export const TEST_SEEDS: string; diff --git a/hardhat.config.js b/hardhat.config.js deleted file mode 100644 index b7aecb87c58..00000000000 --- a/hardhat.config.js +++ /dev/null @@ -1,17 +0,0 @@ -require('@nomiclabs/hardhat-waffle'); - -// You need to export an object to set up your config -// Go to https://hardhat.org/config/ to learn more - -/** - * @type import('hardhat/config').HardhatUserConfig - */ -module.exports = { - networks: { - hardhat: { - chainId: 1, - initialBaseFeePerGas: 100000000, // 0.1 gwei - }, - }, - solidity: '0.8.4', -}; diff --git a/package.json b/package.json index c7bd68d69cf..016bfd20be7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "ds": "cd src/design-system/docs && yarn dev", "fast": "yarn install && yarn setup && yarn install-pods-fast", "gradle": "cd android && ./gradlew", - "hardhat": "sh ./scripts/hardhat.sh", + "anvil": "sh ./scripts/anvil.sh", "install-all": "yarn install && yarn setup && yarn install-pods", "install-all-no-flipper": "yarn install && yarn setup && yarn install-pods-no-flipper", "install-bundle": "cd ios && bundle install && cd ..", @@ -329,8 +329,6 @@ "@babel/preset-env": "7.22.0", "@babel/runtime": "7.22.0", "@lavamoat/allow-scripts": "3.0.1", - "@nomiclabs/hardhat-ethers": "2.2.3", - "@nomiclabs/hardhat-waffle": "2.0.6", "@react-native/babel-preset": "0.74.83", "@react-native/typescript-config": "0.74.83", "@rnx-kit/align-deps": "2.2.4", @@ -365,7 +363,6 @@ "eslint": "8.22.0", "eslint-config-rainbow": "4.3.0", "graphql": "15.3.0", - "hardhat": "2.18.1", "husky": "8.0.1", "image-size": "1.0.0", "jest": "29.7.0", @@ -479,16 +476,14 @@ "detox>bunyan>dtrace-provider": false, "detox>ws>bufferutil": false, "detox>ws>utf-8-validate": false, - "hardhat>@ethereumjs/vm>core-js-pure": false, "ethereumjs-util>ethereum-cryptography>secp256k1": false, - "hardhat>@nomicfoundation/ethereumjs-blockchain>level>classic-level": false, - "hardhat>keccak": false, "react-native-storage": false, "react-native-storage>opencollective>babel-polyfill>core-js": false, "viem>ws>bufferutil": false, "viem>ws>utf-8-validate": false, "@types/detox>detox": false, - "react-native-bootsplash>sharp": false + "react-native-bootsplash>sharp": false, + "ethereumjs-util>ethereum-cryptography>keccak": false } }, "lint-staged": { diff --git a/scripts/anvil.sh b/scripts/anvil.sh new file mode 100644 index 00000000000..9ec2c5e7956 --- /dev/null +++ b/scripts/anvil.sh @@ -0,0 +1,3 @@ +#!/bin/bash +source .env +anvil --fork-url $ETHEREUM_MAINNET_RPC_DEV diff --git a/scripts/hardhat.sh b/scripts/hardhat.sh deleted file mode 100644 index 1ef0a0e1ce3..00000000000 --- a/scripts/hardhat.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -source .env -npx hardhat node --fork $ETHEREUM_MAINNET_RPC_DEV \ No newline at end of file diff --git a/scripts/run-parallel-e2e.sh b/scripts/run-parallel-e2e.sh index 71961395466..3932bdcf69a 100755 --- a/scripts/run-parallel-e2e.sh +++ b/scripts/run-parallel-e2e.sh @@ -1,17 +1,14 @@ #!/bin/bash -max_retries="$1" -count=0 -until (( count >= max_retries )) -do - ./node_modules/.bin/detox test parallel/ -c ios.sim.release --maxWorkers 2 -- --forceExit --bail 1 - ret_val=$? - if [ $ret_val -eq 0 ]; then - exit 0 - fi - ((count++)) - echo "Test failed, attempt $count/$max_retries..." -done +# 0) Read build type from first argument; default to "release" if not specified +BUILD_TYPE=${1:-release} # can be "debug" or "release" +# 0.1) Decide Detox config based on BUILD_TYPE +if [ "$BUILD_TYPE" = "debug" ]; then + DETOX_CONFIG="ios.sim.debug" +else + DETOX_CONFIG="ios.sim.release" +fi +./node_modules/.bin/detox test ./e2e/parallel/ -c "$DETOX_CONFIG" --maxWorkers 2 --R 3 diff --git a/scripts/run-serial-e2e.sh b/scripts/run-serial-e2e.sh index dc1a1f6640e..922729eaeaf 100755 --- a/scripts/run-serial-e2e.sh +++ b/scripts/run-serial-e2e.sh @@ -1,18 +1,72 @@ #!/bin/bash +MAX_RETRIES=${1:-3} # number of times that a test should be retried in case of failure. default to 3 if not specified +BUILD_TYPE=${2:-release} # can be "debug" or "release". default to "release" if not specified -max_retries="$1" -count=0 - -until (( count >= max_retries )) -do - ./node_modules/.bin/detox test serial/ -c ios.sim.release --maxWorkers 1 -- --forceExit --bail 1 - ret_val=$? - if [ $ret_val -eq 0 ]; then - exit 0 - fi - ((count++)) - echo "Test failed, attempt $count/$max_retries..." + +# 0.1) Decide Detox config based on BUILD_TYPE +if [ "$BUILD_TYPE" = "debug" ]; then + DETOX_CONFIG="ios.sim.debug" +else + DETOX_CONFIG="ios.sim.release" +fi + +SUCCESS=false + +# Loop through each file in the ./e2e/serial/ directory +for test_file in ./e2e/serial/*.ts; do + COUNT=0 + until (( $COUNT >= MAX_RETRIES )) + do + echo "=====================================" + echo "Running test file: $test_file (attempt ${COUNT+1}/$MAX_RETRIES)" + echo "=====================================" + + echo "Starting anvil..." + # 1) Start Anvil in the background (show logs in terminal + save to file) + yarn anvil 2>&1 | grep -v "eth_" | tee anvil.log & + ANVIL_PID=$! + echo "Anvil started (PID: $ANVIL_PID)" + + # 2) Wait for Anvil to initialize + sleep 5 + + # 3) Run Detox for this single file with retries + ./node_modules/.bin/detox test "$test_file" -c "$DETOX_CONFIG" --maxWorkers 1 + ret_val=$? + + # 4) Kill the Anvil process + echo "Killing Anvil (PID: $ANVIL_PID)" + kill "$ANVIL_PID" 2>/dev/null || true + # kill any other processes using port 8545 + kill $(lsof -t -i:8545) 2>/dev/null || true + + # 5) Remove the Anvil log file + rm -rf anvil.log + + + # 6) Decide what to do if Detox failed or succeeded + if [ $ret_val -eq 0 ]; then + echo "✅ Tests passed for $test_file" + SUCCESS=true + break + fi + ((COUNT++)) + echo "❌ Test failed, attempt $COUNT/$MAX_RETRIES..." + + if(($COUNT >= MAX_RETRIES)) + then + SUCCESS=false + fi + done + + if [ "$SUCCESS" = "false" ]; then + echo "❌ Tests failed after $MAX_RETRIES attempts. Bailing out!" + exit 1 + fi done -echo "Tests failed after $max_retries attempts." -exit 1 \ No newline at end of file +echo "✅ All tests passed for every file!" +exit 0 + + + diff --git a/src/App.tsx b/src/App.tsx index 157ff68b606..1a749b8ce6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import '@/languages'; import * as Sentry from '@sentry/react-native'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, memo } from 'react'; import { AppRegistry, Dimensions, LogBox, StyleSheet, View } from 'react-native'; import { Toaster } from 'sonner-native'; import { MobileWalletProtocolProvider } from '@coinbase/mobile-wallet-protocol-host'; @@ -9,9 +9,8 @@ import { useApplicationSetup } from '@/hooks/useApplicationSetup'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { enableScreens } from 'react-native-screens'; -import { connect, Provider as ReduxProvider } from 'react-redux'; +import { connect, Provider as ReduxProvider, shallowEqual } from 'react-redux'; import { RecoilRoot } from 'recoil'; -import PortalConsumer from '@/components/PortalConsumer'; import ErrorBoundary from '@/components/error-boundary/ErrorBoundary'; import { OfflineToast } from '@/components/toasts'; import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from '@/config/debug'; @@ -24,7 +23,6 @@ import store, { AppDispatch, type AppState } from '@/redux/store'; import { MainThemeProvider } from '@/theme/ThemeContext'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; import { InitialRouteContext } from '@/navigation/initialRoute'; -import { Portal } from '@/react-native-cool-modals/Portal'; import { NotificationsHandler } from '@/notifications/NotificationsHandler'; import { analyticsV2 } from '@/analytics'; import { getOrCreateDeviceId } from '@/analytics/utils'; @@ -39,7 +37,9 @@ import { RootStackParamList } from '@/navigation/types'; import { IS_ANDROID, IS_DEV } from '@/env'; import { prefetchDefaultFavorites } from '@/resources/favorites'; import Routes from '@/navigation/Routes'; +import { BackupsSync } from '@/state/sync/BackupsSync'; import { BackendNetworks } from '@/components/BackendNetworks'; +import { AbsolutePortalRoot } from './components/AbsolutePortal'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -67,12 +67,11 @@ function App({ walletReady }: AppProps) { }, []); return ( - + <> {initialRoute && ( - )} @@ -80,14 +79,27 @@ function App({ walletReady }: AppProps) { + - + + ); } -const AppWithRedux = connect(state => ({ - walletReady: state.appState.walletReady, -}))(App); +const AppWithRedux = connect( + state => ({ + walletReady: state.appState.walletReady, + }), + null, + null, + { + areStatesEqual: (next, prev) => { + // Only update if walletReady actually changed + return next.appState.walletReady === prev.appState.walletReady; + }, + areOwnPropsEqual: shallowEqual, + } +)(memo(App)); function Root() { const [initializing, setInitializing] = useState(true); diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index fea7bb0b197..9c04cd72def 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -128,6 +128,7 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...asse > ({ + ...borders.buildCircleAsObject(size), + position: 'absolute', +}); + +/** + * If mainnet asset is available, get the token under /ethereum/ (token) url. + * Otherwise let it use whatever type it has + * @param param0 - optional mainnetAddress, address and network + * @returns a proper type and address to use for the url + */ +function resolveChainIdAndAddress({ address, mainnetAddress }: { mainnetAddress?: string; address: string }) { + if (mainnetAddress) { + return { + resolvedAddress: mainnetAddress, + }; + } + + return { + resolvedAddress: address, + }; +} + +export const SwapCoinIcon = React.memo(function FeedCoinIcon({ + address, + color, + iconUrl, + disableShadow = true, + forceDarkMode, + mainnetAddress, + chainId, + symbol, + size = 32, + chainSize, +}: { + address: string; + color?: string; + iconUrl?: string; + disableShadow?: boolean; + forceDarkMode?: boolean; + mainnetAddress?: string; + chainId: ChainId; + symbol: string; + size?: number; + chainSize?: number; +}) { + const theme = useTheme(); + + const { resolvedAddress } = resolveChainIdAndAddress({ + address, + mainnetAddress, + }); + + const fallbackIconColor = color ?? theme.colors.purpleUniswap; + const shadowColor = theme.isDarkMode || forceDarkMode ? theme.colors.shadow : color || fallbackIconColor; + const eth = isETH(resolvedAddress); + + return ( + + {eth ? ( + + + + ) : ( + + {() => ( + + )} + + )} + + {chainId && chainId !== ChainId.mainnet && size > 16 && ( + + + + )} + + ); +}); + +const styles = { + container: (size: number) => ({ + elevation: 6, + height: size, + overflow: 'visible' as const, + }), + coinIcon: (size: number) => ({ + borderRadius: size / 2, + height: size, + width: size, + overflow: 'visible' as const, + }), +}; + +const sx = StyleSheet.create({ + badge: { + bottom: -0, + left: -8, + position: 'absolute', + shadowColor: globalColors.grey100, + shadowOffset: { + height: 4, + width: 0, + }, + shadowRadius: 6, + shadowOpacity: 0.2, + }, + reactCoinIconContainer: { + alignItems: 'center', + justifyContent: 'center', + }, + withShadow: { + elevation: 6, + shadowOffset: { + height: 4, + width: 0, + }, + shadowOpacity: 0.2, + shadowRadius: 6, + }, +}); diff --git a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts index 4033417ed62..9760b638d49 100644 --- a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts +++ b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts @@ -8,13 +8,14 @@ import { useFavorites } from '@/resources/favorites'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { isAddress } from '@ethersproject/address'; import { rankings } from 'match-sorter'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { TokenToBuyListItem } from '../components/TokenList/TokenToBuyList'; import { useSwapContext } from '../providers/swap-provider'; import { RecentSwap } from '@/__swaps__/types/swap'; import { useTokenDiscovery } from '../resources/search'; +import { analyticsV2 } from '@/analytics'; export type AssetToBuySectionId = 'bridge' | 'recent' | 'favorites' | 'verified' | 'unverified' | 'other_networks' | 'popular'; @@ -321,7 +322,7 @@ export function useSearchCurrencyLists() { const { data: verifiedAssets, isLoading: isLoadingVerifiedAssets } = useTokenSearch( { list: 'verifiedAssets', - chainId: isAddress(query) ? state.toChainId : undefined, + chainId: state.toChainId, keys: isAddress(query) ? ['address'] : ['name', 'symbol'], threshold: isAddress(query) ? 'CASE_SENSITIVE_EQUAL' : 'CONTAINS', query: query.length > 0 ? query : undefined, @@ -417,33 +418,34 @@ export function useSearchCurrencyLists() { { enabled: memoizedData.enableUnverifiedSearch, select: (data: TokenSearchResult) => { - return getExactMatches(data, query).slice(0, MAX_UNVERIFIED_RESULTS); + return isAddress(query) ? getExactMatches(data, query).slice(0, MAX_UNVERIFIED_RESULTS) : data.slice(0, MAX_UNVERIFIED_RESULTS); }, } ); - return useMemo(() => { + const searchCurrencyLists = useMemo(() => { const toChainId = selectedOutputChainId.value ?? ChainId.mainnet; const bridgeResult = memoizedData.filteredBridgeAsset ?? undefined; const crosschainMatches = query === '' ? undefined : verifiedAssets?.filter(asset => asset.chainId !== toChainId); const verifiedResults = query === '' ? verifiedAssets : verifiedAssets?.filter(asset => asset.chainId === toChainId); const unverifiedResults = memoizedData.enableUnverifiedSearch ? unverifiedAssets : undefined; - return { - results: buildListSectionsData({ - combinedData: { - bridgeAsset: bridgeResult, - crosschainExactMatches: crosschainMatches, - unverifiedAssets: unverifiedResults, - verifiedAssets: verifiedResults, - recentSwaps: recentsForChain, - popularAssets: popularAssetsForChain, - }, - favoritesList, - filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address, - }), - isLoading: isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets, - }; + const results = buildListSectionsData({ + combinedData: { + bridgeAsset: bridgeResult, + crosschainExactMatches: crosschainMatches, + unverifiedAssets: unverifiedResults, + verifiedAssets: verifiedResults, + recentSwaps: recentsForChain, + popularAssets: popularAssetsForChain, + }, + favoritesList, + filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address, + }); + + const isLoading = isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets; + + return { results, isLoading }; }, [ favoritesList, isLoadingUnverifiedAssets, @@ -458,4 +460,17 @@ export function useSearchCurrencyLists() { recentsForChain, popularAssetsForChain, ]); + + useEffect(() => { + if (searchCurrencyLists.isLoading) return; + const params = { screen: 'swap' as const, total_tokens: 0, no_icon: 0, query }; + for (const assetOrHeader of searchCurrencyLists.results) { + if (assetOrHeader.listItemType === 'header') continue; + if (!assetOrHeader.icon_url) params.no_icon += 1; + params.total_tokens += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [searchCurrencyLists.results, searchCurrencyLists.isLoading, query]); + + return searchCurrencyLists; } diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 4d1f66ae1c4..c7ef1482127 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -56,7 +56,7 @@ import { useSwapOutputQuotesDisabled } from '../hooks/useSwapOutputQuotesDisable import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './SyncSwapStateAndSharedValues'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { getRemoteConfig } from '@/model/remoteConfig'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { useBackendNetworksStore, getChainsNativeAssetWorklet } from '@/state/backendNetworks/backendNetworks'; import { getSwapsNavigationParams } from '../navigateToSwaps'; import { LedgerSigner } from '@/handlers/LedgerSigner'; @@ -226,11 +226,14 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { NotificationManager?.postNotification('rapInProgress'); const provider = getProvider({ chainId: parameters.chainId }); - const connectedToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; + const connectedToAnvil = useConnectedToAnvilStore.getState().connectedToAnvil; const isBridge = swapsStore.getState().inputAsset?.mainnetAddress === swapsStore.getState().outputAsset?.mainnetAddress; const isDegenModeEnabled = swapsStore.getState().degenMode; const isSwappingToPopularAsset = swapsStore.getState().outputAsset?.sectionId === 'popular'; + const lastNavigatedTrendingToken = swapsStore.getState().lastNavigatedTrendingToken; + const isSwappingToTrendingAsset = + lastNavigatedTrendingToken === parameters.assetToBuy.uniqueId || lastNavigatedTrendingToken === parameters.assetToSell.uniqueId; const selectedGas = getSelectedGas(parameters.chainId); if (!selectedGas) { @@ -283,7 +286,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }; } - const chainId = connectedToHardhat ? ChainId.hardhat : parameters.chainId; + const chainId = connectedToAnvil ? ChainId.anvil : parameters.chainId; const nonce = await getNextNonce({ address: parameters.quote.from, chainId }); const { errorMessage } = await performanceTracking.getState().executeFn({ @@ -325,6 +328,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, errorMessage, isHardwareWallet, }); @@ -341,7 +345,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { userAssetsQueryKey({ address: parameters.quote.from, currency: nativeCurrency, - testnetMode: connectedToHardhat, + testnetMode: connectedToAnvil, }) ); @@ -389,6 +393,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, isHardwareWallet, }); } catch (error) { @@ -403,6 +408,11 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }, }); } + + // reset the last navigated trending token after a swap has taken place + swapsStore.setState({ + lastNavigatedTrendingToken: undefined, + }); }; const executeSwap = performanceTracking.getState().executeFn({ diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 6c2e0611f70..429b6c4f745 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -14,9 +14,9 @@ import { parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/helpers/utilities'; import { fetchUserAssetsByChain } from './userAssetsByChain'; -import { fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; +import { fetchAnvilBalancesByChainId } from '@/resources/assets/anvilAssets'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { staleBalancesStore } from '@/state/staleBalances'; import { IS_TEST } from '@/env'; import store from '@/redux/store'; @@ -106,7 +106,7 @@ async function userAssetsQueryFunction({ return {}; } if (testnetMode) { - const { assets, chainIdsInResponse } = await fetchHardhatBalancesByChainId(address); + const { assets, chainIdsInResponse } = await fetchAnvilBalancesByChainId(address); const parsedAssets: Array<{ asset: ZerionAsset; quantity: string; @@ -258,8 +258,8 @@ export function useUserAssets( { address, currency }: UserAssetsArgs, config: QueryConfigWithSelect = {} ) { - const { connectedToHardhat } = useConnectedToHardhatStore(); - return useQuery(userAssetsQueryKey({ address, currency, testnetMode: connectedToHardhat }), userAssetsQueryFunction, { + const { connectedToAnvil } = useConnectedToAnvilStore(); + return useQuery(userAssetsQueryKey({ address, currency, testnetMode: connectedToAnvil }), userAssetsQueryFunction, { ...config, enabled: !!address && !!currency, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, diff --git a/src/__swaps__/screens/Swap/resources/search/discovery.ts b/src/__swaps__/screens/Swap/resources/search/discovery.ts index ebb15d0f59b..40496e0d2d7 100644 --- a/src/__swaps__/screens/Swap/resources/search/discovery.ts +++ b/src/__swaps__/screens/Swap/resources/search/discovery.ts @@ -7,7 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import { parseTokenSearch } from './utils'; const tokenSearchHttp = new RainbowFetchClient({ - baseURL: 'https://token-search.rainbow.me/v3/discovery', + baseURL: 'https://token-search.rainbow.me/v3/trending/swaps', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index b270d55c6cd..52ed1d53443 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -12,7 +12,7 @@ import { parseTokenSearch } from './utils'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; const tokenSearchHttp = new RainbowFetchClient({ - baseURL: 'https://token-search.rainbow.me/v2', + baseURL: 'https://token-search.rainbow.me/v3/tokens', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -30,13 +30,19 @@ export type TokenSearchArgs = { list: TokenSearchListId; threshold?: TokenSearchThreshold; query?: string; + shouldPersist?: boolean; }; // /////////////////////////////////////////////// // Query Key -const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs) => - createQueryKey('TokenSearch', { chainId, fromChainId, keys, list, threshold, query }, { persisterVersion: 2 }); +const tokenSearchQueryKey = ({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }: TokenSearchArgs) => { + return createQueryKey( + 'TokenSearch', + { chainId, fromChainId, keys, list, threshold, query }, + { persisterVersion: shouldPersist ? 3 : undefined } + ); +}; type TokenSearchQueryKey = ReturnType; @@ -77,6 +83,7 @@ async function tokenSearchQueryFunction({ return parseTokenSearch(tokenSearch.data.data, chainId); } + // search for address on other chains const allVerifiedTokens = await tokenSearchHttp.get<{ data: SearchAsset[] }>(ALL_VERIFIED_TOKENS_PARAM); const addressQuery = query.trim().toLowerCase(); @@ -104,8 +111,9 @@ export async function fetchTokenSearch( { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, config: QueryConfigWithSelect = {} ) { + const shouldPersist = query === undefined; return await queryClient.fetchQuery( - tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query }), + tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }), tokenSearchQueryFunction, config ); @@ -130,7 +138,8 @@ export function useTokenSearch( { chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs, config: QueryConfigWithSelect = {} ) { - return useQuery(tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query }), tokenSearchQueryFunction, { + const shouldPersist = query === undefined; + return useQuery(tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query, shouldPersist }), tokenSearchQueryFunction, { ...config, keepPreviousData: true, }); diff --git a/src/analytics/event.ts b/src/analytics/event.ts index adfcbee8a6d..3696b908736 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -9,6 +9,7 @@ import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { AnyPerformanceLog, Screen } from '../state/performance/operations'; import { FavoritedSite } from '@/state/browser/favoriteDappsStore'; +import { TrendingToken } from '@/resources/trendingTokens/trendingTokens'; /** * All events, used by `analytics.track()` @@ -167,6 +168,17 @@ export const event = { // token details tokenDetailsErc20: 'token_details.erc20', tokenDetailsNFT: 'token_details.nft', + + // token lists (wallet, swap, send) + tokenList: 'token_list', + + // trending tokens + viewTrendingToken: 'trending_tokens.view_trending_token', + viewRankedCategory: 'trending_tokens.view_ranked_category', + changeNetworkFilter: 'trending_tokens.change_network_filter', + changeTimeframeFilter: 'trending_tokens.change_timeframe_filter', + changeSortFilter: 'trending_tokens.change_sort_filter', + hasLinkedFarcaster: 'trending_tokens.has_linked_farcaster', } as const; type SwapEventParameters = { @@ -186,6 +198,7 @@ type SwapEventParameters = { tradeAmountUSD: number; degenMode: boolean; isSwappingToPopularAsset: boolean; + isSwappingToTrendingAsset: boolean; isHardwareWallet: boolean; }; @@ -706,4 +719,45 @@ export type EventProperties = { eventSentAfterMs: number; available_data: { description: boolean; image_url: boolean; floorPrice: boolean }; }; + + [event.tokenList]: { + screen: 'wallet' | 'swap' | 'send' | 'discover'; + total_tokens: number; + no_icon: number; + no_price?: number; + query?: string; // query is only sent for the swap screen + }; + + [event.viewTrendingToken]: { + address: TrendingToken['address']; + chainId: TrendingToken['chainId']; + symbol: TrendingToken['symbol']; + name: TrendingToken['name']; + highlightedFriends: number; + }; + + [event.viewRankedCategory]: { + category: string; + chainId: ChainId | undefined; + isLimited: boolean; + isEmpty: boolean; + }; + + [event.changeNetworkFilter]: { + chainId: ChainId | undefined; + }; + + [event.changeTimeframeFilter]: { + timeframe: string; + }; + + [event.changeSortFilter]: { + sort: string | undefined; + }; + + [event.hasLinkedFarcaster]: { + hasFarcaster: boolean; + personalizedTrending: boolean; + walletHash: string; + }; }; diff --git a/src/analytics/index.ts b/src/analytics/index.ts index 222cf513b34..89aaa0a696a 100644 --- a/src/analytics/index.ts +++ b/src/analytics/index.ts @@ -1,13 +1,12 @@ import rudderClient from '@rudderstack/rudder-sdk-react-native'; -import { REACT_NATIVE_RUDDERSTACK_WRITE_KEY, RUDDERSTACK_DATA_PLANE_URL, IS_TESTING } from 'react-native-dotenv'; +import { REACT_NATIVE_RUDDERSTACK_WRITE_KEY, RUDDERSTACK_DATA_PLANE_URL } from 'react-native-dotenv'; import { EventProperties, event } from '@/analytics/event'; import { UserProperties } from '@/analytics/userProperties'; import { logger, RainbowError } from '@/logger'; import { device } from '@/storage'; import { WalletContext } from './utils'; - -const isTesting = IS_TESTING === 'true'; +import { IS_TEST } from '@/env'; export class Analytics { client: typeof rudderClient; @@ -19,8 +18,8 @@ export class Analytics { constructor() { this.client = rudderClient; - this.disabled = isTesting || !!device.get(['doNotTrack']); - if (isTesting) { + this.disabled = IS_TEST || !!device.get(['doNotTrack']); + if (IS_TEST) { logger.debug('[Analytics]: disabled for testing'); } else { logger.debug('[Analytics]: client initialized'); diff --git a/src/analytics/userProperties.ts b/src/analytics/userProperties.ts index b42d5518a61..8a467e4b09a 100644 --- a/src/analytics/userProperties.ts +++ b/src/analytics/userProperties.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@/state/backendNetworks/types'; import { NativeCurrencyKey } from '@/entities'; import { Language } from '@/languages'; @@ -36,6 +37,9 @@ export interface UserProperties { hiddenCOins?: string[]; appIcon?: string; + // most used networks at the time the user first opens the network switcher + mostUsedNetworks?: ChainId[]; + // assets NFTs?: number; poaps?: number; diff --git a/src/components/AbsolutePortal.tsx b/src/components/AbsolutePortal.tsx index b578234bd0a..a992e84c11c 100644 --- a/src/components/AbsolutePortal.tsx +++ b/src/components/AbsolutePortal.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren, ReactNode, useEffect, useState } from 'react'; -import { View } from 'react-native'; +import { StyleProp, ViewStyle, View } from 'react-native'; const absolutePortal = { nodes: [] as ReactNode[], @@ -24,7 +24,7 @@ const absolutePortal = { }, }; -export const AbsolutePortalRoot = () => { +export const AbsolutePortalRoot = ({ style }: { style?: StyleProp }) => { const [nodes, setNodes] = useState(absolutePortal.nodes); useEffect(() => { @@ -32,17 +32,15 @@ export const AbsolutePortalRoot = () => { return () => unsubscribe(); }, []); - return ( - - {nodes} - - ); + return {nodes}; }; export const AbsolutePortal = ({ children }: PropsWithChildren) => { useEffect(() => { absolutePortal.addNode(children); - return () => absolutePortal.removeNode(children); + return () => { + absolutePortal.removeNode(children); + }; }, [children]); return null; diff --git a/src/components/ContactRowInfoButton.js b/src/components/ContactRowInfoButton.js index 28ce8a1f159..fd93f487012 100644 --- a/src/components/ContactRowInfoButton.js +++ b/src/components/ContactRowInfoButton.js @@ -2,7 +2,6 @@ import lang from 'i18n-js'; import { startCase } from 'lodash'; import React from 'react'; import { View } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { ContextMenuButton } from 'react-native-ios-context-menu'; import RadialGradient from 'react-native-radial-gradient'; import { ButtonPressAnimation } from './animations'; @@ -12,7 +11,7 @@ import { Text } from './text'; import { useClipboard } from '@/hooks'; import styled from '@/styled-thing'; import { fonts, fontWithWidth, padding } from '@/styles'; - +import { IS_TEST } from '@/env'; import { abbreviations, ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils'; const InfoButton = styled(Centered)({ @@ -27,7 +26,7 @@ const InfoButton = styled(Centered)({ ...padding.object(0, 0), }); -const Circle = styled(IS_TESTING === 'true' ? View : RadialGradient).attrs(({ theme: { colors } }) => ({ +const Circle = styled(IS_TEST ? View : RadialGradient).attrs(({ theme: { colors } }) => ({ center: [0, 15], colors: colors.gradients.lightestGrey, }))({ diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index b8b04e5250d..08006168811 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -311,7 +311,7 @@ export const ControlPanel = () => { ); }; -const TapToDismiss = memo(function TapToDismiss() { +export const TapToDismiss = memo(function TapToDismiss() { const { goBack } = useNavigation(); return ( diff --git a/src/screens/discover/components/DiscoverFeaturedResultsCard.tsx b/src/components/Discover/DiscoverFeaturedResultsCard.tsx similarity index 100% rename from src/screens/discover/components/DiscoverFeaturedResultsCard.tsx rename to src/components/Discover/DiscoverFeaturedResultsCard.tsx diff --git a/src/screens/discover/components/DiscoverHome.tsx b/src/components/Discover/DiscoverHome.tsx similarity index 86% rename from src/screens/discover/components/DiscoverHome.tsx rename to src/components/Discover/DiscoverHome.tsx index 7132c19c016..cb9fd14fed0 100644 --- a/src/screens/discover/components/DiscoverHome.tsx +++ b/src/components/Discover/DiscoverHome.tsx @@ -6,9 +6,10 @@ import useExperimentalFlag, { MINTS, NFT_OFFERS, FEATURED_RESULTS, + TRENDING_TOKENS, } from '@rainbow-me/config/experimentalHooks'; import { isTestnetChain } from '@/handlers/web3'; -import { Inline, Inset, Stack, Box } from '@/design-system'; +import { Inline, Inset, Stack, Box, Separator } from '@/design-system'; import { useAccountSettings, useWallets } from '@/hooks'; import { ENSCreateProfileCard } from '@/components/cards/ENSCreateProfileCard'; import { ENSSearchCard } from '@/components/cards/ENSSearchCard'; @@ -28,11 +29,12 @@ import { FeaturedResultStack } from '@/components/FeaturedResult/FeaturedResultS import Routes from '@/navigation/routesNames'; import { useNavigation } from '@/navigation'; import { DiscoverFeaturedResultsCard } from './DiscoverFeaturedResultsCard'; +import { TrendingTokens } from '@/components/Discover/TrendingTokens'; export const HORIZONTAL_PADDING = 20; export default function DiscoverHome() { - const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results } = useRemoteConfig(); + const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results, trending_tokens_enabled } = useRemoteConfig(); const { chainId } = useAccountSettings(); const profilesEnabledLocalFlag = useExperimentalFlag(PROFILES); const profilesEnabledRemoteFlag = profiles_enabled; @@ -42,6 +44,7 @@ export default function DiscoverHome() { const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; const opRewardsLocalFlag = useExperimentalFlag(OP_REWARDS); const opRewardsRemoteFlag = op_rewards_enabled; + const trendingTokensEnabled = (useExperimentalFlag(TRENDING_TOKENS) || trending_tokens_enabled) && !IS_TEST; const testNetwork = isTestnetChain({ chainId }); const { navigate } = useNavigation(); const isProfilesEnabled = profilesEnabledLocalFlag && profilesEnabledRemoteFlag; @@ -67,6 +70,13 @@ export default function DiscoverHome() { {isProfilesEnabled && } + + {trendingTokensEnabled && ( + <> + + + + )} {mintsEnabled && ( @@ -76,7 +86,7 @@ export default function DiscoverHome() { )} - {/* FIXME: IS_TESTING disables nftOffers this makes some DETOX tests hang forever at exit - investigate */} + {/* FIXME: IS_TEST disables nftOffers this makes some DETOX tests hang forever at exit - investigate */} {!IS_TEST && nftOffersEnabled && } {/* We have both flags here to be able to override the remote flag and show the card anyway in Dev*/} {featuredResultsEnabled && ( diff --git a/src/screens/discover/components/DiscoverScreenContent.tsx b/src/components/Discover/DiscoverScreenContent.tsx similarity index 76% rename from src/screens/discover/components/DiscoverScreenContent.tsx rename to src/components/Discover/DiscoverScreenContent.tsx index 1e3a7650013..99271271ded 100644 --- a/src/screens/discover/components/DiscoverScreenContent.tsx +++ b/src/components/Discover/DiscoverScreenContent.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { View } from 'react-native'; import { FlexItem, Page } from '@/components/layout'; -import DiscoverHome from './DiscoverHome'; -import DiscoverSearch from './DiscoverSearch'; -import DiscoverSearchContainer from './DiscoverSearchContainer'; +import DiscoverHome from '@/components/Discover/DiscoverHome'; +import DiscoverSearch from '@/components/Discover/DiscoverSearch'; +import DiscoverSearchContainer from '@/components/Discover/DiscoverSearchContainer'; import { Box, Inset } from '@/design-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDiscoverScreenContext } from '../DiscoverScreenContext'; +import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; function Switcher({ children }: { children: React.ReactNode[] }) { const { isSearching } = useDiscoverScreenContext(); diff --git a/src/screens/discover/DiscoverScreenContext.tsx b/src/components/Discover/DiscoverScreenContext.tsx similarity index 96% rename from src/screens/discover/DiscoverScreenContext.tsx rename to src/components/Discover/DiscoverScreenContext.tsx index eb9b276443d..31a8e89106b 100644 --- a/src/screens/discover/DiscoverScreenContext.tsx +++ b/src/components/Discover/DiscoverScreenContext.tsx @@ -2,6 +2,7 @@ import { analytics } from '@/analytics'; import React, { createContext, Dispatch, SetStateAction, RefObject, useState, useRef, useCallback } from 'react'; import { SectionList, TextInput } from 'react-native'; import Animated from 'react-native-reanimated'; +import { useTrackDiscoverScreenTime } from './useTrackDiscoverScreenTime'; type DiscoverScreenContextType = { scrollViewRef: RefObject; @@ -80,6 +81,8 @@ const DiscoverScreenProvider = ({ children }: { children: React.ReactNode }) => setIsSearching(false); }, [searchQuery]); + useTrackDiscoverScreenTime(); + return ( { + const assets = currencyList + .filter(a => a.key !== 'profiles') + .map(asset => asset.data) + .flat(); + if (assets.length === 0) return; + const params = { + screen: 'discover' as const, + no_icon: 0, + no_price: 0, + total_tokens: assets.length, + query: searchQueryForSearch, + }; + for (const asset of assets) { + if (!asset.icon_url) params.no_icon += 1; + if (!isNaN(asset.price?.value)) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, + { timeout: 3000, enabled: !isLoading } + ); + return ( diff --git a/src/screens/discover/components/DiscoverSearchContainer.tsx b/src/components/Discover/DiscoverSearchContainer.tsx similarity index 92% rename from src/screens/discover/components/DiscoverSearchContainer.tsx rename to src/components/Discover/DiscoverSearchContainer.tsx index fa7fabff669..2e5a425b631 100644 --- a/src/screens/discover/components/DiscoverSearchContainer.tsx +++ b/src/components/Discover/DiscoverSearchContainer.tsx @@ -3,8 +3,8 @@ import React, { useEffect } from 'react'; import { ButtonPressAnimation } from '@/components/animations'; import { Column, Row } from '@/components/layout'; import { Text } from '@/components/text'; -import DiscoverSearchInput from '@/screens/discover/components/DiscoverSearchInput'; -import { useDiscoverScreenContext } from '../DiscoverScreenContext'; +import DiscoverSearchInput from '@/components/Discover/DiscoverSearchInput'; +import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; import { deviceUtils } from '@/utils'; import { useDelayedValueWithLayoutAnimation } from '@/hooks'; import styled from '@/styled-thing'; diff --git a/src/screens/discover/components/DiscoverSearchInput.tsx b/src/components/Discover/DiscoverSearchInput.tsx similarity index 98% rename from src/screens/discover/components/DiscoverSearchInput.tsx rename to src/components/Discover/DiscoverSearchInput.tsx index 2d21419dbd5..f8dc62ae696 100644 --- a/src/screens/discover/components/DiscoverSearchInput.tsx +++ b/src/components/Discover/DiscoverSearchInput.tsx @@ -13,7 +13,7 @@ import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; import { deviceUtils } from '@/utils'; import { ThemeContextProps } from '@/theme'; -import { useDiscoverScreenContext } from '@/screens/discover/DiscoverScreenContext'; +import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { ChainId } from '@/state/backendNetworks/types'; diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx new file mode 100644 index 00000000000..223a8041981 --- /dev/null +++ b/src/components/Discover/TrendingTokens.tsx @@ -0,0 +1,714 @@ +import { DropdownMenu } from '@/components/DropdownMenu'; +import { globalColors, Text, TextIcon, useBackgroundColor, useColorMode } from '@/design-system'; +import { useForegroundColor } from '@/design-system/color/useForegroundColor'; + +import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon'; +import { analyticsV2 } from '@/analytics'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; +import { ChainImage } from '@/components/coin-icon/ChainImage'; +import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; +import { SortDirection, Timeframe, TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; +import { formatCurrency, formatNumber } from '@/helpers/strings'; +import * as i18n from '@/languages'; +import { Navigation } from '@/navigation'; +import Routes from '@/navigation/routesNames'; +import { FarcasterUser, TrendingToken, useTrendingTokens } from '@/resources/trendingTokens/trendingTokens'; +import { useNavigationStore } from '@/state/navigation/navigationStore'; +import { swapsStore } from '@/state/swaps/swapsStore'; +import { sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; +import { colors } from '@/styles'; +import { darkModeThemeColors } from '@/styles/colors'; +import { useCallback, useEffect, useMemo } from 'react'; +import React, { Dimensions, FlatList, View } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated, { runOnJS, useSharedValue } from 'react-native-reanimated'; +import { ButtonPressAnimation } from '../animations'; +import { useFarcasterAccountForWallets } from '@/hooks/useFarcasterAccountForWallets'; +import { ImgixImage } from '../images'; +import { useRemoteConfig } from '@/model/remoteConfig'; +import { useAccountSettings } from '@/hooks'; +import { getColorWorklet, getMixedColor, opacity } from '@/__swaps__/utils/swaps'; +import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { IS_IOS } from '@/env'; + +const t = i18n.l.trending_tokens; + +function FilterButton({ + icon, + label, + onPress, + selected, + iconColor, + highlightedBackgroundColor, +}: { + onPress?: VoidFunction; + label: string; + icon: string | JSX.Element; + selected: boolean; + iconColor?: string; + highlightedBackgroundColor?: string; +}) { + const { isDarkMode } = useColorMode(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const separatorSecondary = useForegroundColor('separatorSecondary'); + const borderColor = selected && isDarkMode ? globalColors.white80 : separatorSecondary; + const defaultIconColor = getColorWorklet('labelSecondary', selected ? false : isDarkMode); + + const gradientColors = useMemo(() => { + if (!selected) return [fillTertiary, fillTertiary]; + return highlightedBackgroundColor + ? [highlightedBackgroundColor, globalColors.white100] + : [ + isDarkMode ? opacity(globalColors.white100, 0.72) : opacity(fillTertiary, 0.2), + isDarkMode ? opacity(globalColors.white100, 0.92) : opacity(fillTertiary, 0), + ]; + }, [fillTertiary, highlightedBackgroundColor, selected, isDarkMode]); + + return ( + + + {typeof icon === 'string' ? ( + + {icon} + + ) : ( + icon + )} + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + + 􀆏 + + + + ); +} + +function useTrendingTokensData() { + const { nativeCurrency } = useAccountSettings(); + const remoteConfig = useRemoteConfig(); + const { chainId, category, timeframe, sort } = useTrendingTokensStore(state => ({ + chainId: state.chainId, + category: state.category, + timeframe: state.timeframe, + sort: state.sort, + })); + + const walletAddress = useFarcasterAccountForWallets(); + + return useTrendingTokens({ + chainId, + category, + timeframe, + sortBy: sort, + sortDirection: SortDirection.Desc, + limit: remoteConfig.trending_tokens_limit, + walletAddress: walletAddress, + currency: nativeCurrency, + }); +} + +function ReportAnalytics() { + const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); + const { category, chainId } = useTrendingTokensStore(state => ({ category: state.category, chainId: state.chainId })); + const { data: trendingTokens, isLoading } = useTrendingTokensData(); + + useEffect(() => { + if (isLoading || activeSwipeRoute !== Routes.DISCOVER_SCREEN) return; + + const isEmpty = (trendingTokens?.length ?? 0) === 0; + const isLimited = !isEmpty && (trendingTokens?.length ?? 0) < 6; + + analyticsV2.track(analyticsV2.event.viewRankedCategory, { + category, + chainId, + isLimited, + isEmpty, + }); + }, [isLoading, activeSwipeRoute, trendingTokens?.length, category, chainId]); + + return null; +} + +function CategoryFilterButton({ + category, + icon, + iconWidth = 16, + iconColor, + label, + highlightedBackgroundColor, +}: { + category: TrendingCategory; + icon: string; + iconColor: string | { default: string; selected: string }; + highlightedBackgroundColor: string; + iconWidth?: number; + label: string; +}) { + const { isDarkMode } = useColorMode(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const separatorSecondary = useForegroundColor('separatorSecondary'); + + const selected = useTrendingTokensStore(state => state.category === category); + + const borderColor = selected && isDarkMode ? globalColors.white80 : separatorSecondary; + + const gradientColors = useMemo(() => { + if (!selected) return [fillTertiary, fillTertiary]; + return [highlightedBackgroundColor, globalColors.white100]; + }, [fillTertiary, highlightedBackgroundColor, selected]); + + const selectCategory = useCallback(() => { + useTrendingTokensStore.getState().setCategory(category); + }, [category]); + + return ( + + + + {icon} + + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + + + ); +} + +function FriendPfp({ pfp_url }: { pfp_url: string }) { + const backgroundColor = useBackgroundColor('surfacePrimary'); + return ( + + ); +} +function FriendHolders({ friends }: { friends: FarcasterUser[] }) { + if (friends.length === 0) return null; + const howManyOthers = Math.max(1, friends.length - 2); + const separator = howManyOthers === 1 && friends.length === 2 ? ` ${i18n.t(t.and)} ` : ', '; + + return ( + + + + {friends[1] && } + + + + + {friends[0].username} + {friends[1] && ( + <> + + {separator} + + {friends[1].username} + + )} + + {friends.length > 2 && ( + + {' '} + {i18n.t(t.and_others[howManyOthers === 1 ? 'one' : 'other'], { count: howManyOthers })} + + )} + + + ); +} + +function TrendingTokenLoadingRow() { + const backgroundColor = useBackgroundColor('surfacePrimary'); + const { isDarkMode } = useColorMode(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function getPriceChangeColor(priceChange: number) { + if (priceChange === 0) return 'labelTertiary'; + return priceChange > 0 ? 'green' : 'red'; +} + +function TrendingTokenRow({ token }: { token: TrendingToken }) { + const separatorColor = useForegroundColor('separator'); + + const price = formatCurrency(token.price); + const marketCap = formatNumber(token.marketCap, { useOrderSuffix: true, decimals: 1, style: '$' }); + const volume = formatNumber(token.volume, { useOrderSuffix: true, decimals: 1, style: '$' }); + + const handleNavigateToToken = useCallback(() => { + analyticsV2.track(analyticsV2.event.viewTrendingToken, { + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + name: token.name, + highlightedFriends: token.highlightedFriends.length, + }); + + swapsStore.setState({ + lastNavigatedTrendingToken: token.uniqueId, + }); + + Navigation.handleAction(Routes.EXPANDED_ASSET_SHEET, { + asset: token, + type: 'token', + }); + }, [token]); + + if (!token) return null; + + return ( + + + + + + + + + + + + {token.name} + + + {token.symbol} + + + {price} + + + + + + + VOL + + + {volume} + + + + + | + + + + + MCAP + + + {marketCap} + + + + + + + + + {formatNumber(token.priceChange.day, { decimals: 2, useOrderSuffix: true })}% + + + + + 1H + + + {formatNumber(token.priceChange.hr, { decimals: 2, useOrderSuffix: true })}% + + + + + + + + ); +} + +function NoResults() { + const { isDarkMode } = useColorMode(); + const fillQuaternary = useBackgroundColor('fillQuaternary'); + const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; + + return ( + + + + {i18n.t(t.no_results.title)} + + + {i18n.t(t.no_results.body)} + + + + + 􀙭 + + + + ); +} + +function NetworkFilter() { + const { isDarkMode } = useColorMode(); + + const selected = useSharedValue(undefined); + const chainId = useTrendingTokensStore(state => state.chainId); + const setChainId = useTrendingTokensStore(state => state.setChainId); + + const { icon, label, lightenedNetworkColor } = useMemo(() => { + if (!chainId) return { icon: '􀤆', label: i18n.t(t.all), lightenedNetworkColor: undefined }; + const lightenedNetworkColor = useBackendNetworksStore.getState().getColorsForChainId(chainId, isDarkMode); + + return { + icon: ( + + + + ), + label: useBackendNetworksStore.getState().getChainsLabel()[chainId], + lightenedNetworkColor: lightenedNetworkColor + ? getMixedColor(lightenedNetworkColor, globalColors.white100, isDarkMode ? 0.55 : 0.6) + : undefined, + }; + }, [chainId, isDarkMode]); + + const setSelected = useCallback( + (chainId: ChainId | undefined) => { + 'worklet'; + selected.value = chainId; + runOnJS(setChainId)(chainId); + }, + [selected, setChainId] + ); + + const navigateToNetworkSelector = useCallback(() => { + Navigation.handleAction(Routes.NETWORK_SELECTOR, { + selected, + setSelected, + }); + }, [selected, setSelected]); + + return ( + + ); +} + +function TimeFilter() { + const timeframe = useTrendingTokensStore(state => state.timeframe); + const shouldAbbreviate = timeframe === Timeframe.H24 || timeframe === Timeframe.H12; + + return ( + ({ + actionTitle: i18n.t(t.filters.time[time]), + menuState: time === timeframe ? 'on' : 'off', + actionKey: time, + })), + }} + side="bottom" + onPressMenuItem={timeframe => useTrendingTokensStore.getState().setTimeframe(timeframe)} + > + + + ); +} + +function SortFilter() { + const sort = useTrendingTokensStore(state => state.sort); + const selected = sort !== TrendingSort.Recommended; + + const iconColor = useForegroundColor(selected ? 'labelSecondary' : 'labelTertiary'); + + const sortLabel = useMemo(() => { + if (sort === TrendingSort.Recommended) return i18n.t(t.filters.sort.RECOMMENDED.label); + return i18n.t(t.filters.sort[sort]); + }, [sort]); + + return ( + ({ + actionTitle: s === TrendingSort.Recommended ? i18n.t(t.filters.sort.RECOMMENDED.menuOption) : i18n.t(t.filters.sort[s]), + menuState: s === sort ? 'on' : 'off', + actionKey: s, + })), + }} + side="bottom" + onPressMenuItem={selection => { + if (selection === sort) return useTrendingTokensStore.getState().setSort(TrendingSort.Recommended); + useTrendingTokensStore.getState().setSort(selection); + }} + > + + 􀄬 + + } + /> + + ); +} + +function TrendingTokensLoader() { + const { trending_tokens_limit } = useRemoteConfig(); + + return ( + + {Array.from({ length: trending_tokens_limit }).map((_, index) => ( + + ))} + + ); +} + +function TrendingTokenData() { + const { data: trendingTokens, isLoading } = useTrendingTokensData(); + if (isLoading) return ; + + return ( + } + data={trendingTokens} + renderItem={({ item }) => } + /> + ); +} + +const padding = 20; + +export function TrendingTokens() { + const { isDarkMode } = useColorMode(); + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Discover/useTrackDiscoverScreenTime.ts b/src/components/Discover/useTrackDiscoverScreenTime.ts new file mode 100644 index 00000000000..b6f9e02f6cf --- /dev/null +++ b/src/components/Discover/useTrackDiscoverScreenTime.ts @@ -0,0 +1,21 @@ +import { useNavigationStore } from '@/state/navigation/navigationStore'; +import { useEffect } from 'react'; +import Routes from '@/navigation/routesNames'; +import { PerformanceTracking, currentlyTrackedMetrics } from '@/performance/tracking'; +import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; + +export const useTrackDiscoverScreenTime = () => { + const isOnDiscoverScreen = useNavigationStore(state => state.isRouteActive(Routes.DISCOVER_SCREEN)); + + useEffect(() => { + const data = currentlyTrackedMetrics.get(PerformanceMetrics.timeSpentOnDiscoverScreen); + + if (!isOnDiscoverScreen && data?.startTimestamp) { + PerformanceTracking.finishMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + + if (isOnDiscoverScreen) { + PerformanceTracking.startMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + }, [isOnDiscoverScreen]); +}; diff --git a/src/components/ExchangeTokenRow.tsx b/src/components/ExchangeTokenRow.tsx index 68ca29293da..351d5026855 100644 --- a/src/components/ExchangeTokenRow.tsx +++ b/src/components/ExchangeTokenRow.tsx @@ -2,7 +2,7 @@ import React from 'react'; import isEqual from 'react-fast-compare'; import { Box, Column, Columns, Inline, Stack, Text } from '@/design-system'; import { isNativeAsset } from '@/handlers/assets'; -import { useAsset, useDimensions } from '@/hooks'; +import { useAsset } from '@/hooks'; import { ButtonPressAnimation } from '@/components/animations'; import { FloatingEmojis } from '@/components/floating-emojis'; import { IS_IOS } from '@/env'; @@ -34,7 +34,6 @@ export default React.memo(function ExchangeTokenRow({ disabled, }, }: ExchangeTokenRowProps) { - const { width: deviceWidth } = useDimensions(); const item = useAsset({ address, chainId, @@ -106,10 +105,8 @@ export default React.memo(function ExchangeTokenRow({ {isInfoButtonVisible && } {showFavoriteButton && (IS_IOS ? ( - // @ts-ignore { // there's an e2e modifying this panel so I needed values that aren't dependent on the network conditions - const maxBaseFeeToValidate = IS_TESTING === 'true' ? 100 : currentBaseFee; + const maxBaseFeeToValidate = IS_TEST ? 100 : currentBaseFee; if (!maxBaseFee || isZero(maxBaseFee) || greaterThan(multiply(0.1, maxBaseFeeToValidate), maxBaseFee)) { setMaxBaseFeeError({ @@ -404,10 +403,7 @@ export default function FeesPanel({ currentGasTrend, colorForAsset, setCanGoBack } // there's an e2e modifying this panel so I needed values that aren't dependant on the network conditions if ( - greaterThan( - multiply(MINER_TIP_RANGE[0], IS_TESTING === 'true' ? 1 : gasFeeParamsBySpeed?.[NORMAL]?.maxPriorityFeePerGas?.gwei), - maxPriorityFee - ) + greaterThan(multiply(MINER_TIP_RANGE[0], IS_TEST ? 1 : gasFeeParamsBySpeed?.[NORMAL]?.maxPriorityFeePerGas?.gwei), maxPriorityFee) ) { setMaxPriorityFeeWarning({ message: lang.t('gas.lower_than_suggested'), @@ -415,10 +411,7 @@ export default function FeesPanel({ currentGasTrend, colorForAsset, setCanGoBack }); } else if ( // there's an e2e modifying this panel so I needed values that aren't dependant on the network conditions - greaterThan( - maxPriorityFee, - multiply(MINER_TIP_RANGE[1], IS_TESTING === 'true' ? 1 : gasFeeParamsBySpeed?.[URGENT]?.maxPriorityFeePerGas?.gwei) - ) + greaterThan(maxPriorityFee, multiply(MINER_TIP_RANGE[1], IS_TEST ? 1 : gasFeeParamsBySpeed?.[URGENT]?.maxPriorityFeePerGas?.gwei)) ) { setMaxPriorityFeeWarning({ message: lang.t('gas.higher_than_suggested'), diff --git a/src/components/GweiInputPill.tsx b/src/components/GweiInputPill.tsx index a1f9657de32..2c61642f224 100644 --- a/src/components/GweiInputPill.tsx +++ b/src/components/GweiInputPill.tsx @@ -1,5 +1,4 @@ import React, { useCallback } from 'react'; -import { IS_TESTING } from 'react-native-dotenv'; import LinearGradient from 'react-native-linear-gradient'; // @ts-expect-error - no declaration file import TextInputMask from 'react-native-text-input-mask'; @@ -9,7 +8,7 @@ import { buildTextStyles, margin, padding } from '@/styles'; import { useTheme } from '@/theme'; import { TextInput } from 'react-native'; import { Box, Inline, Inset, Text } from '@/design-system'; -import { IS_ANDROID } from '@/env'; +import { IS_ANDROID, IS_TEST } from '@/env'; const ANDROID_EXTRA_LINE_HEIGHT = 6; @@ -98,7 +97,7 @@ function GweiInputPill( testID={testID} value={value} /> - {IS_TESTING !== 'true' && ( + {!IS_TEST && ( Gwei diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx new file mode 100644 index 00000000000..2b385abb1fe --- /dev/null +++ b/src/components/NetworkSwitcher.tsx @@ -0,0 +1,872 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { opacity } from '@/__swaps__/utils/swaps'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; +import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView'; +import { ButtonPressAnimation } from '@/components/animations'; +import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs'; +import { ChainImage } from '@/components/coin-icon/ChainImage'; +import { AnimatedText, Box, DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; +import { useForegroundColor } from '@/design-system/color/useForegroundColor'; +import * as i18n from '@/languages'; +import deviceUtils, { DEVICE_WIDTH } from '@/utils/deviceUtils'; +import MaskedView from '@react-native-masked-view/masked-view'; +import chroma from 'chroma-js'; +import { PropsWithChildren, useEffect } from 'react'; +import React, { Pressable, StyleSheet, View } from 'react-native'; +import { RouteProp, useRoute } from '@react-navigation/native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated, { + Easing, + FadeIn, + FadeOutUp, + LinearTransition, + runOnJS, + SharedValue, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withDelay, + withSequence, + withSpring, + withTiming, +} from 'react-native-reanimated'; +import Svg, { Path } from 'react-native-svg'; +import { + customizeNetworksBannerStore, + defaultPinnedNetworks, + dismissCustomizeNetworksBanner, + networkSwitcherStore, + shouldShowCustomizeNetworksBanner, +} from '@/state/networkSwitcher/networkSwitcher'; +import { RootStackParamList } from '@/navigation/types'; +import { IS_IOS } from '@/env'; +import { safeAreaInsetValues } from '@/utils'; +import { noop } from 'lodash'; +import { TapToDismiss } from './DappBrowser/control-panel/ControlPanel'; +import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; +import { useTheme } from '@/theme'; + +const t = i18n.l.network_switcher; + +const translations = { + edit: i18n.t(t.edit), + done: i18n.t(i18n.l.done), + networks: i18n.t(t.networks), + more: i18n.t(t.more), + show_more: i18n.t(t.show_more), + show_less: i18n.t(t.show_less), + drag_to_rearrange: i18n.t(t.drag_to_rearrange), +}; + +function EditButton({ editing }: { editing: SharedValue }) { + const blue = useForegroundColor('blue'); + const borderColor = chroma(blue).alpha(0.08).hex(); + + const text = useDerivedValue(() => (editing.value ? translations.done : translations.edit)); + + return ( + { + 'worklet'; + editing.value = !editing.value; + }} + scaleTo={0.95} + style={{ + borderColor, + borderCurve: 'continuous', + borderRadius: 14, + borderWidth: THICK_BORDER_WIDTH, + height: 28, + justifyContent: 'center', + overflow: 'hidden', + paddingHorizontal: 10, + position: 'absolute', + right: 0, + }} + > + + {text} + + + ); +} + +function Header({ editing }: { editing: SharedValue }) { + const separatorTertiary = useForegroundColor('separatorTertiary'); + const fill = useForegroundColor('fill'); + + const title = useDerivedValue(() => { + return editing.value ? translations.edit : translations.networks; + }); + + return ( + + + + + + + + {title} + + + + + + ); +} + +const CustomizeNetworksBanner = !shouldShowCustomizeNetworksBanner(customizeNetworksBannerStore.getState().dismissedAt) + ? () => null + : function CustomizeNetworksBanner({ editing }: { editing: SharedValue }) { + useAnimatedReaction( + () => editing.value, + (editing, prev) => { + if (!prev && editing) runOnJS(dismissCustomizeNetworksBanner)(); + } + ); + + const dismissedAt = customizeNetworksBannerStore(s => s.dismissedAt); + if (!shouldShowCustomizeNetworksBanner(dismissedAt)) return null; + + const height = 75; + const blue = '#268FFF'; + + return ( + + + + + + } + > + + + + + 􀍱 + + + + + {i18n.t(t.customize_networks_banner.title)} + + + {i18n.t(t.customize_networks_banner.tap_the)}{' '} + + {i18n.t(t.edit)} + {' '} + {i18n.t(t.customize_networks_banner.button_to_set_up)} + + + + + 􀆄 + + + + + + + + ); + }; + +const BADGE_BORDER_COLORS = { + default: { + dark: globalColors.white10, + light: '#F2F3F4', + }, + selected: { + dark: '#1E2E40', + light: '#D7E9FD', + }, +}; + +const useNetworkOptionStyle = (isSelected: SharedValue, color?: string) => { + const { isDarkMode } = useColorMode(); + const label = useForegroundColor('labelTertiary'); + + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + const separatorTertiary = useForegroundColor('separatorTertiary'); + + const defaultStyle = { + backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, + borderColor: isDarkMode ? opacity(separatorTertiary, 0.02) : separatorTertiary, + }; + const selectedStyle = { + backgroundColor: chroma + .scale([networkSwitcherBackgroundColor, color || label])(0.16) + .hex(), + borderColor: chroma(color || label) + .alpha(0.16) + .hex(), + }; + + const scale = useSharedValue(1); + useAnimatedReaction( + () => isSelected.value, + (current, prev) => { + if (current === true && prev === false) { + scale.value = withSequence( + withTiming(0.9, { duration: 120, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }), + withTiming(1, TIMING_CONFIGS.fadeConfig) + ); + } + } + ); + + const animatedStyle = useAnimatedStyle(() => { + const colors = isSelected.value ? selectedStyle : defaultStyle; + return { + backgroundColor: colors.backgroundColor, + borderColor: colors.borderColor, + transform: [{ scale: scale.value }], + }; + }); + + return { + animatedStyle, + selectedStyle, + defaultStyle, + }; +}; + +function AllNetworksOption({ + selected, + setSelected, +}: { + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; +}) { + const { isDarkMode } = useColorMode(); + const blue = useForegroundColor('blue'); + + const isSelected = useDerivedValue(() => selected.value === undefined); + const { animatedStyle } = useNetworkOptionStyle(isSelected, blue); + + const overlappingBadge = useAnimatedStyle(() => { + return { + borderColor: isSelected.value + ? BADGE_BORDER_COLORS.selected[isDarkMode ? 'dark' : 'light'] + : BADGE_BORDER_COLORS.default[isDarkMode ? 'dark' : 'light'], + }; + }); + + return ( + { + 'worklet'; + setSelected(undefined); + }} + scaleTo={0.95} + > + + + + + + + + + + + + + + + + + {i18n.t(t.all_networks)} + + + + ); +} + +function AllNetworksSection({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { + const style = useAnimatedStyle(() => ({ + opacity: editing.value ? withTiming(0, TIMING_CONFIGS.fastFadeConfig) : withTiming(1, TIMING_CONFIGS.fastFadeConfig), + height: withTiming( + editing.value ? 0 : ITEM_HEIGHT + 14, // 14 is the gap to the separator + TIMING_CONFIGS.fastFadeConfig + ), + marginTop: editing.value ? 0 : 14, + pointerEvents: editing.value ? 'none' : 'auto', + })); + return ( + + + + + ); +} + +function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { + const { isDarkMode } = useColorMode(); + const getColorsForChainId = useBackendNetworksStore(state => state.getColorsForChainId); + const chainName = useBackendNetworksStore.getState().getChainsLabel()[chainId]; + const chainColor = getColorsForChainId(chainId, isDarkMode); + const isSelected = useDerivedValue(() => selected.value === chainId); + const { animatedStyle } = useNetworkOptionStyle(isSelected, chainColor); + + return ( + + + + {chainName} + + + ); +} + +const SHEET_OUTER_INSET = 8; +const SHEET_INNER_PADDING = 16; +const GAP = 12; +const ITEM_WIDTH = (DEVICE_WIDTH - SHEET_INNER_PADDING * 2 - SHEET_OUTER_INSET * 2 - GAP) / 2; +const ITEM_HEIGHT = 48; +const SEPARATOR_HEIGHT = 68; + +const ALL_NETWORKS_BADGE_SIZE = 16; +const THICKER_BORDER_WIDTH = 5 / 3; + +const enum Section { + pinned, + separator, + unpinned, +} + +function Draggable({ + children, + dragging, + chainId, + networks, + sectionsOffsets, + isUnpinnedHidden, +}: PropsWithChildren<{ + chainId: ChainId; + dragging: SharedValue; + networks: SharedValue>; + sectionsOffsets: SharedValue>; + isUnpinnedHidden: SharedValue; +}>) { + const zIndex = useSharedValue(0); + useAnimatedReaction( + () => dragging.value?.chainId, + (current, prev) => { + if (current === prev) return; + if (current === chainId) zIndex.value = 2; + if (prev === chainId) zIndex.value = 1; + } + ); + + const draggableStyles = useAnimatedStyle(() => { + const section = networks.value[Section.pinned].includes(chainId) ? Section.pinned : Section.unpinned; + const itemIndex = networks.value[section].indexOf(chainId); + const slotPosition = positionFromIndex(itemIndex, sectionsOffsets.value[section]); + + const opacity = + section === Section.unpinned && isUnpinnedHidden.value + ? withTiming(0, TIMING_CONFIGS.fastFadeConfig) + : withDelay(100, withTiming(1, TIMING_CONFIGS.fadeConfig)); + + const isBeingDragged = dragging.value?.chainId === chainId; + const position = isBeingDragged ? dragging.value!.position : slotPosition; + + return { + opacity, + zIndex: zIndex.value, + transform: [ + { scale: withSpring(isBeingDragged ? 1.05 : 1, SPRING_CONFIGS.springConfig) }, + { translateX: isBeingDragged ? position.x : withSpring(position.x, SPRING_CONFIGS.springConfig) }, + { translateY: isBeingDragged ? position.y : withSpring(position.y, SPRING_CONFIGS.springConfig) }, + ], + }; + }); + + return {children}; +} + +const indexFromPosition = (x: number, y: number, offset: { y: number }) => { + 'worklet'; + const yoffsets = y > offset.y ? offset.y : 0; + const column = x > ITEM_WIDTH + GAP / 2 ? 1 : 0; + const row = Math.floor((y - yoffsets) / (ITEM_HEIGHT + GAP)); + const index = row * 2 + column; + return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row +}; + +const positionFromIndex = (index: number, offset: { y: number }) => { + 'worklet'; + const column = index % 2; + const row = Math.floor(index / 2); + const position = { x: column * (ITEM_WIDTH + GAP), y: row * (ITEM_HEIGHT + GAP) + offset.y }; + return position; +}; + +type Point = { x: number; y: number }; +type DraggingState = { + chainId: ChainId; + position: Point; +}; + +function SectionSeparator({ + sectionsOffsets, + editing, + expanded, + networks, +}: { + sectionsOffsets: SharedValue>; + editing: SharedValue; + expanded: SharedValue; + networks: SharedValue>; +}) { + const pressed = useSharedValue(false); + + const showExpandButtonAsNetworkChip = useDerivedValue(() => { + return !expanded.value && !editing.value && networks.value[Section.pinned].length % 2 !== 0; + }); + + const visible = useDerivedValue(() => { + return networks.value[Section.unpinned].length > 0 || editing.value; + }); + + const tapExpand = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value || !visible.value) return s.fail(); + pressed.value = true; + }) + .onEnd(() => { + pressed.value = false; + expanded.value = !expanded.value; + }); + + const text = useDerivedValue(() => { + if (editing.value) return translations.drag_to_rearrange; + if (showExpandButtonAsNetworkChip.value) return translations.more; + return expanded.value ? translations.show_less : translations.show_more; + }); + + const unpinnedNetworksLength = useDerivedValue(() => networks.value[Section.unpinned].length.toString()); + const showMoreAmountStyle = useAnimatedStyle(() => ({ + opacity: expanded.value || editing.value ? 0 : 1, + })); + const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈') as string); + const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); + + const { isDarkMode } = useColorMode(); + + const separatorContainerStyles = useAnimatedStyle(() => { + if (showExpandButtonAsNetworkChip.value) { + const position = positionFromIndex(networks.value[Section.pinned].length, sectionsOffsets.value[Section.pinned]); + return { + backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, + borderColor: '#F5F8FF05', + height: ITEM_HEIGHT, + width: ITEM_WIDTH, + flexDirection: 'row', + alignItems: 'center', + borderRadius: 24, + borderWidth: THICK_BORDER_WIDTH, + transform: [{ translateX: position.x }, { translateY: position.y }], + }; + } + + return { + backgroundColor: 'transparent', + opacity: visible.value ? 1 : 0, + transform: [{ translateY: sectionsOffsets.value[Section.separator].y }, { scale: withTiming(pressed.value ? 0.95 : 1) }], + position: 'absolute', + width: '100%', + height: SEPARATOR_HEIGHT, + }; + }); + + return ( + + + + + {unpinnedNetworksLength} + + + + {text} + + + + {showMoreOrLessIcon} + + + + + ); +} + +function EmptyUnpinnedPlaceholder({ + sectionsOffsets, + networks, + isUnpinnedHidden, +}: { + sectionsOffsets: SharedValue>; + networks: SharedValue>; + isUnpinnedHidden: SharedValue; +}) { + const styles = useAnimatedStyle(() => { + const isVisible = networks.value[Section.unpinned].length === 0 && !isUnpinnedHidden.value; + return { + opacity: isVisible ? withTiming(1, { duration: 800 }) : 0, + transform: [{ translateY: sectionsOffsets.value[Section.unpinned].y }], + }; + }); + const { isDarkMode } = useColorMode(); + return ( + + + {i18n.t(t.drag_here_to_unpin)} + + + ); +} + +function NetworksGrid({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { + const initialPinned = networkSwitcherStore.getState().pinnedNetworks; + const sortedSupportedChainIds = useBackendNetworksStore.getState().getSortedSupportedChainIds(); + const initialUnpinned = sortedSupportedChainIds.filter(chainId => !initialPinned.includes(chainId)); + const networks = useSharedValue({ [Section.pinned]: initialPinned, [Section.unpinned]: initialUnpinned }); + + useEffect(() => { + // persists pinned networks when closing the sheet + // should be the only time this component is unmounted + return () => { + if (networks.value[Section.pinned].length > 0) { + networkSwitcherStore.setState({ pinnedNetworks: networks.value[Section.pinned] }); + } else { + networkSwitcherStore.setState({ pinnedNetworks: defaultPinnedNetworks }); + } + }; + }, [networks]); + + const expanded = useSharedValue(false); + const isUnpinnedHidden = useDerivedValue(() => !expanded.value && !editing.value); + + const dragging = useSharedValue(null); + + const sectionsOffsets = useDerivedValue(() => { + const pinnedHeight = Math.ceil(networks.value[Section.pinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP; + return { + [Section.pinned]: { y: 0 }, + [Section.separator]: { y: pinnedHeight }, + [Section.unpinned]: { y: pinnedHeight + SEPARATOR_HEIGHT }, + }; + }); + const containerHeight = useDerivedValue(() => { + const length = networks.value[Section.unpinned].length; + const paddingBottom = 32; + const unpinnedHeight = isUnpinnedHidden.value + ? length === 0 + ? -SEPARATOR_HEIGHT + paddingBottom + : 0 + : length === 0 + ? ITEM_HEIGHT + paddingBottom + : Math.ceil((length + 1) / 2) * (ITEM_HEIGHT + GAP) - GAP + paddingBottom; + const height = sectionsOffsets.value[Section.unpinned].y + unpinnedHeight; + return height; + }); + const containerStyle = useAnimatedStyle(() => ({ + height: withDelay(expanded.value ? 0 : 25, withTiming(containerHeight.value, TIMING_CONFIGS.slowerFadeConfig)), + })); + + const dragNetwork = Gesture.Pan() + .maxPointers(1) + .onTouchesDown((e, s) => { + if (!editing.value) { + s.fail(); + return; + } + const touch = e.allTouches[0]; + const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const sectionOffset = sectionsOffsets.value[section]; + const index = indexFromPosition(touch.x, touch.y, sectionOffset); + const sectionNetworks = networks.value[section]; + const chainId = sectionNetworks[index]; + + if (!chainId || (section === Section.pinned && sectionNetworks.length === 1)) { + s.fail(); + return; + } + + const position = positionFromIndex(index, sectionOffset); + dragging.value = { chainId, position }; + }) + .onChange(e => { + if (!dragging.value) return; + const chainId = dragging.value.chainId; + if (!chainId) return; + + const section = e.y > sectionsOffsets.value[Section.unpinned].y - SEPARATOR_HEIGHT / 2 ? Section.unpinned : Section.pinned; + const sectionArray = networks.value[section]; + + const currentIndex = sectionArray.indexOf(chainId); + const newIndex = Math.min(indexFromPosition(e.x, e.y, sectionsOffsets.value[section]), sectionArray.length - 1); + + networks.modify(networks => { + if (currentIndex === -1) { + // Pin/Unpin + if (section === Section.unpinned) networks[Section.pinned].splice(currentIndex, 1); + else networks[Section.pinned].push(chainId); + networks[Section.unpinned] = sortedSupportedChainIds.filter(chainId => !networks[Section.pinned].includes(chainId)); + } else if (section === Section.pinned && newIndex !== currentIndex) { + // Reorder + networks[Section.pinned].splice(currentIndex, 1); + networks[Section.pinned].splice(newIndex, 0, chainId); + } + return networks; + }); + dragging.modify(dragging => { + if (!dragging) return dragging; + dragging.position.x += e.changeX; + dragging.position.y += e.changeY; + return dragging; + }); + }) + .onFinalize(() => { + dragging.value = null; + }); + + const tapNetwork = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value) return s.fail(); + }) + .onEnd(e => { + const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const index = indexFromPosition(e.x, e.y, sectionsOffsets.value[section]); + const chainId = networks.value[section][index]; + if (!chainId) return; + + setSelected(chainId); + }); + + const gridGesture = Gesture.Exclusive(dragNetwork, tapNetwork); + + return ( + + + {initialPinned.map(chainId => ( + + + + ))} + + + + + + {initialUnpinned.map(chainId => ( + + + + ))} + + + ); +} + +function Sheet({ children, editing, onClose }: PropsWithChildren<{ editing: SharedValue; onClose: VoidFunction }>) { + const { isDarkMode } = useColorMode(); + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + const separatorSecondary = useForegroundColor('separatorSecondary'); + + // make sure the onClose function is called when the sheet unmounts + useEffect(() => { + return () => onClose?.(); + }, [onClose]); + + return ( + <> + +
+ {children} + + + + ); +} + +export function NetworkSelector() { + const { + params: { onClose = noop, selected, setSelected }, + } = useRoute>(); + + const editing = useSharedValue(false); + + return ( + + + + + + ); +} + +const sx = StyleSheet.create({ + overlappingBadge: { + borderWidth: THICKER_BORDER_WIDTH, + borderRadius: ALL_NETWORKS_BADGE_SIZE, + marginLeft: -9, + width: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + height: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + }, + sheet: { + flex: 1, + width: deviceUtils.dimensions.width - 16, + bottom: Math.max(safeAreaInsetValues.bottom + 5, IS_IOS ? 8 : 30), + pointerEvents: 'box-none', + position: 'absolute', + zIndex: 30000, + left: 8, + right: 8, + paddingHorizontal: 16, + borderCurve: 'continuous', + borderRadius: 42, + borderWidth: THICK_BORDER_WIDTH, + overflow: 'hidden', + }, +}); diff --git a/src/components/PortalConsumer.js b/src/components/PortalConsumer.js deleted file mode 100644 index 351b2271f02..00000000000 --- a/src/components/PortalConsumer.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useEffect } from 'react'; -import { LoadingOverlay } from './modal'; -import { useWallets } from '@/hooks'; -import { sheetVerticalOffset } from '@/navigation/effects'; -import { usePortal } from '@/react-native-cool-modals/Portal'; - -export default function PortalConsumer() { - const { isWalletLoading } = useWallets(); - const { setComponent, hide } = usePortal(); - useEffect(() => { - if (isWalletLoading) { - setComponent(, true); - } - return hide; - }, [hide, isWalletLoading, setComponent]); - - return null; -} diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index d33af18643b..9e32d25576e 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { StyleProp, ViewStyle } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import SpinnerImageSource from '../assets/spinner.png'; import { useTheme } from '../theme/ThemeContext'; import { SpinAnimation } from './animations'; import { Centered } from './layout'; import { ImgixImage } from '@/components/images'; import { position } from '@/styles'; +import { IS_TEST } from '@/env'; type SpinnerProps = { color?: string; @@ -32,7 +32,7 @@ const Spinner = ({ color = '', duration = 1500, size = 20, ...props }: SpinnerPr return ( - {IS_TESTING !== 'true' && ( + {!IS_TEST && ( diff --git a/src/components/WalletLoadingListener.tsx b/src/components/WalletLoadingListener.tsx new file mode 100644 index 00000000000..6a9e605ab4f --- /dev/null +++ b/src/components/WalletLoadingListener.tsx @@ -0,0 +1,17 @@ +import React, { useEffect } from 'react'; +import { LoadingOverlay } from './modal'; +import { sheetVerticalOffset } from '@/navigation/effects'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; + +export default function WalletLoadingListener() { + const loadingState = walletLoadingStore(state => state.loadingState); + + useEffect(() => { + if (loadingState) { + walletLoadingStore.getState().setComponent(); + } + return walletLoadingStore.getState().hide; + }, [loadingState]); + + return null; +} diff --git a/src/components/animations/ShimmerAnimation.js b/src/components/animations/ShimmerAnimation.js index 7d17fdbf480..c988721f6c8 100644 --- a/src/components/animations/ShimmerAnimation.js +++ b/src/components/animations/ShimmerAnimation.js @@ -3,7 +3,7 @@ import LinearGradient from 'react-native-linear-gradient'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; import styled from '@/styled-thing'; import { position } from '@/styles'; -import { IS_TESTING } from 'react-native-dotenv'; +import { IS_TEST } from '@/env'; const timingConfig = { duration: 2500, @@ -59,7 +59,7 @@ export default function ShimmerAnimation({ transform: [{ translateX: positionX.value }], })); - if (IS_TESTING === 'true') { + if (IS_TEST) { return null; } diff --git a/src/components/asset-list/AssetListHeader.js b/src/components/asset-list/AssetListHeader.js index 7e1013861be..6d6203ba608 100644 --- a/src/components/asset-list/AssetListHeader.js +++ b/src/components/asset-list/AssetListHeader.js @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { IS_TESTING } from 'react-native-dotenv'; import LinearGradient from 'react-native-linear-gradient'; import { abbreviations, magicMemo, measureText } from '../../utils'; import { ButtonPressAnimation } from '../animations'; @@ -17,6 +16,7 @@ import { fonts, position } from '@/styles'; import { useTheme } from '@/theme'; import * as lang from '@/languages'; import { useUserAssetsStore } from '@/state/assets/userAssets'; +import { IS_TEST } from '@/env'; export const AssetListHeaderHeight = ListHeaderHeight; @@ -82,7 +82,7 @@ const WalletSelectButton = ({ accountName, onChangeWallet, deviceWidth, textWidt {truncatedAccountName ? ( - {IS_TESTING !== 'true' && ( + {!IS_TEST && ( - + @@ -216,13 +216,17 @@ function SendButton() { ); } -export function MoreButton() { - // //////////////////////////////////////////////////// - // Handlers - +export function CopyButton() { const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); const { accountAddress } = useAccountProfile(); + const { isDamaged } = useWallets(); + const handlePressCopy = React.useCallback(() => { + if (isDamaged) { + showWalletErrorAlert(); + return; + } + if (!isToastActive) { setToastActive(true); setTimeout(() => { @@ -230,7 +234,7 @@ export function MoreButton() { }, 2000); } Clipboard.setString(accountAddress); - }, [accountAddress, isToastActive, setToastActive]); + }, [accountAddress, isDamaged, isToastActive, setToastActive]); return ( <> diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx index 616935fe6ff..9859a4ae105 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx @@ -109,7 +109,6 @@ export function ProfileNameRow({ scaleTo={0} size={50} wiggleFactor={0} - // @ts-expect-error – JS component setOnNewEmoji={newOnNewEmoji => (onNewEmoji.current = newOnNewEmoji)} /> diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx deleted file mode 100644 index 62f92a99e2f..00000000000 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useCallback } from 'react'; -import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; -import { ImgixImage } from '../images'; -import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png'; -import { Source } from 'react-native-fast-image'; -import { cloudPlatform } from '@/utils/platform'; -import { ButtonPressAnimation } from '../animations'; -import Routes from '@/navigation/routesNames'; -import { useNavigation } from '@/navigation'; -import { useWallets } from '@/hooks'; -import { WalletCountPerType, useVisibleWallets } from '@/screens/SettingsSheet/useVisibleWallets'; -import { format } from 'date-fns'; -import { useCreateBackup } from './useCreateBackup'; -import { login } from '@/handlers/cloudBackup'; - -const imageSize = 72; - -export default function AddWalletToCloudBackupStep() { - const { goBack } = useNavigation(); - const { wallets, selectedWallet } = useWallets(); - - const walletTypeCount: WalletCountPerType = { - phrase: 0, - privateKey: 0, - }; - - const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const { onSubmit } = useCreateBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }); - - const potentiallyLoginAndSubmit = useCallback(async () => { - await login(); - return onSubmit({}); - }, [onSubmit]); - - const onMaybeLater = useCallback(() => goBack(), [goBack]); - - return ( - - - - - - {lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)} - - - - - - - - - potentiallyLoginAndSubmit().then(success => success && goBack())}> - - - - - 􀎽{' '} - {lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, { - cloudPlatform, - })} - - - - - - - - - - - - - - - - {lang.t(lang.l.back_up.cloud.mayber_later)} - - - - - - - - - - - {lastBackupDate && ( - - - - - {lang.t(lang.l.back_up.cloud.latest_backup, { - date: format(lastBackupDate, "M/d/yy 'at' h:mm a"), - })} - - - - - )} - - ); -} diff --git a/src/components/backup/BackupManuallyStep.tsx b/src/components/backup/BackupManuallyStep.tsx deleted file mode 100644 index da18d73806a..00000000000 --- a/src/components/backup/BackupManuallyStep.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useCallback } from 'react'; -import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; -import { ImgixImage } from '../images'; -import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png'; -import { Source } from 'react-native-fast-image'; -import { ButtonPressAnimation } from '../animations'; -import { useNavigation } from '@/navigation'; -import Routes from '@/navigation/routesNames'; -import { useWallets } from '@/hooks'; -import walletTypes from '@/helpers/walletTypes'; -import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; - -const imageSize = 72; - -export default function BackupManuallyStep() { - const { navigate, goBack } = useNavigation(); - const { selectedWallet } = useWallets(); - - const onManualBackup = async () => { - const title = - selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey - ? (selectedWallet.addresses || [])[0].label - : selectedWallet.name; - - goBack(); - navigate(Routes.SETTINGS_SHEET, { - screen: SETTINGS_BACKUP_ROUTES.SECRET_WARNING, - params: { - isBackingUp: true, - title, - backupType: walletBackupTypes.manual, - walletId: selectedWallet.id, - }, - }); - }; - - const onMaybeLater = useCallback(() => goBack(), [goBack]); - - return ( - - - - - - {lang.t(lang.l.back_up.manual.backup_manually_now)} - - - - - - - - - - - - - - {lang.t(lang.l.back_up.manual.back_up_now)} - - - - - - - - - - - - - - - - {lang.t(lang.l.back_up.manual.already_backed_up)} - - - - - - - - - - - ); -} diff --git a/src/components/backup/BackupSheet.tsx b/src/components/backup/BackupSheet.tsx index 5b6a0a4300a..12e15c60190 100644 --- a/src/components/backup/BackupSheet.tsx +++ b/src/components/backup/BackupSheet.tsx @@ -2,13 +2,10 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import React, { useCallback } from 'react'; import { BackupCloudStep, RestoreCloudStep } from '.'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import BackupChooseProviderStep from '@/components/backup/BackupChooseProviderStep'; +import BackupWalletPrompt from '@/components/backup/BackupWalletPrompt'; import { BackgroundProvider } from '@/design-system'; import { SimpleSheet } from '@/components/sheet/SimpleSheet'; -import AddWalletToCloudBackupStep from '@/components/backup/AddWalletToCloudBackupStep'; -import BackupManuallyStep from './BackupManuallyStep'; import { getHeightForStep } from '@/navigation/config'; -import { CloudBackupProvider } from './CloudBackupProvider'; type BackupSheetParams = { BackupSheet: { @@ -21,38 +18,32 @@ type BackupSheetParams = { }; export default function BackupSheet() { - const { params: { step = WalletBackupStepTypes.no_provider } = {} } = useRoute>(); + const { params: { step = WalletBackupStepTypes.backup_prompt } = {} } = useRoute>(); const renderStep = useCallback(() => { switch (step) { - case WalletBackupStepTypes.backup_now_to_cloud: - return ; - case WalletBackupStepTypes.backup_now_manually: - return ; case WalletBackupStepTypes.backup_cloud: return ; case WalletBackupStepTypes.restore_from_backup: return ; - case WalletBackupStepTypes.no_provider: + case WalletBackupStepTypes.backup_prompt: default: - return ; + return ; } }, [step]); return ( - - - {({ backgroundColor }) => ( - - {renderStep()} - - )} - - + + {({ backgroundColor }) => ( + + {renderStep()} + + )} + ); } diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupWalletPrompt.tsx similarity index 66% rename from src/components/backup/BackupChooseProviderStep.tsx rename to src/components/backup/BackupWalletPrompt.tsx index 38325639704..77071534f48 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupWalletPrompt.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import React, { useCallback, useMemo } from 'react'; import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; -import * as lang from '@/languages'; +import * as i18n from '@/languages'; import { ImgixImage } from '../images'; import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png'; import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png'; @@ -14,13 +13,13 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes'; import { useWallets } from '@/hooks'; -import walletTypes from '@/helpers/walletTypes'; +import WalletTypes from '@/helpers/walletTypes'; import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { IS_ANDROID } from '@/env'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { RainbowError, logger } from '@/logger'; -import { Linking } from 'react-native'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { TextColor } from '@/design-system/color/palettes'; +import { CustomColor } from '@/design-system/color/useForegroundColor'; const imageSize = 72; @@ -28,67 +27,31 @@ export default function BackupSheetSectionNoProvider() { const { colors } = useTheme(); const { navigate, goBack } = useNavigation(); const { selectedWallet } = useWallets(); + const createBackup = useCreateBackup(); + const { status } = backupsStore(state => ({ + status: state.status, + })); - const { onSubmit, loading } = useCreateBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }); - - const onCloudBackup = async () => { - if (loading !== 'none') { - return; - } - // NOTE: On Android we need to make sure the user is signed into a Google account before trying to backup - // otherwise we'll fake backup and it's confusing... - if (IS_ANDROID) { - try { - await login(); - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (!accountDetails) { - Alert.alert(lang.t(lang.l.back_up.errors.no_account_found)); - return; - } - }); - } catch (e) { - logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { - error: e, - }); - Alert.alert(lang.t(lang.l.back_up.errors.no_account_found)); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.label), - lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } + const onCloudBackup = useCallback(() => { + // pop the bottom sheet, and navigate to the backup section inside settings sheet + goBack(); + navigate(Routes.SETTINGS_SHEET, { + screen: Routes.SETTINGS_SECTION_BACKUP, + initial: false, + }); - onSubmit({}); - }; + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId: selectedWallet.id, + }), + logout: true, + }); + }, [createBackup, goBack, navigate, selectedWallet.id]); - const onManualBackup = async () => { + const onManualBackup = useCallback(async () => { const title = - selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey + selectedWallet?.imported && selectedWallet.type === WalletTypes.privateKey ? (selectedWallet.addresses || [])[0].label : selectedWallet.name; @@ -102,13 +65,38 @@ export default function BackupSheetSectionNoProvider() { walletId: selectedWallet.id, }, }); - }; + }, [goBack, navigate, selectedWallet.addresses, selectedWallet.id, selectedWallet?.imported, selectedWallet.name, selectedWallet.type]); + + const isCloudBackupDisabled = useMemo(() => { + return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable; + }, [status]); + + const { color, text } = useMemo<{ text: string; color: TextColor | CustomColor }>(() => { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + color: 'primary (Deprecated)', + }; + } + + if (status === CloudBackupState.Ready) { + return { + text: i18n.t(i18n.l.back_up.cloud.cloud_backup), + color: 'primary (Deprecated)', + }; + } + + return { + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + color: 'yellow', + }; + }, [status]); return ( - {lang.t(lang.l.back_up.cloud.how_would_you_like_to_backup)} + {i18n.t(i18n.l.back_up.cloud.how_would_you_like_to_backup)} @@ -116,8 +104,7 @@ export default function BackupSheetSectionNoProvider() { - {/* replace this with BackUpMenuButton */} - + @@ -133,18 +120,18 @@ export default function BackupSheetSectionNoProvider() { marginRight={{ custom: -12 }} marginTop={{ custom: 0 }} marginBottom={{ custom: -8 }} - source={WalletsAndBackupIcon as Source} + source={WalletsAndBackupIcon} width={{ custom: imageSize }} size={imageSize} /> - - {lang.t(lang.l.back_up.cloud.cloud_backup)} + + {text} - {lang.t(lang.l.back_up.cloud.recommended_for_beginners)} + {i18n.t(i18n.l.back_up.cloud.recommended_for_beginners)} {' '} - {lang.t(lang.l.back_up.cloud.choose_backup_cloud_description, { + {i18n.t(i18n.l.back_up.cloud.choose_backup_cloud_description, { cloudPlatform, })} @@ -192,10 +179,10 @@ export default function BackupSheetSectionNoProvider() { size={imageSize} /> - {lang.t(lang.l.back_up.cloud.manual_backup)} + {i18n.t(i18n.l.back_up.cloud.manual_backup)} - {lang.t(lang.l.back_up.cloud.choose_backup_manual_description)} + {i18n.t(i18n.l.back_up.cloud.choose_backup_manual_description)} diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index 2f7b68cedf6..d08d4cdb0e2 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -6,26 +6,24 @@ import { useDimensions } from '@/hooks'; import { useNavigation } from '@/navigation'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; -import { Box, Stack, Text } from '@/design-system'; -import { RouteProp, useRoute } from '@react-navigation/native'; +import { Box, Stack } from '@/design-system'; import { sharedCoolModalTopOffset } from '@/navigation/config'; -import { ImgixImage } from '../images'; +import { ImgixImage } from '@/components/images'; import MenuContainer from '@/screens/SettingsSheet/components/MenuContainer'; import Menu from '@/screens/SettingsSheet/components/Menu'; import { format } from 'date-fns'; import MenuItem from '@/screens/SettingsSheet/components/MenuItem'; import Routes from '@/navigation/routesNames'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { RestoreSheetParams } from '@/screens/RestoreSheet'; +import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import { Source } from 'react-native-fast-image'; import { IS_ANDROID } from '@/env'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import useCloudBackups, { CloudBackupStep } from '@/hooks/useCloudBackups'; -import { Centered } from '../layout'; -import { cloudPlatform } from '@/utils/platform'; -import Spinner from '../Spinner'; -import ActivityIndicator from '../ActivityIndicator'; +import { Page } from '@/components/layout'; +import Spinner from '@/components/Spinner'; +import ActivityIndicator from '@/components/ActivityIndicator'; import { useTheme } from '@/theme'; +import { backupsStore, CloudBackupState, LoadingStates } from '@/state/backups/backups'; +import { titleForBackupState } from '@/screens/SettingsSheet/utils'; const Title = styled(RNText).attrs({ align: 'left', @@ -53,60 +51,27 @@ const Masthead = styled(Box).attrs({ }); export function ChooseBackupStep() { - const { - params: { fromSettings }, - } = useRoute>(); const { colors } = useTheme(); - const { isFetching, backups, userData, step, fetchBackups } = useCloudBackups(); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); + + const isLoading = LoadingStates.includes(status); const { top } = useSafeAreaInsets(); const { height: deviceHeight } = useDimensions(); const { navigate } = useNavigation(); - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); - const onSelectCloudBackup = useCallback( - (selectedBackup: Backup) => { + (selectedBackup: BackupFile) => { navigate(Routes.RESTORE_CLOUD_SHEET, { - backups, - userData, selectedBackup, - fromSettings, }); }, - [navigate, userData, backups, fromSettings] + [navigate] ); const height = IS_ANDROID ? deviceHeight - top : deviceHeight - sharedCoolModalTopOffset - 48; @@ -132,7 +97,7 @@ export function ChooseBackupStep() { - {!isFetching && step === CloudBackupStep.FAILED && ( + {status === CloudBackupState.FailedToInitialize && ( backupsStore.getState().syncAndFetchBackups()} titleComponent={} /> )} - {!isFetching && !cloudBackups.length && step !== CloudBackupStep.FAILED && ( + {status === CloudBackupState.Ready && backups.files.length === 0 && ( + + + } + /> + + + - } /> + backupsStore.getState().syncAndFetchBackups()} + titleComponent={} + /> )} - {!isFetching && cloudBackups.length > 0 && ( + {status === CloudBackupState.Ready && backups.files.length > 0 && ( {mostRecentBackup && ( - - } - onPress={() => onSelectCloudBackup(mostRecentBackup)} - size={52} - width="full" - titleComponent={} - /> - + + + } + onPress={() => onSelectCloudBackup(mostRecentBackup)} + size={52} + width="full" + titleComponent={} + /> + + )} - - {cloudBackups.map( - backup => - backup.name !== mostRecentBackup?.name && ( + + + + {backups.files + .filter(backup => backup.name !== mostRecentBackup?.name) + .sort((a, b) => { + const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime(); + const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime(); + return timestampB - timestampA; + }) + .map(backup => ( + onSelectCloudBackup(backup)} + size={52} + width="full" + titleComponent={ + + } + /> + ))} + {backups.files.length === 1 && ( onSelectCloudBackup(backup)} + disabled size={52} - width="full" - titleComponent={ - - } + titleComponent={} /> - ) - )} + )} + + - {cloudBackups.length === 1 && ( + } + width="full" + onPress={() => backupsStore.getState().syncAndFetchBackups()} + titleComponent={} /> - )} - + + )} - {isFetching && ( - + {isLoading && ( + {android ? : } - - {lang.t(lang.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - + {titleForBackupState[status]} + )} diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx deleted file mode 100644 index 377e9d13a83..00000000000 --- a/src/components/backup/CloudBackupProvider.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; -import type { BackupUserData, CloudBackups } from '@/model/backup'; -import { - fetchAllBackups, - fetchUserDataFromCloud, - getGoogleAccountUserData, - isCloudBackupAvailable, - syncCloud, -} from '@/handlers/cloudBackup'; -import { RainbowError, logger } from '@/logger'; -import { IS_ANDROID } from '@/env'; - -type CloudBackupContext = { - isFetching: boolean; - backups: CloudBackups; - fetchBackups: () => Promise; - userData: BackupUserData | undefined; -}; - -const CloudBackupContext = createContext({} as CloudBackupContext); - -export function CloudBackupProvider({ children }: PropsWithChildren) { - const [isFetching, setIsFetching] = useState(false); - const [backups, setBackups] = useState({ - files: [], - }); - - const [userData, setUserData] = useState(); - - const fetchBackups = async () => { - try { - setIsFetching(true); - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[CloudBackupProvider]: Cloud backup is not available'); - setIsFetching(false); - return; - } - - if (IS_ANDROID) { - const gdata = await getGoogleAccountUserData(); - if (!gdata) { - return; - } - } - - logger.debug('[CloudBackupProvider]: Syncing with cloud'); - await syncCloud(); - - logger.debug('[CloudBackupProvider]: Fetching user data'); - const userData = await fetchUserDataFromCloud(); - setUserData(userData); - - logger.debug('[CloudBackupProvider]: Fetching all backups'); - const backups = await fetchAllBackups(); - - logger.debug(`[CloudBackupProvider]: Retrieved ${backups.files.length} backup files`); - setBackups(backups); - } catch (e) { - logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), { - error: e, - }); - } - setIsFetching(false); - }; - - useEffect(() => { - fetchBackups(); - }, []); - - const value = { - isFetching, - backups, - fetchBackups, - userData, - }; - - return {children}; -} - -export function useCloudBackups() { - const context = useContext(CloudBackupContext); - if (context === null) { - throw new Error('useCloudBackups must be used within a CloudBackupProvider'); - } - return context; -} diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index e8bd83aa7a3..ce0774f2ec3 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -6,8 +6,7 @@ import WalletAndBackup from '@/assets/WalletsAndBackup.png'; import { KeyboardArea } from 'react-native-keyboard-area'; import { - Backup, - fetchBackupPassword, + BackupFile, getLocalBackupPassword, restoreCloudBackup, RestoreCloudBackupResultStates, @@ -17,10 +16,10 @@ import { cloudPlatform } from '@/utils/platform'; import { PasswordField } from '../fields'; import { Text } from '../text'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; +import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { useDimensions, useInitializeWallet } from '@/hooks'; -import { useNavigation } from '@/navigation'; +import { Navigation, useNavigation } from '@/navigation'; import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; @@ -35,8 +34,16 @@ import RainbowButtonTypes from '../buttons/rainbow-button/RainbowButtonTypes'; import { RouteProp, useRoute } from '@react-navigation/native'; import { RestoreSheetParams } from '@/screens/RestoreSheet'; import { Source } from 'react-native-fast-image'; -import { useTheme } from '@/theme'; -import useCloudBackups from '@/hooks/useCloudBackups'; +import { ThemeContextProps, useTheme } from '@/theme'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { isEmpty } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; + +type ComponentProps = { + theme: ThemeContextProps; + color: ThemeContextProps['colors'][keyof ThemeContextProps['colors']]; +}; const Title = styled(Text).attrs({ size: 'big', @@ -45,7 +52,7 @@ const Title = styled(Text).attrs({ ...padding.object(12, 0, 0), }); -const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({ +const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({ align: 'left', color: color || colors.alpha(colors.blueGreyDark, 0.5), lineHeight: 'looser', @@ -53,7 +60,7 @@ const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) = weight: 'medium', }))({}); -const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({ +const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({ align: 'center', letterSpacing: 'rounded', color: color || colors.alpha(colors.blueGreyDark, 0.5), @@ -71,38 +78,46 @@ const Masthead = styled(Box).attrs({ }); const KeyboardSizeView = styled(KeyboardArea)({ - backgroundColor: ({ theme: { colors } }: any) => colors.transparent, + backgroundColor: ({ theme: { colors } }: ComponentProps) => colors.transparent, }); type RestoreCloudStepParams = { RestoreSheet: { - selectedBackup: Backup; + selectedBackup: BackupFile; }; }; export default function RestoreCloudStep() { const { params } = useRoute>(); + const { password } = backupsStore(state => ({ + password: state.password, + })); - const { userData } = useCloudBackups(); + const loadingState = walletLoadingStore(state => state.loadingState); const { selectedBackup } = params; const { isDarkMode } = useTheme(); - const [loading, setLoading] = useState(false); + const { canGoBack, goBack } = useNavigation(); + + const onRestoreSuccess = useCallback(() => { + while (canGoBack()) { + goBack(); + } + }, [canGoBack, goBack]); const dispatch = useDispatch(); const { width: deviceWidth, height: deviceHeight } = useDimensions(); - const { replace, navigate, getState: dangerouslyGetState, goBack } = useNavigation(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); - const [password, setPassword] = useState(''); const passwordRef = useRef(null); const initializeWallet = useInitializeWallet(); useEffect(() => { const fetchPasswordIfPossible = async () => { - const pwd = await fetchBackupPassword(); + const pwd = await getLocalBackupPassword(); if (pwd) { - setPassword(pwd); + backupsStore.getState().setStoredPassword(pwd); + backupsStore.getState().setPassword(pwd); } }; fetchPasswordIfPossible(); @@ -118,35 +133,42 @@ export default function RestoreCloudStep() { }, [incorrectPassword, password]); const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { - setPassword(inputText); + backupsStore.getState().setPassword(inputText); setIncorrectPassword(false); }, []); const onSubmit = useCallback(async () => { - setLoading(true); + // NOTE: Localizing password to prevent an empty string from being saved if we re-render + const pwd = password.trim(); + let filename = selectedBackup.name; + + const prevWalletsState = await dispatch(walletsLoadState()); + try { if (!selectedBackup.name) { throw new Error('No backup file selected'); } - const prevWalletsState = await dispatch(walletsLoadState()); - + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.RESTORING_WALLET, + }); const status = await restoreCloudBackup({ - password, - userData, - nameOfSelectedBackupFile: selectedBackup.name, + password: pwd, + backupFilename: filename, }); - if (status === RestoreCloudBackupResultStates.success) { // Store it in the keychain in case it was missing - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { - await saveLocalBackupPassword(password); + if (backupsStore.getState().storedPassword !== pwd) { + await saveLocalBackupPassword(pwd); + } + + // Reset the storedPassword state for next restoration process + if (backupsStore.getState().storedPassword) { + backupsStore.getState().setStoredPassword(''); } InteractionManager.runAfterInteractions(async () => { const newWalletsState = await dispatch(walletsLoadState()); - let filename = selectedBackup.name; if (IS_ANDROID && filename) { filename = normalizeAndroidBackupFilename(filename); } @@ -188,14 +210,21 @@ export default function RestoreCloudStep() { const p2 = dispatch(addressSetSelected(firstAddress)); await Promise.all([p1, p2]); await initializeWallet(null, null, null, false, false, null, true, null); - - const operation = dangerouslyGetState()?.index === 1 ? navigate : replace; - operation(Routes.SWIPE_LAYOUT, { - screen: Routes.WALLET_SCREEN, - }); - - setLoading(false); }); + + onRestoreSuccess(); + backupsStore.getState().setPassword(''); + if (isEmpty(prevWalletsState)) { + Navigation.handleAction( + Routes.SWIPE_LAYOUT, + { + screen: Routes.WALLET_SCREEN, + }, + true + ); + } else { + Navigation.handleAction(Routes.WALLET_SCREEN, {}); + } } else { switch (status) { case RestoreCloudBackupResultStates.incorrectPassword: @@ -211,18 +240,17 @@ export default function RestoreCloudStep() { } } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); } - - setLoading(false); - }, [selectedBackup.name, password, userData, dispatch, initializeWallet, dangerouslyGetState, navigate, replace]); + }, [password, selectedBackup.name, dispatch, onRestoreSuccess, initializeWallet]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); }, [onSubmit, validPassword]); - const isPasswordValid = - (password !== '' && password.length < cloudBackupPasswordMinLength && !passwordRef?.current?.isFocused()) || incorrectPassword; - return ( @@ -248,8 +276,8 @@ export default function RestoreCloudStep() { ; }; }; -export type useCreateBackupStateType = 'none' | 'loading' | 'success' | 'error'; +type ConfirmBackupProps = { + password: string; +} & UseCreateBackupProps; -export enum BackupTypes { - Single = 'single', - All = 'all', -} - -export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupProps) => { +export const useCreateBackup = () => { const dispatch = useDispatch(); const { navigate } = useNavigation(); - const { fetchBackups } = useCloudBackups(); const walletCloudBackup = useWalletCloudBackup(); const { wallets } = useWallets(); - const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); - const [loading, setLoading] = useState('none'); - - const [password, setPassword] = useState(''); const setLoadingStateWithTimeout = useCallback( - (state: useCreateBackupStateType, resetInMS = 2500) => { - setLoading(state); + ({ state, outOfSync = false, failInMs = 10_000 }: { state: CloudBackupState; outOfSync?: boolean; failInMs?: number }) => { + backupsStore.getState().setStatus(state); + if (outOfSync) { + setTimeout(() => { + backupsStore.getState().setStatus(CloudBackupState.Syncing); + }, 1_000); + } setTimeout(() => { - setLoading('none'); - }, resetInMS); + const currentState = backupsStore.getState().status; + if (currentState === state) { + backupsStore.getState().setStatus(CloudBackupState.Ready); + } + }, failInMs); }, - [setLoading] + [] + ); + + const onSuccess = useCallback( + async (password: string) => { + if (backupsStore.getState().storedPassword !== password) { + await saveLocalBackupPassword(password); + } + // Reset the storedPassword state for next backup + backupsStore.getState().setStoredPassword(''); + analytics.track('Backup Complete', { + category: 'backup', + label: cloudPlatform, + }); + setLoadingStateWithTimeout({ + state: CloudBackupState.Success, + outOfSync: true, + }); + backupsStore.getState().syncAndFetchBackups(); + }, + [setLoadingStateWithTimeout] ); - const onSuccess = useCallback(async () => { - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { - await saveLocalBackupPassword(password); - } - analytics.track('Backup Complete', { - category: 'backup', - label: cloudPlatform, - }); - setLoadingStateWithTimeout('success'); - fetchBackups(); - }, [setLoadingStateWithTimeout, fetchBackups, password]); const onError = useCallback( - (msg: string) => { + (msg: string, isDamaged?: boolean) => { InteractionManager.runAfterInteractions(async () => { - DelayedAlert({ title: msg }, 500); - setLoadingStateWithTimeout('error', 5000); + if (isDamaged) { + showWalletErrorAlert(); + } else { + DelayedAlert({ title: msg }, 500); + } + setLoadingStateWithTimeout({ state: CloudBackupState.Error }); }); }, [setLoadingStateWithTimeout] ); const onConfirmBackup = useCallback( - async ({ password, type }: { password: string; type: BackupTypes }) => { + async ({ password, walletId, navigateToRoute }: ConfirmBackupProps) => { analytics.track('Tapped "Confirm Backup"'); - setLoading('loading'); + backupsStore.getState().setStatus(CloudBackupState.InProgress); - if (type === BackupTypes.All) { + if (typeof walletId === 'undefined') { if (!wallets) { - onError('Error loading wallets. Please try again.'); - setLoading('error'); + onError(i18n.t(i18n.l.back_up.errors.no_keys_found)); + backupsStore.getState().setStatus(CloudBackupState.Error); + return; + } + + const validWallets = Object.fromEntries(Object.entries(wallets).filter(([_, wallet]) => !wallet.damaged)); + if (Object.keys(validWallets).length === 0) { + onError(i18n.t(i18n.l.back_up.errors.no_keys_found), true); + backupsStore.getState().setStatus(CloudBackupState.Error); return; } + backupAllWalletsToCloud({ - wallets: wallets as AllRainbowWallets, + wallets: validWallets, password, - latestBackup, onError, onSuccess, dispatch, @@ -94,12 +114,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr return; } - if (!walletId) { - onError('Wallet not found. Please try again.'); - setLoading('error'); - return; - } - await walletCloudBackup({ onError, onSuccess, @@ -111,13 +125,13 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr navigate(navigateToRoute.route, navigateToRoute.params || {}); } }, - [walletId, walletCloudBackup, onError, onSuccess, navigateToRoute, wallets, latestBackup, dispatch, navigate] + [walletCloudBackup, onError, wallets, onSuccess, dispatch, navigate] ); - const getPassword = useCallback(async (): Promise => { + const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { const password = await getLocalBackupPassword(); if (password) { - setPassword(password); + backupsStore.getState().setStoredPassword(password); return password; } @@ -126,32 +140,36 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr nativeScreen: true, step: walletBackupStepTypes.backup_cloud, onSuccess: async (password: string) => { - setPassword(password); - resolve(password); + return resolve(password); }, onCancel: async () => { - resolve(null); + return resolve(null); }, - walletId, + ...props, }); }); - }, [walletId]); + }, []); - const onSubmit = useCallback( - async ({ type = BackupTypes.Single }: { type?: BackupTypes }) => { - const password = await getPassword(); - if (password) { - onConfirmBackup({ - password, - type, + const createBackup = useCallback( + async (props: UseCreateBackupProps) => { + if (backupsStore.getState().status !== CloudBackupState.Ready) { + return false; + } + const password = await getPassword(props); + if (!password) { + setLoadingStateWithTimeout({ + state: CloudBackupState.Ready, }); - return true; + return false; } - setLoadingStateWithTimeout('error'); - return false; + onConfirmBackup({ + password, + ...props, + }); + return true; }, [getPassword, onConfirmBackup, setLoadingStateWithTimeout] ); - return { onSuccess, onError, onSubmit, loading }; + return createBackup; }; diff --git a/src/components/cards/OpRewardsCard.tsx b/src/components/cards/OpRewardsCard.tsx index 3c1251010b3..b823be5e198 100644 --- a/src/components/cards/OpRewardsCard.tsx +++ b/src/components/cards/OpRewardsCard.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { GenericCard, Gradient } from './GenericCard'; -import { AccentColorProvider, Box, Cover, globalColors, Stack, Text } from '@/design-system'; +import { AccentColorProvider, Box, Cover, globalColors, Stack, Text, useColorMode } from '@/design-system'; import { ButtonPressAnimation } from '@/components/animations'; -import { Image } from 'react-native'; +import { ImageBackground } from 'react-native'; import OpRewardsCardBackgroundImage from '../../assets/opRewardsCardBackgroundImage.png'; import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { colors } from '@/styles'; import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const GRADIENT: Gradient = { colors: ['#520907', '#B22824'], @@ -18,17 +18,20 @@ const GRADIENT: Gradient = { export const OpRewardsCard: React.FC = () => { const { navigate } = useNavigation(); + const { isDarkMode } = useColorMode(); + + const color = useBackendNetworksStore.getState().getColorsForChainId(ChainId.optimism, isDarkMode); const navigateToRewardsSheet = () => { navigate(Routes.OP_REWARDS_SHEET); }; return ( - + { - + {i18n.t(i18n.l.discover.op_rewards.button_title)} diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index becea0deb38..775b32f4f1f 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -1,10 +1,10 @@ -import React, { useMemo } from 'react'; -import { ChainId } from '@/state/backendNetworks/types'; +import React, { useMemo, forwardRef } from 'react'; import FastImage from 'react-native-fast-image'; -import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; import styled from '@/styled-thing'; import { Centered } from '../layout'; import { position as positions } from '@/styles'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type ChainIconProps = { containerSize: number; @@ -36,40 +36,45 @@ const ChainIconPositionWrapper = styled(Centered)( }) ); -export function ChainImage({ - chainId, - size = 20, - showBadge = true, - badgeXPosition = 0, - badgeYPosition = 0, - position = 'absolute', -}: { +type ChainImageProps = { chainId: ChainId | null | undefined; size?: number; position?: 'absolute' | 'relative'; showBadge?: boolean; badgeXPosition?: number; badgeYPosition?: number; -}) { - const { containerSize, iconSize } = useMemo( - () => ({ - containerSize: size, - iconSize: size, - }), - [size] - ); +}; + +export const ChainImage = forwardRef, ChainImageProps>( + ({ chainId, size = 20, showBadge = true, badgeXPosition = 0, badgeYPosition = 0, position = 'absolute' }, ref) => { + const { containerSize, iconSize } = useMemo( + () => ({ + containerSize: size, + iconSize: size, + }), + [size] + ); - if (!chainId) return null; + if (!chainId) return null; - const badgeUrl = useBackendNetworksStore.getState().getChainsBadge()[chainId]; + const badgeUrl = useBackendNetworksStore.getState().getChainsBadge()[chainId]; - if (!badgeUrl || !showBadge) return null; + if (!badgeUrl || !showBadge) return null; + + return badgeXPosition || badgeYPosition ? ( + + + + ) : ( + + ); + } +); - return badgeXPosition || badgeYPosition ? ( - - - - ) : ( - - ); -} +ChainImage.displayName = 'ChainImage'; diff --git a/src/components/coin-row/CoinRowAddButton.js b/src/components/coin-row/CoinRowAddButton.js index 2b602ef8363..a96d17e26ed 100644 --- a/src/components/coin-row/CoinRowAddButton.js +++ b/src/components/coin-row/CoinRowAddButton.js @@ -1,6 +1,5 @@ import React from 'react'; import { View } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import RadialGradient from 'react-native-radial-gradient'; import { ButtonPressAnimation } from '../animations'; import { Centered } from '../layout'; @@ -9,6 +8,7 @@ import { CoinRowHeight } from './CoinRow'; import styled from '@/styled-thing'; import { padding } from '@/styles'; import { magicMemo } from '@/utils'; +import { IS_TEST } from '@/env'; const AddButtonPadding = 19; @@ -23,7 +23,7 @@ const AddButton = styled(Centered)({ width: 68, }); -const Circle = styled(IS_TESTING === 'true' ? View : RadialGradient).attrs(({ theme: { colors } }) => ({ +const Circle = styled(IS_TEST ? View : RadialGradient).attrs(({ theme: { colors } }) => ({ center: [0, 15], colors: colors.gradients.lightestGrey, }))({ diff --git a/src/components/coin-row/CoinRowFavoriteButton.js b/src/components/coin-row/CoinRowFavoriteButton.js index 303b2efd735..007434274c5 100644 --- a/src/components/coin-row/CoinRowFavoriteButton.js +++ b/src/components/coin-row/CoinRowFavoriteButton.js @@ -1,6 +1,5 @@ import React from 'react'; import { View } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { BaseButton } from 'react-native-gesture-handler'; import RadialGradient from 'react-native-radial-gradient'; import { useTheme } from '../../theme/ThemeContext'; @@ -10,6 +9,7 @@ import { CoinRowHeight } from './CoinRow'; import styled from '@/styled-thing'; import { padding } from '@/styles'; import { magicMemo } from '@/utils'; +import { IS_TEST } from '@/env'; const FavoriteButtonPadding = 19; @@ -24,7 +24,7 @@ const FavoriteButton = styled(Centered)({ width: 68, }); -const Circle = styled(IS_TESTING === 'true' ? View : RadialGradient).attrs(({ isFavorited, theme: { colors, isDarkMode } }) => ({ +const Circle = styled(IS_TEST ? View : RadialGradient).attrs(({ isFavorited, theme: { colors, isDarkMode } }) => ({ center: [0, 15], colors: isFavorited ? [colors.alpha('#FFB200', isDarkMode ? 0.15 : 0), colors.alpha('#FFB200', isDarkMode ? 0.05 : 0.2)] diff --git a/src/components/ens-registration/RegistrationAvatar/RegistrationAvatar.tsx b/src/components/ens-registration/RegistrationAvatar/RegistrationAvatar.tsx index 603f8258d6c..80a2afcfb47 100644 --- a/src/components/ens-registration/RegistrationAvatar/RegistrationAvatar.tsx +++ b/src/components/ens-registration/RegistrationAvatar/RegistrationAvatar.tsx @@ -1,6 +1,5 @@ import ConditionalWrap from 'conditional-wrap'; import React, { useCallback, useEffect, useState } from 'react'; -import { IS_TESTING } from 'react-native-dotenv'; import { Image } from 'react-native-image-crop-picker'; import { atom, useSetRecoilState } from 'recoil'; import ButtonPressAnimation from '../../animations/ButtonPressAnimation'; @@ -13,6 +12,7 @@ import { useENSModifiedRegistration, useENSRegistration, useENSRegistrationForm, import { ImgixImage } from '@/components/images'; import { magicMemo, stringifyENSNFTRecord } from '@/utils'; import { ENS_RECORDS } from '@/helpers/ens'; +import { IS_TEST } from '@/env'; export const avatarMetadataAtom = atom({ default: undefined, @@ -20,7 +20,6 @@ export const avatarMetadataAtom = atom({ }); const size = 70; -const isTesting = IS_TESTING === 'true'; const RegistrationAvatar = ({ hasSeenExplainSheet, @@ -131,11 +130,11 @@ const RegistrationAvatar = ({ ) : ( {children}} > diff --git a/src/components/expanded-state/UniqueTokenExpandedState.tsx b/src/components/expanded-state/UniqueTokenExpandedState.tsx index 1d031de8f8b..360df882f57 100644 --- a/src/components/expanded-state/UniqueTokenExpandedState.tsx +++ b/src/components/expanded-state/UniqueTokenExpandedState.tsx @@ -417,18 +417,17 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE const hideNftMarketplaceAction = isPoap || !slug; - const mountedAt = useRef(Date.now()); useTimeoutEffect( - () => { + ({ elapsedTime }) => { const { address, chainId } = getAddressAndChainIdFromUniqueId(uniqueId); const { name, description, image_url } = asset; analyticsV2.track(analyticsV2.event.tokenDetailsNFT, { - eventSentAfterMs: Date.now() - mountedAt.current, + eventSentAfterMs: elapsedTime, token: { isPoap, isParty: !!isParty, isENS, address, chainId, name, image_url }, available_data: { description: !!description, image_url: !!image_url, floorPrice: !!offer?.floorPrice }, }); }, - 5 * 1000 // 5s + { timeout: 5 * 1000 } ); return ( <> diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 3bf7aff28ff..4704316db9b 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -257,17 +257,16 @@ export default function ChartExpandedState({ asset }) { [nativeCurrency] ); - const mountedAt = useRef(Date.now()); useTimeoutEffect( - () => { + ({ elapsedTime }) => { const { address, chainId, symbol, name, icon_url, price } = assetWithPrice; analyticsV2.track(analyticsV2.event.tokenDetailsErc20, { - eventSentAfterMs: Date.now() - mountedAt.current, + eventSentAfterMs: elapsedTime, token: { address, chainId, symbol, name, icon_url, price }, available_data: { chart: showChart, description: !!data?.description, iconUrl: !!icon_url }, }); }, - 5 * 1000 // 5s + { timeout: 5 * 1000 } ); return ( diff --git a/src/components/fields/PasswordField.tsx b/src/components/fields/PasswordField.tsx index 0925b29862c..6d28e81e802 100644 --- a/src/components/fields/PasswordField.tsx +++ b/src/components/fields/PasswordField.tsx @@ -1,14 +1,37 @@ import React, { forwardRef, useCallback, Ref } from 'react'; -import { useTheme } from '../../theme/ThemeContext'; +import { ThemeContextProps, useTheme } from '../../theme/ThemeContext'; import { Input } from '../inputs'; import { cloudBackupPasswordMinLength } from '@/handlers/cloudBackup'; import { useDimensions } from '@/hooks'; import styled from '@/styled-thing'; -import { padding } from '@/styles'; +import { padding, position } from '@/styles'; import ShadowStack from '@/react-native-shadow-stack'; import { Box } from '@/design-system'; import { TextInput, TextInputProps, View } from 'react-native'; import { IS_IOS, IS_ANDROID } from '@/env'; +import { Icon } from '../icons'; + +const FieldAccessoryBadgeSize = 22; +const FieldAccessoryBadgeWrapper = styled(ShadowStack).attrs( + ({ theme: { colors, isDarkMode }, color }: { theme: ThemeContextProps; color: string }) => ({ + ...position.sizeAsObject(FieldAccessoryBadgeSize), + borderRadius: FieldAccessoryBadgeSize, + shadows: [[0, 4, 12, isDarkMode ? colors.shadow : color, isDarkMode ? 0.1 : 0.4]], + }) +)({ + marginBottom: 12, + position: 'absolute', + right: 12, + top: 12, +}); + +function FieldAccessoryBadge({ color, name }: { color: string; name: string }) { + return ( + + + + ); +} const Container = styled(Box)({ width: '100%', @@ -53,9 +76,9 @@ interface PasswordFieldProps extends TextInputProps { } const PasswordField = forwardRef( - ({ password, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => { + ({ password, isInvalid, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => { const { width: deviceWidth } = useDimensions(); - const { isDarkMode } = useTheme(); + const { isDarkMode, colors } = useTheme(); const handleFocus = useCallback(() => { if (ref && 'current' in ref && ref.current) { @@ -67,6 +90,7 @@ const PasswordField = forwardRef( + {isInvalid && } ); diff --git a/src/components/floating-emojis/FloatingEmojis.js b/src/components/floating-emojis/FloatingEmojis.js deleted file mode 100644 index f4dc8342f50..00000000000 --- a/src/components/floating-emojis/FloatingEmojis.js +++ /dev/null @@ -1,156 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Animated, View } from 'react-native'; -import FloatingEmoji from './FloatingEmoji'; -import GravityEmoji from './GravityEmoji'; -import { useTimeout } from '@/hooks'; -import { position } from '@/styles'; - -const EMPTY_ARRAY = []; -const getEmoji = emojis => Math.floor(Math.random() * emojis.length); -const getRandomNumber = (min, max) => Math.random() * (max - min) + min; - -const FloatingEmojis = ({ - centerVertically, - children, - disableHorizontalMovement, - disableRainbow, - disableVerticalMovement, - distance, - duration, - emojis, - fadeOut, - gravityEnabled, - marginTop, - opacity, - opacityThreshold, - range, - scaleTo, - setOnNewEmoji, - size, - wiggleFactor, - ...props -}) => { - const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]); - const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY); - const [startTimeout, stopTimeout] = useTimeout(); - const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []); - - // 🚧️ TODO: 🚧️ - // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something) - // otherwise, the FloatingEmojis look weird during stack transitions - - const onNewEmoji = useCallback( - (x, y) => { - // Set timeout to automatically clearEmojis after the latest one has finished animating - stopTimeout(); - startTimeout(clearEmojis, duration * 1.1); - - setEmojis(existingEmojis => { - const newEmoji = { - // if a user has smashed the button 7 times, they deserve a 🌈 rainbow - emojiToRender: - (existingEmojis.length + 1) % 7 === 0 && !disableRainbow - ? 'rainbow' - : emojisArray.length === 1 - ? emojisArray[0] - : emojisArray[getEmoji(emojisArray)], - x: x ? x - getRandomNumber(-20, 20) : getRandomNumber(...range), - y: y || 0, - }; - return [...existingEmojis, newEmoji]; - }); - }, - [clearEmojis, disableRainbow, duration, emojisArray, range, startTimeout, stopTimeout] - ); - - useEffect(() => { - setOnNewEmoji?.(onNewEmoji); - return () => setOnNewEmoji?.(undefined); - }, [setOnNewEmoji, onNewEmoji]); - - return ( - - {typeof children === 'function' ? children({ onNewEmoji }) : children} - - {gravityEnabled - ? floatingEmojis.map(({ emojiToRender, x, y }, index) => ( - - )) - : floatingEmojis.map(({ emojiToRender, x, y }, index) => ( - - ))} - - - ); -}; - -FloatingEmojis.propTypes = { - centerVertically: PropTypes.bool, - children: PropTypes.node, - disableHorizontalMovement: PropTypes.bool, - disableRainbow: PropTypes.bool, - disableVerticalMovement: PropTypes.bool, - distance: PropTypes.number, - duration: PropTypes.number, - emojis: PropTypes.arrayOf(PropTypes.string).isRequired, - fadeOut: PropTypes.bool, - gravityEnabled: PropTypes.bool, - marginTop: PropTypes.number, - opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.object]), - opacityThreshold: PropTypes.number, - range: PropTypes.arrayOf(PropTypes.number), - scaleTo: PropTypes.number, - setOnNewEmoji: PropTypes.func, - size: PropTypes.string.isRequired, - wiggleFactor: PropTypes.number, -}; - -FloatingEmojis.defaultProps = { - distance: 130, - duration: 2000, - // Defaults the emoji to 👍️ (thumbs up). - // To view complete list of emojis compatible with this component, - // head to https://github.com/muan/unicode-emoji-json/blob/master/data-by-emoji.json - emojis: ['thumbs_up'], - fadeOut: true, - opacity: 1, - range: [0, 80], - scaleTo: 1, - size: 30, - wiggleFactor: 0.5, -}; - -export default FloatingEmojis; diff --git a/src/components/floating-emojis/FloatingEmojis.tsx b/src/components/floating-emojis/FloatingEmojis.tsx new file mode 100644 index 00000000000..7eccc8b69de --- /dev/null +++ b/src/components/floating-emojis/FloatingEmojis.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react'; +import { Animated, View, ViewProps } from 'react-native'; +import FloatingEmoji from './FloatingEmoji'; +import GravityEmoji from './GravityEmoji'; +import { useTimeout } from '@/hooks'; +import { position } from '@/styles'; +import { DebugLayout } from '@/design-system'; +import { DEVICE_HEIGHT, DEVICE_WIDTH } from '@/utils/deviceUtils'; +import { AbsolutePortal } from '../AbsolutePortal'; + +interface Emoji { + emojiToRender: string; + x: number; + y: number; +} + +interface FloatingEmojisProps extends Omit { + centerVertically?: boolean; + children?: ReactNode | ((props: { onNewEmoji: (x?: number, y?: number) => void }) => ReactNode); + disableHorizontalMovement?: boolean; + disableRainbow?: boolean; + disableVerticalMovement?: boolean; + distance?: number; + duration?: number; + emojis: string[]; + fadeOut?: boolean; + gravityEnabled?: boolean; + marginTop?: number; + opacity?: number | Animated.AnimatedInterpolation; + opacityThreshold?: number; + range?: [number, number]; + scaleTo?: number; + setOnNewEmoji?: (fn: ((x?: number, y?: number) => void) | undefined) => void; + size: number; + wiggleFactor?: number; +} + +const EMPTY_ARRAY: Emoji[] = []; +const getEmoji = (emojis: string[]) => Math.floor(Math.random() * emojis.length); +const getRandomNumber = (min: number, max: number) => Math.random() * (max - min) + min; + +const FloatingEmojis: React.FC = ({ + centerVertically, + children, + disableHorizontalMovement, + disableRainbow, + disableVerticalMovement, + distance = 130, + duration = 2000, + emojis, + fadeOut = true, + gravityEnabled, + marginTop, + opacity = 1, + opacityThreshold, + range: [rangeMin, rangeMax] = [0, 80], + scaleTo = 1, + setOnNewEmoji, + size = 30, + wiggleFactor = 0.5, + style, + ...props +}) => { + const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]); + const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY); + const [startTimeout, stopTimeout] = useTimeout(); + const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []); + + // 🚧️ TODO: 🚧️ + // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something) + // otherwise, the FloatingEmojis look weird during stack transitions + + const onNewEmoji = useCallback( + (x?: number, y?: number) => { + // Set timeout to automatically clearEmojis after the latest one has finished animating + stopTimeout(); + startTimeout(clearEmojis, duration * 1.1); + + setEmojis(existingEmojis => { + const newEmoji = { + emojiToRender: + (existingEmojis.length + 1) % 7 === 0 && !disableRainbow + ? 'rainbow' + : emojisArray.length === 1 + ? emojisArray[0] + : emojisArray[getEmoji(emojisArray)], + x: x !== undefined ? x - getRandomNumber(-20, 20) : getRandomNumber(rangeMin, rangeMax), + y: y || 0, + }; + return [...existingEmojis, newEmoji]; + }); + }, + [clearEmojis, disableRainbow, duration, emojisArray, rangeMin, rangeMax, startTimeout, stopTimeout] + ); + + useEffect(() => { + setOnNewEmoji?.(onNewEmoji); + return () => setOnNewEmoji?.(undefined); + }, [setOnNewEmoji, onNewEmoji]); + + return ( + + {typeof children === 'function' ? children({ onNewEmoji }) : children} + + + {gravityEnabled + ? floatingEmojis.map(({ emojiToRender, x, y }, index) => ( + + )) + : floatingEmojis.map(({ emojiToRender, x, y }, index) => ( + + ))} + + + + ); +}; + +export default FloatingEmojis; diff --git a/src/components/floating-emojis/GravityEmoji.tsx b/src/components/floating-emojis/GravityEmoji.tsx index 2bf06a3901f..0b1de95b47c 100644 --- a/src/components/floating-emojis/GravityEmoji.tsx +++ b/src/components/floating-emojis/GravityEmoji.tsx @@ -4,7 +4,9 @@ import { Emoji } from '../text'; interface GravityEmojiProps { distance: number; + duration: number; emoji: string; + index: number; left: number; size: number; top: number; diff --git a/src/components/info-alert/info-alert.tsx b/src/components/info-alert/info-alert.tsx index bb17325be89..dbfa3aef5f1 100644 --- a/src/components/info-alert/info-alert.tsx +++ b/src/components/info-alert/info-alert.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Text, useForegroundColor } from '@/design-system'; +import { Box, Text } from '@/design-system'; type InfoAlertProps = { title: string; diff --git a/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts b/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts index e2f68b89723..0a8414475a0 100644 --- a/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts +++ b/src/components/remote-promo-sheet/check-fns/hasNonZeroTotalBalance.ts @@ -1,7 +1,7 @@ import { selectorFilterByUserChains, selectUserAssetsList } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { userAssetsFetchQuery } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import store from '@/redux/store'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; export const hasNonZeroTotalBalance = async (): Promise => { const { accountAddress, nativeCurrency } = store.getState().settings; @@ -9,7 +9,7 @@ export const hasNonZeroTotalBalance = async (): Promise => { const userAssetsDictByChain = await userAssetsFetchQuery({ address: accountAddress, currency: nativeCurrency, - testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat, + testnetMode: useConnectedToAnvilStore.getState().connectedToAnvil, }); const assets = selectorFilterByUserChains({ data: userAssetsDictByChain, selector: selectUserAssetsList }); diff --git a/src/components/remote-promo-sheet/runChecks.ts b/src/components/remote-promo-sheet/runChecks.ts index f83170eecce..9167de41182 100644 --- a/src/components/remote-promo-sheet/runChecks.ts +++ b/src/components/remote-promo-sheet/runChecks.ts @@ -1,7 +1,6 @@ import { IS_TEST } from '@/env'; -import { runFeatureUnlockChecks } from '@/handlers/walletReadyEvents'; +import { runFeaturesLocalCampaignAndBackupChecks } from '@/handlers/walletReadyEvents'; import { logger } from '@/logger'; -import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks'; import { checkForRemotePromoSheet } from '@/components/remote-promo-sheet/checkForRemotePromoSheet'; import { useCallback, useEffect } from 'react'; import { InteractionManager } from 'react-native'; @@ -19,11 +18,7 @@ export const useRunChecks = ({ runChecksOnMount = true, walletReady }: { runChec return; } - const showedFeatureUnlock = await runFeatureUnlockChecks(); - if (showedFeatureUnlock) return; - - const showedLocalPromo = await runLocalCampaignChecks(); - if (showedLocalPromo) return; + if (await runFeaturesLocalCampaignAndBackupChecks()) return; if (!remotePromoSheets) { logger.debug('[useRunChecks]: remote promo sheets is disabled'); diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx index 0ef93ba05e6..3cd37f05611 100644 --- a/src/components/secret-display/SecretDisplaySection.tsx +++ b/src/components/secret-display/SecretDisplaySection.tsx @@ -1,5 +1,4 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import { captureException } from '@sentry/react-native'; import React, { ReactNode, useCallback, useEffect, useState } from 'react'; import { createdWithBiometricError, identifyWalletType, loadPrivateKey, loadSeedPhraseAndMigrateIfNeeded } from '@/model/wallet'; import ActivityIndicator from '../ActivityIndicator'; @@ -25,6 +24,7 @@ import { useNavigation } from '@/navigation'; import { ImgixImage } from '../images'; import RoutesWithPlatformDifferences from '@/navigation/routesNames'; import { Source } from 'react-native-fast-image'; +import { backupsStore } from '@/state/backups/backups'; const MIN_HEIGHT = 740; @@ -63,6 +63,9 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const { colors } = useTheme(); const { params } = useRoute>(); const { selectedWallet, wallets } = useWallets(); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); const { onManuallyBackupWalletId } = useWalletManualBackup(); const { navigate } = useNavigation(); @@ -124,9 +127,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const handleConfirmSaved = useCallback(() => { if (backupType === WalletBackupTypes.manual) { onManuallyBackupWalletId(walletId); + if (!backupProvider) { + backupsStore.getState().setBackupProvider(WalletBackupTypes.manual); + } navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP); } - }, [backupType, walletId, onManuallyBackupWalletId, navigate]); + }, [backupType, onManuallyBackupWalletId, walletId, backupProvider, navigate]); const getIconForBackupType = useCallback(() => { if (isBackingUp) { diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 9a7ea341748..81f82de844b 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -31,7 +31,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, weight = const goToSwap = useCallback(async () => { const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); const chainsName = useBackendNetworksStore.getState().getChainsName(); - const chainId = chainsIdByName[asset.network]; + const chainId = asset.chainId || chainsIdByName[asset.network]; const uniqueId = `${asset.address}_${chainId}`; const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); diff --git a/src/components/skeleton/Skeleton.tsx b/src/components/skeleton/Skeleton.tsx index 628090fc8a6..afd02fa4532 100644 --- a/src/components/skeleton/Skeleton.tsx +++ b/src/components/skeleton/Skeleton.tsx @@ -1,7 +1,6 @@ import MaskedView from '@react-native-masked-view/masked-view'; import React from 'react'; import { View, ViewProps } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { ThemeContextProps, withThemeContext } from '../../theme/ThemeContext'; import { deviceUtils } from '../../utils'; import { ShimmerAnimation } from '../animations'; @@ -9,6 +8,7 @@ import { CoinRowHeight } from '../coin-row'; import { Row } from '../layout'; import styled from '@/styled-thing'; import { position } from '@/styles'; +import { IS_TEST } from '@/env'; export const AssetListItemSkeletonHeight = CoinRowHeight; @@ -75,7 +75,7 @@ function Skeleton({ skeletonColor?: string; width?: number; }) { - if (animated && IS_TESTING !== 'true') { + if (animated && !IS_TEST) { return ( {children}} style={{ flex: 1 }}> diff --git a/src/components/toasts/OfflineToast.js b/src/components/toasts/OfflineToast.js index 053791ce854..266455625ca 100644 --- a/src/components/toasts/OfflineToast.js +++ b/src/components/toasts/OfflineToast.js @@ -3,13 +3,13 @@ import React from 'react'; import Toast from './Toast'; import { useAccountSettings, useInternetStatus } from '@/hooks'; import { ChainId } from '@/state/backendNetworks/types'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; const OfflineToast = () => { const isConnected = useInternetStatus(); const { chainId } = useAccountSettings(); - const { connectedToHardhat } = useConnectedToHardhatStore(); - const isMainnet = chainId === ChainId.mainnet && !connectedToHardhat; + const { connectedToAnvil } = useConnectedToAnvilStore(); + const isMainnet = chainId === ChainId.mainnet && !connectedToAnvil; return ; }; diff --git a/src/components/toasts/TestnetToast.js b/src/components/toasts/TestnetToast.js index 5618d0706d4..136fc021d78 100644 --- a/src/components/toasts/TestnetToast.js +++ b/src/components/toasts/TestnetToast.js @@ -4,11 +4,11 @@ import { Nbsp, Text } from '../text'; import Toast from './Toast'; import { useInternetStatus } from '@/hooks'; import { ChainId } from '@/state/backendNetworks/types'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const TestnetToast = ({ chainId }) => { - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToAnvil } = useConnectedToAnvilStore(); const isConnected = useInternetStatus(); const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; const name = useBackendNetworksStore.getState().getChainsName()[chainId]; @@ -18,9 +18,9 @@ const TestnetToast = ({ chainId }) => { useEffect(() => { if (chainId === ChainId.mainnet) { - if (connectedToHardhat) { + if (connectedToAnvil) { setVisible(true); - setNetworkName('Hardhat'); + setNetworkName('Anvil'); } else { setVisible(false); } @@ -28,7 +28,7 @@ const TestnetToast = ({ chainId }) => { setVisible(true); setNetworkName(name + (isConnected ? '' : ' (offline)')); } - }, [isConnected, chainId, connectedToHardhat, name]); + }, [isConnected, chainId, connectedToAnvil, name]); const { colors, isDarkMode } = useTheme(); diff --git a/src/config/defaultDebug.ts b/src/config/defaultDebug.ts index e3e349d1203..e39b0a01b45 100644 --- a/src/config/defaultDebug.ts +++ b/src/config/defaultDebug.ts @@ -9,7 +9,7 @@ export const debugLayoutAnimations = false; export const alwaysRequireApprove = false; export const showReloadButton = false; export const showSwitchModeButton = false; -export const showConnectToHardhatButton = false; +export const showConnectToAnvilButton = false; export const parseAllTxnsOnReceive = false; export const reactNativeDisableYellowBox = true; export const showNetworkRequests = false; diff --git a/src/config/experimental.ts b/src/config/experimental.ts index b68e23260ff..cef7e04d7d7 100644 --- a/src/config/experimental.ts +++ b/src/config/experimental.ts @@ -29,6 +29,7 @@ export const DEGEN_MODE = 'Degen Mode'; export const FEATURED_RESULTS = 'Featured Results'; export const CLAIMABLES = 'Claimables'; export const NFTS_ENABLED = 'Nfts Enabled'; +export const TRENDING_TOKENS = 'Trending Tokens'; /** * A developer setting that pushes log lines to an array in-memory so that @@ -66,6 +67,7 @@ export const defaultConfig: Record = { [FEATURED_RESULTS]: { settings: true, value: false }, [CLAIMABLES]: { settings: true, value: false }, [NFTS_ENABLED]: { settings: true, value: !!IS_TEST }, + [TRENDING_TOKENS]: { settings: true, value: false }, }; export const defaultConfigValues: Record = Object.fromEntries( diff --git a/src/design-system/components/Inline/Inline.tsx b/src/design-system/components/Inline/Inline.tsx index 5754bae6a93..3f93791cbc8 100644 --- a/src/design-system/components/Inline/Inline.tsx +++ b/src/design-system/components/Inline/Inline.tsx @@ -51,7 +51,7 @@ export function Inline({ > {wrap || !separator ? children - : Children.map(children, (child, index) => { + : Children.toArray(children).map((child, index) => { if (!child) return null; return ( <> diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql index 68e797864e5..17dccca539c 100644 --- a/src/graphql/queries/arc.graphql +++ b/src/graphql/queries/arc.graphql @@ -522,6 +522,7 @@ query trendingTokens( $sortBy: TrendingSort $sortDirection: SortDirection $walletAddress: String + $limit: Int ) { trendingTokens( chainId: $chainId @@ -531,6 +532,7 @@ query trendingTokens( sortBy: $sortBy sortDirection: $sortDirection walletAddress: $walletAddress + limit: $limit ) { data { colors { diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index 1eb3f5be795..14347c42a75 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -1,14 +1,15 @@ import { sortBy } from 'lodash'; // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message import RNCloudFs from 'react-native-cloud-fs'; -import { RAINBOW_MASTER_KEY } from 'react-native-dotenv'; import RNFS from 'react-native-fs'; import AesEncryptor from '../handlers/aesEncryption'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { CloudBackups } from '@/model/backup'; +import { BackupFile, CloudBackups } from '@/model/backup'; + const REMOTE_BACKUP_WALLET_DIR = 'rainbow.me/wallet-backups'; -const USERDATA_FILE = 'UserData.json'; +export const USERDATA_FILE = 'UserData.json'; + const encryptor = new AesEncryptor(); export const CLOUD_BACKUP_ERRORS = { @@ -65,13 +66,18 @@ export async function fetchAllBackups(): Promise { if (android) { await RNCloudFs.loginIfNeeded(); } - return RNCloudFs.listFiles({ + + const files = await RNCloudFs.listFiles({ scope: 'hidden', targetPath: REMOTE_BACKUP_WALLET_DIR, }); + + return { + files: files?.files?.filter((file: BackupFile) => normalizeAndroidBackupFilename(file.name) !== USERDATA_FILE) || [], + }; } -export async function encryptAndSaveDataToCloud(data: any, password: any, filename: any) { +export async function encryptAndSaveDataToCloud(data: Record, password: string, filename: string) { // Encrypt the data try { const encryptedData = await encryptor.encrypt(password, JSON.stringify(data)); @@ -100,6 +106,7 @@ export async function encryptAndSaveDataToCloud(data: any, password: any, filena scope, sourcePath: sourceUri, targetPath: destinationPath, + update: true, }); // Now we need to verify the file has been stored in the cloud const exists = await RNCloudFs.fileExists( @@ -201,19 +208,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n throw error; } -export async function backupUserDataIntoCloud(data: any) { - const filename = USERDATA_FILE; - const password = RAINBOW_MASTER_KEY; - return encryptAndSaveDataToCloud(data, password, filename); -} - -export async function fetchUserDataFromCloud() { - const filename = USERDATA_FILE; - const password = RAINBOW_MASTER_KEY; - - return getDataFromCloud(password, filename); -} - export const cloudBackupPasswordMinLength = 8; export function isCloudBackupPasswordValid(password: any) { diff --git a/src/handlers/swap.ts b/src/handlers/swap.ts index 7d811afc0d4..877dc315e40 100644 --- a/src/handlers/swap.ts +++ b/src/handlers/swap.ts @@ -3,7 +3,6 @@ import { Block, StaticJsonRpcProvider } from '@ethersproject/providers'; import { CrosschainQuote, getQuoteExecutionDetails, getRainbowRouterContractAddress, Quote } from '@rainbow-me/swaps'; import { Contract } from '@ethersproject/contracts'; import { MaxUint256 } from '@ethersproject/constants'; -import { IS_TESTING } from 'react-native-dotenv'; import { Token } from '../entities/tokens'; import { estimateGasWithPadding, getProvider, toHexNoLeadingZeros } from './web3'; import { getRemoteConfig } from '@/model/remoteConfig'; @@ -13,6 +12,7 @@ import { erc20ABI, ethUnits } from '@/references'; import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { ChainId } from '@/state/backendNetworks/types'; +import { IS_TEST } from '@/env'; export enum Field { INPUT = 'INPUT', @@ -191,7 +191,7 @@ export const estimateCrosschainSwapGasLimit = async ({ } try { if (requiresApprove) { - if (CHAIN_IDS_WITH_TRACE_SUPPORT.includes(chainId) && IS_TESTING !== 'true') { + if (CHAIN_IDS_WITH_TRACE_SUPPORT.includes(chainId) && !IS_TEST) { try { const gasLimitWithFakeApproval = await getSwapGasLimitWithFakeApproval(chainId, provider, tradeDetails); logger.debug('[swap]: Got gasLimitWithFakeApproval!', { diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 1cfa62be144..b8e53674c66 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -1,4 +1,3 @@ -import { IS_TESTING } from 'react-native-dotenv'; import { triggerOnSwipeLayout } from '../navigation/onNavigationStateChange'; import { getKeychainIntegrityState } from './localstorage/globalSettings'; import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks'; @@ -6,18 +5,15 @@ import { EthereumAddress } from '@/entities'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import WalletTypes from '@/helpers/walletTypes'; import { featureUnlockChecks } from '@/featuresToUnlock'; -import { AllRainbowWallets, RainbowAccount, RainbowWallet } from '@/model/wallet'; +import { AllRainbowWallets, RainbowAccount } from '@/model/wallet'; import { Navigation } from '@/navigation'; import store from '@/redux/store'; import { checkKeychainIntegrity } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; -import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { InteractionManager } from 'react-native'; - -const BACKUP_SHEET_DELAY_MS = 3000; +import { IS_TEST } from '@/env'; +import { backupsStore, LoadingStates } from '@/state/backups/backups'; export const runKeychainIntegrityChecks = async () => { const keychainIntegrityState = await getKeychainIntegrityState(); @@ -26,60 +22,36 @@ export const runKeychainIntegrityChecks = async () => { } }; -export const runWalletBackupStatusChecks = () => { - const { - selected, - wallets, - }: { - wallets: AllRainbowWallets | null; - selected: RainbowWallet | undefined; - } = store.getState().wallets; - - // count how many visible, non-imported and non-readonly wallets are not backed up - if (!wallets) return; - - const { backupProvider } = checkWalletsForBackupStatus(wallets); - - const rainbowWalletsNotBackedUp = Object.values(wallets).filter(wallet => { - const hasVisibleAccount = wallet.addresses?.find((account: RainbowAccount) => account.visible); - return ( - !wallet.imported && - !!hasVisibleAccount && - wallet.type !== WalletTypes.readOnly && - wallet.type !== WalletTypes.bluetooth && - !wallet.backedUp - ); +const delay = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); }); - if (!rainbowWalletsNotBackedUp.length) return; - - logger.debug('[walletReadyEvents]: there is a rainbow wallet not backed up'); +const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { + let { status } = backupsStore.getState(); + while (LoadingStates.includes(status)) { + await delay(1000); + status = backupsStore.getState().status; + } - const hasSelectedWallet = rainbowWalletsNotBackedUp.find(notBackedUpWallet => notBackedUpWallet.id === selected!.id); - logger.debug('[walletReadyEvents]: rainbow wallet not backed up that is selected?', { - hasSelectedWallet, - }); + logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); + triggerOnSwipeLayout(() => + Navigation.handleAction(Routes.BACKUP_SHEET, { + step: WalletBackupStepTypes.backup_prompt, + }) + ); + return true; +}; - // if one of them is selected, show the default BackupSheet - if (selected && hasSelectedWallet && IS_TESTING !== 'true') { - let stepType: string = WalletBackupStepTypes.no_provider; - if (backupProvider === walletBackupTypes.cloud) { - stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { - stepType = WalletBackupStepTypes.backup_now_manually; - } +export const runWalletBackupStatusChecks = async (): Promise => { + const { selected } = store.getState().wallets; + if (!selected || IS_TEST) return false; - setTimeout(() => { - logger.debug(`[walletReadyEvents]: showing ${stepType} backup sheet for selected wallet`); - triggerOnSwipeLayout(() => - Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, - }) - ); - }, BACKUP_SHEET_DELAY_MS); - return; + if (!selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly && selected.type !== WalletTypes.bluetooth) { + logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); + return promptForBackupOnceReadyOrNotAvailable(); } - return; + return false; }; export const runFeatureUnlockChecks = async (): Promise => { @@ -107,19 +79,24 @@ export const runFeatureUnlockChecks = async (): Promise => { // short circuits once the first feature is unlocked for (const featureUnlockCheck of featureUnlockChecks) { - InteractionManager.runAfterInteractions(async () => { - const unlockNow = await featureUnlockCheck(walletsToCheck); - if (unlockNow) { - return true; - } - }); + const unlockNow = await featureUnlockCheck(walletsToCheck); + if (unlockNow) { + return true; + } } return false; }; -export const runFeatureAndLocalCampaignChecks = async () => { - const showingFeatureUnlock: boolean = await runFeatureUnlockChecks(); - if (!showingFeatureUnlock) { - await runLocalCampaignChecks(); +export const runFeaturesLocalCampaignAndBackupChecks = async () => { + if (await runFeatureUnlockChecks()) { + return true; } + if (await runLocalCampaignChecks()) { + return true; + } + if (await runWalletBackupStatusChecks()) { + return true; + } + + return false; }; diff --git a/src/handlers/web3.ts b/src/handlers/web3.ts index 96f099109a2..1e5c7ae00d8 100644 --- a/src/handlers/web3.ts +++ b/src/handlers/web3.ts @@ -24,9 +24,9 @@ import { import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_IOS, RPC_PROXY_API_KEY, RPC_PROXY_BASE_URL } from '@/env'; -import { ChainId, chainHardhat } from '@/state/backendNetworks/types'; +import { ChainId, chainAnvil } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; export enum TokenStandard { ERC1155 = 'ERC1155', @@ -111,8 +111,8 @@ export const getCachedProviderForNetwork = (chainId: ChainId = ChainId.mainnet): }; export const getBatchedProvider = ({ chainId = ChainId.mainnet }: { chainId?: number }): JsonRpcBatchProvider => { - if (useConnectedToHardhatStore.getState().connectedToHardhat) { - const provider = new JsonRpcBatchProvider(chainHardhat.rpcUrls.default.http[0], ChainId.mainnet); + if (useConnectedToAnvilStore.getState().connectedToAnvil) { + const provider = new JsonRpcBatchProvider(chainAnvil.rpcUrls.default.http[0], ChainId.mainnet); chainsBatchProviders.set(chainId, provider); return provider; @@ -131,8 +131,8 @@ export const getBatchedProvider = ({ chainId = ChainId.mainnet }: { chainId?: nu }; export const getProvider = ({ chainId = ChainId.mainnet }: { chainId?: number }): StaticJsonRpcProvider => { - if (useConnectedToHardhatStore.getState().connectedToHardhat) { - const provider = new StaticJsonRpcProvider(chainHardhat.rpcUrls.default.http[0], ChainId.mainnet); + if (useConnectedToAnvilStore.getState().connectedToAnvil) { + const provider = new StaticJsonRpcProvider(chainAnvil.rpcUrls.default.http[0], ChainId.mainnet); chainsProviders.set(chainId, provider); return provider; diff --git a/src/helpers/RainbowContext.tsx b/src/helpers/RainbowContext.tsx index 5fe0d9f4f38..3a0f90586fa 100644 --- a/src/helpers/RainbowContext.tsx +++ b/src/helpers/RainbowContext.tsx @@ -3,17 +3,17 @@ import { MMKV } from 'react-native-mmkv'; import { useSharedValue } from 'react-native-reanimated'; import DevButton from '../components/dev-buttons/DevButton'; import Emoji from '../components/text/Emoji'; -import { showReloadButton, showSwitchModeButton, showConnectToHardhatButton } from '../config/debug'; +import { showReloadButton, showSwitchModeButton, showConnectToAnvilButton } from '../config/debug'; import { defaultConfig } from '@/config/experimental'; import { useDispatch } from 'react-redux'; import { useTheme } from '../theme/ThemeContext'; import { STORAGE_IDS } from '@/model/mmkv'; -import { IS_TESTING } from 'react-native-dotenv'; import { logger, RainbowError } from '@/logger'; import { Navigation } from '@/navigation'; import Routes from '@rainbow-me/routes'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; +import { IS_TEST } from '@/env'; export type RainbowContextType = { config: Record; @@ -37,7 +37,7 @@ export default function RainbowContextWrapper({ children }: PropsWithChildren) { // This value is hold here to prevent JS VM from shutting down // on unmounting all shared values. useSharedValue(0); - const { setConnectedToHardhat } = useConnectedToHardhatStore(); + const { setConnectedToAnvil } = useConnectedToAnvilStore(); const [config, setConfig] = useState>( Object.entries(defaultConfig).reduce((acc, [key, { value }]) => ({ ...acc, [key]: value }), {}) ); @@ -71,26 +71,26 @@ export default function RainbowContextWrapper({ children }: PropsWithChildren) { const dispatch = useDispatch(); - const connectToHardhat = useCallback(async () => { + const connectToAnvil = useCallback(async () => { try { - setConnectedToHardhat(true); - logger.debug('connected to hardhat'); + setConnectedToAnvil(true); + logger.debug('connected to anvil'); } catch (e: any) { - setConnectedToHardhat(false); - logger.error(new RainbowError('error connecting to hardhat'), { + setConnectedToAnvil(false); + logger.error(new RainbowError('error connecting to anvil'), { message: e.message, }); } Navigation.handleAction(Routes.WALLET_SCREEN, {}); - }, [dispatch, setConnectedToHardhat]); + }, [dispatch, setConnectedToAnvil]); return ( {children} {/* @ts-expect-error ts-migrate(2741) FIXME: Property 'color' is missing in type... Remove this comment to see the full error message */} {showReloadButton && __DEV__ && } - {((showConnectToHardhatButton && __DEV__) || IS_TESTING === 'true') && ( - + {((showConnectToAnvilButton && __DEV__) || IS_TEST) && ( + {/* @ts-ignore */} 👷 diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index 4e7fd76ab91..b53bb781d40 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -1,4 +1,8 @@ +import store from '@/redux/store'; import { memoFn } from '../utils/memoFn'; +import { supportedNativeCurrencies } from '@/references'; +import { NativeCurrencyKey } from '@/entities'; +import { convertAmountToNativeDisplayWorklet } from './utilities'; /** * @desc subtracts two numbers * @param {String} str @@ -10,3 +14,128 @@ export const containsEmoji = memoFn(str => { // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. return !!str.match(ranges.join('|')); }); + +/* + * Return the given number as a formatted string. The default format is a plain + * integer with thousands-separator commas. The optional parameters facilitate + * other formats: + * - decimals = the number of decimals places to round to and show + * - valueIfNaN = the value to show for non-numeric input + * - style + * - '%': multiplies by 100 and appends a percent symbol + * - '$': prepends a dollar sign + * - useOrderSuffix = whether to use suffixes like k for 1,000, etc. + * - orderSuffixes = the list of suffixes to use + * - minOrder and maxOrder allow the order to be constrained. Examples: + * - minOrder = 1 means the k suffix should be used for numbers < 1,000 + * - maxOrder = 1 means the k suffix should be used for numbers >= 1,000,000 + */ +export function formatNumber( + number: string | number, + { + decimals = 0, + valueIfNaN = '', + style = '', + useOrderSuffix = false, + orderSuffixes = ['', 'K', 'M', 'B', 'T'], + minOrder = 0, + maxOrder = Infinity, + } = {} +) { + let x = parseFloat(`${number}`); + + if (isNaN(x)) return valueIfNaN; + + if (style === '%') x *= 100.0; + + let order; + if (!isFinite(x) || !useOrderSuffix) order = 0; + else if (minOrder === maxOrder) order = minOrder; + else { + const unboundedOrder = Math.floor(Math.log10(Math.abs(x)) / 3); + order = Math.max(0, minOrder, Math.min(unboundedOrder, maxOrder, orderSuffixes.length - 1)); + } + + const orderSuffix = orderSuffixes[order]; + if (order !== 0) x /= Math.pow(10, order * 3); + + return ( + (style === '$' ? '$' : '') + + x.toLocaleString('en-US', { + style: 'decimal', + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + orderSuffix + + (style === '%' ? '%' : '') + ); +} + +type CurrencyFormatterOptions = { + decimals?: number; + valueIfNaN?: string; + currency?: NativeCurrencyKey; +}; + +const toSubscript = (str: string | number) => str.toString().replace(/[0-9]/g, num => String.fromCharCode(0x2080 + +num)); + +/* + converts 6.9e-7 to 0.00000069 +*/ +const toDecimalString = (num: number): string => { + const [coefficient, exponent] = num.toExponential(20).split('e'); + const exp = parseInt(exponent); + const digits = coefficient.replace('.', '').replace(/0+$/, ''); + + if (exp >= 0) { + const position = exp + 1; + if (position >= digits.length) return digits + '0'.repeat(position - digits.length); + return digits.slice(0, position) + (digits.slice(position) && '.' + digits.slice(position)); + } + return '0.' + '0'.repeat(Math.abs(exp) - 1) + digits; +}; + +/* + formats a numeric string like 0000069 to 0₅69 +*/ +function formatFraction(fraction: string) { + const leadingZeros = fraction.match(/^[0]+/)?.[0].length || 0; + if (+fraction === 0) return '00'; + + const significantDigits = fraction.slice(leadingZeros, leadingZeros + 2); + if (+significantDigits === 0) return '00'; + + if (leadingZeros >= 4) return `0${toSubscript(leadingZeros)}${significantDigits}`; + return `${'0'.repeat(leadingZeros)}${significantDigits}`; +} + +export function formatCurrency( + value: string | number, + { valueIfNaN = '', currency = store.getState().settings.nativeCurrency }: CurrencyFormatterOptions = {} +): string { + const numericString = typeof value === 'number' ? toDecimalString(value) : String(value); + if (isNaN(+numericString)) return valueIfNaN; + + const currencySymbol = supportedNativeCurrencies[currency].symbol; + const [whole, fraction = ''] = numericString.split('.'); + + const numericalWholeNumber = +whole; + if (numericalWholeNumber > 0) { + // if the fraction is empty and the numeric string is less than 6 characters, we can just run it through our native currency display worklet + if (whole.length <= 6) { + return convertAmountToNativeDisplayWorklet(numericString, currency, false, true); + } + + const decimals = supportedNativeCurrencies[currency].decimals; + // otherwise for > 6 figs native value we need to format in compact notation + const formattedWhole = formatNumber(numericString, { decimals, useOrderSuffix: true }); + return `${currencySymbol}${formattedWhole}`; + } + + const formattedWhole = formatNumber(whole, { decimals: 0, useOrderSuffix: true }); + const formattedFraction = formatFraction(fraction); + // if it ends with a non-numeric character, it's in compact notation like '1.2K' + if (isNaN(+formattedWhole[formattedWhole.length - 1])) return `${currencySymbol}${formattedWhole}`; + + return `${currencySymbol}${formattedWhole}.${formattedFraction}`; +} diff --git a/src/helpers/walletBackupStepTypes.ts b/src/helpers/walletBackupStepTypes.ts index d3afc9598a2..2fbf0cb8f9e 100644 --- a/src/helpers/walletBackupStepTypes.ts +++ b/src/helpers/walletBackupStepTypes.ts @@ -1,5 +1,5 @@ export default { - no_provider: 'no_provider', + backup_prompt: 'backup_prompt', backup_manual: 'backup_manual', backup_cloud: 'backup_cloud', restore_from_backup: 'restore_from_backup', diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts new file mode 100644 index 00000000000..a9cdd674d2e --- /dev/null +++ b/src/helpers/walletLoadingStates.ts @@ -0,0 +1,10 @@ +import * as i18n from '@/languages'; + +export const WalletLoadingStates = { + BACKING_UP_WALLET: i18n.t('loading.backing_up'), + CREATING_WALLET: i18n.t('loading.creating_wallet'), + IMPORTING_WALLET: i18n.t('loading.importing_wallet'), + RESTORING_WALLET: i18n.t('loading.restoring'), +} as const; + +export type WalletLoadingStates = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; diff --git a/src/hooks/reanimated/useSyncSharedValue.ts b/src/hooks/reanimated/useSyncSharedValue.ts index f8c19c71a0c..c48f83a3643 100644 --- a/src/hooks/reanimated/useSyncSharedValue.ts +++ b/src/hooks/reanimated/useSyncSharedValue.ts @@ -9,14 +9,14 @@ interface BaseSyncParams { /** A boolean or shared value boolean that controls whether synchronization is paused. */ pauseSync?: DerivedValue | SharedValue | boolean; /** The JS state to be synchronized. */ - state: T | undefined; + state: T; } interface SharedToStateParams extends BaseSyncParams { /** The setter function for the JS state (only applicable when `syncDirection` is `'sharedValueToState'`). */ setState: (value: T) => void; /** The shared value to be synchronized. */ - sharedValue: DerivedValue | DerivedValue | SharedValue | SharedValue; + sharedValue: DerivedValue | SharedValue; /** The direction of synchronization. */ syncDirection: 'sharedValueToState'; } @@ -24,7 +24,7 @@ interface SharedToStateParams extends BaseSyncParams { interface StateToSharedParams extends BaseSyncParams { setState?: never; /** The shared value to be synchronized. */ - sharedValue: SharedValue | SharedValue; + sharedValue: SharedValue; /** The direction of synchronization. */ syncDirection: 'stateToSharedValue'; } @@ -73,7 +73,7 @@ export function useSyncSharedValue({ compareDepth = 'deep', pauseSync, setSta }, shouldSync => { if (shouldSync) { - if (syncDirection === 'sharedValueToState' && sharedValue.value !== undefined) { + if (syncDirection === 'sharedValueToState') { runOnJS(setState)(sharedValue.value); } else if (syncDirection === 'stateToSharedValue') { sharedValue.value = state; diff --git a/src/hooks/useAccountENSDomains.ts b/src/hooks/useAccountENSDomains.ts index c9923dfa606..f50aaef778d 100644 --- a/src/hooks/useAccountENSDomains.ts +++ b/src/hooks/useAccountENSDomains.ts @@ -14,7 +14,7 @@ const STALE_TIME = 10000; async function fetchAccountENSDomains({ accountAddress }: { accountAddress: string }) { const result = await fetchAccountDomains(accountAddress); - if (!result.account) return []; + if (!result?.account) return []; const { domains: controlledDomains, registrations } = result.account; const registarDomains = registrations?.map(({ domain }) => domain); diff --git a/src/hooks/useActiveRoute.ts b/src/hooks/useActiveRoute.ts new file mode 100644 index 00000000000..523eb741004 --- /dev/null +++ b/src/hooks/useActiveRoute.ts @@ -0,0 +1,16 @@ +import { Navigation, useNavigation } from '@/navigation'; +import { useEffect, useState } from 'react'; + +export const useActiveRoute = () => { + const { addListener } = useNavigation(); + const [activeRoute, setActiveRoute] = useState(Navigation.getActiveRoute()); + + useEffect(() => { + const unsubscribe = addListener('state', () => { + setActiveRoute(Navigation.getActiveRoute()); + }); + return unsubscribe; + }, [addListener]); + + return activeRoute?.name; +}; diff --git a/src/hooks/useCloudBackups.ts b/src/hooks/useCloudBackups.ts deleted file mode 100644 index 506e669c682..00000000000 --- a/src/hooks/useCloudBackups.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { BackupUserData, CloudBackups } from '../model/backup'; -import { fetchAllBackups, fetchUserDataFromCloud, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup'; -import { RainbowError, logger } from '@/logger'; - -export const enum CloudBackupStep { - IDLE, - SYNCING, - FETCHING_USER_DATA, - FETCHING_ALL_BACKUPS, - FAILED, -} - -export default function useCloudBackups() { - const [isFetching, setIsFetching] = useState(false); - const [backups, setBackups] = useState({ - files: [], - }); - - const [step, setStep] = useState(CloudBackupStep.SYNCING); - - const [userData, setUserData] = useState(); - - const fetchBackups = async () => { - try { - setIsFetching(true); - const isAvailable = isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[useCloudBackups]: Cloud backup is not available'); - setIsFetching(false); - setStep(CloudBackupStep.IDLE); - return; - } - - setStep(CloudBackupStep.SYNCING); - logger.debug('[useCloudBackups]: Syncing with cloud'); - await syncCloud(); - - setStep(CloudBackupStep.FETCHING_USER_DATA); - logger.debug('[useCloudBackups]: Fetching user data'); - const userData = await fetchUserDataFromCloud(); - setUserData(userData); - - setStep(CloudBackupStep.FETCHING_ALL_BACKUPS); - logger.debug('[useCloudBackups]: Fetching all backups'); - const backups = await fetchAllBackups(); - - logger.debug(`[useCloudBackups]: Retrieved ${backups.files.length} backup files`); - setBackups(backups); - setStep(CloudBackupStep.IDLE); - } catch (e) { - setStep(CloudBackupStep.FAILED); - logger.error(new RainbowError('[useCloudBackups]: Failed to fetch all backups'), { - error: e, - }); - } - setIsFetching(false); - }; - - useEffect(() => { - fetchBackups(); - }, []); - - return { - isFetching, - backups, - fetchBackups, - userData, - step, - }; -} diff --git a/src/hooks/useENSRegistrationStepHandler.tsx b/src/hooks/useENSRegistrationStepHandler.tsx index d99183c9bec..8072546ac9d 100644 --- a/src/hooks/useENSRegistrationStepHandler.tsx +++ b/src/hooks/useENSRegistrationStepHandler.tsx @@ -15,16 +15,16 @@ import { } from '@/helpers/ens'; import { updateTransactionRegistrationParameters } from '@/redux/ensRegistration'; import { ChainId } from '@/state/backendNetworks/types'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; const checkRegisterBlockTimestamp = async ({ registrationParameters, secondsSinceCommitConfirmed, - connectedToHardhat, + connectedToAnvil, }: { registrationParameters: RegistrationParameters; secondsSinceCommitConfirmed: number; - connectedToHardhat: boolean; + connectedToAnvil: boolean; }) => { try { const provider = getProvider({ chainId: ChainId.mainnet }); @@ -35,7 +35,7 @@ const checkRegisterBlockTimestamp = async ({ (secs > ENS_SECONDS_WAIT_WITH_PADDING && secondsSinceCommitConfirmed > ENS_SECONDS_WAIT_WITH_PADDING) || // sometimes the provider.getBlock('latest) takes a long time to update to newest block secondsSinceCommitConfirmed > ENS_SECONDS_WAIT_PROVIDER_PADDING || - connectedToHardhat + connectedToAnvil ) { return true; } @@ -62,12 +62,12 @@ export default function useENSRegistrationStepHandler(observer = true) { -1 ); - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToAnvil } = useConnectedToAnvilStore(); const [readyToRegister, setReadyToRegister] = useState(secondsSinceCommitConfirmed > ENS_SECONDS_WAIT); // flag to wait 10 secs before we get the tx block, to be able to simulate not confirmed tx when testing - const shouldLoopForConfirmation = useRef(connectedToHardhat); + const shouldLoopForConfirmation = useRef(connectedToAnvil); const registrationStep = useMemo(() => { if (mode === REGISTRATION_MODES.EDIT) return REGISTRATION_STEPS.EDIT; @@ -100,8 +100,8 @@ export default function useENSRegistrationStepHandler(observer = true) { if (!shouldLoopForConfirmation.current && block?.timestamp) { const now = Date.now(); const msBlockTimestamp = getBlockMsTimestamp(block); - // hardhat block timestamp is behind - const timeDifference = connectedToHardhat ? now - msBlockTimestamp : 0; + // anvil block timestamp is behind + const timeDifference = connectedToAnvil ? now - msBlockTimestamp : 0; const commitTransactionConfirmedAt = msBlockTimestamp + timeDifference; const secs = differenceInSeconds(now, commitTransactionConfirmedAt); setSecondsSinceCommitConfirmed(secondsSinceCommitConfirmed < 0 ? 0 : secs); @@ -115,7 +115,7 @@ export default function useENSRegistrationStepHandler(observer = true) { shouldLoopForConfirmation.current = false; } return confirmed; - }, [observer, commitTransactionHash, connectedToHardhat, secondsSinceCommitConfirmed, dispatch]); + }, [observer, commitTransactionHash, connectedToAnvil, secondsSinceCommitConfirmed, dispatch]); const startPollingWatchCommitTransaction = useCallback(async () => { if (observer) return; @@ -168,7 +168,7 @@ export default function useENSRegistrationStepHandler(observer = true) { if (!observer && secondsSinceCommitConfirmed % 2 === 0 && secondsSinceCommitConfirmed >= ENS_SECONDS_WAIT && !readyToRegister) { const checkIfReadyToRegister = async () => { const readyToRegister = await checkRegisterBlockTimestamp({ - connectedToHardhat, + connectedToAnvil, registrationParameters, secondsSinceCommitConfirmed, }); @@ -176,7 +176,7 @@ export default function useENSRegistrationStepHandler(observer = true) { }; checkIfReadyToRegister(); } - }, [connectedToHardhat, observer, readyToRegister, registrationParameters, secondsSinceCommitConfirmed]); + }, [connectedToAnvil, observer, readyToRegister, registrationParameters, secondsSinceCommitConfirmed]); useEffect( () => () => { diff --git a/src/hooks/useFarcasterAccountForWallets.ts b/src/hooks/useFarcasterAccountForWallets.ts index 4c5702051b7..80873b867e3 100644 --- a/src/hooks/useFarcasterAccountForWallets.ts +++ b/src/hooks/useFarcasterAccountForWallets.ts @@ -12,12 +12,12 @@ import { AllRainbowWallets } from '@/model/wallet'; type SummaryData = ReturnType['data']; -const getWalletForAddress = (wallets: AllRainbowWallets, address: string) => { +const getWalletForAddress = (wallets: AllRainbowWallets | null, address: string) => { return Object.values(wallets || {}).find(wallet => wallet.addresses.some(addr => isLowerCaseMatch(addr.address, address))); }; -export const useFarcasterWalletAddress = () => { - const [farcasterWalletAddress, setFarcasterWalletAddress] = useState(null); +export const useFarcasterAccountForWallets = () => { + const [farcasterWalletAddress, setFarcasterWalletAddress] = useState
(); const { accountAddress } = useAccountSettings(); const { wallets } = useWallets(); @@ -33,31 +33,32 @@ export const useFarcasterWalletAddress = () => { currency: store.getState().settings.nativeCurrency, }) ); - if (isEmpty(summaryData?.data.addresses) || isEmpty(wallets)) { - setFarcasterWalletAddress(null); + const addresses = summaryData?.data.addresses; + + if (!addresses || isEmpty(addresses) || isEmpty(wallets)) { + setFarcasterWalletAddress(undefined); return; } - const selectedAddressFid = summaryData?.data.addresses[accountAddress as Address]?.meta?.farcaster?.fid; - - if (selectedAddressFid && getWalletForAddress(wallets || {}, accountAddress)?.type !== walletTypes.readOnly) { + const selectedAddressFid = addresses[accountAddress]?.meta?.farcaster?.fid; + if (selectedAddressFid && getWalletForAddress(wallets, accountAddress)?.type !== walletTypes.readOnly) { setFarcasterWalletAddress(accountAddress); return; } - const farcasterWalletAddress = Object.keys(summaryData?.data.addresses || {}).find(addr => { + const farcasterWalletAddress = Object.keys(addresses).find(addr => { const address = addr as Address; const faracsterId = summaryData?.data.addresses[address]?.meta?.farcaster?.fid; - if (faracsterId && getWalletForAddress(wallets || {}, address)?.type !== walletTypes.readOnly) { - return faracsterId; + if (faracsterId && getWalletForAddress(wallets, address)?.type !== walletTypes.readOnly) { + return address; } - }); + }) as Address | undefined; if (farcasterWalletAddress) { setFarcasterWalletAddress(farcasterWalletAddress); return; } - setFarcasterWalletAddress(null); + setFarcasterWalletAddress(undefined); }, [wallets, allAddresses, accountAddress]); return farcasterWalletAddress; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index d0f92e12a3a..096099c8f61 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -3,7 +3,6 @@ import lang from 'i18n-js'; import { keys } from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { InteractionManager, Keyboard, TextInput } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { useDispatch } from 'react-redux'; import useAccountSettings from './useAccountSettings'; import { fetchENSAvatar } from './useENSAvatar'; @@ -29,9 +28,9 @@ import { deriveAccountFromWalletInput } from '@/utils/wallet'; import { logger, RainbowError } from '@/logger'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; -import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; import { ChainId } from '@/state/backendNetworks/types'; +import { backupsStore } from '@/state/backups/backups'; +import { IS_TEST } from '@/env'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -52,6 +51,10 @@ export default function useImportingWallet({ showImportModal = true } = {}) { const { updateWalletENSAvatars } = useWalletENSAvatar(); const profilesEnabled = useExperimentalFlag(PROFILES); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); + const inputRef = useRef(null); const { handleFocus } = useMagicAutofocus(inputRef); @@ -291,7 +294,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { image, true ); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); handleSetImporting(false); } else { const previousWalletCount = keys(wallets).length; @@ -336,32 +339,6 @@ export default function useImportingWallet({ showImportModal = true } = {}) { }); }, 1_000); - setTimeout(() => { - // If it's not read only or hardware, show the backup sheet - if ( - !( - isENSAddressFormat(input) || - isUnstoppableAddressFormat(input) || - isValidAddress(input) || - isValidBluetoothDeviceId(input) - ) - ) { - const { backupProvider } = checkWalletsForBackupStatus(wallets); - - let stepType: string = WalletBackupStepTypes.no_provider; - if (backupProvider === walletBackupTypes.cloud) { - stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { - stepType = WalletBackupStepTypes.backup_now_manually; - } - - IS_TESTING !== 'true' && - Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, - }); - } - }, 1000); - analytics.track('Imported seed phrase', { isWalletEthZero, }); @@ -414,6 +391,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { showImportModal, profilesEnabled, dangerouslyGetParent, + backupProvider, ]); return { diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts index 5f934050e9d..80aa4e903ea 100644 --- a/src/hooks/useInitializeWallet.ts +++ b/src/hooks/useInitializeWallet.ts @@ -68,7 +68,7 @@ export default function useInitializeWallet() { if (shouldRunMigrations && !seedPhrase) { logger.debug('[useInitializeWallet]: shouldRunMigrations && !seedPhrase? => true'); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); logger.debug('[useInitializeWallet]: walletsLoadState call #1'); await runMigrations(); logger.debug('[useInitializeWallet]: done with migrations'); @@ -110,7 +110,7 @@ export default function useInitializeWallet() { if (seedPhrase || isNew) { logger.debug('[useInitializeWallet]: walletsLoadState call #2'); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); } if (isNil(walletAddress)) { diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index 141f26b7f4e..323fd1d62db 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -3,12 +3,21 @@ import lang from 'i18n-js'; import { useDispatch } from 'react-redux'; import { cloudPlatform } from '../utils/platform'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { GoogleDriveUserData, getGoogleAccountUserData, deleteAllBackups, logoutFromGoogleDrive } from '@/handlers/cloudBackup'; -import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets'; +import { + GoogleDriveUserData, + getGoogleAccountUserData, + deleteAllBackups, + logoutFromGoogleDrive as logout, + login, +} from '@/handlers/cloudBackup'; +import { clearAllWalletsBackupStatus } from '@/redux/wallets'; import { showActionSheetWithOptions } from '@/utils'; import { IS_ANDROID } from '@/env'; import { RainbowError, logger } from '@/logger'; import * as i18n from '@/languages'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import * as keychain from '@/keychain'; +import { authenticateWithPIN } from '@/handlers/authentication'; export default function useManageCloudBackups() { const dispatch = useDispatch(); @@ -48,10 +57,21 @@ export default function useManageCloudBackups() { await dispatch(clearAllWalletsBackupStatus()); }; + const logoutFromGoogleDrive = async () => { + await logout(); + backupsStore.setState({ + backupProvider: undefined, + backups: { files: [] }, + mostRecentBackup: undefined, + status: CloudBackupState.NotAvailable, + }); + }; + const loginToGoogleDrive = async () => { - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); try { + await login(); const accountDetails = await getGoogleAccountUserData(); + backupsStore.getState().syncAndFetchBackups(); setAccountDetails(accountDetails ?? undefined); } catch (error) { logger.error(new RainbowError(`[useManageCloudBackups]: Logging into Google Drive failed.`), { @@ -78,14 +98,36 @@ export default function useManageCloudBackups() { }, async (buttonIndex: any) => { if (buttonIndex === 0) { - if (IS_ANDROID) { - logoutFromGoogleDrive(); - setAccountDetails(undefined); - } - removeBackupStateFromAllWallets(); + try { + let userPIN: string | undefined; + const hasBiometricsEnabled = await keychain.getSupportedBiometryType(); + if (IS_ANDROID && !hasBiometricsEnabled) { + try { + userPIN = (await authenticateWithPIN()) ?? undefined; + } catch (e) { + Alert.alert(i18n.t(i18n.l.back_up.wrong_pin)); + return; + } + } - await deleteAllBackups(); - Alert.alert(lang.t('back_up.backup_deleted_successfully')); + // Prompt for authentication before allowing them to delete backups + await keychain.getAllKeys(); + + if (IS_ANDROID) { + logoutFromGoogleDrive(); + setAccountDetails(undefined); + } + removeBackupStateFromAllWallets(); + + await deleteAllBackups(); + Alert.alert(lang.t('back_up.backup_deleted_successfully')); + } catch (e) { + logger.error(new RainbowError(`[useManageCloudBackups]: Error deleting all backups`), { + error: (e as Error).message, + }); + + Alert.alert(lang.t('back_up.errors.keychain_access')); + } } } ); @@ -94,7 +136,7 @@ export default function useManageCloudBackups() { if (_buttonIndex === 1 && IS_ANDROID) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets().then(() => loginToGoogleDrive()); + loginToGoogleDrive(); } } ); diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index fd2ffbb4b87..d38b57ac45b 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -13,14 +13,14 @@ import { Address } from 'viem'; import { addysSummaryQueryKey } from '@/resources/summary/summary'; import useWallets from './useWallets'; import { claimablesQueryKey } from '@/resources/addys/claimables/query'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; export default function useRefreshAccountData() { const dispatch = useDispatch(); const { accountAddress, nativeCurrency } = useAccountSettings(); const [isRefreshing, setIsRefreshing] = useState(false); const profilesEnabled = useExperimentalFlag(PROFILES); - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToAnvil } = useConnectedToAnvilStore(); const { wallets } = useWallets(); @@ -34,9 +34,7 @@ export default function useRefreshAccountData() { queryClient.invalidateQueries(positionsQueryKey({ address: accountAddress as Address, currency: nativeCurrency })); queryClient.invalidateQueries(claimablesQueryKey({ address: accountAddress, currency: nativeCurrency })); queryClient.invalidateQueries(addysSummaryQueryKey({ addresses: allAddresses, currency: nativeCurrency })); - queryClient.invalidateQueries( - userAssetsQueryKey({ address: accountAddress, currency: nativeCurrency, testnetMode: connectedToHardhat }) - ); + queryClient.invalidateQueries(userAssetsQueryKey({ address: accountAddress, currency: nativeCurrency, testnetMode: connectedToAnvil })); try { const getWalletNames = dispatch(fetchWalletNames()); @@ -50,7 +48,7 @@ export default function useRefreshAccountData() { logger.error(new RainbowError(`[useRefreshAccountData]: Error refreshing data: ${error}`)); throw error; } - }, [accountAddress, allAddresses, connectedToHardhat, dispatch, nativeCurrency, profilesEnabled]); + }, [accountAddress, allAddresses, connectedToAnvil, dispatch, nativeCurrency, profilesEnabled]); const refresh = useCallback(async () => { if (isRefreshing) return; diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts index 3c663ea779c..a7d6d44b505 100644 --- a/src/hooks/useTimeout.ts +++ b/src/hooks/useTimeout.ts @@ -18,7 +18,10 @@ export default function useTimeout(): [(func: () => void, ms?: number) => void, return [start, stop, handle]; } -export function useTimeoutEffect(onTimeout: (cancelled: boolean) => void, delay: number) { +export function useTimeoutEffect( + onTimeout: (e: { cancelled: boolean; elapsedTime: number }) => void, + { timeout, enabled = true }: { timeout: number; enabled?: boolean } +) { const callback = useRef(onTimeout); useLayoutEffect(() => { callback.current = onTimeout; @@ -26,12 +29,21 @@ export function useTimeoutEffect(onTimeout: (cancelled: boolean) => void, delay: const timeoutRef = useRef(); useEffect(() => { + if (!enabled) return; const startedAt = Date.now(); - timeoutRef.current = setTimeout(() => callback.current(false), delay); - const timeout = timeoutRef.current; + timeoutRef.current = setTimeout(() => { + callback.current({ + cancelled: false, + elapsedTime: Date.now() - startedAt, + }); + }, timeout); return () => { - clearTimeout(timeout); - if (Date.now() - startedAt < delay) callback.current(true); + if (!timeoutRef.current) return; + clearTimeout(timeoutRef.current); + const elapsedTime = Date.now() - startedAt; + if (elapsedTime < timeout) { + callback.current({ cancelled: true, elapsedTime }); + } }; - }, [delay]); + }, [timeout, enabled]); } diff --git a/src/hooks/useUpdateEmoji.ts b/src/hooks/useUpdateEmoji.ts index d38f229ae20..7a6781788b0 100644 --- a/src/hooks/useUpdateEmoji.ts +++ b/src/hooks/useUpdateEmoji.ts @@ -17,11 +17,11 @@ export default function useUpdateEmoji() { const saveInfo = useCallback( async (name: string, color: number) => { const walletId = selectedWallet.id; - const newWallets: typeof wallets = { + const newWallets = { ...wallets, [walletId]: { ...wallets![walletId], - addresses: wallets![walletId].addresses.map((singleAddress: { address: string }) => + addresses: wallets![walletId].addresses.map(singleAddress => singleAddress.address.toLowerCase() === accountAddress.toLowerCase() ? { ...singleAddress, diff --git a/src/hooks/useWalletBalances.ts b/src/hooks/useWalletBalances.ts index fbafc1c909e..9f1e912e437 100644 --- a/src/hooks/useWalletBalances.ts +++ b/src/hooks/useWalletBalances.ts @@ -3,11 +3,7 @@ import { useMemo } from 'react'; import { Address } from 'viem'; import useAccountSettings from './useAccountSettings'; import { useAddysSummary } from '@/resources/summary/summary'; -import { useQueries } from '@tanstack/react-query'; -import { fetchPositions, positionsQueryKey } from '@/resources/defi/PositionsQuery'; -import { RainbowPositions } from '@/resources/defi/types'; import { add, convertAmountToNativeDisplay } from '@/helpers/utilities'; -import { queryClient } from '@/react-query'; const QUERY_CONFIG = { staleTime: 60_000, // 1 minute @@ -43,7 +39,7 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { [wallets] ); - const { data: summaryData, isLoading: isSummaryLoading } = useAddysSummary( + const { data: summaryData, isLoading } = useAddysSummary( { addresses: allAddresses, currency: nativeCurrency, @@ -51,17 +47,6 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { QUERY_CONFIG ); - const positionQueries = useQueries({ - queries: allAddresses.map(address => ({ - queryKey: positionsQueryKey({ address, currency: nativeCurrency }), - queryFn: () => fetchPositions({ address, currency: nativeCurrency }), - enabled: !!address, - ...QUERY_CONFIG, - })), - }); - - const isLoading = isSummaryLoading || positionQueries.some(query => query.isLoading); - const balances = useMemo(() => { const result: Record = {}; @@ -70,9 +55,10 @@ const useWalletBalances = (wallets: AllRainbowWallets): WalletBalanceResult => { for (const address of allAddresses) { const lowerCaseAddress = address.toLowerCase() as Address; const assetBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.asset_value?.toString() || '0'; - const positionData = queryClient.getQueryData(positionsQueryKey({ address, currency: nativeCurrency })); - const positionsBalance = positionData ? positionData.totals.total.amount : '0'; - const totalAccountBalance = add(assetBalance, positionsBalance); + const positionsBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.positions_value?.toString() || '0'; + const claimablesBalance = summaryData?.data?.addresses?.[lowerCaseAddress]?.summary?.claimables_value?.toString() || '0'; + + const totalAccountBalance = add(assetBalance, add(positionsBalance, claimablesBalance)); result[lowerCaseAddress] = { assetBalanceAmount: assetBalance, diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 57b9caac681..cb5d6350a5e 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,16 +1,14 @@ -import { captureException } from '@sentry/react-native'; -import lang from 'i18n-js'; import { values } from 'lodash'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { Linking } from 'react-native'; import { useDispatch } from 'react-redux'; -import { addWalletToCloudBackup, backupWalletToCloud, findLatestBackUp } from '../model/backup'; +import { backupWalletToCloud } from '../model/backup'; import { setWalletBackedUp } from '../redux/wallets'; import { cloudPlatform } from '../utils/platform'; import useWallets from './useWallets'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { analytics } from '@/analytics'; -import { CLOUD_BACKUP_ERRORS, isCloudBackupAvailable } from '@/handlers/cloudBackup'; +import { CLOUD_BACKUP_ERRORS, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; import WalletBackupTypes from '@/helpers/walletBackupTypes'; import { logger, RainbowError } from '@/logger'; import { getSupportedBiometryType } from '@/keychain'; @@ -41,7 +39,6 @@ export function getUserError(e: Error) { export default function useWalletCloudBackup() { const dispatch = useDispatch(); const { wallets } = useWallets(); - const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const walletCloudBackup = useCallback( async ({ @@ -52,36 +49,63 @@ export default function useWalletCloudBackup() { }: { handleNoLatestBackup?: () => void; handlePasswordNotFound?: () => void; - onError?: (error: string) => void; - onSuccess?: () => void; + onError?: (error: string, isDamaged?: boolean) => void; + onSuccess?: (password: string) => void; password: string; walletId: string; }): Promise => { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - analytics.track('iCloud not enabled', { - category: 'backup', - }); - Alert.alert(lang.t('modal.back_up.alerts.cloud_not_enabled.label'), lang.t('modal.back_up.alerts.cloud_not_enabled.description'), [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - analytics.track('View how to Enable iCloud', { - category: 'backup', - }); - }, - text: lang.t('modal.back_up.alerts.cloud_not_enabled.show_me'), - }, - { - onPress: () => { - analytics.track('Ignore how to enable iCloud', { - category: 'backup', - }); - }, - style: 'cancel', - text: lang.t('modal.back_up.alerts.cloud_not_enabled.no_thanks'), - }, - ]); + if (IS_ANDROID) { + try { + await login(); + const userData = await getGoogleAccountUserData(); + if (!userData) { + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + return false; + } + } catch (e) { + logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { + error: e, + }); + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + return false; + } + } else { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + analytics.track('iCloud not enabled', { + category: 'backup', + }); + Alert.alert( + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), + [ + { + onPress: () => { + Linking.openURL('https://support.apple.com/en-us/HT204025'); + analytics.track('View how to Enable iCloud', { + category: 'backup', + }); + }, + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), + }, + { + onPress: () => { + analytics.track('Ignore how to enable iCloud', { + category: 'backup', + }); + }, + style: 'cancel', + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), + }, + ] + ); + return false; + } + } + + const wallet = wallets?.[walletId]; + if (wallet?.damaged) { + onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet), true); return false; } @@ -101,23 +125,14 @@ export default function useWalletCloudBackup() { logger.debug('[useWalletCloudBackup]: password fetched correctly'); let updatedBackupFile = null; + try { - if (!latestBackup) { - logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${wallets![walletId]}`); - updatedBackupFile = await backupWalletToCloud({ - password, - wallet: wallets![walletId], - userPIN, - }); - } else { - logger.debug(`[useWalletCloudBackup]: adding wallet to ${cloudPlatform} backup: ${wallets![walletId]}`); - updatedBackupFile = await addWalletToCloudBackup({ - password, - wallet: wallets![walletId], - filename: latestBackup, - userPIN, - }); - } + logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${(wallets || {})[walletId]}`); + updatedBackupFile = await backupWalletToCloud({ + password, + wallet: (wallets || {})[walletId], + userPIN, + }); } catch (e: any) { const userError = getUserError(e); !!onError && onError(userError); @@ -134,7 +149,7 @@ export default function useWalletCloudBackup() { logger.debug('[useWalletCloudBackup]: backup completed!'); await dispatch(setWalletBackedUp(walletId, WalletBackupTypes.cloud, updatedBackupFile)); logger.debug('[useWalletCloudBackup]: backup saved everywhere!'); - !!onSuccess && onSuccess(); + !!onSuccess && onSuccess(password); return true; } catch (e) { logger.error(new RainbowError(`[useWalletCloudBackup]: error while trying to save wallet backup state: ${e}`)); @@ -148,7 +163,7 @@ export default function useWalletCloudBackup() { return false; }, - [dispatch, latestBackup, wallets] + [dispatch, wallets] ); return walletCloudBackup; diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index a8f6072f1ea..9013f7106c2 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -123,6 +123,16 @@ export default function useWalletSectionsData({ const { isCoinListEdited } = useCoinListEdited(); + useEffect(() => { + if (isLoadingUserAssets || type !== 'wallet') return; + const params = { screen: 'wallet' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length }; + for (const asset of sortedAssets) { + if (!asset.icon_url) params.no_icon += 1; + if (!asset.price?.relative_change_24h) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [isLoadingUserAssets, sortedAssets, type]); + const walletSections = useMemo(() => { const accountInfo = { hiddenAssets, diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts index 38363886917..20de06f22a1 100644 --- a/src/hooks/useWallets.ts +++ b/src/hooks/useWallets.ts @@ -5,14 +5,12 @@ import { RainbowWallet } from '@/model/wallet'; import { AppState } from '@/redux/store'; const walletSelector = createSelector( - ({ wallets: { isWalletLoading, selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ - isWalletLoading, - selectedWallet: selected as any, + ({ wallets: { selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ + selectedWallet: selected, walletNames, wallets, }), - ({ isWalletLoading, selectedWallet, walletNames, wallets }) => ({ - isWalletLoading, + ({ selectedWallet, walletNames, wallets }) => ({ selectedWallet, walletNames, wallets, @@ -20,13 +18,12 @@ const walletSelector = createSelector( ); export default function useWallets() { - const { isWalletLoading, selectedWallet, walletNames, wallets } = useSelector(walletSelector); + const { selectedWallet, walletNames, wallets } = useSelector(walletSelector); return { isDamaged: selectedWallet?.damaged, isReadOnlyWallet: selectedWallet.type === WalletTypes.readOnly, isHardwareWallet: !!selectedWallet.deviceId, - isWalletLoading, selectedWallet, walletNames, wallets, diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index 517185d1407..4c90fcfa16c 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -10,7 +10,7 @@ import { invalidateAddressNftsQueries } from '@/resources/nfts'; import { usePendingTransactionsStore } from '@/state/pendingTransactions'; import { Address } from 'viem'; import { staleBalancesStore } from '@/state/staleBalances'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const useWatchPendingTransactions = ({ address }: { address: string }) => { @@ -18,7 +18,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => storePendingTransactions: state.pendingTransactions, setPendingTransactions: state.setPendingTransactions, })); - const { connectedToHardhat } = useConnectedToHardhatStore(); + const { connectedToAnvil } = useConnectedToAnvilStore(); const pendingTransactions = useMemo(() => storePendingTransactions[address] || [], [address, storePendingTransactions]); @@ -31,12 +31,12 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => userAssetsQueryKey({ address, currency: nativeCurrency, - testnetMode: connectedToHardhat, + testnetMode: connectedToAnvil, }) ); invalidateAddressNftsQueries(address); }, - [address, connectedToHardhat, nativeCurrency] + [address, connectedToAnvil, nativeCurrency] ); const processSupportedNetworkTransaction = useCallback( @@ -115,7 +115,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => }); queryClient.refetchQueries({ - queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToHardhat }), + queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToAnvil }), }); const supportedMainnetChainIds = useBackendNetworksStore.getState().getSupportedMainnetChainIds(); @@ -143,7 +143,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => address, pendingTransactions: newPendingTransactions, }); - }, [address, connectedToHardhat, nativeCurrency, pendingTransactions, processPendingTransaction, setPendingTransactions]); + }, [address, connectedToAnvil, nativeCurrency, pendingTransactions, processPendingTransaction, setPendingTransactions]); return { watchPendingTransactions }; }; diff --git a/src/languages/ar_AR.json b/src/languages/ar_AR.json index 581387865ed..680154a854b 100644 --- a/src/languages/ar_AR.json +++ b/src/languages/ar_AR.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "مسح ذاكرة التخزين المؤقت لبيانات الصور", "clear_local_storage": "مسح التخزين المحلي", "clear_mmkv_storage": "مسح التخزين MMKV", - "connect_to_hardhat": "الاتصال بـ hardhat", - "disconnect_to_hardhat": "افصل عن Hardhat", + "connect_to_anvil": "الاتصال بـ anvil", + "disconnect_to_anvil": "افصل عن Anvil", "crash_app_render_error": "تعطيل التطبيق (خطأ في التصيير)", "enable_testnets": "تمكين شبكات الاختبار", "installing_update": "تثبيت التحديث", diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 725d9b674d1..66bdb0dec0d 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -96,7 +96,8 @@ "generic": "Error while trying to backup. Error code: %{errorCodes}", "no_keys_found": "No keys found. Please try again.", "backup_not_found": "Backup not found. Please try again.", - "no_account_found": "Unable to retrieve backup files. Make sure you're logged in." + "no_account_found": "Unable to retrieve backup files. Make sure you're logged in.", + "damaged_wallet": "Unable to backup wallet. Missing keychain data." }, "wrong_pin": "The PIN code you entered was incorrect and we can't make a backup. Please try again with the correct code.", "already_backed_up": { @@ -115,6 +116,8 @@ "no_backups": "No backups found", "failed_to_fetch_backups": "Failed to fetch backups", "retry": "Retry", + "refresh": "Refresh", + "syncing_cloud_store": "Syncing to %{cloudPlatformName}", "fetching_backups": "Retrieving backups from %{cloudPlatformName}", "back_up_to_platform": "Back up to %{cloudPlatformName}", "restore_from_platform": "Restore from %{cloudPlatformName}", @@ -137,7 +140,7 @@ "choose_backup_cloud_description": "Securely back up your wallet to %{cloudPlatform} so you can restore it if you lose your device or get a new one.", "choose_backup_manual_description": "Back up your wallet manually by saving your secret phrase in a secure location.", "enable_cloud_backups_description": "If you prefer to back up your wallets manually, you can do so below.", - "latest_backup": "Last Backup: %{date}", + "latest_backup": "Latest Backup: %{date}", "back_up_all_wallets_to_cloud": "Back Up All Wallets to %{cloudPlatformName}", "most_recent_backup": "Most Recent Backup", "out_of_date": "Out of Date", @@ -145,6 +148,12 @@ "older_backups": "Older Backups", "no_older_backups": "No Older Backups", "older_backups_title": "%{date} at %{time}", + "statuses": { + "not_enabled": "Not Enabled", + "syncing": "Syncing", + "out_of_date": "Out of Date", + "up_to_date": "Up to Date" + }, "password": { "a_password_youll_remember_part_one": "This password is", "not": "not", @@ -514,8 +523,8 @@ "clear_image_metadata_cache": "Clear Image Metadata Cache", "clear_local_storage": "Clear local storage", "clear_mmkv_storage": "Clear MMKV storage", - "connect_to_hardhat": "Connect to hardhat", - "disconnect_to_hardhat": "Disconnect from hardhat", + "connect_to_anvil": "Connect to anvil", + "disconnect_to_anvil": "Disconnect from anvil", "crash_app_render_error": "Crash app (render error)", "enable_testnets": "Enable Testnets", "installing_update": "Installing update", @@ -1220,6 +1229,12 @@ "check_out_this_wallet": "Check out this wallet's collectibles on 🌈 Rainbow at %{showcaseUrl}" } }, + "loading": { + "backing_up": "Backing up...", + "creating_wallet": "Creating wallet...", + "importing_wallet": "Importing...", + "restoring": "Restoring..." + }, "message": { "click_to_copy_to_clipboard": "Click to copy to clipboard", "coming_soon": "Coming soon...", @@ -3018,6 +3033,59 @@ "new_tab": "New Tab" } }, + "trending_tokens": { + "all": "All", + "no_results": { + "title": "No results", + "body": "Try browsing a larger timeframe or a different network or category." + }, + "and": "and", + "and_others": { + "one": "and %{count} other", + "other": "and %{count} others" + }, + "filters": { + "categories": { + "TRENDING": "Trending", + "NEW": "New", + "FARCASTER": "Farcaster" + }, + "sort": { + "RECOMMENDED": { + "label": "Sort", + "menuOption": "Default" + }, + "VOLUME": "Volume", + "MARKET_CAP": "Market Cap", + "TOP_GAINERS": "Top Gainers", + "TOP_LOSERS": "Top Losers" + }, + "time": { + "H12": "12 Hours", + "H12_ABBREVIATED": "12h", + "H24": "24 Hours", + "H24_ABBREVIATED": "24h", + "D7": "1 Week", + "D3": "3 Days" + } + } + }, + "network_switcher": { + "customize_networks_banner": { + "title": "Customize Networks", + "tap_the": "Tap the", + "button_to_set_up": "button below to set up" + }, + "drag_here_to_unpin": "Drop Here to Unpin", + "edit": "Edit", + "networks": "Networks", + "drag_to_rearrange": "Drag to Rearrange", + "show_less": "Show Less", + "more": "More", + "show_more": "More Networks", + "all_networks": "All Networks" + }, + "done": "Done", "copy": "Copy", "paste": "Paste" } diff --git a/src/languages/es_419.json b/src/languages/es_419.json index e75f3ecab54..be671fb2e61 100644 --- a/src/languages/es_419.json +++ b/src/languages/es_419.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Borrar Cache de Metadatos de Imagen", "clear_local_storage": "Borrar almacenamiento local", "clear_mmkv_storage": "Borrar almacenamiento de MMKV", - "connect_to_hardhat": "Conectar a Hardhat", - "disconnect_to_hardhat": "Desconectar de hardhat", + "connect_to_anvil": "Conectar a Anvil", + "disconnect_to_anvil": "Desconectar de anvil", "crash_app_render_error": "Crash de la aplicación (error de renderización)", "enable_testnets": "Habilitar Testnets", "installing_update": "Instalando actualización", diff --git a/src/languages/fr_FR.json b/src/languages/fr_FR.json index 5219996ea32..2d36b80ad5e 100644 --- a/src/languages/fr_FR.json +++ b/src/languages/fr_FR.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Effacer le cache des métadonnées d'image", "clear_local_storage": "Effacer le stockage local", "clear_mmkv_storage": "Effacer le stockage MMKV", - "connect_to_hardhat": "Se connecter à hardhat", - "disconnect_to_hardhat": "Déconnecter de hardhat", + "connect_to_anvil": "Se connecter à anvil", + "disconnect_to_anvil": "Déconnecter de anvil", "crash_app_render_error": "Crash app (render error) :)", "enable_testnets": "Activer Testnets", "installing_update": "Installation de la mise à jour", diff --git a/src/languages/hi_IN.json b/src/languages/hi_IN.json index 9f1e63ce207..6225599e370 100644 --- a/src/languages/hi_IN.json +++ b/src/languages/hi_IN.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "छवि मेटाडाटा कैश साफ करें", "clear_local_storage": "लोकल स्टोरेज को साफ करें", "clear_mmkv_storage": "MMKV स्टोरेज को साफ करें", - "connect_to_hardhat": "Hardhat से कनेक्ट करें", - "disconnect_to_hardhat": "हार्डहैट से डिस्कनेक्ट करें", + "connect_to_anvil": "Anvil से कनेक्ट करें", + "disconnect_to_anvil": "हार्डहैट से डिस्कनेक्ट करें", "crash_app_render_error": "ऐप्र्प्लिकेशन क्रैश करें (रेंडर त्रुटि)", "enable_testnets": "टेस्टनेट्स सक्षम करें", "installing_update": "अद्यतन स्थापित कर रहा है", diff --git a/src/languages/id_ID.json b/src/languages/id_ID.json index 2d46f0dc67e..33f73e497d8 100644 --- a/src/languages/id_ID.json +++ b/src/languages/id_ID.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Hapus Cache Metadata Gambar", "clear_local_storage": "Hapus penyimpanan lokal", "clear_mmkv_storage": "Hapus penyimpanan MMKV", - "connect_to_hardhat": "Hubungkan ke hardhat", - "disconnect_to_hardhat": "Putuskan hubungan dari hardhat", + "connect_to_anvil": "Hubungkan ke anvil", + "disconnect_to_anvil": "Putuskan hubungan dari anvil", "crash_app_render_error": "Aplikasi crash (kesalahan render)", "enable_testnets": "Aktifkan Testnets", "installing_update": "Menginstal pembaruan", diff --git a/src/languages/ja_JP.json b/src/languages/ja_JP.json index 0087d9cc208..a9df28a7a1d 100644 --- a/src/languages/ja_JP.json +++ b/src/languages/ja_JP.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "クリアイメージメタデータキャッシュ", "clear_local_storage": "ローカルストレージをクリア", "clear_mmkv_storage": "MMKVストレージをクリア", - "connect_to_hardhat": "Hardhatに接続する", - "disconnect_to_hardhat": "hardhatから切断する", + "connect_to_anvil": "Anvilに接続する", + "disconnect_to_anvil": "anvilから切断する", "crash_app_render_error": "アプリをクラッシュさせる(レンダーエラー)", "enable_testnets": "テストネットを有効にする", "installing_update": "アップデートをインストール中", diff --git a/src/languages/ko_KR.json b/src/languages/ko_KR.json index 90da37af11c..4e9d0b3ac39 100644 --- a/src/languages/ko_KR.json +++ b/src/languages/ko_KR.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "이미지 메타데이터 캐시 지우기", "clear_local_storage": "로컬 저장소 지우기", "clear_mmkv_storage": "MMKV 저장소 지우기", - "connect_to_hardhat": "하드햇에 연결", - "disconnect_to_hardhat": "hardhat 연결 해제", + "connect_to_anvil": "하드햇에 연결", + "disconnect_to_anvil": "anvil 연결 해제", "crash_app_render_error": "애플리케이션 종료 (렌더링 오류)", "enable_testnets": "테스트넷 활성화", "installing_update": "업데이트 설치 중", diff --git a/src/languages/pt_BR.json b/src/languages/pt_BR.json index 2216e16b584..38db975c1f1 100644 --- a/src/languages/pt_BR.json +++ b/src/languages/pt_BR.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Limpar Cache de Metadados da Imagem", "clear_local_storage": "Limpar Armazenamento Local", "clear_mmkv_storage": "Limpar Armazenamento MMKV", - "connect_to_hardhat": "Conectar ao Hardhat", - "disconnect_to_hardhat": "Desconectar do hardhat", + "connect_to_anvil": "Conectar ao Anvil", + "disconnect_to_anvil": "Desconectar do anvil", "crash_app_render_error": "Crash no app (erro de renderização)", "enable_testnets": "Ativar Testnets", "installing_update": "Instalando atualização", diff --git a/src/languages/ru_RU.json b/src/languages/ru_RU.json index 2867e9180ef..2bc33887870 100644 --- a/src/languages/ru_RU.json +++ b/src/languages/ru_RU.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Очистить кэш метаданных изображения", "clear_local_storage": "Очистить локальное хранилище", "clear_mmkv_storage": "Очистить хранилище MMKV", - "connect_to_hardhat": "Подключиться к hardhat", - "disconnect_to_hardhat": "Отключиться от hardhat", + "connect_to_anvil": "Подключиться к anvil", + "disconnect_to_anvil": "Отключиться от anvil", "crash_app_render_error": "Падение приложения (ошибка отображения)", "enable_testnets": "Включить тестовые сети", "installing_update": "Установка обновления", diff --git a/src/languages/th_TH.json b/src/languages/th_TH.json index 80714c9a455..343029d2ee7 100644 --- a/src/languages/th_TH.json +++ b/src/languages/th_TH.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "ล้างแคชข้อมูลภาพ", "clear_local_storage": "ล้าง local storage", "clear_mmkv_storage": "ล้าง MMKV storage", - "connect_to_hardhat": "เชื่อมต่อกับ hardhat", - "disconnect_to_hardhat": "ตัดการเชื่อมต่อจาก Hardhat", + "connect_to_anvil": "เชื่อมต่อกับ anvil", + "disconnect_to_anvil": "ตัดการเชื่อมต่อจาก Anvil", "crash_app_render_error": "คว่ำ app (render error)", "enable_testnets": "เปิดใช้งาน Testnets", "installing_update": "กำลังติดตั้งการอัปเดต", diff --git a/src/languages/tr_TR.json b/src/languages/tr_TR.json index d7233d4a4ae..ffaa85ee5a8 100644 --- a/src/languages/tr_TR.json +++ b/src/languages/tr_TR.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "Resim Metaveri Önbelleğini Temizle", "clear_local_storage": "Yerel depolamayı temizle", "clear_mmkv_storage": "MMKV depolamasını temizle", - "connect_to_hardhat": "Hardhat'a bağlan", - "disconnect_to_hardhat": "Hardhat'tan bağlantıyı kes", + "connect_to_anvil": "Anvil'a bağlan", + "disconnect_to_anvil": "Anvil'tan bağlantıyı kes", "crash_app_render_error": "Uygulamayı çökert (render hatası)", "enable_testnets": "Testnetleri Etkinleştir", "installing_update": "Güncelleme Yükleniyor", diff --git a/src/languages/zh_CN.json b/src/languages/zh_CN.json index 92b9ff64e78..0d2540fa6e0 100644 --- a/src/languages/zh_CN.json +++ b/src/languages/zh_CN.json @@ -503,8 +503,8 @@ "clear_image_metadata_cache": "清除图片内元数据缓存", "clear_local_storage": "清除本地存储", "clear_mmkv_storage": "清除MMKV存储", - "connect_to_hardhat": "连接到hardhat", - "disconnect_to_hardhat": "断开硬帽连接", + "connect_to_anvil": "连接到anvil", + "disconnect_to_anvil": "断开硬帽连接", "crash_app_render_error": "崩溃应用程序(渲染错误)", "enable_testnets": "启用Testnets", "installing_update": "正在安装更新", diff --git a/src/model/backup.ts b/src/model/backup.ts index 2eb50a7c297..c838796664d 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -1,15 +1,23 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { NativeModules } from 'react-native'; +import { NativeModules, Linking } from 'react-native'; import { captureException } from '@sentry/react-native'; import { endsWith } from 'lodash'; -import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup'; +import { + CLOUD_BACKUP_ERRORS, + encryptAndSaveDataToCloud, + getDataFromCloud, + isCloudBackupAvailable, + getGoogleAccountUserData, + login, + logoutFromGoogleDrive, + normalizeAndroidBackupFilename, +} from '@/handlers/cloudBackup'; +import { Alert as NativeAlert } from '@/components/alerts'; import WalletBackupTypes from '../helpers/walletBackupTypes'; -import WalletTypes from '../helpers/walletTypes'; -import { Alert } from '@/components/alerts'; import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants'; import * as keychain from '@/model/keychain'; import * as kc from '@/keychain'; -import { AllRainbowWallets, allWalletsVersion, createWallet, RainbowWallet } from './wallet'; +import { AllRainbowWallets, createWallet, RainbowWallet } from './wallet'; import { analytics } from '@/analytics'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_DEV } from '@/env'; @@ -24,16 +32,19 @@ import Routes from '@/navigation/routesNames'; import { clearAllStorages } from './mmkv'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { getRemoteConfig } from './remoteConfig'; +import { WrappedAlert as Alert } from '@/helpers/alert'; +import { AppDispatch } from '@/redux/store'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; const { DeviceUUID } = NativeModules; const encryptor = new AesEncryptor(); const PIN_REGEX = /^\d{4}$/; export interface CloudBackups { - files: Backup[]; + files: BackupFile[]; } -export interface Backup { +export interface BackupFile { isDirectory: boolean; isFile: boolean; lastModified: string; @@ -44,8 +55,9 @@ export interface Backup { } export const parseTimestampFromFilename = (filename: string) => { + const name = normalizeAndroidBackupFilename(filename); return Number( - filename + name .replace('.backup_', '') .replace('backup_', '') .replace('.json', '') @@ -54,6 +66,27 @@ export const parseTimestampFromFilename = (filename: string) => { ); }; +/** + * Parse the timestamp from a backup file name + * @param filename - The name of the backup file backup_${now}.json + * @returns The timestamp as a number + */ +export const parseTimestampFromBackupFile = (filename: string | null): number | undefined => { + if (!filename) { + return; + } + const match = filename.match(/backup_(\d+)\.json/); + if (!match) { + return; + } + + if (Number.isNaN(Number(match[1]))) { + return; + } + + return Number(match[1]); +}; + type BackupPassword = string; interface BackedUpData { @@ -63,9 +96,72 @@ interface BackedUpData { export interface BackupUserData { wallets: AllRainbowWallets; } +type MaybePromise = T | Promise; + +export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: { fn: () => MaybePromise; logout?: boolean }) => { + backupsStore.getState().setStatus(CloudBackupState.InProgress); + + if (IS_ANDROID) { + try { + if (logout) { + await logoutFromGoogleDrive(); + } + + const currentUser = await getGoogleAccountUserData(); + if (!currentUser) { + await login(); + await backupsStore.getState().syncAndFetchBackups(); + } + + const userData = await getGoogleAccountUserData(); + if (!userData) { + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + return; + } + // execute the function + + // NOTE: Set this back to ready in order to process the backup + backupsStore.getState().setStatus(CloudBackupState.Ready); + return await fn(); + } catch (e) { + logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), { + error: e, + }); + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + } + } else { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + Alert.alert( + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), + i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), + [ + { + onPress: () => { + Linking.openURL('https://support.apple.com/en-us/HT204025'); + }, + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), + }, + { + style: 'cancel', + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), + }, + ] + ); + backupsStore.getState().setStatus(CloudBackupState.NotAvailable); + return; + } + + // NOTE: Set this back to ready in order to process the backup + backupsStore.getState().setStatus(CloudBackupState.Ready); + return await fn(); + } +}; async function extractSecretsForWallet(wallet: RainbowWallet) { - const allKeys = await keychain.loadAllKeys(); + const allKeys = await kc.getAllKeys(); if (!allKeys) throw new Error(CLOUD_BACKUP_ERRORS.KEYCHAIN_ACCESS_ERROR); const secrets = {} as { [key: string]: string }; @@ -100,17 +196,15 @@ async function extractSecretsForWallet(wallet: RainbowWallet) { export async function backupAllWalletsToCloud({ wallets, password, - latestBackup, onError, onSuccess, dispatch, }: { wallets: AllRainbowWallets; password: BackupPassword; - latestBackup: string | null; onError?: (message: string) => void; - onSuccess?: () => void; - dispatch: any; + onSuccess?: (password: BackupPassword) => void; + dispatch: AppDispatch; }) { let userPIN: string | undefined; const hasBiometricsEnabled = await kc.getSupportedBiometryType(); @@ -126,11 +220,9 @@ export async function backupAllWalletsToCloud({ try { /** * Loop over all keys and decrypt if necessary for android - * if no latest backup, create first backup with all secrets - * if latest backup, update updatedAt and add new secrets to the backup */ - const allKeys = await keychain.loadAllKeys(); + const allKeys = await kc.getAllKeys(); if (!allKeys) { onError?.(i18n.t(i18n.l.back_up.errors.no_keys_found)); return; @@ -157,49 +249,21 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - let updatedBackupFile: any = null; - if (!latestBackup) { - const data = { - createdAt: now, - secrets: {}, - }; - const promises = Object.entries(allSecrets).map(async ([username, password]) => { - const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - - data.secrets = { - ...data.secrets, - ...processedNewSecrets, - }; - }); - - await Promise.all(promises); - updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); - } else { - // if we have a latest backup file, we need to update the updatedAt and add new secrets to the backup file.. - const backup = await getDataFromCloud(password, latestBackup); - if (!backup) { - onError?.(i18n.t(i18n.l.back_up.errors.backup_not_found)); - return; - } + const data = { + createdAt: now, + secrets: {}, + }; + const promises = Object.entries(allSecrets).map(async ([username, password]) => { + const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - const data = { - createdAt: backup.createdAt, - secrets: backup.secrets, + data.secrets = { + ...data.secrets, + ...processedNewSecrets, }; + }); - const promises = Object.entries(allSecrets).map(async ([username, password]) => { - const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN); - - data.secrets = { - ...data.secrets, - ...processedNewSecrets, - }; - }); - - await Promise.all(promises); - updatedBackupFile = await encryptAndSaveDataToCloud(data, password, latestBackup); - } - + await Promise.all(promises); + const updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); const walletIdsToUpdate = Object.keys(wallets); await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, WalletBackupTypes.cloud, updatedBackupFile)); @@ -209,16 +273,18 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - onSuccess?.(); - } catch (error: any) { - const userError = getUserError(error); - onError?.(userError); - captureException(error); - analytics.track(`Error backing up all wallets to ${cloudPlatform}`, { - category: 'backup', - error: userError, - label: cloudPlatform, - }); + onSuccess?.(password); + } catch (error) { + if (error instanceof Error) { + const userError = getUserError(error); + onError?.(userError); + captureException(error); + analytics.track(`Error backing up all wallets to ${cloudPlatform}`, { + category: 'backup', + error: userError, + label: cloudPlatform, + }); + } } } @@ -251,9 +317,15 @@ export async function addWalletToCloudBackup({ wallet: RainbowWallet; filename: string; userPIN?: string; -}): Promise { - // @ts-ignore +}): Promise { const backup = await getDataFromCloud(password, filename); + if (!backup) { + logger.error(new RainbowError('[backup]: Unable to get backup data for filename'), { + filename, + }); + return null; + } + const now = Date.now(); const newSecretsToBeAddedToBackup = await extractSecretsForWallet(wallet); const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded(newSecretsToBeAddedToBackup, userPIN); @@ -321,25 +393,6 @@ export async function decryptAllPinEncryptedSecretsIfNeeded(secrets: Record { - // Check if there's a wallet backed up - if (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) { - // If there is one, let's grab the latest backup - if (!latestBackup || Number(wallet.backupDate) > latestBackup) { - filename = wallet.backupFile; - latestBackup = Number(wallet.backupDate); - } - } - }); - } - return filename; -} - export const RestoreCloudBackupResultStates = { success: 'success', failedWhenRestoring: 'failedWhenRestoring', @@ -368,16 +421,14 @@ const sanitizeFilename = (filename: string) => { */ export async function restoreCloudBackup({ password, - userData, - nameOfSelectedBackupFile, + backupFilename, }: { password: BackupPassword; - userData: BackupUserData | undefined; - nameOfSelectedBackupFile: string; + backupFilename: string; }): Promise { try { // 1 - sanitize filename to remove extra things we don't care about - const filename = sanitizeFilename(nameOfSelectedBackupFile); + const filename = sanitizeFilename(backupFilename); if (!filename) { return RestoreCloudBackupResultStates.failedWhenRestoring; } @@ -402,26 +453,6 @@ export async function restoreCloudBackup({ } } - if (userData) { - // Restore only wallets that were backed up in cloud - // or wallets that are read-only - const walletsToRestore: AllRainbowWallets = {}; - Object.values(userData?.wallets ?? {}).forEach(wallet => { - if ( - (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) || - wallet.type === WalletTypes.readOnly - ) { - walletsToRestore[wallet.id] = wallet; - } - }); - - // All wallets - dataToRestore[allWalletsKey] = { - version: allWalletsVersion, - wallets: walletsToRestore, - }; - } - const restoredSuccessfully = await restoreSpecificBackupIntoKeychain(dataToRestore, userPIN); return restoredSuccessfully ? RestoreCloudBackupResultStates.success : RestoreCloudBackupResultStates.failedWhenRestoring; } catch (error) { @@ -525,74 +556,6 @@ async function restoreSpecificBackupIntoKeychain(backedUpData: BackedUpData, use } } -async function restoreCurrentBackupIntoKeychain(backedUpData: BackedUpData, newPIN?: string): Promise { - try { - // Access control config per each type of key - const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions(); - const encryptedBackupPinData = backedUpData[pinKey]; - const backupPIN = await decryptPIN(encryptedBackupPinData); - - await Promise.all( - Object.keys(backedUpData).map(async key => { - let value = backedUpData[key]; - const theKeyIsASeedPhrase = endsWith(key, seedPhraseKey); - const theKeyIsAPrivateKey = endsWith(key, privateKeyKey); - const accessControl: typeof kc.publicAccessControlOptions = - theKeyIsASeedPhrase || theKeyIsAPrivateKey ? privateAccessControlOptions : kc.publicAccessControlOptions; - - /* - * Backups that were saved encrypted with PIN to the cloud need to be - * decrypted with the backup PIN first, and then if we still need - * to store them as encrypted, - * we need to re-encrypt them with a new PIN - */ - if (theKeyIsASeedPhrase) { - const parsedValue = JSON.parse(value); - parsedValue.seedphrase = await decryptSecretFromBackupPin({ - secret: parsedValue.seedphrase, - backupPIN, - }); - value = JSON.stringify(parsedValue); - } else if (theKeyIsAPrivateKey) { - const parsedValue = JSON.parse(value); - parsedValue.privateKey = await decryptSecretFromBackupPin({ - secret: parsedValue.privateKey, - backupPIN, - }); - value = JSON.stringify(parsedValue); - } - - /* - * Since we're decrypting the data that was saved as PIN code encrypted, - * we will allow the user to create a new PIN code. - * We store the old PIN code in the backup, but we don't want to restore it, - * since it will override the new PIN code that we just saved to keychain. - */ - if (key === pinKey) { - return; - } - - if (typeof value === 'string') { - return kc.set(key, value, { - ...accessControl, - androidEncryptionPin: newPIN, - }); - } else { - return kc.setObject(key, value, { - ...accessControl, - androidEncryptionPin: newPIN, - }); - } - }) - ); - - return true; - } catch (e) { - logger.error(new RainbowError(`[backup]: Error restoring current backup into keychain: ${e}`)); - return false; - } -} - async function decryptSecretFromBackupPin({ secret, backupPIN }: { secret?: string; backupPIN?: string }) { let processedSecret = secret; @@ -638,13 +601,9 @@ export async function saveBackupPassword(password: BackupPassword): Promise { - const rainbowBackupPassword = await keychain.loadString('RainbowBackupPassword'); - if (typeof rainbowBackupPassword === 'number') { - return null; - } - - if (rainbowBackupPassword) { - return rainbowBackupPassword; + const { value } = await kc.get('RainbowBackupPassword'); + if (value) { + return value; } return await fetchBackupPassword(); @@ -653,7 +612,7 @@ export async function getLocalBackupPassword(): Promise { export async function saveLocalBackupPassword(password: string) { const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions(); - await keychain.saveString('RainbowBackupPassword', password, privateAccessControlOptions); + await kc.set('RainbowBackupPassword', password, privateAccessControlOptions); saveBackupPassword(password); } @@ -666,7 +625,7 @@ export async function fetchBackupPassword(): Promise { try { const { value: results } = await kc.getSharedWebCredentials(); if (results) { - return results.password as BackupPassword; + return results.password; } return null; } catch (e) { @@ -695,7 +654,7 @@ export async function getDeviceUUID(): Promise { } const FailureAlert = () => - Alert({ + NativeAlert({ buttons: [ { style: 'cancel', diff --git a/src/model/migrations.ts b/src/model/migrations.ts index 98a68bffe86..e7d2e90a8e1 100644 --- a/src/model/migrations.ts +++ b/src/model/migrations.ts @@ -43,7 +43,7 @@ import { getAddressAndChainIdFromUniqueId, getUniqueId, getUniqueIdNetwork } fro import { ParsedAssetsDictByChain, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; import { userAssetsStore } from '@/state/assets/userAssets'; import { userAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { selectorFilterByUserChains, selectUserAssetsList } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { UnlockableAppIconKey, unlockableAppIcons } from '@/appIcons/appIcons'; import { unlockableAppIconStorage } from '@/featuresToUnlock/unlockableAppIconCheck'; @@ -729,8 +729,8 @@ export default async function runMigrations() { for (const wallet of Object.values(wallets)) { for (const { address } of (wallet as RainbowWallet).addresses) { - const { connectedToHardhat } = useConnectedToHardhatStore.getState(); - const queryKey = userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToHardhat }); + const { connectedToAnvil } = useConnectedToAnvilStore.getState(); + const queryKey = userAssetsQueryKey({ address, currency: nativeCurrency, testnetMode: connectedToAnvil }); const queryData: ParsedAssetsDictByChain | undefined = queryClient.getQueryData(queryKey); if (!queryData) continue; diff --git a/src/model/remoteConfig.ts b/src/model/remoteConfig.ts index 40382af707b..3341a3a0000 100644 --- a/src/model/remoteConfig.ts +++ b/src/model/remoteConfig.ts @@ -57,6 +57,8 @@ export interface RainbowConfig extends Record featured_results: boolean; claimables: boolean; nfts_enabled: boolean; + + trending_tokens_limit: number; } export const DEFAULT_CONFIG: RainbowConfig = { @@ -147,6 +149,8 @@ export const DEFAULT_CONFIG: RainbowConfig = { featured_results: true, claimables: true, nfts_enabled: true, + + trending_tokens_limit: 10, }; export async function fetchRemoteConfig(): Promise { @@ -205,6 +209,8 @@ export async function fetchRemoteConfig(): Promise { key === 'nfts_enabled' ) { config[key] = entry.asBoolean(); + } else if (key === 'trending_tokens_limit') { + config[key] = entry.asNumber(); } else { config[key] = entry.asString(); } diff --git a/src/navigation/HardwareWalletTxNavigator.tsx b/src/navigation/HardwareWalletTxNavigator.tsx index 28e290065dc..4b209dabe30 100644 --- a/src/navigation/HardwareWalletTxNavigator.tsx +++ b/src/navigation/HardwareWalletTxNavigator.tsx @@ -63,7 +63,7 @@ export const HardwareWalletTxNavigator = () => { const { navigate } = useNavigation(); - const deviceId = selectedWallet?.deviceId; + const deviceId = selectedWallet.deviceId ?? ''; const [isReady, setIsReady] = useRecoilState(LedgerIsReadyAtom); const [readyForPolling, setReadyForPolling] = useRecoilState(readyForPollingAtom); const [triggerPollerCleanup, setTriggerPollerCleanup] = useRecoilState(triggerPollerCleanupAtom); diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index d246a004f6a..1e03c2d96a3 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -90,6 +90,9 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel'; import { RootStackParamList } from './types'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; +import { NetworkSelector } from '@/components/NetworkSwitcher'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -212,7 +215,7 @@ function BSNavigator() { step === walletBackupStepTypes.restore_from_backup ) { heightForStep = backupSheetSizes.long; - } else if (step === walletBackupStepTypes.no_provider) { + } else if (step === walletBackupStepTypes.backup_prompt) { heightForStep = backupSheetSizes.medium; } @@ -242,6 +245,7 @@ function BSNavigator() { + @@ -272,6 +276,10 @@ const AppContainerWithAnalytics = React.forwardRef + + {/* NOTE: Internally, these use some navigational checks */} + + )); diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index e65e309538a..201eb3aa374 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -70,6 +70,7 @@ import { swapConfig, checkIdentifierSheetConfig, recieveModalSheetConfig, + networkSelectorConfig, } from './config'; import { addCashSheet, emojiPreset, emojiPresetWallet, overlayExpandedPreset, sheetPreset } from './effects'; import { InitialRouteContext } from './initialRoute'; @@ -102,6 +103,9 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel'; import { RootStackParamList } from './types'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; +import { NetworkSelector } from '@/components/NetworkSwitcher'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -273,6 +277,7 @@ function NativeStackNavigator() { + @@ -286,6 +291,10 @@ const AppContainerWithAnalytics = React.forwardRef + + {/* NOTE: Internally, these use some navigational checks */} + + )); diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index 33f84504828..781ca8932d4 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -15,7 +15,7 @@ import RecyclerListViewScrollToTopProvider, { useRecyclerListViewScrollToTopContext, } from '@/navigation/RecyclerListViewScrollToTopContext'; import DappBrowserScreen from '@/screens/dapp-browser/DappBrowserScreen'; -import { discoverOpenSearchFnRef } from '@/screens/discover/components/DiscoverSearchContainer'; +import { discoverOpenSearchFnRef } from '@/components/Discover/DiscoverSearchContainer'; import { PointsScreen } from '@/screens/points/PointsScreen'; import WalletScreen from '@/screens/WalletScreen'; import { useTheme } from '@/theme'; @@ -39,7 +39,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { useBrowserStore } from '@/state/browser/browserStore'; import { opacityWorklet } from '@/__swaps__/utils/swaps'; import ProfileScreen from '../screens/ProfileScreen'; -import DiscoverScreen, { discoverScrollToTopFnRef } from '../screens/discover/DiscoverScreen'; +import DiscoverScreen, { discoverScrollToTopFnRef } from '@/screens/DiscoverScreen'; import { ScrollPositionContext } from './ScrollPositionContext'; import SectionListScrollToTopProvider, { useSectionListScrollToTopContext } from './SectionListScrollToTopContext'; import Routes from './routesNames'; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 5428d27e37b..9097c84e1d6 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -103,12 +103,10 @@ export const getHeightForStep = (step: string) => { case WalletBackupStepTypes.backup_manual: case WalletBackupStepTypes.restore_from_backup: return backupSheetSizes.long; - case WalletBackupStepTypes.no_provider: + case WalletBackupStepTypes.backup_prompt: return backupSheetSizes.medium; case WalletBackupStepTypes.check_identifier: return backupSheetSizes.check_identifier; - case WalletBackupStepTypes.backup_now_manually: - return backupSheetSizes.shorter; default: return backupSheetSizes.short; } @@ -248,6 +246,20 @@ export const consoleSheetConfig = { }), }; +export const networkSelectorConfig = { + options: ({ route: { params = {} } }) => ({ + ...buildCoolModalConfig({ + ...params, + backgroundColor: '#000000B2', + backgroundOpacity: 0.7, + cornerRadius: 0, + springDamping: 1, + topOffset: 0, + transitionDuration: 0.3, + }), + }), +}; + export const panelConfig = { options: ({ route: { params = {} } }) => ({ ...buildCoolModalConfig({ diff --git a/src/navigation/routesNames.ts b/src/navigation/routesNames.ts index 96cc67fb146..ff4372906e3 100644 --- a/src/navigation/routesNames.ts +++ b/src/navigation/routesNames.ts @@ -100,6 +100,7 @@ const Routes = { SETTINGS_SECTION_NOTIFICATIONS: 'NotificationsSection', SETTINGS_SECTION_PRIVACY: 'PrivacySection', DAPP_BROWSER_CONTROL_PANEL: 'DappBrowserControlPanel', + NETWORK_SELECTOR: 'NetworkSelector', CLAIM_REWARDS_PANEL: 'ClaimRewardsPanel', } as const; diff --git a/src/navigation/types.ts b/src/navigation/types.ts index cc2c8842ab5..c1877d015da 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -10,6 +10,9 @@ import { Claimable } from '@/resources/addys/claimables/types'; import { WalletconnectApprovalSheetRouteParams, WalletconnectResultType } from '@/walletConnect/types'; import { WalletConnectApprovalSheetType } from '@/helpers/walletConnectApprovalSheetTypes'; import { RainbowPosition } from '@/resources/defi/types'; +import { Address } from 'viem'; +import { SharedValue } from 'react-native-reanimated'; +import { ChainId } from '@/state/backendNetworks/types'; export type PartialNavigatorConfigOptions = Pick['Screen']>[0]>, 'options'>; @@ -31,7 +34,7 @@ export type RootStackParamList = { [Routes.CHANGE_WALLET_SHEET]: { watchOnly: boolean; currentAccountAddress: string; - onChangeWallet: (address: string) => void; + onChangeWallet: (address: Address) => void; }; [Routes.SPEED_UP_AND_CANCEL_BOTTOM_SHEET]: { accentColor?: string; @@ -104,4 +107,9 @@ export type RootStackParamList = { [Routes.POSITION_SHEET]: { position: RainbowPosition; }; + [Routes.NETWORK_SELECTOR]: { + onClose?: VoidFunction; + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + }; }; diff --git a/src/performance/tracking/index.ts b/src/performance/tracking/index.ts index 425faba867e..a0b3ebbcf3a 100644 --- a/src/performance/tracking/index.ts +++ b/src/performance/tracking/index.ts @@ -1,15 +1,16 @@ -import { IS_TESTING, SENTRY_ENVIRONMENT } from 'react-native-dotenv'; +import { SENTRY_ENVIRONMENT } from 'react-native-dotenv'; import { PerformanceMetricData } from './types/PerformanceMetricData'; import { PerformanceMetricsType } from './types/PerformanceMetrics'; import { PerformanceTagsType } from './types/PerformanceTags'; import { analytics } from '@/analytics'; +import { IS_TEST } from '@/env'; /* This will be a version for all performance tracking events. If we make breaking changes we will be able to take it into consideration when doing analytics */ const performanceTrackingVersion = 2; const shouldLogToConsole = __DEV__ || SENTRY_ENVIRONMENT === 'LocalRelease'; -const shouldReportMeasurement = IS_TESTING === 'false' && !__DEV__ && SENTRY_ENVIRONMENT !== 'LocalRelease'; +const shouldReportMeasurement = !IS_TEST && !__DEV__ && SENTRY_ENVIRONMENT !== 'LocalRelease'; const logTag = '[PERFORMANCE]: '; function logDurationIfAppropriate(metric: PerformanceMetricsType, durationInMs: number, ...additionalArgs: any[]) { @@ -18,7 +19,7 @@ function logDurationIfAppropriate(metric: PerformanceMetricsType, durationInMs: } } -const currentlyTrackedMetrics = new Map(); +export const currentlyTrackedMetrics = new Map(); interface AdditionalParams extends Record { tag?: PerformanceTagsType; diff --git a/src/performance/tracking/types/PerformanceMetrics.ts b/src/performance/tracking/types/PerformanceMetrics.ts index 3baf050eb54..3d272e1e71b 100644 --- a/src/performance/tracking/types/PerformanceMetrics.ts +++ b/src/performance/tracking/types/PerformanceMetrics.ts @@ -11,6 +11,7 @@ export const PerformanceMetrics = { initializeWalletconnect: 'Performance WalletConnect Initialize Time', quoteFetching: 'Performance Quote Fetching Time', + timeSpentOnDiscoverScreen: 'Time spent on the Discover screen', } as const; export type PerformanceMetricsType = (typeof PerformanceMetrics)[keyof typeof PerformanceMetrics]; diff --git a/src/raps/actions/ens.ts b/src/raps/actions/ens.ts index e4e315c98a4..ca380e1d0da 100644 --- a/src/raps/actions/ens.ts +++ b/src/raps/actions/ens.ts @@ -1,5 +1,4 @@ import { Signer } from '@ethersproject/abstract-signer'; -import { IS_TESTING } from 'react-native-dotenv'; import { ENSActionParameters, ENSRap, ENSRapActionType, RapENSAction, RapENSActionParameters } from '@/raps/common'; import { analytics } from '@/analytics'; import { ENSRegistrationRecords, NewTransaction, TransactionGasParamAmounts, TransactionStatus } from '@/entities'; @@ -23,6 +22,7 @@ import { } from '../registerENS'; import { Logger } from '@ethersproject/logger'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; +import { IS_TEST } from '@/env'; export interface ENSRapActionResponse { baseNonce?: number | null; @@ -327,7 +327,7 @@ const ensAction = async ( // (MULTICALL || SET_TEXT) it's going to fail if we put the account address // since the account doesn't have the ENS yet const notUseOwnerAddress = - IS_TESTING !== 'true' && + !IS_TEST && mode === REGISTRATION_MODES.CREATE && (type === ENSRegistrationTransactionType.MULTICALL || type === ENSRegistrationTransactionType.SET_TEXT); diff --git a/src/react-native-cool-modals/Portal.js b/src/react-native-cool-modals/Portal.js deleted file mode 100644 index 5d03cdadeb8..00000000000 --- a/src/react-native-cool-modals/Portal.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; -import { Platform, requireNativeComponent, StyleSheet, View } from 'react-native'; - -const NativePortalContext = createContext(); - -export function usePortal() { - return useContext(NativePortalContext); -} - -const NativePortal = Platform.OS === 'ios' ? requireNativeComponent('WindowPortal') : View; - -const Wrapper = Platform.OS === 'ios' ? ({ children }) => children : View; - -export function Portal({ children }) { - const [Component, setComponentState] = useState(null); - const [blockTouches, setBlockTouches] = useState(false); - - const hide = useCallback(() => { - setComponentState(); - setBlockTouches(false); - }, []); - - const setComponent = useCallback((value, blockTouches) => { - setComponentState(value); - setBlockTouches(blockTouches); - }, []); - - const contextValue = useMemo( - () => ({ - hide, - setComponent, - }), - [hide, setComponent] - ); - - return ( - - - {children} - - {Component} - - - - ); -} diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx new file mode 100644 index 00000000000..dd2830ee0b4 --- /dev/null +++ b/src/react-native-cool-modals/Portal.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { IS_IOS } from '@/env'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; +import { requireNativeComponent, StyleSheet, View } from 'react-native'; +import Routes from '@/navigation/routesNames'; +import { useActiveRoute } from '@/hooks/useActiveRoute'; + +const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View; +const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => children : View; + +export function Portal() { + const activeRoute = useActiveRoute(); + + const { blockTouches, Component } = walletLoadingStore(state => ({ + blockTouches: state.blockTouches, + Component: state.Component, + })); + + if (!Component || (activeRoute === Routes.PIN_AUTHENTICATION_SCREEN && !IS_IOS)) { + return null; + } + + console.log('blockTouches', blockTouches); + + return ( + + + {Component} + + + ); +} diff --git a/src/redux/gas.ts b/src/redux/gas.ts index 496bec67a67..3ef75dbe5db 100644 --- a/src/redux/gas.ts +++ b/src/redux/gas.ts @@ -1,7 +1,6 @@ import { Mutex } from 'async-mutex'; import BigNumber from 'bignumber.js'; import { isEmpty } from 'lodash'; -import { IS_TESTING } from 'react-native-dotenv'; import { AppDispatch, AppGetState } from './store'; import { analytics } from '@/analytics'; import { logger, RainbowError } from '@/logger'; @@ -37,10 +36,11 @@ import { import { ethUnits } from '@/references'; import { ethereumUtils, gasUtils } from '@/utils'; import { ChainId } from '@/state/backendNetworks/types'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { MeteorologyLegacyResponse, MeteorologyResponse } from '@/entities/gas'; import { addBuffer } from '@/helpers/utilities'; +import { IS_TEST } from '@/env'; const { CUSTOM, NORMAL, URGENT } = gasUtils; @@ -368,9 +368,9 @@ export const gasPricesStartPolling = meteorologyGasParams as MeterologyGasParams; // Set a really gas estimate to guarantee that we're gonna be over - // the basefee at the time we fork mainnet during our hardhat tests + // the basefee at the time we fork mainnet during our anvil tests let baseFee = baseFeePerGas; - if (chainId === ChainId.mainnet && IS_TESTING === 'true' && useConnectedToHardhatStore.getState().connectedToHardhat) { + if (chainId === ChainId.mainnet && IS_TEST && useConnectedToAnvilStore.getState().connectedToAnvil) { baseFee = parseGasFeeParam(gweiToWei(1000)); } diff --git a/src/redux/settings.ts b/src/redux/settings.ts index ce19a5f6131..4535437ea7c 100644 --- a/src/redux/settings.ts +++ b/src/redux/settings.ts @@ -24,6 +24,7 @@ import { getProvider } from '@/handlers/web3'; import { AppState } from '@/redux/store'; import { logger, RainbowError } from '@/logger'; import { Network, ChainId } from '@/state/backendNetworks/types'; +import { Address } from 'viem'; // -- Constants ------------------------------------------------------------- // const SETTINGS_UPDATE_SETTINGS_ADDRESS = 'settings/SETTINGS_UPDATE_SETTINGS_ADDRESS'; @@ -41,7 +42,7 @@ const SETTINGS_UPDATE_ACCOUNT_SETTINGS_SUCCESS = 'settings/SETTINGS_UPDATE_ACCOU */ interface SettingsState { appIcon: string; - accountAddress: string; + accountAddress: Address; chainId: number; language: Language; nativeCurrency: NativeCurrencyKey; @@ -205,7 +206,7 @@ export const settingsChangeAppIcon = (appIcon: string) => (dispatch: Dispatch async (dispatch: Dispatch) => { dispatch({ - payload: accountAddress, + payload: accountAddress as Address, type: SETTINGS_UPDATE_SETTINGS_ADDRESS, }); }; @@ -254,7 +255,7 @@ export const settingsChangeNativeCurrency = // -- Reducer --------------------------------------------------------------- // export const INITIAL_STATE: SettingsState = { - accountAddress: '', + accountAddress: '' as Address, appIcon: 'og', chainId: 1, language: Language.EN_US, diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index deb49a5ea9b..d17f8b4c0d8 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -3,10 +3,8 @@ import { toChecksumAddress } from 'ethereumjs-util'; import { isEmpty, keys } from 'lodash'; import { Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { backupUserDataIntoCloud, fetchUserDataFromCloud } from '../handlers/cloudBackup'; import { saveKeychainIntegrityState } from '../handlers/localstorage/globalSettings'; import { getWalletNames, saveWalletNames } from '../handlers/localstorage/walletNames'; -import WalletBackupTypes from '../helpers/walletBackupTypes'; import WalletTypes from '../helpers/walletTypes'; import { fetchENSAvatar } from '../hooks/useENSAvatar'; import { hasKey } from '../model/keychain'; @@ -30,6 +28,7 @@ import { AppGetState, AppState } from './store'; import { fetchReverseRecord } from '@/handlers/ens'; import { lightModeThemeColors } from '@/styles'; import { RainbowError, logger } from '@/logger'; +import { parseTimestampFromBackupFile } from '@/model/backup'; // -- Types ---------------------------------------- // @@ -37,11 +36,6 @@ import { RainbowError, logger } from '@/logger'; * The current state of the `wallets` reducer. */ interface WalletsState { - /** - * The current loading state of the wallet. - */ - isWalletLoading: any; - /** * The currently selected wallet. */ @@ -62,21 +56,12 @@ interface WalletsState { * An action for the `wallets` reducer. */ type WalletsAction = - | WalletsSetIsLoadingAction | WalletsSetSelectedAction | WalletsUpdateAction | WalletsUpdateNamesAction | WalletsLoadAction | WalletsAddedAccountAction; -/** - * An action that sets the wallet loading state. - */ -interface WalletsSetIsLoadingAction { - type: typeof WALLETS_SET_IS_LOADING; - payload: WalletsState['isWalletLoading']; -} - /** * An action that sets the selected wallet. */ @@ -130,90 +115,88 @@ const WALLETS_SET_SELECTED = 'wallets/SET_SELECTED'; /** * Loads wallet information from storage and updates state accordingly. */ -export const walletsLoadState = - (profilesEnabled = false) => - async (dispatch: ThunkDispatch, getState: AppGetState) => { - try { - const { accountAddress } = getState().settings; - let addressFromKeychain: string | null = accountAddress; - const allWalletsResult = await getAllWallets(); - const wallets = allWalletsResult?.wallets || {}; - if (isEmpty(wallets)) return; - const selected = await getSelectedWallet(); - // Prevent irrecoverable state (no selected wallet) - let selectedWallet = selected?.wallet; - // Check if the selected wallet is among all the wallets - if (selectedWallet && !wallets[selectedWallet.id]) { - // If not then we should clear it and default to the first one - const firstWalletKey = Object.keys(wallets)[0]; - selectedWallet = wallets[firstWalletKey]; - await setSelectedWallet(selectedWallet); - } +export const walletsLoadState = () => async (dispatch: ThunkDispatch, getState: AppGetState) => { + try { + const { accountAddress } = getState().settings; + let addressFromKeychain: string | null = accountAddress; + const allWalletsResult = await getAllWallets(); + const wallets = allWalletsResult?.wallets || {}; + if (isEmpty(wallets)) return; + const selected = await getSelectedWallet(); + // Prevent irrecoverable state (no selected wallet) + let selectedWallet = selected?.wallet; + // Check if the selected wallet is among all the wallets + if (selectedWallet && !wallets[selectedWallet.id]) { + // If not then we should clear it and default to the first one + const firstWalletKey = Object.keys(wallets)[0]; + selectedWallet = wallets[firstWalletKey]; + await setSelectedWallet(selectedWallet); + } - if (!selectedWallet) { - const address = await loadAddress(); - if (!address) { - selectedWallet = wallets[Object.keys(wallets)[0]]; - } else { - keys(wallets).some(key => { - const someWallet = wallets[key]; - const found = (someWallet.addresses || []).some(account => { - return toChecksumAddress(account.address) === toChecksumAddress(address!); - }); - if (found) { - selectedWallet = someWallet; - logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result'); - } - return found; + if (!selectedWallet) { + const address = await loadAddress(); + if (!address) { + selectedWallet = wallets[Object.keys(wallets)[0]]; + } else { + keys(wallets).some(key => { + const someWallet = wallets[key]; + const found = (someWallet.addresses || []).some(account => { + return toChecksumAddress(account.address) === toChecksumAddress(address!); }); - } + if (found) { + selectedWallet = someWallet; + logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result'); + } + return found; + }); } + } - // Recover from broken state (account address not in selected wallet) - if (!addressFromKeychain) { - addressFromKeychain = await loadAddress(); - logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress"); - } + // Recover from broken state (account address not in selected wallet) + if (!addressFromKeychain) { + addressFromKeychain = await loadAddress(); + logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress"); + } - const selectedAddress = selectedWallet?.addresses.find(a => { - return a.visible && a.address === addressFromKeychain; - }); + const selectedAddress = selectedWallet?.addresses.find(a => { + return a.visible && a.address === addressFromKeychain; + }); - // Let's select the first visible account if we don't have a selected address - if (!selectedAddress) { - const allWallets = Object.values(allWalletsResult?.wallets || {}); - let account = null; - for (const wallet of allWallets) { - for (const rainbowAccount of wallet.addresses || []) { - if (rainbowAccount.visible) { - account = rainbowAccount; - break; - } + // Let's select the first visible account if we don't have a selected address + if (!selectedAddress) { + const allWallets = Object.values(allWalletsResult?.wallets || {}); + let account = null; + for (const wallet of allWallets) { + for (const rainbowAccount of wallet.addresses || []) { + if (rainbowAccount.visible) { + account = rainbowAccount; + break; } } - if (!account) return; - await dispatch(settingsUpdateAccountAddress(account.address)); - await saveAddress(account.address); - logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one'); } + if (!account) return; + await dispatch(settingsUpdateAccountAddress(account.address)); + await saveAddress(account.address); + logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one'); + } - const walletNames = await getWalletNames(); - dispatch({ - payload: { - selected: selectedWallet, - walletNames, - wallets, - }, - type: WALLETS_LOAD, - }); + const walletNames = await getWalletNames(); + dispatch({ + payload: { + selected: selectedWallet, + walletNames, + wallets, + }, + type: WALLETS_LOAD, + }); - return wallets; - } catch (error) { - logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), { - message: (error as Error)?.message, - }); - } - }; + return wallets; + } catch (error) { + logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), { + message: (error as Error)?.message, + }); + } +}; /** * Saves new wallets to storage and updates state accordingly. @@ -252,21 +235,21 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di * @param updateUserMetadata Whether to update user metadata. */ export const setAllWalletsWithIdsAsBackedUp = - ( - walletIds: RainbowWallet['id'][], - method: RainbowWallet['backupType'], - backupFile: RainbowWallet['backupFile'] = null, - updateUserMetadata = true - ) => + (walletIds: RainbowWallet['id'][], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) => async (dispatch: ThunkDispatch, getState: AppGetState) => { const { wallets, selected } = getState().wallets; const newWallets = { ...wallets }; + let backupDate = Date.now(); + if (backupFile) { + backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now(); + } + walletIds.forEach(walletId => { newWallets[walletId] = { ...newWallets[walletId], backedUp: true, - backupDate: Date.now(), + backupDate, backupFile, backupType: method, }; @@ -276,17 +259,6 @@ export const setAllWalletsWithIdsAsBackedUp = if (selected?.id && walletIds.includes(selected?.id)) { await dispatch(walletsSetSelected(newWallets[selected.id])); } - - if (method === WalletBackupTypes.cloud && updateUserMetadata) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError('[redux/wallets]: Saving multiple wallets UserData to cloud failed.'), { - message: (e as Error)?.message, - }); - throw e; - } - } }; /** @@ -296,122 +268,28 @@ export const setAllWalletsWithIdsAsBackedUp = * @param walletId The ID of the wallet to modify. * @param method The backup type used. * @param backupFile The backup file, if present. - * @param updateUserMetadata Whether to update user metadata. */ export const setWalletBackedUp = - ( - walletId: RainbowWallet['id'], - method: RainbowWallet['backupType'], - backupFile: RainbowWallet['backupFile'] = null, - updateUserMetadata = true - ) => + (walletId: RainbowWallet['id'], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) => async (dispatch: ThunkDispatch, getState: AppGetState) => { const { wallets, selected } = getState().wallets; const newWallets = { ...wallets }; + let backupDate = Date.now(); + if (backupFile) { + backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now(); + } newWallets[walletId] = { ...newWallets[walletId], backedUp: true, - backupDate: Date.now(), + backupDate, backupFile, backupType: method, }; await dispatch(walletsUpdate(newWallets)); - if (selected!.id === walletId) { + if (selected?.id === walletId) { await dispatch(walletsSetSelected(newWallets[walletId])); } - - if (method === WalletBackupTypes.cloud && updateUserMetadata) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError('[redux/wallets]: Saving wallet UserData to cloud failed.'), { - message: (e as Error)?.message, - }); - throw e; - } - } - }; - -/** - * Grabs user data stored in the cloud and based on this data marks wallets - * as backed up or not - */ -export const updateWalletBackupStatusesBasedOnCloudUserData = - () => async (dispatch: ThunkDispatch, getState: AppGetState) => { - const { wallets, selected } = getState().wallets; - const newWallets = { ...wallets }; - - let currentUserData: { wallets: { [p: string]: RainbowWallet } } | undefined; - try { - currentUserData = await fetchUserDataFromCloud(); - } catch (error) { - logger.error(new RainbowError('[redux/wallets]: There was an error when trying to update wallet backup statuses'), { - error: (error as Error).message, - }); - return; - } - if (currentUserData === undefined) { - return; - } - - // build hashmap of address to wallet based on backup metadata - const addressToWalletLookup = new Map(); - Object.values(currentUserData.wallets).forEach(wallet => { - wallet.addresses?.forEach(account => { - addressToWalletLookup.set(account.address, wallet); - }); - }); - - /* - marking wallet as already backed up if all addresses are backed up properly - and linked to the same wallet - - we assume it's not backed up if: - * we don't have an address in the backup metadata - * we have an address in the backup metadata, but it's linked to multiple - wallet ids (should never happen, but that's a sanity check) - */ - Object.values(newWallets).forEach(wallet => { - const localWalletId = wallet.id; - - let relatedCloudWalletId: string | null = null; - for (const account of wallet.addresses || []) { - const walletDataForCurrentAddress = addressToWalletLookup.get(account.address); - if (!walletDataForCurrentAddress) { - return; - } - if (relatedCloudWalletId === null) { - relatedCloudWalletId = walletDataForCurrentAddress.id; - } else if (relatedCloudWalletId !== walletDataForCurrentAddress.id) { - logger.warn( - '[redux/wallets]: Wallet address is linked to multiple or different accounts in the cloud backup metadata. It could mean that there is an issue with the cloud backup metadata.' - ); - return; - } - } - - if (relatedCloudWalletId === null) { - return; - } - - // update only if we checked the wallet is actually backed up - const cloudBackupData = currentUserData?.wallets[relatedCloudWalletId]; - if (cloudBackupData) { - newWallets[localWalletId] = { - ...newWallets[localWalletId], - backedUp: cloudBackupData.backedUp, - backupDate: cloudBackupData.backupDate, - backupFile: cloudBackupData.backupFile, - backupType: cloudBackupData.backupType, - }; - } - }); - - await dispatch(walletsUpdate(newWallets)); - if (selected?.id) { - await dispatch(walletsSetSelected(newWallets[selected.id])); - } }; /** @@ -706,7 +584,6 @@ export const checkKeychainIntegrity = () => async (dispatch: ThunkDispatch { switch (action.type) { - case WALLETS_SET_IS_LOADING: - return { ...state, isWalletLoading: action.payload }; case WALLETS_SET_SELECTED: return { ...state, selected: action.payload }; case WALLETS_UPDATE: diff --git a/src/resources/assets/hardhatAssets.ts b/src/resources/assets/anvilAssets.ts similarity index 85% rename from src/resources/assets/hardhatAssets.ts rename to src/resources/assets/anvilAssets.ts index 409a3ee1a1c..e14963668a9 100644 --- a/src/resources/assets/hardhatAssets.ts +++ b/src/resources/assets/anvilAssets.ts @@ -13,7 +13,7 @@ import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks const MAINNET_BALANCE_CHECKER = '0x4dcf4562268dd384fe814c00fad239f06c2a0c2b'; -const fetchHardhatBalancesWithBalanceChecker = async ( +const fetchAnvilBalancesWithBalanceChecker = async ( tokens: string[], address: string ): Promise<{ [tokenAddress: string]: string } | null> => { @@ -32,19 +32,19 @@ const fetchHardhatBalancesWithBalanceChecker = async ( }); return balances; } catch (e) { - logger.error(new RainbowError(`[hardhatAssets]: Error fetching balances from balanceCheckerContract: ${e}`)); + logger.error(new RainbowError(`[anvilAssets]: Error fetching balances from balanceCheckerContract: ${e}`)); return null; } }; /** * @deprecated - to be removed once rest of the app is converted to new userAssetsStore - * Fetches the balances of the hardhat assets for the given account address and network. + * Fetches the balances of the anvil assets for the given account address and network. * @param accountAddress - The address of the account to fetch the balances for. * @param network - The network to fetch the balances for. - * @returns The balances of the hardhat assets for the given account address and network. + * @returns The balances of the anvil assets for the given account address and network. */ -export const fetchHardhatBalances = async (accountAddress: string, chainId: ChainId = ChainId.mainnet): Promise => { +export const fetchAnvilBalances = async (accountAddress: string, chainId: ChainId = ChainId.mainnet): Promise => { const chainAssetsMap = keyBy( chainAssets[`${chainId}` as keyof typeof chainAssets], ({ asset }) => `${asset.asset_code}_${asset.chainId}` @@ -53,7 +53,7 @@ export const fetchHardhatBalances = async (accountAddress: string, chainId: Chai const tokenAddresses = Object.values(chainAssetsMap).map(({ asset: { asset_code } }) => asset_code === ETH_ADDRESS ? AddressZero : asset_code.toLowerCase() ); - const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress); + const balances = await fetchAnvilBalancesWithBalanceChecker(tokenAddresses, accountAddress); if (!balances) return {}; const updatedAssets = mapValues(chainAssetsMap, chainAsset => { @@ -70,7 +70,7 @@ export const fetchHardhatBalances = async (accountAddress: string, chainId: Chai return updatedAssets; }; -export const fetchHardhatBalancesByChainId = async ( +export const fetchAnvilBalancesByChainId = async ( accountAddress: string, chainId: ChainId = ChainId.mainnet ): Promise<{ @@ -88,7 +88,7 @@ export const fetchHardhatBalancesByChainId = async ( asset.asset_code === ETH_ADDRESS ? AddressZero : asset.asset_code.toLowerCase() ); - const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress); + const balances = await fetchAnvilBalancesWithBalanceChecker(tokenAddresses, accountAddress); if (!balances) return { assets: {}, diff --git a/src/resources/metadata/sharedQueries.js b/src/resources/metadata/sharedQueries.js index 5937062f69b..4c348349eb4 100644 --- a/src/resources/metadata/sharedQueries.js +++ b/src/resources/metadata/sharedQueries.js @@ -4,6 +4,10 @@ const BACKEND_NETWORKS_QUERY = ` id name label + colors { + light + dark + } icons { badgeURL } diff --git a/src/resources/summary/summary.ts b/src/resources/summary/summary.ts index cd0ef1d8542..71c68ff92b5 100644 --- a/src/resources/summary/summary.ts +++ b/src/resources/summary/summary.ts @@ -51,6 +51,8 @@ interface AddysSummary { num_erc20s: number; last_activity: number; asset_value: number | null; + claimables_value: number | null; + positions_value: number | null; }; }; summary_by_chain: { @@ -63,6 +65,8 @@ interface AddysSummary { num_erc20s: number; last_activity: number; asset_value: number | null; + claimables_value: number | null; + positions_value: number | null; }; }; }; diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts index a749e2f72d0..26db739a248 100644 --- a/src/resources/transactions/consolidatedTransactions.ts +++ b/src/resources/transactions/consolidatedTransactions.ts @@ -135,6 +135,7 @@ export function useConsolidatedTransactions( keepPreviousData: true, getNextPageParam: lastPage => lastPage?.nextPage, refetchInterval: CONSOLIDATED_TRANSACTIONS_INTERVAL, + enabled: !!address, retry: 3, } ); diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index 169a24ab6f4..4bbb62e9308 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -2,23 +2,117 @@ import { QueryConfigWithSelect, createQueryKey } from '@/react-query'; import { useQuery } from '@tanstack/react-query'; import { arcClient } from '@/graphql'; -export type TrendingTokensVariables = Parameters['0']; -export type TrendingTokens = Awaited>; +import { TrendingCategory, TrendingSort, TrendingTimeframe } from '@/state/trendingTokens/trendingTokens'; +import { Address } from 'viem'; +import { NativeCurrencyKey } from '@/entities'; +import store from '@/redux/store'; +import { SortDirection } from '@/graphql/__generated__/arc'; +import { UniqueId } from '@/__swaps__/types/assets'; +import { ChainId } from '@/state/backendNetworks/types'; + +export type FarcasterUser = { + username: string; + pfp_url: string; +}; +export type TrendingToken = { + uniqueId: UniqueId; + chainId: ChainId; + address: string; + name: string; + symbol: string; + decimals: number; + price: number; + priceChange: { + hr: number; + day: number; + }; + marketCap: number; + volume: number; + highlightedFriends: FarcasterUser[]; + colors: { + primary: string; + }; + icon_url: string; +}; // /////////////////////////////////////////////// // Query Key -export const trendingTokensQueryKey = (props: TrendingTokensVariables) => createQueryKey('trending-tokens', props, { persisterVersion: 0 }); +export const trendingTokensQueryKey = (props: FetchTrendingTokensArgs) => createQueryKey('trending-tokens', props, { persisterVersion: 2 }); export type TrendingTokensQueryKey = ReturnType; +type FetchTrendingTokensArgs = { + chainId?: ChainId; + category: TrendingCategory; + sortBy: TrendingSort; + sortDirection: SortDirection | undefined; + timeframe: TrendingTimeframe; + walletAddress: Address | undefined; + limit?: number; + currency?: NativeCurrencyKey; +}; + +async function fetchTrendingTokens({ + queryKey: [ + { currency = store.getState().settings.nativeCurrency, category, sortBy, sortDirection, timeframe, walletAddress, chainId, limit }, + ], +}: { + queryKey: TrendingTokensQueryKey; +}) { + const response = await arcClient.trendingTokens({ + category, + sortBy, + sortDirection, + timeframe, + walletAddress, + limit, + chainId, + currency: currency.toLowerCase(), + }); + const trendingTokens: TrendingToken[] = []; + + for (const token of response.trendingTokens.data) { + const { uniqueId, address, name, symbol, chainId, decimals, trending, market, icon_url, colors } = token; + const { bought_stats } = trending.swap_data; + const highlightedFriends = (bought_stats.farcaster_users || []).reduce((friends, friend) => { + const { username, pfp_url } = friend; + if (username && pfp_url) friends.push({ username, pfp_url }); + return friends; + }, [] as FarcasterUser[]); + + trendingTokens.push({ + uniqueId, + chainId: chainId as ChainId, + address, + name, + symbol, + decimals, + price: market.price?.value || 0, + priceChange: { + hr: trending.pool_data.h1_price_change || 0, + day: trending.pool_data.h24_price_change || 0, + }, + marketCap: market.market_cap?.value || 0, + volume: market.volume_24h || 0, + highlightedFriends, + icon_url, + colors: { + primary: colors.primary, + }, + }); + } + + return trendingTokens; +} + // /////////////////////////////////////////////// // Query Hook -export function useTrendingTokens( - props: TrendingTokensVariables, - config: QueryConfigWithSelect = {} +export function useTrendingTokens( + args: FetchTrendingTokensArgs, + config: QueryConfigWithSelect = {} ) { - return useQuery(trendingTokensQueryKey(props), () => arcClient.trendingTokens(props), { + return useQuery(trendingTokensQueryKey(args), fetchTrendingTokens, { ...config, staleTime: 60_000, // 1 minute cacheTime: 60_000 * 30, // 30 minutes diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 4b010ff3f16..92712d4241d 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -5,11 +5,10 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import React, { useRef } from 'react'; import * as i18n from '@/languages'; -import { HARDWARE_WALLETS, PROFILES, useExperimentalFlag } from '@/config'; +import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; -import { InteractionManager, Linking } from 'react-native'; +import { InteractionManager } from 'react-native'; import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; -import WalletBackupTypes from '@/helpers/walletBackupTypes'; import { createWallet } from '@/model/wallet'; import WalletTypes from '@/helpers/walletTypes'; import { logger, RainbowError } from '@/logger'; @@ -19,20 +18,13 @@ import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; import { useDispatch } from 'react-redux'; -import { - backupUserDataIntoCloud, - getGoogleAccountUserData, - GoogleDriveUserData, - isCloudBackupAvailable, - login, - logoutFromGoogleDrive, -} from '@/handlers/cloudBackup'; import showWalletErrorAlert from '@/helpers/support'; import { cloudPlatform } from '@/utils/platform'; -import { IS_ANDROID } from '@/env'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { WrappedAlert as Alert } from '@/helpers/alert'; import { useInitializeWallet, useWallets } from '@/hooks'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -52,7 +44,6 @@ export const AddWalletSheet = () => { const { goBack, navigate } = useNavigation(); const hardwareWalletsEnabled = useExperimentalFlag(HARDWARE_WALLETS); - const profilesEnabled = useExperimentalFlag(PROFILES); const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); const creatingWallet = useRef(); @@ -83,6 +74,10 @@ export const AddWalletSheet = () => { }, onCloseModal: async (args: any) => { if (args) { + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); + const name = args?.name ?? ''; const color = args?.color ?? null; // Check if the selected wallet is the primary @@ -113,31 +108,18 @@ export const AddWalletSheet = () => { try { // If we found it and it's not damaged use it to create the new account if (primaryWalletKey && !wallets?.[primaryWalletKey].damaged) { - const newWallets = await dispatch(createAccountForWallet(primaryWalletKey, color, name)); + await dispatch(createAccountForWallet(primaryWalletKey, color, name)); // @ts-ignore await initializeWallet(); - // If this wallet was previously backed up to the cloud - // We need to update userData backup so it can be restored too - if (wallets?.[primaryWalletKey].backedUp && wallets[primaryWalletKey].backupType === WalletBackupTypes.cloud) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError('[AddWalletSheet]: Updating wallet userdata failed after new account creation'), { - error: e, - }); - throw e; - } - } - - // If doesn't exist, we need to create a new wallet } else { + // If doesn't exist, we need to create a new wallet await createWallet({ color, name, clearCallbackOnStartCreation: true, }); - await dispatch(walletsLoadState(profilesEnabled)); - // @ts-ignore + await dispatch(walletsLoadState()); + // @ts-expect-error - needs refactor to object params await initializeWallet(); } } catch (e) { @@ -149,6 +131,10 @@ export const AddWalletSheet = () => { showWalletErrorAlert(); }, 1000); } + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); } } creatingWallet.current = false; @@ -197,47 +183,11 @@ export const AddWalletSheet = () => { isFirstWallet, type: 'seed', }); - if (IS_ANDROID) { - try { - await logoutFromGoogleDrive(); - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return navigate(Routes.RESTORE_SHEET); - } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError('[AddWalletSheet]: Error while trying to restore from cloud'), { - error: e, - }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - navigate(Routes.RESTORE_SHEET); - } + executeFnIfCloudBackupAvailable({ + fn: () => navigate(Routes.RESTORE_SHEET), + logout: true, + }); }; const restoreFromCloudDescription = i18n.t(TRANSLATIONS.options.cloud.description_restore_sheet, { diff --git a/src/screens/discover/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx similarity index 95% rename from src/screens/discover/DiscoverScreen.tsx rename to src/screens/DiscoverScreen.tsx index 601066d6260..2a0d43df6a2 100644 --- a/src/screens/discover/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -4,7 +4,7 @@ import { useIsFocused } from '@react-navigation/native'; import { Box } from '@/design-system'; import { Page } from '@/components/layout'; import { Navbar } from '@/components/navbar/Navbar'; -import DiscoverScreenContent from './components/DiscoverScreenContent'; +import DiscoverScreenContent from '@/components/Discover/DiscoverScreenContent'; import { ButtonPressAnimation } from '@/components/animations'; import { ContactAvatar } from '@/components/contacts'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -14,7 +14,7 @@ import { useNavigation } from '@/navigation'; import { safeAreaInsetValues } from '@/utils'; import * as i18n from '@/languages'; import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'; -import DiscoverScreenProvider, { useDiscoverScreenContext } from './DiscoverScreenContext'; +import DiscoverScreenProvider, { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; export let discoverScrollToTopFnRef: () => number | null = () => null; @@ -30,18 +30,18 @@ const Content = () => { navigate(Routes.CHANGE_WALLET_SHEET); }, [navigate]); - React.useEffect(() => { - if (isSearching && !isFocused) { - Keyboard.dismiss(); - } - }, [isFocused, isSearching]); - const scrollHandler = useAnimatedScrollHandler({ onScroll: event => { scrollY.value = event.contentOffset.y; }, }); + useEffect(() => { + if (isSearching && !isFocused) { + Keyboard.dismiss(); + } + }, [isFocused, isSearching]); + useEffect(() => { discoverScrollToTopFnRef = scrollToTop; }, [scrollToTop]); diff --git a/src/screens/ENSIntroSheet.tsx b/src/screens/ENSIntroSheet.tsx index edcd087e44f..c9d82e6b9f3 100644 --- a/src/screens/ENSIntroSheet.tsx +++ b/src/screens/ENSIntroSheet.tsx @@ -1,6 +1,5 @@ import MaskedView from '@react-native-masked-view/masked-view'; import { useRoute } from '@react-navigation/native'; -import { IS_TESTING } from 'react-native-dotenv'; import lang from 'i18n-js'; import React, { useCallback, useMemo } from 'react'; import { InteractionManager, View } from 'react-native'; @@ -16,7 +15,7 @@ import { REGISTRATION_MODES } from '@/helpers/ens'; import { useAccountENSDomains, useDimensions, useENSAvatar, useENSRecords, useENSRegistration } from '@/hooks'; import Routes from '@/navigation/routesNames'; import { useTheme } from '@/theme'; -import { IS_ANDROID } from '@/env'; +import { IS_ANDROID, IS_TEST } from '@/env'; import ContextMenu from '@/components/context-menu/ContextMenu.android'; enum AnotherENSEnum { @@ -187,7 +186,7 @@ export default function ENSIntroSheet() { - {IS_TESTING !== 'true' && } + {!IS_TEST && } diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index 18441d70c3b..b40a088ba5c 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -131,8 +131,6 @@ const MINER_TIP_EXPLAINER = lang.t('explain.miner_tip.text'); const VERIFIED_EXPLAINER = lang.t('explain.verified.text'); -const SWAP_RESET_EXPLAINER = `Rainbow doesn’t have the ability to swap across networks yet, but we’re on it. For now, Rainbow will match networks between selected tokens.`; - const BACKUP_EXPLAINER = lang.t('back_up.explainers.backup', { cloudPlatformName: cloudPlatformAccountName, }); @@ -421,18 +419,6 @@ export const explainers = (params, theme) => { }), title: 'Rainbow Fee', }, - swapResetInputs: { - button: { - label: `Continue with ${chainsLabel[chainId]}`, - bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.06), - textColor: colors?.networkColors?.[chainId], - }, - emoji: '🔐', - extraHeight: -90, - text: SWAP_RESET_EXPLAINER, - title: `Switching to ${chainsLabel[chainId]}`, - logo: , - }, f2cSemiSupportedAssetPurchased: { emoji: '🎉', title: lang.t(lang.l.wallet.add_cash_v2.explain_sheet.semi_supported.title), diff --git a/src/screens/RestoreSheet.tsx b/src/screens/RestoreSheet.tsx index 4a3e324bb65..f8186c86341 100644 --- a/src/screens/RestoreSheet.tsx +++ b/src/screens/RestoreSheet.tsx @@ -1,5 +1,5 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import RestoreCloudStep from '../components/backup/RestoreCloudStep'; import ChooseBackupStep from '@/components/backup/ChooseBackupStep'; import Routes from '@/navigation/routesNames'; diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index f7f71b7173f..b09d7d66bbd 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -10,7 +10,7 @@ import { SendAssetForm, SendAssetList, SendContactList, SendHeader } from '../co import { SheetActionButton } from '../components/sheet'; import { getDefaultCheckboxes } from './SendConfirmationSheet'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { analytics } from '@/analytics'; +import { analytics, analyticsV2 } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { AssetTypes, NewTransaction, ParsedAddressAsset, TransactionStatus, UniqueAsset } from '@/entities'; import { isNativeAsset } from '@/handlers/assets'; @@ -50,7 +50,7 @@ import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { borders } from '@/styles'; import { convertAmountAndPriceToNativeDisplay, convertAmountFromNativeValue, formatInputDecimals, lessThan } from '@/helpers/utilities'; -import { deviceUtils, ethereumUtils, getUniqueTokenType, safeAreaInsetValues } from '@/utils'; +import { deviceUtils, ethereumUtils, getUniqueTokenType, isLowerCaseMatch, safeAreaInsetValues } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; import { NoResults } from '@/components/list'; @@ -62,13 +62,14 @@ import { getNextNonce } from '@/state/nonces'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { REGISTRATION_STEPS } from '@/helpers/ens'; -import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ChainId } from '@/state/backendNetworks/types'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { RootStackParamList } from '@/navigation/types'; import { ThemeContextProps, useTheme } from '@/theme'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { Contact } from '@/redux/contacts'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; +import store from '@/redux/store'; const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10); const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight; @@ -95,6 +96,17 @@ const SheetContainer = styled(Column).attrs({ }); const validateRecipient = (toAddress?: string, tokenAddress?: string) => { + const { wallets } = store.getState().wallets; + // check for if the recipient is in a damaged wallet state and prevent + if (wallets) { + const internalWallet = Object.values(wallets).find(wallet => + wallet.addresses.some(address => isLowerCaseMatch(address.address, toAddress)) + ); + if (internalWallet?.damaged) { + return false; + } + } + if (!toAddress || toAddress?.toLowerCase() === tokenAddress?.toLowerCase()) { return false; } @@ -117,6 +129,7 @@ type OnSubmitProps = { export default function SendSheet() { const { goBack, navigate } = useNavigation(); const sortedAssets = useUserAssetsStore(state => state.legacyUserAssets); + const isLoadingUserAssets = useUserAssetsStore(state => state.isLoadingUserAssets); const { gasFeeParamsBySpeed, gasLimit, @@ -882,6 +895,16 @@ export default function SendSheet() { isUniqueAsset, ]); + useEffect(() => { + if (isLoadingUserAssets || !sortedAssets) return; + const params = { screen: 'send' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length }; + for (const asset of sortedAssets) { + if (!asset.icon_url) params.no_icon += 1; + if (!asset.price?.relative_change_24h) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [isLoadingUserAssets, sortedAssets]); + const sendContactListDataKey = useMemo(() => `${ensSuggestions?.[0]?.address || '_'}`, [ensSuggestions]); const isEmptyWallet = !sortedAssets?.length && !sendableUniqueTokens?.length; diff --git a/src/screens/SettingsSheet/SettingsSheet.tsx b/src/screens/SettingsSheet/SettingsSheet.tsx index 7a68ad83d86..094cdc17456 100644 --- a/src/screens/SettingsSheet/SettingsSheet.tsx +++ b/src/screens/SettingsSheet/SettingsSheet.tsx @@ -21,7 +21,6 @@ import { useDimensions } from '@/hooks'; import { SETTINGS_BACKUP_ROUTES } from './components/Backups/routes'; import { IS_ANDROID } from '@/env'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); @@ -52,102 +51,100 @@ export function SettingsSheet() { const memoSettingsOptions = useMemo(() => settingsOptions(colors), [colors]); return ( - - - {({ backgroundColor }) => ( - + {({ backgroundColor }) => ( + + - - - {() => ( - - )} - - {Object.values(SettingsPages).map( - ({ component, getTitle, key }) => - component && ( - - ) + {() => ( + )} - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - ({ - cardStyleInterpolator: settingsCardStyleInterpolator, - title: route.params?.title, - })} - /> - - - )} - - + + {Object.values(SettingsPages).map( + ({ component, getTitle, key }) => + component && ( + + ) + )} + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + ({ + cardStyleInterpolator: settingsCardStyleInterpolator, + title: route.params?.title, + })} + /> + + + )} + ); } diff --git a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx index 1b2f4334e8e..ba33ae5da99 100644 --- a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx +++ b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx @@ -1,4 +1,3 @@ -import { useCreateBackupStateType } from '@/components/backup/useCreateBackup'; import { useTheme } from '@/theme'; import React, { useState, useMemo, useEffect } from 'react'; import * as i18n from '@/languages'; @@ -6,102 +5,103 @@ import MenuItem from '../MenuItem'; import Spinner from '@/components/Spinner'; import { FloatingEmojis } from '@/components/floating-emojis'; import { useDimensions } from '@/hooks'; +import { CloudBackupState } from '@/state/backups/backups'; export const BackUpMenuItem = ({ icon = '􀊯', - loading, + backupState, onPress, title, + disabled, }: { icon?: string; - loading: useCreateBackupStateType; + backupState: CloudBackupState; title: string; onPress: () => void; + disabled?: boolean; }) => { const { colors } = useTheme(); const { width: deviceWidth } = useDimensions(); const [emojiTrigger, setEmojiTrigger] = useState void)>(null); useEffect(() => { - if (loading === 'success') { + if (backupState === CloudBackupState.Success) { for (let i = 0; i < 20; i++) { setTimeout(() => { emojiTrigger?.(); }, 100 * i); } } - }, [emojiTrigger, loading]); + }, [emojiTrigger, backupState]); const accentColor = useMemo(() => { - switch (loading) { - case 'success': + switch (backupState) { + case CloudBackupState.Success: return colors.green; - case 'error': + case CloudBackupState.Error: return colors.red; default: return undefined; } - }, [colors, loading]); + }, [colors, backupState]); const titleText = useMemo(() => { - switch (loading) { - case 'loading': + switch (backupState) { + case CloudBackupState.InProgress: return i18n.t(i18n.l.back_up.cloud.backing_up); - case 'success': + case CloudBackupState.Success: return i18n.t(i18n.l.back_up.cloud.backup_success); - case 'error': + case CloudBackupState.Error: return i18n.t(i18n.l.back_up.cloud.backup_failed); default: return title; } - }, [loading, title]); + }, [backupState, title]); const localIcon = useMemo(() => { - switch (loading) { - case 'success': + switch (backupState) { + case CloudBackupState.Success: return '􀁢'; - case 'error': + case CloudBackupState.Error: return '􀀲'; default: return icon; } - }, [icon, loading]); + }, [icon, backupState]); return ( - <> - {/* @ts-ignore js */} - - {({ onNewEmoji }: { onNewEmoji: () => void }) => ( - - ) : ( - - ) - } - onPress={() => { - setEmojiTrigger(() => onNewEmoji); - onPress(); - }} - size={52} - titleComponent={} - /> - )} - - + + {({ onNewEmoji }) => ( + + ) : ( + + ) + } + onPress={() => { + setEmojiTrigger(() => onNewEmoji); + onPress(); + }} + size={52} + titleComponent={} + /> + )} + ); }; diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index 1842d3fae2a..90cbdddeff3 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -5,19 +5,18 @@ import { Text as RNText } from '@/components/text'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; import MenuItem from '../MenuItem'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; +import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import { format } from 'date-fns'; -import { Stack } from '@/design-system'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { IS_ANDROID } from '@/env'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { Centered } from '@/components/layout'; +import { Page } from '@/components/layout'; import Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; -import { cloudPlatform } from '@/utils/platform'; import { useTheme } from '@/theme'; +import { CloudBackupState, LoadingStates, backupsStore } from '@/state/backups/backups'; +import { titleForBackupState } from '../../utils'; +import { Box } from '@/design-system'; const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({ color: colors.blueGreyDark, @@ -32,43 +31,14 @@ const ViewCloudBackups = () => { const { navigate } = useNavigation(); const { colors } = useTheme(); - const { isFetching, backups } = useCloudBackups(); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); const onSelectCloudBackup = useCallback( - async (selectedBackup: Backup) => { + async (selectedBackup: BackupFile) => { navigate(Routes.BACKUP_SHEET, { step: walletBackupStepTypes.restore_from_backup, selectedBackup, @@ -77,80 +47,110 @@ const ViewCloudBackups = () => { [navigate] ); - return ( - - - {!isFetching && !cloudBackups.length && ( - - } /> - - )} + const renderNoBackupsState = () => ( + <> + + } /> + + + ); + + const renderMostRecentBackup = () => { + if (!mostRecentBackup) { + return null; + } + + return ( + + + } + onPress={() => onSelectCloudBackup(mostRecentBackup)} + size={52} + width="full" + titleComponent={} + /> + + + ); + }; + + const renderOlderBackups = () => ( + <> + + + {backups.files + .filter(backup => backup.name !== mostRecentBackup?.name) + .sort((a, b) => { + const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime(); + const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime(); + return timestampB - timestampA; + }) + .map(backup => ( + onSelectCloudBackup(backup)} + size={52} + width="full" + titleComponent={ + + } + /> + ))} + {backups.files.length === 1 && ( + } + /> + )} + + + + + backupsStore.getState().syncAndFetchBackups()} + titleComponent={} + /> + + + ); - {!isFetching && cloudBackups.length && ( - <> - {mostRecentBackup && ( - - } - onPress={() => onSelectCloudBackup(mostRecentBackup)} - size={52} - width="full" - titleComponent={} - /> - - )} + const renderBackupsList = () => ( + <> + {renderMostRecentBackup()} + {renderOlderBackups()} + + ); - - {cloudBackups.map( - backup => - backup.name !== mostRecentBackup?.name && ( - onSelectCloudBackup(backup)} - size={52} - width="full" - titleComponent={ - - } - /> - ) - )} + const isLoading = LoadingStates.includes(status); - {cloudBackups.length === 1 && ( - } - /> - )} - - - )} + if (isLoading) { + return ( + + {android ? : } + {titleForBackupState[status]} + + ); + } - {isFetching && ( - - {android ? : } - { - - {i18n.t(i18n.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - } - - )} - + return ( + + {status === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()} + {status === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()} ); }; diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index d085c3f62fd..9fddd15964d 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -29,31 +29,23 @@ import Routes from '@/navigation/routesNames'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { SETTINGS_BACKUP_ROUTES } from './routes'; import { analyticsV2 } from '@/analytics'; -import { InteractionManager, Linking } from 'react-native'; +import { InteractionManager } from 'react-native'; import { useDispatch } from 'react-redux'; -import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; -import { - GoogleDriveUserData, - backupUserDataIntoCloud, - getGoogleAccountUserData, - isCloudBackupAvailable, - login, -} from '@/handlers/cloudBackup'; +import { createAccountForWallet } from '@/redux/wallets'; import { logger, RainbowError } from '@/logger'; -import { RainbowAccount, createWallet } from '@/model/wallet'; -import { PROFILES, useExperimentalFlag } from '@/config'; +import { RainbowAccount } from '@/model/wallet'; import showWalletErrorAlert from '@/helpers/support'; -import { IS_ANDROID, IS_IOS } from '@/env'; +import { IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; -import { checkWalletsForBackupStatus } from '../../utils'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore } from '@/state/backups/backups'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { isWalletBackedUpForCurrentAccount } from '../../utils'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -126,107 +118,38 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }: const ViewWalletBackup = () => { const { params } = useRoute>(); - const { backups } = useCloudBackups(); + const createBackup = useCreateBackup(); + const { status, backupProvider, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backupProvider: state.backupProvider, + mostRecentBackup: state.mostRecentBackup, + })); const { walletId, title: incomingTitle } = params; const creatingWallet = useRef(); const { isDamaged, wallets } = useWallets(); const wallet = wallets?.[walletId]; const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); - const profilesEnabled = useExperimentalFlag(PROFILES); - - const walletTypeCount: WalletCountPerType = { - phrase: 0, - privateKey: 0, - }; - - const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); - - const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const isSecretPhrase = WalletTypes.mnemonic === wallet?.type; - const title = wallet?.type === WalletTypes.privateKey ? wallet?.addresses[0].label : incomingTitle; + const isBackedUp = isWalletBackedUpForCurrentAccount({ + backupType: wallet?.backupType, + backedUp: wallet?.backedUp, + backupFile: wallet?.backupFile, + }); const { navigate } = useNavigation(); const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); - const { onSubmit, loading } = useCreateBackup({ - walletId, - }); const backupWalletsToCloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return onSubmit({}); - } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError(`[ViewWalletBackup]: Logging into Google Drive failed`), { error: e }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } - - onSubmit({}); - }, [onSubmit]); + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId, + }), + }); + }, [createBackup, walletId]); const onNavigateToSecretWarning = useCallback(() => { navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, { @@ -265,36 +188,17 @@ const ViewWalletBackup = () => { }, onCloseModal: async (args: any) => { if (args) { + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); + const name = args?.name ?? ''; const color = args?.color ?? null; // Check if the selected wallet is the primary try { // If we found it and it's not damaged use it to create the new account if (wallet && !wallet.damaged) { - const newWallets = await dispatch(createAccountForWallet(wallet.id, color, name)); - // @ts-expect-error - no params - await initializeWallet(); - // If this wallet was previously backed up to the cloud - // We need to update userData backup so it can be restored too - if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { - try { - await backupUserDataIntoCloud({ wallets: newWallets }); - } catch (e) { - logger.error(new RainbowError(`[ViewWalletBackup]: Updating wallet userdata failed after new account creation`), { - error: e, - }); - throw e; - } - } - - // If doesn't exist, we need to create a new wallet - } else { - await createWallet({ - color, - name, - clearCallbackOnStartCreation: true, - }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(createAccountForWallet(wallet.id, color, name)); // @ts-expect-error - no params await initializeWallet(); } @@ -307,6 +211,10 @@ const ViewWalletBackup = () => { showWalletErrorAlert(); }, 1000); } + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); } } creatingWallet.current = false; @@ -324,7 +232,7 @@ const ViewWalletBackup = () => { error: e, }); } - }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, profilesEnabled, wallet]); + }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, wallet]); const handleCopyAddress = React.useCallback( (address: string) => { @@ -386,7 +294,7 @@ const ViewWalletBackup = () => { return ( - {!wallet?.backedUp && ( + {!isBackedUp && ( <> { /> - {backupProvider === walletBackupTypes.cloud && ( + { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - loading={loading} + backupState={status} onPress={backupWalletsToCloud} /> - - )} - - {backupProvider !== walletBackupTypes.cloud && ( - } @@ -456,20 +355,12 @@ const ViewWalletBackup = () => { titleComponent={} testID={'back-up-manually'} /> - - )} + )} - {wallet?.backedUp && ( + {isBackedUp && ( <> { paddingBottom={{ custom: 24 }} iconComponent={ } titleComponent={ { { )} - - } - onPress={onNavigateToSecretWarning} - size={52} - titleComponent={ - + + - } - /> - + + + )} + + + + } + onPress={onNavigateToSecretWarning} + size={52} + titleComponent={ + + } + /> + + {wallet?.addresses .filter(a => a.visible) - .map((account: RainbowAccount) => ( - - } - labelComponent={ - account.label.endsWith('.eth') || account.label !== '' ? ( - - ) : null - } - titleComponent={ - - } - rightComponent={} - /> - - ))} + .map((account: RainbowAccount) => { + const isNamedOrEns = account.label.endsWith('.eth') || removeFirstEmojiFromString(account.label) !== ''; + const label = isNamedOrEns ? abbreviations.address(account.address, 3, 5) : undefined; + const title = isNamedOrEns + ? abbreviations.abbreviateEnsForDisplay(removeFirstEmojiFromString(account.label), 20) ?? '' + : abbreviations.address(account.address, 3, 5) ?? ''; + + return ( + + } + labelComponent={label ? : null} + titleComponent={} + rightComponent={} + /> + + ); + })} {wallet?.type !== WalletTypes.privateKey && ( - - } - onPress={onCreateNewWallet} - size={52} - titleComponent={} - /> - + + + } + onPress={onCreateNewWallet} + size={52} + titleComponent={} + /> + + )} diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 9823fd2555f..f1a38d7ec24 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -1,5 +1,4 @@ -/* eslint-disable no-nested-ternary */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { cloudPlatform } from '@/utils/platform'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; @@ -12,11 +11,11 @@ import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { useENSAvatar, useInitializeWallet, useManageCloudBackups, useWallets } from '@/hooks'; import { useNavigation } from '@/navigation'; -import { abbreviations } from '@/utils'; +import { abbreviations, deviceUtils } from '@/utils'; import { addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; -import MenuHeader from '../MenuHeader'; -import { checkWalletsForBackupStatus } from '../../utils'; +import MenuHeader, { StatusType } from '../MenuHeader'; +import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils'; import { Inline, Text, Box, Stack } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; import { useTheme } from '@/theme'; @@ -25,26 +24,40 @@ import { backupsCard } from '@/components/cards/utils/constants'; import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets'; import { SETTINGS_BACKUP_ROUTES } from './routes'; import { RainbowAccount, createWallet } from '@/model/wallet'; -import { PROFILES, useExperimentalFlag } from '@/config'; import { useDispatch } from 'react-redux'; import { walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { useCloudBackups } from '@/components/backup/CloudBackupProvider'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { Linking } from 'react-native'; -import { noop } from 'lodash'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; +import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; +import { FlatList, ScrollView } from 'react-native'; type WalletPillProps = { account: RainbowAccount; }; +// constants for the account section +const menuContainerPadding = 19.5; // 19px is the padding on the left and right of the container but we need 1px more to account for the shadows on each container +const accountsContainerWidth = deviceUtils.dimensions.width - menuContainerPadding * 4; +const spaceBetweenAccounts = 4; +const accountsItemWidth = accountsContainerWidth / 3; +const basePadding = 16; +const rowHeight = 36; + +const getAccountSectionHeight = (numAccounts: number) => { + const rows = Math.ceil(Math.max(1, numAccounts) / 3); + const paddingBetween = (rows - 1) * 4; + + return basePadding + rows * rowHeight - paddingBetween; +}; + const WalletPill = ({ account }: WalletPillProps) => { const label = useMemo(() => removeFirstEmojiFromString(account.label), [account.label]); @@ -58,7 +71,7 @@ const WalletPill = ({ account }: WalletPillProps) => { key={account.address} flexDirection="row" alignItems="center" - backgroundColor={colors.alpha(colors.grey, 0.4)} + backgroundColor={colors.alpha(colors.grey, 0.24)} borderRadius={23} shadowColor={isDarkMode ? colors.shadow : colors.alpha(colors.blueGreyDark, 0.1)} elevation={12} @@ -67,6 +80,7 @@ const WalletPill = ({ account }: WalletPillProps) => { paddingLeft={{ custom: 4 }} paddingRight={{ custom: 8 }} padding={{ custom: 4 }} + width={{ custom: accountsItemWidth }} > {ENSAvatar?.imageUrl ? ( @@ -82,27 +96,22 @@ const WalletPill = ({ account }: WalletPillProps) => { ); }; -const getAccountSectionHeight = (numAccounts: number) => { - const basePadding = 16; - const rowHeight = 36; - const rows = Math.ceil(Math.max(1, numAccounts) / 3); - const paddingBetween = (rows - 1) * 4; - - return basePadding + rows * rowHeight - paddingBetween; -}; - export const WalletsAndBackup = () => { const { navigate } = useNavigation(); const { wallets } = useWallets(); - const profilesEnabled = useExperimentalFlag(PROFILES); - const { backups } = useCloudBackups(); const dispatch = useDispatch(); - const initializeWallet = useInitializeWallet(); + const scrollviewRef = useRef(null); - const { onSubmit, loading } = useCreateBackup({ - walletId: undefined, // NOTE: This is not used when backing up All wallets - }); + const createBackup = useCreateBackup(); + const { status, backupProvider, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backupProvider: state.backupProvider, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); + + const initializeWallet = useInitializeWallet(); const { manageCloudBackups } = useManageCloudBackups(); @@ -111,52 +120,15 @@ export const WalletsAndBackup = () => { privateKey: 0, }; - const { allBackedUp, backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); - const { visibleWallets, lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); - - const cloudBackups = backups.files - .filter(backup => { - if (IS_ANDROID) { - return !backup.name.match(/UserData/i); - } - - return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i); - }) - .sort((a, b) => { - return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); - }); - - const mostRecentBackup = cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } - - if (!prev) { - return current; - } - - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - return current; - } - - return prev; - }, - undefined as Backup | undefined - ); + const visibleWallets = useVisibleWallets({ wallets, walletTypeCount }); const sortedWallets = useMemo(() => { - const notBackedUpSecretPhraseWallets = visibleWallets.filter( - wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic - ); - const notBackedUpPrivateKeyWallets = visibleWallets.filter( - wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey - ); - const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic); - const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey); + const notBackedUpSecretPhraseWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.mnemonic); + const notBackedUpPrivateKeyWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.privateKey); + const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.mnemonic); + const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.privateKey); return [ ...notBackedUpSecretPhraseWallets, @@ -166,48 +138,28 @@ export const WalletsAndBackup = () => { ]; }, [visibleWallets]); - const backupAllNonBackedUpWalletsTocloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return onSubmit({ type: BackupTypes.All }); + const backupAllNonBackedUpWalletsTocloud = useCallback(() => { + executeFnIfCloudBackupAvailable({ + fn: () => createBackup({}), + }); + }, [createBackup]); + + const enableCloudBackups = useCallback(() => { + executeFnIfCloudBackupAvailable({ + fn: async () => { + // NOTE: For Android we could be coming from a not-logged-in state, so we + // need to check if we have any wallets to back up first. + if (IS_ANDROID) { + const currentBackups = backupsStore.getState().backups; + if (checkLocalWalletsForBackupStatus(wallets, currentBackups).allBackedUp) { + return; } - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - }); - } catch (e) { - Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); - logger.error(new RainbowError(`[WalletsAndBackup]: Logging into Google Drive failed`), { - error: e, - }); - } - } else { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - Alert.alert( - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label), - i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description), - [ - { - onPress: () => { - Linking.openURL('https://support.apple.com/en-us/HT204025'); - }, - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), - }, - { - style: 'cancel', - text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } - - onSubmit({ type: BackupTypes.All }); - }, [onSubmit]); + } + return createBackup({}); + }, + logout: true, + }); + }, [createBackup, wallets]); const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { @@ -223,13 +175,17 @@ export const WalletsAndBackup = () => { onCloseModal: async ({ name }: { name: string }) => { const nameValue = name.trim() !== '' ? name.trim() : ''; try { + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); + await createWallet({ color: null, name: nameValue, clearCallbackOnStartCreation: true, }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); // @ts-expect-error - no params await initializeWallet(); @@ -237,10 +193,15 @@ export const WalletsAndBackup = () => { logger.error(new RainbowError(`[WalletsAndBackup]: Failed to create new secret phrase`), { error: err, }); + } finally { + walletLoadingStore.setState({ + loadingState: null, + }); + scrollviewRef.current?.scrollTo({ y: 0, animated: true }); } }, }); - }, [dispatch, initializeWallet, navigate, profilesEnabled, walletTypeCount.phrase]); + }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]); const onPressLearnMoreAboutCloudBackups = useCallback(() => { navigate(Routes.LEARN_WEB_VIEW_SCREEN, { @@ -263,6 +224,66 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); + const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => { + if (!backupProvider) { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + }; + } + + if (!allBackedUp) { + return { + status: 'out-of-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), + }; + } + + return { + status: 'up-to-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date), + }; + } + + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), + }; + } + + if (!allBackedUp) { + return { + status: 'out-of-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), + }; + } + + return { + status: 'up-to-date', + text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date), + }; + }, [backupProvider, status, allBackedUp]); + + const isCloudBackupDisabled = useMemo(() => { + return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable; + }, [status]); + const renderView = useCallback(() => { switch (backupProvider) { default: @@ -275,7 +296,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={} + statusComponent={} labelComponent={ { /> - - - + + + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( - + { } > - {!backedUp && ( + {!isBackedUp && ( @@ -330,37 +356,43 @@ export const WalletsAndBackup = () => { {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> ); })} + { titleComponent={} /> - - - } - onPress={onViewCloudBackups} - size={52} - titleComponent={ - - } - /> - } - onPress={manageCloudBackups} - size={52} - titleComponent={ - - } - /> - ); @@ -416,12 +417,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={ - - } + statusComponent={} labelComponent={ allBackedUp ? ( { /> - + - - + } + > + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( - + { } > - {!backedUp && } + {!isBackedUp && ( + + )} {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> @@ -581,12 +588,13 @@ export const WalletsAndBackup = () => { case WalletBackupTypes.manual: { return ( - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, backupType, backupFile, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupType, backupFile }); return ( - + { } > - {!backedUp && } + {!isBackedUp && ( + + )} {imported && } 1 + addresses.length > 1 ? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, { - numAccounts, + numAccounts: addresses.length, }) : i18n.t(i18n.l.wallet.back_ups.wallet_count, { - numAccounts, + numAccounts: addresses.length, }) } /> } leftComponent={} - onPress={() => onNavigateToWalletView(key, name)} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + scrollEnabled={false} + /> } /> @@ -645,26 +664,29 @@ export const WalletsAndBackup = () => { /> - - {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, { - cloudPlatform, - })} + + + {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, { + cloudPlatform, + })} - - {' '} - {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)} + + {' '} + {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)} + - - } - > - - + } + > + + + ); @@ -672,21 +694,29 @@ export const WalletsAndBackup = () => { } }, [ backupProvider, - loading, - backupAllNonBackedUpWalletsTocloud, + iconStatusType, + text, + status, + isCloudBackupDisabled, + enableCloudBackups, sortedWallets, onCreateNewSecretPhrase, - onViewCloudBackups, - manageCloudBackups, navigate, onNavigateToWalletView, allBackedUp, mostRecentBackup, - lastBackupDate, + backupAllNonBackedUpWalletsTocloud, + onViewCloudBackups, + manageCloudBackups, onPressLearnMoreAboutCloudBackups, ]); - return {renderView()}; + return ( + + + {renderView()} + + ); }; export default WalletsAndBackup; diff --git a/src/screens/SettingsSheet/components/DevSection.tsx b/src/screens/SettingsSheet/components/DevSection.tsx index 3a10b3d1460..5df023d3d55 100644 --- a/src/screens/SettingsSheet/components/DevSection.tsx +++ b/src/screens/SettingsSheet/components/DevSection.tsx @@ -33,7 +33,7 @@ import { isAuthenticated } from '@/utils/authentication'; import { getFCMToken } from '@/notifications/tokens'; import { nonceStore } from '@/state/nonces'; import { pendingTransactionsStore } from '@/state/pendingTransactions'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useConnectedToAnvilStore } from '@/state/connectedToAnvil'; import { addDefaultNotificationGroupSettings } from '@/notifications/settings/initialization'; import { unsubscribeAllNotifications } from '@/notifications/settings/settings'; import FastImage from 'react-native-fast-image'; @@ -42,7 +42,7 @@ const DevSection = () => { const { navigate } = useNavigation(); const { config, setConfig } = useContext(RainbowContext) as any; const { wallets } = useWallets(); - const setConnectedToHardhat = useConnectedToHardhatStore.getState().setConnectedToHardhat; + const setConnectedToAnvil = useConnectedToAnvilStore.getState().setConnectedToAnvil; const dispatch = useDispatch(); const [loadingStates, setLoadingStates] = useState({ @@ -62,17 +62,17 @@ const DevSection = () => { [config, setConfig] ); - const connectToHardhat = useCallback(async () => { + const connectToAnvil = useCallback(async () => { try { - const connectToHardhat = useConnectedToHardhatStore.getState().connectedToHardhat; - setConnectedToHardhat(!connectToHardhat); - logger.debug(`[DevSection] connected to hardhat`); + const connectToAnvil = useConnectedToAnvilStore.getState().connectedToAnvil; + setConnectedToAnvil(!connectToAnvil); + logger.debug(`[DevSection] connected to anvil`); } catch (e) { - setConnectedToHardhat(false); - logger.error(new RainbowError(`[DevSection] error connecting to hardhat: ${e}`)); + setConnectedToAnvil(false); + logger.error(new RainbowError(`[DevSection] error connecting to anvil: ${e}`)); } navigate(Routes.PROFILE_SCREEN); - }, [dispatch, navigate, setConnectedToHardhat]); + }, [dispatch, navigate, setConnectedToAnvil]); const checkAlert = useCallback(async () => { try { @@ -312,15 +312,15 @@ const DevSection = () => { /> } - onPress={connectToHardhat} + onPress={connectToAnvil} size={52} - testID="hardhat-section" + testID="anvil-section" titleComponent={ } diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx index b415e1d4d30..10e28e6ebc6 100644 --- a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx +++ b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx @@ -3,14 +3,12 @@ import { getGoogleAccountUserData, GoogleDriveUserData, logoutFromGoogleDrive } import ImageAvatar from '@/components/contacts/ImageAvatar'; import { showActionSheetWithOptions } from '@/utils'; import * as i18n from '@/languages'; -import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets'; -import { useDispatch } from 'react-redux'; import Menu from './Menu'; import MenuItem from './MenuItem'; import { logger, RainbowError } from '@/logger'; +import { backupsStore } from '@/state/backups/backups'; export const GoogleAccountSection: React.FC = () => { - const dispatch = useDispatch(); const [accountDetails, setAccountDetails] = useState(undefined); const [loading, setLoading] = useState(true); @@ -29,12 +27,6 @@ export const GoogleAccountSection: React.FC = () => { }); }, []); - const removeBackupStateFromAllWallets = async () => { - setLoading(true); - await dispatch(clearAllWalletsBackupStatus()); - setLoading(false); - }; - const onGoogleAccountPress = () => { showActionSheetWithOptions( { @@ -49,11 +41,10 @@ export const GoogleAccountSection: React.FC = () => { if (buttonIndex === 0) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets().then(() => loginToGoogleDrive()); + loginToGoogleDrive(); } else if (buttonIndex === 1) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets(); } } ); @@ -61,10 +52,10 @@ export const GoogleAccountSection: React.FC = () => { const loginToGoogleDrive = async () => { setLoading(true); - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); try { const accountDetails = await getGoogleAccountUserData(); setAccountDetails(accountDetails ?? undefined); + backupsStore.getState().syncAndFetchBackups(); } catch (error) { logger.error(new RainbowError(`[GoogleAccountSection]: Logging into Google Drive failed`), { error: (error as Error).message, diff --git a/src/screens/SettingsSheet/components/MenuContainer.tsx b/src/screens/SettingsSheet/components/MenuContainer.tsx index 500960c55a5..cabb0157fb7 100644 --- a/src/screens/SettingsSheet/components/MenuContainer.tsx +++ b/src/screens/SettingsSheet/components/MenuContainer.tsx @@ -3,13 +3,14 @@ import { ScrollView } from 'react-native'; import { Box, Inset, Space, Stack } from '@/design-system'; interface MenuContainerProps { + scrollviewRef?: React.RefObject; children: React.ReactNode; Footer?: React.ReactNode; testID?: string; space?: Space; } -const MenuContainer = ({ children, testID, Footer, space = '36px' }: MenuContainerProps) => { +const MenuContainer = ({ scrollviewRef, children, testID, Footer, space = '36px' }: MenuContainerProps) => { return ( // ios scroll fix ( ); -type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date'; +export type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date' | 'out-of-sync'; interface StatusIconProps { status: StatusType; @@ -87,6 +87,10 @@ const StatusIcon = ({ status, text }: StatusIconProps) => { backgroundColor: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.1) : colors.alpha(colors.blueGreyDark, 0.1), color: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.6) : colors.alpha(colors.blueGreyDark, 0.8), }, + 'out-of-sync': { + backgroundColor: colors.alpha(colors.yellow, 0.2), + color: colors.yellow, + }, 'out-of-date': { backgroundColor: colors.alpha(colors.brightRed, 0.2), color: colors.brightRed, diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 9fae44a89eb..095b88cbb85 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -28,9 +28,11 @@ import { showActionSheetWithOptions } from '@/utils'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { SettingsExternalURLs } from '../constants'; -import { capitalizeFirstLetter, checkWalletsForBackupStatus } from '../utils'; +import { checkLocalWalletsForBackupStatus } from '../utils'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { Box } from '@/design-system'; +import { capitalize } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; interface SettingsSectionProps { onCloseModal: () => void; @@ -59,10 +61,14 @@ const SettingsSection = ({ const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS); const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS); + const { backupProvider, backups } = backupsStore(state => ({ + backupProvider: state.backupProvider, + backups: state.backups, + })); + const { isDarkMode, setTheme, colorScheme } = useTheme(); const onSendFeedback = useSendFeedback(); - const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const onPressReview = useCallback(async () => { if (ios) { @@ -85,7 +91,7 @@ const SettingsSection = ({ const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []); - const { allBackedUp, canBeBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); const themeMenuConfig = useMemo(() => { return { @@ -170,21 +176,19 @@ const SettingsSection = ({ return ( }> - {canBeBackedUp && ( - } - onPress={onPressBackup} - rightComponent={ - - - - } - size={60} - testID={'backup-section'} - titleComponent={} - /> - )} + } + onPress={onPressBackup} + rightComponent={ + + + + } + size={60} + testID={'backup-section'} + titleComponent={} + /> {isNotificationsEnabled && ( } - rightComponent={{colorScheme ? capitalizeFirstLetter(colorScheme) : ''}} + rightComponent={{colorScheme ? capitalize(colorScheme) : ''}} size={60} testID={`theme-section-${isDarkMode ? 'dark' : 'light'}`} titleComponent={} diff --git a/src/screens/SettingsSheet/useVisibleWallets.ts b/src/screens/SettingsSheet/useVisibleWallets.ts index 64e73aa0929..c677dd738db 100644 --- a/src/screens/SettingsSheet/useVisibleWallets.ts +++ b/src/screens/SettingsSheet/useVisibleWallets.ts @@ -1,9 +1,7 @@ -import { useState } from 'react'; import * as i18n from '@/languages'; import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes'; -import { DEFAULT_WALLET_NAME, RainbowAccount, RainbowWallet } from '@/model/wallet'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { RainbowWallet } from '@/model/wallet'; type WalletByKey = { [key: string]: RainbowWallet; @@ -19,20 +17,6 @@ export type WalletCountPerType = { privateKey: number; }; -export type AmendedRainbowWallet = RainbowWallet & { - name: string; - isBackedUp: boolean | undefined; - accounts: RainbowAccount[]; - key: string; - label: string; - numAccounts: number; -}; - -type UseVisibleWalletReturnType = { - visibleWallets: AmendedRainbowWallet[]; - lastBackupDate: number | undefined; -}; - export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: WalletCountPerType) => { switch (type) { case EthereumWalletType.mnemonic: @@ -48,51 +32,26 @@ export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: } }; -const isWalletGroupNamed = (wallet: RainbowWallet) => wallet.name && wallet.name.trim() !== '' && wallet.name !== DEFAULT_WALLET_NAME; - -export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): UseVisibleWalletReturnType => { - const [lastBackupDate, setLastBackupDate] = useState(undefined); - +export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): RainbowWallet[] => { if (!wallets) { - return { - visibleWallets: [], - lastBackupDate, - }; + return []; } - return { - visibleWallets: Object.keys(wallets) - .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) - .map(key => { - const wallet = wallets[key]; - const visibleAccounts = (wallet.addresses || []).filter(a => a.visible); - const totalAccounts = visibleAccounts.length; - - if ( - wallet.backedUp && - wallet.backupDate && - wallet.backupType === walletBackupTypes.cloud && - (!lastBackupDate || Number(wallet.backupDate) > lastBackupDate) - ) { - setLastBackupDate(Number(wallet.backupDate)); - } - - if (wallet.type === WalletTypes.mnemonic) { - walletTypeCount.phrase += 1; - } else if (wallet.type === WalletTypes.privateKey) { - walletTypeCount.privateKey += 1; - } - - return { - ...wallet, - name: isWalletGroupNamed(wallet) ? wallet.name : getTitleForWalletType(wallet.type, walletTypeCount), - isBackedUp: wallet.backedUp, - accounts: visibleAccounts, - key, - label: wallet.name, - numAccounts: totalAccounts, - }; - }), - lastBackupDate, - }; + return Object.keys(wallets) + .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) + .map(key => { + const wallet = wallets[key]; + + if (wallet.type === WalletTypes.mnemonic) { + walletTypeCount.phrase += 1; + } else if (wallet.type === WalletTypes.privateKey) { + walletTypeCount.privateKey += 1; + } + + return { + ...wallet, + name: getTitleForWalletType(wallet.type, walletTypeCount), + addresses: Object.values(wallet.addresses).filter(address => address.visible), + }; + }); }; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 08fa3e03e22..cda0da5ef72 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -1,118 +1,121 @@ import WalletBackupTypes from '@/helpers/walletBackupTypes'; import WalletTypes from '@/helpers/walletTypes'; +import { useWallets } from '@/hooks'; +import { isEmpty } from 'lodash'; +import { BackupFile, CloudBackups, parseTimestampFromFilename } from '@/model/backup'; +import * as i18n from '@/languages'; +import { cloudPlatform } from '@/utils/platform'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { RainbowWallet } from '@/model/wallet'; -import { Navigation } from '@/navigation'; -import { BackupUserData, getLocalBackupPassword } from '@/model/backup'; -import Routes from '@/navigation/routesNames'; -import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; - -type WalletsByKey = { - [key: string]: RainbowWallet; -}; +import { IS_ANDROID, IS_IOS } from '@/env'; +import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; type WalletBackupStatus = { allBackedUp: boolean; areBackedUp: boolean; canBeBackedUp: boolean; - backupProvider: string | undefined; }; -export const capitalizeFirstLetter = (str: string) => { - return str.charAt(0).toUpperCase() + str.slice(1); +export const hasManuallyBackedUpWallet = (wallets: ReturnType['wallets']) => { + if (!wallets) return false; + return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual); }; -export const checkUserDataForBackupProvider = (userData?: BackupUserData): { backupProvider: string | undefined } => { - let backupProvider: string | undefined = undefined; - - if (!userData?.wallets) return { backupProvider }; - - Object.values(userData.wallets).forEach(wallet => { - if (wallet.backedUp && wallet.type !== WalletTypes.readOnly) { - if (wallet.backupType === WalletBackupTypes.cloud) { - backupProvider = WalletBackupTypes.cloud; - } else if (backupProvider !== WalletBackupTypes.cloud && wallet.backupType === WalletBackupTypes.manual) { - backupProvider = WalletBackupTypes.manual; - } - } - }); - - return { backupProvider }; -}; - -export const checkWalletsForBackupStatus = (wallets: WalletsByKey | null): WalletBackupStatus => { - if (!wallets) +export const checkLocalWalletsForBackupStatus = ( + wallets: ReturnType['wallets'], + backups: CloudBackups +): WalletBackupStatus => { + if (!wallets || isEmpty(wallets)) { return { allBackedUp: false, areBackedUp: false, canBeBackedUp: false, - backupProvider: undefined, }; + } + + // FOR ANDROID, we need to check if the current google account also has the backup file + if (IS_ANDROID) { + return Object.values(wallets).reduce( + (acc, wallet) => { + const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; + const hasBackupFile = backups.files.some( + file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(wallet.backupFile ?? '') + ); + + return { + allBackedUp: acc.allBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), + areBackedUp: acc.areBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, + }; + }, + { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } + ); + } + + return Object.values(wallets).reduce( + (acc, wallet) => { + const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; + + return { + allBackedUp: acc.allBackedUp && (wallet.backedUp || !isBackupEligible), + areBackedUp: acc.areBackedUp && (wallet.backedUp || !isBackupEligible || wallet.imported), + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, + }; + }, + { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } + ); +}; - let backupProvider: string | undefined = undefined; - let areBackedUp = true; - let canBeBackedUp = false; - let allBackedUp = true; - - Object.keys(wallets).forEach(key => { - if (wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) { - if (wallets[key].backupType === WalletBackupTypes.cloud) { - backupProvider = WalletBackupTypes.cloud; - } else if (backupProvider !== WalletBackupTypes.cloud && wallets[key].backupType === WalletBackupTypes.manual) { - backupProvider = WalletBackupTypes.manual; - } - } +export const getMostRecentCloudBackup = (backups: BackupFile[]) => { + const cloudBackups = backups.sort((a, b) => { + return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); + }); - if (!wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) { - allBackedUp = false; + return cloudBackups.reduce((prev, current) => { + if (!current) { + return prev; } - if ( - !wallets[key].backedUp && - wallets[key].type !== WalletTypes.readOnly && - wallets[key].type !== WalletTypes.bluetooth && - !wallets[key].imported - ) { - areBackedUp = false; + if (!prev) { + return current; } - if (wallets[key].type !== WalletTypes.bluetooth && wallets[key].type !== WalletTypes.readOnly) { - canBeBackedUp = true; + const prevTimestamp = new Date(prev.lastModified).getTime(); + const currentTimestamp = new Date(current.lastModified).getTime(); + if (currentTimestamp > prevTimestamp) { + return current; } - }); - return { - allBackedUp, - areBackedUp, - canBeBackedUp, - backupProvider, - }; + + return prev; + }, cloudBackups[0]); }; -export const getWalletsThatNeedBackedUp = (wallets: { [key: string]: RainbowWallet } | null): RainbowWallet[] => { - if (!wallets) return []; - const walletsToBackup: RainbowWallet[] = []; - Object.keys(wallets).forEach(key => { - if ( - !wallets[key].backedUp && - wallets[key].type !== WalletTypes.readOnly && - wallets[key].type !== WalletTypes.bluetooth && - !wallets[key].imported - ) { - walletsToBackup.push(wallets[key]); - } - }); - return walletsToBackup; +export const titleForBackupState: Partial> = { + [CloudBackupState.Initializing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, { + cloudPlatformName: cloudPlatform, + }), + [CloudBackupState.Syncing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, { + cloudPlatformName: cloudPlatform, + }), + [CloudBackupState.Fetching]: i18n.t(i18n.l.back_up.cloud.fetching_backups, { + cloudPlatformName: cloudPlatform, + }), }; -export const fetchBackupPasswordAndNavigate = async () => { - const password = await getLocalBackupPassword(); +export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { + if (IS_IOS || backupType === WalletBackupTypes.manual) { + return backedUp; + } - return new Promise(resolve => { - return Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_cloud, - password, - onSuccess: async (password: string) => { - resolve(password); - }, - }); - }); + if (!backupType || !backupFile) { + return false; + } + + // NOTE: For Android, we also need to check if the current google account has the matching backup file + if (!backupFile) { + return false; + } + + const backupFiles = backupsStore.getState().backups; + return backupFiles.files.some(file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(backupFile)); }; diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index acfcbcfb176..95cf97c05db 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -25,11 +25,11 @@ import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync'; import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync'; import { UserAssetsSync } from '@/state/sync/UserAssetsSync'; import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener'; -import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents'; import { RouteProp, useRoute } from '@react-navigation/native'; import { RootStackParamList } from '@/navigation/types'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/Routes'; +import { BackendNetworks } from '@/components/BackendNetworks'; import walletTypes from '@/helpers/walletTypes'; enum WalletLoadingStates { @@ -45,14 +45,18 @@ function WalletScreen() { const walletState = useRef(WalletLoadingStates.IDLE); const initializeWallet = useInitializeWallet(); const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings(); - const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData(); const insets = useSafeAreaInsets(); const { wallets } = useWallets(); const walletReady = useSelector(({ appState: { walletReady } }: AppState) => walletReady); - const { isWalletEthZero, isLoadingUserAssets, isLoadingBalance, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); + const { + isWalletEthZero, + isLoadingUserAssets, + isLoadingBalance, + briefSectionsData: walletBriefSectionsData, + } = useWalletSectionsData({ type: 'wallet' }); useEffect(() => { if (!wallets) return; @@ -149,7 +153,6 @@ function WalletScreen() { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); - runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); @@ -185,6 +188,7 @@ function WalletScreen() { + {/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */} diff --git a/src/screens/rewards/components/RewardsEarnings.tsx b/src/screens/rewards/components/RewardsEarnings.tsx index 48e70481cec..b351d4be895 100644 --- a/src/screens/rewards/components/RewardsEarnings.tsx +++ b/src/screens/rewards/components/RewardsEarnings.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Image } from 'react-native'; +import { Image, ImageBackground } from 'react-native'; import { RewardsSectionCard } from '@/screens/rewards/components/RewardsSectionCard'; import { AccentColorProvider, Box, Columns, Inline, Stack, Text } from '@/design-system'; import * as i18n from '@/languages'; @@ -63,7 +63,7 @@ export const RewardsEarnings: React.FC = ({ airdropTitle, airdropTime, }; - }, [pendingEarningsToken, tokenSymbol, totalEarnings.token, totalEarnings.usd, nextAirdropTimestamp]); + }, [pendingEarningsToken, tokenSymbol, totalEarnings.token, totalEarnings.usd, assetPrice, nativeCurrency, nextAirdropTimestamp]); const navigateToTimingExplainer = () => { analyticsV2.track(analyticsV2.event.rewardsPressedPendingEarningsCard); @@ -85,7 +85,7 @@ export const RewardsEarnings: React.FC = ({ = ({ (backendNetworksQueryKey()) ?? buildTimeNetworks; const DEFAULT_PRIVATE_MEMPOOL_TIMEOUT = 2 * 60 * 1_000; // 2 minutes @@ -19,6 +20,7 @@ export interface BackendNetworksState { getBackendChains: () => Chain[]; getSupportedChains: () => Chain[]; + getSortedSupportedChainIds: () => number[]; getDefaultChains: () => Record; getSupportedChainIds: () => ChainId[]; @@ -32,6 +34,8 @@ export interface BackendNetworksState { getChainsBadge: () => Record; getChainsIdByName: () => Record; + getColorsForChainId: (chainId: ChainId, isDarkMode: boolean) => string; + defaultGasSpeeds: (chainId: ChainId) => GasSpeed[]; getChainsGasSpeeds: () => Record; @@ -73,7 +77,12 @@ export const useBackendNetworksStore = createRainbowStore( getSupportedChains: () => { const backendChains = get().getBackendChains(); - return IS_TEST ? [...backendChains, chainHardhat, chainHardhatOptimism] : backendChains; + return IS_TEST ? [...backendChains, chainAnvil, chainAnvilOptimism] : backendChains; + }, + + getSortedSupportedChainIds: () => { + const supportedChains = get().getSupportedChains(); + return supportedChains.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); }, getDefaultChains: () => { @@ -175,6 +184,17 @@ export const useBackendNetworksStore = createRainbowStore( ); }, + getColorsForChainId: (chainId: ChainId, isDarkMode: boolean) => { + const { backendNetworks } = get(); + + const colors = backendNetworks.networks.find(chain => +chain.id === chainId)?.colors; + if (!colors) { + return isDarkMode ? globalColors.white : globalColors.black; + } + + return isDarkMode ? colors.dark : colors.light; + }, + // TODO: This should come from the backend at some point defaultGasSpeeds: chainId => { switch (chainId) { @@ -355,8 +375,8 @@ export const useBackendNetworksStore = createRainbowStore( const defaultChains = get().getDefaultChains(); switch (chainId) { case ChainId.mainnet: - return useConnectedToHardhatStore.getState().connectedToHardhat - ? chainHardhat.rpcUrls.default.http[0] + return useConnectedToAnvilStore.getState().connectedToAnvil + ? chainAnvil.rpcUrls.default.http[0] : defaultChains[ChainId.mainnet].rpcUrls.default.http[0]; default: return defaultChains[chainId].rpcUrls.default.http[0]; @@ -383,7 +403,7 @@ export const getBackendChainsWorklet = (backendNetworks: SharedValue) => { 'worklet'; const backendChains = getBackendChainsWorklet(backendNetworks); - return IS_TEST ? [...backendChains, chainHardhat, chainHardhatOptimism] : backendChains; + return IS_TEST ? [...backendChains, chainAnvil, chainAnvilOptimism] : backendChains; }; export const getDefaultChainsWorklet = (backendNetworks: SharedValue) => { @@ -663,7 +683,7 @@ export const getChainDefaultRpcWorklet = (backendNetworks: SharedValue void; + + backupProvider: string | undefined; + setBackupProvider: (backupProvider: string | undefined) => void; + + status: CloudBackupState; + setStatus: (status: CloudBackupState) => void; + + backups: CloudBackups; + setBackups: (backups: CloudBackups) => void; + + mostRecentBackup: BackupFile | undefined; + setMostRecentBackup: (backup: BackupFile | undefined) => void; + + password: string; + setPassword: (password: string) => void; + + syncAndFetchBackups: ( + retryOnFailure?: boolean, + retryCount?: number + ) => Promise<{ + success: boolean; + retry?: boolean; + }>; +} + +const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching]; + +export const backupsStore = createRainbowStore((set, get) => ({ + storedPassword: '', + setStoredPassword: storedPassword => set({ storedPassword }), + + backupProvider: undefined, + setBackupProvider: provider => set({ backupProvider: provider }), + + status: CloudBackupState.Initializing, + setStatus: status => set({ status }), + + backups: { files: [] }, + setBackups: backups => set({ backups }), + + mostRecentBackup: undefined, + setMostRecentBackup: backup => set({ mostRecentBackup: backup }), + + password: '', + setPassword: password => set({ password }), + + syncAndFetchBackups: async (retryOnFailure = true, retryCount = 0) => { + const { status } = get(); + + const timeoutPromise = new Promise<{ success: boolean; retry?: boolean }>(resolve => { + setTimeout(() => { + resolve({ success: false, retry: retryOnFailure }); + }, DEFAULT_TIMEOUT); + }); + + const syncAndPullFiles = async (): Promise<{ success: boolean; retry?: boolean }> => { + try { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + logger.debug('[backupsStore]: Cloud backup is not available'); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); + return { + success: false, + retry: false, + }; + } + + if (IS_ANDROID) { + const gdata = await getGoogleAccountUserData(); + if (!gdata) { + logger.debug('[backupsStore]: Google account is not available'); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); + return { + success: false, + retry: false, + }; + } + } + + set({ status: CloudBackupState.Syncing }); + logger.debug('[backupsStore]: Syncing with cloud'); + await syncCloud(); + + set({ status: CloudBackupState.Fetching }); + logger.debug('[backupsStore]: Fetching backups'); + const backups = await fetchAllBackups(); + + set({ backups }); + + const { wallets } = store.getState().wallets; + + // if the user has any cloud backups, set the provider to cloud + if (backups.files.length > 0) { + set({ + backupProvider: walletBackupTypes.cloud, + mostRecentBackup: getMostRecentCloudBackup(backups.files), + }); + } else if (hasManuallyBackedUpWallet(wallets)) { + set({ backupProvider: walletBackupTypes.manual }); + } else { + set({ backupProvider: undefined }); + } + + logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`); + + set({ status: CloudBackupState.Ready }); + return { + success: true, + retry: false, + }; + } catch (e) { + logger.error(new RainbowError('[backupsStore]: Failed to fetch all backups'), { + error: e, + }); + set({ status: CloudBackupState.FailedToInitialize }); + } + + return { + success: false, + retry: retryOnFailure, + }; + }; + + if (mutex.isLocked() || returnEarlyIfLockedStates.includes(status)) { + logger.debug('[backupsStore]: Mutex is locked or returnEarlyIfLockedStates includes status', { + status, + }); + return { + success: false, + retry: false, + }; + } + + const releaser = await mutex.acquire(); + logger.debug('[backupsStore]: Acquired mutex'); + const { success, retry } = await Promise.race([syncAndPullFiles(), timeoutPromise]); + releaser(); + logger.debug('[backupsStore]: Released mutex'); + if (retry && retryCount < MAX_RETRIES) { + logger.debug(`[backupsStore]: Retrying sync and fetch backups attempt: ${retryCount + 1}`); + return get().syncAndFetchBackups(retryOnFailure, retryCount + 1); + } + + if (retry && retryCount >= MAX_RETRIES) { + logger.error(new RainbowError('[backupsStore]: Max retry attempts reached. Sync failed.')); + } + + return { success, retry }; + }, +})); diff --git a/src/state/connectedToAnvil/index.ts b/src/state/connectedToAnvil/index.ts new file mode 100644 index 00000000000..cefbc2526d8 --- /dev/null +++ b/src/state/connectedToAnvil/index.ts @@ -0,0 +1,27 @@ +import { createRainbowStore } from '../internal/createRainbowStore'; + +export interface ConnectedToAnvilState { + connectedToAnvil: boolean; + setConnectedToAnvil: (connectedToAnvil: boolean) => void; + + connectedToAnvilOp: boolean; + setConnectedToAnvilOp: (connectedToAnvilOp: boolean) => void; +} + +export const useConnectedToAnvilStore = createRainbowStore( + set => ({ + connectedToAnvil: false, + setConnectedToAnvil: connectedToAnvil => { + set({ connectedToAnvil }); + }, + + connectedToAnvilOp: false, + setConnectedToAnvilOp: connectedToAnvilOp => { + set({ connectedToAnvilOp }); + }, + }), + { + storageKey: 'connectedToAnvil', + version: 0, + } +); diff --git a/src/state/connectedToHardhat/index.ts b/src/state/connectedToHardhat/index.ts deleted file mode 100644 index 362d70e577e..00000000000 --- a/src/state/connectedToHardhat/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { createRainbowStore } from '../internal/createRainbowStore'; - -export interface ConnectedToHardhatState { - connectedToHardhat: boolean; - setConnectedToHardhat: (connectedToHardhat: boolean) => void; - - connectedToHardhatOp: boolean; - setConnectedToHardhatOp: (connectedToHardhatOp: boolean) => void; -} - -export const useConnectedToHardhatStore = createRainbowStore( - set => ({ - connectedToHardhat: false, - setConnectedToHardhat: connectedToHardhat => { - set({ connectedToHardhat }); - }, - - connectedToHardhatOp: false, - setConnectedToHardhatOp: connectedToHardhatOp => { - set({ connectedToHardhatOp }); - }, - }), - { - storageKey: 'connectedToHardhat', - version: 0, - } -); diff --git a/src/state/internal/createRainbowStore.ts b/src/state/internal/createRainbowStore.ts index 9b7d9a39dd7..e0c14b79ead 100644 --- a/src/state/internal/createRainbowStore.ts +++ b/src/state/internal/createRainbowStore.ts @@ -43,6 +43,12 @@ interface RainbowPersistConfig { * This function will be called when persisted state versions mismatch with the one specified here. */ migrate?: (persistedState: unknown, version: number) => S | Promise; + /** + * A function returning another (optional) function. + * The main function will be called before the state rehydration. + * The returned function will be called after the state rehydration or when an error occurred. + */ + onRehydrateStorage?: PersistOptions>['onRehydrateStorage']; } /** @@ -157,6 +163,7 @@ export function createRainbowStore( storage: persistStorage, version, migrate: persistConfig.migrate, + onRehydrateStorage: persistConfig.onRehydrateStorage, }) ) ); diff --git a/src/state/networkSwitcher/networkSwitcher.ts b/src/state/networkSwitcher/networkSwitcher.ts new file mode 100644 index 00000000000..aa82dc85a44 --- /dev/null +++ b/src/state/networkSwitcher/networkSwitcher.ts @@ -0,0 +1,57 @@ +import { ChainId } from '@/state/backendNetworks/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { analyticsV2 } from '@/analytics'; +import { nonceStore } from '@/state/nonces'; +import { logger } from '@/logger'; + +export const defaultPinnedNetworks = [ChainId.base, ChainId.mainnet, ChainId.optimism, ChainId.arbitrum, ChainId.polygon, ChainId.zora]; + +function getMostUsedChains() { + try { + const noncesByAddress = nonceStore.getState().nonces; + const summedNoncesByChainId: Record = {}; + for (const addressNonces of Object.values(noncesByAddress)) { + for (const [chainId, { currentNonce }] of Object.entries(addressNonces)) { + summedNoncesByChainId[chainId] ??= 0; + summedNoncesByChainId[chainId] += currentNonce || 0; + } + } + + const mostUsedNetworks = Object.entries(summedNoncesByChainId) + .sort((a, b) => b[1] - a[1]) + .map(([chainId]) => parseInt(chainId)); + + return mostUsedNetworks.length ? mostUsedNetworks.slice(0, 5) : defaultPinnedNetworks; + } catch (error) { + logger.warn('[networkSwitcher]: Error getting most used chains', { error }); + return defaultPinnedNetworks; + } +} + +export const networkSwitcherStore = createRainbowStore<{ + pinnedNetworks: ChainId[]; +}>(() => ({ pinnedNetworks: getMostUsedChains().slice(0, 5) }), { + storageKey: 'network-switcher', + version: 0, + onRehydrateStorage(state) { + // if we are missing pinned networks, use the user most used chains + if (state.pinnedNetworks.length === 0) { + const mostUsedNetworks = getMostUsedChains(); + state.pinnedNetworks = mostUsedNetworks.slice(0, 5); + analyticsV2.identify({ mostUsedNetworks: mostUsedNetworks.filter(Boolean) }); + } + }, +}); + +export const customizeNetworksBannerStore = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); + +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +export const shouldShowCustomizeNetworksBanner = (dismissedAt: number) => Date.now() - dismissedAt > twoWeeks; +export const dismissCustomizeNetworksBanner = () => { + customizeNetworksBannerStore.setState({ dismissedAt: Date.now() }); +}; diff --git a/src/state/swaps/swapsStore.ts b/src/state/swaps/swapsStore.ts index 49cc99d0268..7d4d0d99f4b 100644 --- a/src/state/swaps/swapsStore.ts +++ b/src/state/swaps/swapsStore.ts @@ -1,5 +1,5 @@ import { INITIAL_SLIDER_POSITION } from '@/__swaps__/screens/Swap/constants'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; import { ChainId } from '@/state/backendNetworks/types'; import { RecentSwap } from '@/__swaps__/types/swap'; import { getDefaultSlippage } from '@/__swaps__/utils/swaps'; @@ -42,6 +42,8 @@ export interface SwapsState { // degen mode preferences preferredNetwork: ChainId | undefined; setPreferredNetwork: (preferredNetwork: ChainId | undefined) => void; + + lastNavigatedTrendingToken: UniqueId | undefined; } type StateWithTransforms = Omit, 'latestSwapAt' | 'recentSwaps'> & { @@ -156,6 +158,8 @@ export const swapsStore = createRainbowStore( latestSwapAt: new Map(latestSwapAt), }); }, + + lastNavigatedTrendingToken: undefined, }), { storageKey: 'swapsStore', diff --git a/src/state/sync/BackupsSync.tsx b/src/state/sync/BackupsSync.tsx new file mode 100644 index 00000000000..a409490c205 --- /dev/null +++ b/src/state/sync/BackupsSync.tsx @@ -0,0 +1,12 @@ +import { useEffect, memo } from 'react'; +import { backupsStore } from '@/state/backups/backups'; + +const BackupsSyncComponent = () => { + useEffect(() => { + backupsStore.getState().syncAndFetchBackups(); + }, []); + + return null; +}; + +export const BackupsSync = memo(BackupsSyncComponent); diff --git a/src/state/trendingTokens/trendingTokens.ts b/src/state/trendingTokens/trendingTokens.ts new file mode 100644 index 00000000000..514160352d1 --- /dev/null +++ b/src/state/trendingTokens/trendingTokens.ts @@ -0,0 +1,59 @@ +import { analyticsV2 } from '@/analytics'; +import { ChainId } from '@/state/backendNetworks/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { + TrendingCategory as ArcTrendingCategory, + Timeframe as ArcTimeframe, + TrendingSort as ArcTrendingSort, +} from '@/graphql/__generated__/arc'; + +export const categories = [ArcTrendingCategory.Trending, ArcTrendingCategory.New, ArcTrendingCategory.Farcaster] as const; +export type TrendingCategory = (typeof categories)[number]; +export const sortFilters = [ + ArcTrendingSort.Recommended, + ArcTrendingSort.Volume, + ArcTrendingSort.MarketCap, + ArcTrendingSort.TopGainers, + ArcTrendingSort.TopLosers, +] as const; +export type TrendingSort = (typeof sortFilters)[number]; +export const timeFilters = [ArcTimeframe.H12, ArcTimeframe.H24, ArcTimeframe.D3, ArcTimeframe.D7] as const; +export type TrendingTimeframe = (typeof timeFilters)[number]; + +type TrendingTokensState = { + category: (typeof categories)[number]; + chainId: undefined | ChainId; + timeframe: (typeof timeFilters)[number]; + sort: (typeof sortFilters)[number]; + + setCategory: (category: TrendingTokensState['category']) => void; + setChainId: (chainId: TrendingTokensState['chainId']) => void; + setTimeframe: (timeframe: TrendingTokensState['timeframe']) => void; + setSort: (sort: TrendingTokensState['sort']) => void; +}; + +export const useTrendingTokensStore = createRainbowStore( + set => ({ + category: ArcTrendingCategory.Trending, + chainId: undefined, + timeframe: ArcTimeframe.D3, + sort: ArcTrendingSort.Recommended, + setCategory: category => set({ category }), + setChainId: chainId => { + analyticsV2.track(analyticsV2.event.changeNetworkFilter, { chainId }); + set({ chainId }); + }, + setTimeframe: timeframe => { + analyticsV2.track(analyticsV2.event.changeTimeframeFilter, { timeframe }); + set({ timeframe }); + }, + setSort: sort => { + analyticsV2.track(analyticsV2.event.changeSortFilter, { sort }); + set({ sort }); + }, + }), + { + storageKey: 'trending-tokens', + version: 1, + } +); diff --git a/src/state/walletLoading/walletLoading.ts b/src/state/walletLoading/walletLoading.ts new file mode 100644 index 00000000000..7391b78e760 --- /dev/null +++ b/src/state/walletLoading/walletLoading.ts @@ -0,0 +1,18 @@ +import { createRainbowStore } from '../internal/createRainbowStore'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; + +type WalletLoadingState = { + loadingState: WalletLoadingStates | null; + blockTouches: boolean; + Component: JSX.Element | null; + hide: () => void; + setComponent: (Component: JSX.Element, blockTouches?: boolean) => void; +}; + +export const walletLoadingStore = createRainbowStore(set => ({ + loadingState: null, + blockTouches: false, + Component: null, + hide: () => set({ blockTouches: false, Component: null }), + setComponent: (Component: JSX.Element, blockTouches = true) => set({ blockTouches, Component }), +})); diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 04d23086e48..2deaa5ba2f4 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { globalColors } from '@/design-system'; import currentColors from '../theme/currentColors'; import { memoFn } from '../utils/memoFn'; -import { ChainId } from '@/state/backendNetworks/types'; export type Colors = ReturnType; @@ -186,28 +185,6 @@ const getColorsByTheme = (darkMode?: boolean) => { }, }; - let networkColors = { - [ChainId.zksync]: '#25292E', - [ChainId.sanko]: '#594BA7', - [ChainId.scroll]: '#A6855D', - [ChainId.linea]: '#25292E', - [ChainId.gravity]: '#B75E2C', - [ChainId.ink]: '#7132F5', - [ChainId.arbitrum]: '#2D374B', - [ChainId.base]: '#0052FF', - [ChainId.goerli]: '#f6c343', - [ChainId.gnosis]: '#133629', - [ChainId.mainnet]: '#25292E', - [ChainId.optimism]: '#FF4040', - [ChainId.polygon]: '#8247E5', - [ChainId.bsc]: '#F0B90B', - [ChainId.zora]: '#2B5DF0', - [ChainId.avalanche]: '#E84142', - [ChainId.degen]: '#A36EFD', - [ChainId.blast]: '#25292E', - [ChainId.apechain]: '#0054FA', - }; - let gradients = { appleBlueTintToAppleBlue: ['#15B1FE', base.appleBlue], blueToGreen: ['#4764F7', '#23D67F'], @@ -334,28 +311,6 @@ const getColorsByTheme = (darkMode?: boolean) => { secondGradient: '#12131A80', thirdGradient: '#12131Aff', }; - - networkColors = { - [ChainId.zksync]: '#FFFFFF', - [ChainId.sanko]: '#7F6FC9', - [ChainId.scroll]: '#EBC28E', - [ChainId.linea]: '#FFFFFF', - [ChainId.gravity]: '#B75E2C', - [ChainId.ink]: '#864DFF', - [ChainId.arbitrum]: '#ADBFE3', - [ChainId.base]: '#3979FF', - [ChainId.goerli]: '#f6c343', - [ChainId.gnosis]: '#F0EBDE', - [ChainId.mainnet]: '#E0E8FF', - [ChainId.optimism]: '#FF6A6A', - [ChainId.polygon]: '#A275EE', - [ChainId.bsc]: '#F0B90B', - [ChainId.zora]: '#6183F0', - [ChainId.avalanche]: '#FF5D5E', - [ChainId.degen]: '#A36EFD', - [ChainId.blast]: '#FCFC03', - [ChainId.apechain]: '#397BFF', - }; } return { @@ -370,7 +325,6 @@ const getColorsByTheme = (darkMode?: boolean) => { isColorDark, isColorLight, listHeaders, - networkColors, sendScreen, ...base, ...transparent, diff --git a/src/utils/branch.ts b/src/utils/branch.ts index c0d47bdaa77..c44d0c8ca01 100644 --- a/src/utils/branch.ts +++ b/src/utils/branch.ts @@ -2,10 +2,10 @@ import pako from 'pako'; import qs from 'qs'; import branch from 'react-native-branch'; -import { IS_TESTING } from 'react-native-dotenv'; import { analyticsV2 } from '@/analytics'; import * as ls from '@/storage'; import { logger, RainbowError } from '@/logger'; +import { IS_TEST } from '@/env'; const isEmpty = (obj: T | undefined): obj is undefined => !obj || Object.keys(obj).length === 0; @@ -86,7 +86,7 @@ export const branchListener = async (handleOpenLinkingURL: (url: string) => void */ logger.debug(`[branchListener]: handling event where no link was opened`, {}, logger.DebugContext.deeplinks); - if (IS_TESTING === 'true' && !!uri) { + if (IS_TEST && !!uri) { handleOpenLinkingURL(uri); } } else if (params.uri && typeof params.uri === 'string') { diff --git a/src/utils/reviewAlert.ts b/src/utils/reviewAlert.ts index dfbda01e918..767b305e2ba 100644 --- a/src/utils/reviewAlert.ts +++ b/src/utils/reviewAlert.ts @@ -3,8 +3,8 @@ import * as ls from '@/storage'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { ReviewPromptAction } from '@/storage/schema'; import { logger, RainbowError } from '@/logger'; -import { IS_TESTING } from 'react-native-dotenv'; import * as StoreReview from 'expo-store-review'; +import { IS_TEST } from '@/env'; export const AppleReviewAddress = 'itms-apps://itunes.apple.com/us/app/appName/id1457119021?mt=8&action=write-review'; @@ -29,7 +29,7 @@ export const numberOfTimesBeforePrompt: { export const handleReviewPromptAction = async (action: ReviewPromptAction) => { logger.debug(`[reviewAlert]: handleReviewPromptAction: ${action}`); - if (IS_TESTING === 'true') { + if (IS_TEST) { return; } diff --git a/src/walletConnect/sheets/AuthRequest.tsx b/src/walletConnect/sheets/AuthRequest.tsx index 338acad39b4..724cae00de2 100644 --- a/src/walletConnect/sheets/AuthRequest.tsx +++ b/src/walletConnect/sheets/AuthRequest.tsx @@ -149,7 +149,7 @@ export function AuthRequest({ navigate(Routes.CHANGE_WALLET_SHEET, { watchOnly: true, currentAccountAddress: address, - onChangeWallet(address: string) { + onChangeWallet(address) { setAddress(address); goBack(); }, diff --git a/yarn.lock b/yarn.lock index b5fbcfce5a1..0c47a4cd051 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2240,52 +2240,6 @@ __metadata: languageName: node linkType: hard -"@chainsafe/as-sha256@npm:^0.3.1": - version: 0.3.1 - resolution: "@chainsafe/as-sha256@npm:0.3.1" - checksum: 10c0/72561fc6552a53e4d1fc28880b7f82ecb7a997670568333cb479f323db9482a6a59dd9d0f915210703e51c3a4ca2701ccdb4c66a0202abab4872d81184c9212e - languageName: node - linkType: hard - -"@chainsafe/persistent-merkle-tree@npm:^0.4.2": - version: 0.4.2 - resolution: "@chainsafe/persistent-merkle-tree@npm:0.4.2" - dependencies: - "@chainsafe/as-sha256": "npm:^0.3.1" - checksum: 10c0/9533e478a1a990e8cf8710a2eeb84c6f08c7b61726a43dbe2165316256839c29a2ff17923bce5e5effec446d832de8b0a5bc896ef5db80bce059af5d1bd20d8d - languageName: node - linkType: hard - -"@chainsafe/persistent-merkle-tree@npm:^0.5.0": - version: 0.5.0 - resolution: "@chainsafe/persistent-merkle-tree@npm:0.5.0" - dependencies: - "@chainsafe/as-sha256": "npm:^0.3.1" - checksum: 10c0/73c7a7536f49aceab61870fcc1dafef8a8be2ae0bfff2614846bb4b57a21939da75bca7bc5d1959cd312a5133be0acaf0e30fb323410c57592e9ec384758efe0 - languageName: node - linkType: hard - -"@chainsafe/ssz@npm:^0.10.0": - version: 0.10.2 - resolution: "@chainsafe/ssz@npm:0.10.2" - dependencies: - "@chainsafe/as-sha256": "npm:^0.3.1" - "@chainsafe/persistent-merkle-tree": "npm:^0.5.0" - checksum: 10c0/be427eba9f9c4a542326f9f3c20eb704c1c2500c4f124ba18febf6ffd5bb7bd5755228d99326bf6c4e4d969daa4b6ff2efb743688ec36ef86f20c0c673c0e967 - languageName: node - linkType: hard - -"@chainsafe/ssz@npm:^0.9.2": - version: 0.9.4 - resolution: "@chainsafe/ssz@npm:0.9.4" - dependencies: - "@chainsafe/as-sha256": "npm:^0.3.1" - "@chainsafe/persistent-merkle-tree": "npm:^0.4.2" - case: "npm:^1.6.3" - checksum: 10c0/4ce4b867c60dbee98772fe075037c7ef9a7894f97a4fb04f3cfd57e11fa683b8c23a4d80b53592d10fbd4e2abac43c9099181cfaee587619366f49091b9e5fcb - languageName: node - linkType: hard - "@coinbase/mobile-wallet-protocol-host@npm:0.1.7": version: 0.1.7 resolution: "@coinbase/mobile-wallet-protocol-host@npm:0.1.7" @@ -2517,7 +2471,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.1, @ethersproject/abi@npm:^5.1.2, @ethersproject/abi@npm:^5.5.0, @ethersproject/abi@npm:^5.7.0": +"@ethersproject/abi@npm:5.7.0, @ethersproject/abi@npm:^5.0.1, @ethersproject/abi@npm:^5.5.0, @ethersproject/abi@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" dependencies: @@ -2772,7 +2726,7 @@ __metadata: languageName: node linkType: hard -"@ethersproject/providers@npm:5.7.2, @ethersproject/providers@npm:^5.7.1, @ethersproject/providers@npm:^5.7.2": +"@ethersproject/providers@npm:5.7.2": version: 5.7.2 resolution: "@ethersproject/providers@npm:5.7.2" dependencies: @@ -3411,13 +3365,6 @@ __metadata: languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": - version: 2.1.1 - resolution: "@fastify/busboy@npm:2.1.1" - checksum: 10c0/6f8027a8cba7f8f7b736718b013f5a38c0476eea67034c94a0d3c375e2b114366ad4419e6a6fa7ffc2ef9c6d3e0435d76dd584a7a1cbac23962fda7650b579e3 - languageName: node - linkType: hard - "@flatten-js/interval-tree@npm:^1.1.2": version: 1.1.3 resolution: "@flatten-js/interval-tree@npm:1.1.3" @@ -4315,19 +4262,6 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-sig-util@npm:^4.0.0": - version: 4.0.1 - resolution: "@metamask/eth-sig-util@npm:4.0.1" - dependencies: - ethereumjs-abi: "npm:^0.6.8" - ethereumjs-util: "npm:^6.2.1" - ethjs-util: "npm:^0.1.6" - tweetnacl: "npm:^1.0.3" - tweetnacl-util: "npm:^0.15.1" - checksum: 10c0/957fa16e8f0454ad45203a8416e77181853de1c9e33697f1a1582d46f18da1cca26c803a4e08bee7091a697609fc8916f399210fd5d3d2fccc34bfd0a58715f0 - languageName: node - linkType: hard - "@metamask/superstruct@npm:^3.0.0, @metamask/superstruct@npm:^3.1.0": version: 3.1.0 resolution: "@metamask/superstruct@npm:3.1.0" @@ -4468,13 +4402,6 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.2.0, @noble/hashes@npm:~1.2.0": - version: 1.2.0 - resolution: "@noble/hashes@npm:1.2.0" - checksum: 10c0/8bd3edb7bb6a9068f806a9a5a208cc2144e42940a21c049d8e9a0c23db08bef5cf1cfd844a7e35489b5ab52c6fa6299352075319e7f531e0996d459c38cfe26a - languageName: node - linkType: hard - "@noble/hashes@npm:1.3.2": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" @@ -4510,13 +4437,6 @@ __metadata: languageName: node linkType: hard -"@noble/secp256k1@npm:1.7.1, @noble/secp256k1@npm:~1.7.0": - version: 1.7.1 - resolution: "@noble/secp256k1@npm:1.7.1" - checksum: 10c0/48091801d39daba75520012027d0ff0b1719338d96033890cfe0d287ad75af00d82769c0194a06e7e4fbd816ae3f204f4a59c9e26f0ad16b429f7e9b5403ccd5 - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4544,266 +4464,6 @@ __metadata: languageName: node linkType: hard -"@nomicfoundation/ethereumjs-block@npm:5.0.2": - version: 5.0.2 - resolution: "@nomicfoundation/ethereumjs-block@npm:5.0.2" - dependencies: - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.2" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - ethereum-cryptography: "npm:0.1.3" - ethers: "npm:^5.7.1" - checksum: 10c0/9bbf524706c86b3741eab42a82bce723ef413f2ecd85bc96b6353f619559780995bc21fcf765558a3a7ab5eca5c77926ae7440fe2467774d896f67ec9bfcd63e - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-blockchain@npm:7.0.2": - version: 7.0.2 - resolution: "@nomicfoundation/ethereumjs-blockchain@npm:7.0.2" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.2" - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-ethash": "npm:3.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.2" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - abstract-level: "npm:^1.0.3" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - level: "npm:^8.0.0" - lru-cache: "npm:^5.1.1" - memory-level: "npm:^1.0.0" - checksum: 10c0/388f938288396669108e6513c531e81d02d994dabcbf96261dd6672a882dfd4966cf9e05fd0c98d50c7aef847335a588b21dd0acba9d923cc734f4f61a7a77ba - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-common@npm:4.0.2": - version: 4.0.2 - resolution: "@nomicfoundation/ethereumjs-common@npm:4.0.2" - dependencies: - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - crc-32: "npm:^1.2.0" - checksum: 10c0/ce12038b8b3245a2a20b8a11fe19b4454a8179b7a1bb9185cd42a85b5a17f7fceacf0bf69517d095b52e3cede4eeda71a45044a5a8976f3f37e2d501f0adaea3 - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-ethash@npm:3.0.2": - version: 3.0.2 - resolution: "@nomicfoundation/ethereumjs-ethash@npm:3.0.2" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - abstract-level: "npm:^1.0.3" - bigint-crypto-utils: "npm:^3.0.23" - ethereum-cryptography: "npm:0.1.3" - checksum: 10c0/0a19f9243e9cc348e13bff0b0ec5ec9612c275550c5b0a3028b466f39e0959dd8c0eeeae7fc0c9af920023143658dbb18d87107167af344de92e76aaf683dcc4 - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-evm@npm:2.0.2": - version: 2.0.2 - resolution: "@nomicfoundation/ethereumjs-evm@npm:2.0.2" - dependencies: - "@ethersproject/providers": "npm:^5.7.1" - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - mcl-wasm: "npm:^0.7.1" - rustbn.js: "npm:~0.2.0" - checksum: 10c0/a5711952d8afe1c61c3e8275217b7c3bd600de839ad784848ff556c498fee4d0c0a29834aa2c666263915ed6123a3191297c109bad8e50c135dd9de33d101cd7 - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-rlp@npm:5.0.2": - version: 5.0.2 - resolution: "@nomicfoundation/ethereumjs-rlp@npm:5.0.2" - bin: - rlp: bin/rlp - checksum: 10c0/46c7d317f59690973c41786b7a3ef3abe456efd085d55a0b202f6ead792e34e1a0749815911ab558b83f508c4ae5a6cba4d994aeae9c77c14ce0516f284ed34b - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-statemanager@npm:2.0.2": - version: 2.0.2 - resolution: "@nomicfoundation/ethereumjs-statemanager@npm:2.0.2" - dependencies: - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - ethers: "npm:^5.7.1" - js-sdsl: "npm:^4.1.4" - checksum: 10c0/d3b184adb1b8aaf4c87299194746fc343a94df6f500d677f04a36914f7673eee19c344eb88a6f78718dcb4ae15d63c2c3e87cb9a5076950b67843e5bf9321ace - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-trie@npm:6.0.2": - version: 6.0.2 - resolution: "@nomicfoundation/ethereumjs-trie@npm:6.0.2" - dependencies: - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - "@types/readable-stream": "npm:^2.3.13" - ethereum-cryptography: "npm:0.1.3" - readable-stream: "npm:^3.6.0" - checksum: 10c0/188c5d0a5793fb4512916091bde498e52ec6ecb374963213602f32e98c301d4d62f2daefd4ebefc0944d6d5b8346f104156fa544c0fc26ded488884a0424b2cc - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-tx@npm:5.0.2": - version: 5.0.2 - resolution: "@nomicfoundation/ethereumjs-tx@npm:5.0.2" - dependencies: - "@chainsafe/ssz": "npm:^0.9.2" - "@ethersproject/providers": "npm:^5.7.2" - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - ethereum-cryptography: "npm:0.1.3" - checksum: 10c0/327c093656b4f6e845c3ef543b6ab54f6699436d95e18d0fca9df930dd2ddb975374b2499b3f98070cae4e6f54005b0484c1b40ff1838326cf5a631710116def - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-util@npm:9.0.2": - version: 9.0.2 - resolution: "@nomicfoundation/ethereumjs-util@npm:9.0.2" - dependencies: - "@chainsafe/ssz": "npm:^0.10.0" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - ethereum-cryptography: "npm:0.1.3" - checksum: 10c0/34b5b73f2e23bd883e53fd4d6810954d08451c84887b3d7c8910c093825686c499fe0edbb865db8d064c6790b447ce10f1aea030073befd64dbe62b75126dac6 - languageName: node - linkType: hard - -"@nomicfoundation/ethereumjs-vm@npm:7.0.2": - version: 7.0.2 - resolution: "@nomicfoundation/ethereumjs-vm@npm:7.0.2" - dependencies: - "@nomicfoundation/ethereumjs-block": "npm:5.0.2" - "@nomicfoundation/ethereumjs-blockchain": "npm:7.0.2" - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-evm": "npm:2.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-statemanager": "npm:2.0.2" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.2" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - debug: "npm:^4.3.3" - ethereum-cryptography: "npm:0.1.3" - mcl-wasm: "npm:^0.7.1" - rustbn.js: "npm:~0.2.0" - checksum: 10c0/759c16d471429e06b8a191b3b87c140690e6334586b5467587e7397e7e40dc0ec6aea4a73cea68a1ace125552beefc23624a6e667387031f5379000e56f83018 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-darwin-arm64@npm:0.1.2" - checksum: 10c0/ef3b13bb2133fea6621db98f991036a3a84d2b240160edec50beafa6ce821fe2f0f5cd4aa61adb9685aff60cd0425982ffd15e0b868b7c768e90e26b8135b825 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-darwin-x64@npm:0.1.2" - checksum: 10c0/3cb6a00cd200b94efd6f59ed626c705c6f773b92ccf8b90471285cd0e81b35f01edb30c1aa5a4633393c2adb8f20fd34e90c51990dc4e30658e8a67c026d16c9 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-linux-arm64-gnu@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-linux-arm64-gnu@npm:0.1.2" - checksum: 10c0/cb9725e7bdc3ba9c1feaef96dbf831c1a59c700ca633a9929fd97debdcb5ce06b5d7b4e6dbc076279978707214d01e2cd126d8e3f4cabc5c16525c031a47b95c - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-linux-arm64-musl@npm:0.1.2" - checksum: 10c0/82a90b1d09ad266ddc510ece2e397f51fdaf29abf7263d2a3a85accddcba2ac24cceb670a3120800611cdcc552eed04919d071e259fdda7564818359ed541f5d - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-linux-x64-gnu@npm:0.1.2" - checksum: 10c0/d1f20d4d55683bd041ead957e5461b2e43a39e959f905e8866de1d65f8d96118e9b861e994604d9002cb7f056be0844e36c241a6bb531c336b399609977c0998 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-linux-x64-musl@npm:0.1.2" - checksum: 10c0/6c17f9af3aaf184c0a217cf723076051c502d85e731dbc97f34b838f9ae1b597577abac54a2af49b3fd986b09131c52fa21fd5393b22d05e1ec7fee96a8249c2 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer-win32-x64-msvc@npm:0.1.2": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer-win32-x64-msvc@npm:0.1.2" - checksum: 10c0/da198464f5ee0d19b6decdfaa65ee0df3097b8960b8483bb7080931968815a5d60f27191229d47a198955784d763d5996f0b92bfde3551612ad972c160b0b000 - languageName: node - linkType: hard - -"@nomicfoundation/solidity-analyzer@npm:^0.1.0": - version: 0.1.2 - resolution: "@nomicfoundation/solidity-analyzer@npm:0.1.2" - dependencies: - "@nomicfoundation/solidity-analyzer-darwin-arm64": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-darwin-x64": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-linux-x64-musl": "npm:0.1.2" - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "npm:0.1.2" - dependenciesMeta: - "@nomicfoundation/solidity-analyzer-darwin-arm64": - optional: true - "@nomicfoundation/solidity-analyzer-darwin-x64": - optional: true - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": - optional: true - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": - optional: true - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": - optional: true - "@nomicfoundation/solidity-analyzer-linux-x64-musl": - optional: true - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": - optional: true - checksum: 10c0/e4f503e9287e18967535af669ca7e26e2682203c45a34ea85da53122da1dee1278f2b8c76c20c67fadd7c1b1a98eeecffd2cbc136860665e3afa133817c0de54 - languageName: node - linkType: hard - -"@nomiclabs/hardhat-ethers@npm:2.2.3": - version: 2.2.3 - resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" - peerDependencies: - ethers: ^5.0.0 - hardhat: ^2.0.0 - checksum: 10c0/cae46d1966108ab02b50fabe7945c8987fa1e9d5d0a7a06f79afc274ff1abc312e8a82375191a341b28571b897c22433d3a2826eb30077ed88d5983d01e381d0 - languageName: node - linkType: hard - -"@nomiclabs/hardhat-waffle@npm:2.0.6": - version: 2.0.6 - resolution: "@nomiclabs/hardhat-waffle@npm:2.0.6" - peerDependencies: - "@nomiclabs/hardhat-ethers": ^2.0.0 - "@types/sinon-chai": ^3.2.3 - ethereum-waffle: "*" - ethers: ^5.0.0 - hardhat: ^2.0.0 - checksum: 10c0/9614ab1e76959cfccc586842d990de4c2aa74cea8e82a838d017d91d4c696df931af4a77af9c16325e037ec8438a8c98c9bae5d9e4d0d0fcdaa147c86bce01b5 - languageName: node - linkType: hard - "@notifee/react-native@npm:7.8.2": version: 7.8.2 resolution: "@notifee/react-native@npm:7.8.2" @@ -6431,17 +6091,6 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:1.1.5": - version: 1.1.5 - resolution: "@scure/bip32@npm:1.1.5" - dependencies: - "@noble/hashes": "npm:~1.2.0" - "@noble/secp256k1": "npm:~1.7.0" - "@scure/base": "npm:~1.1.0" - checksum: 10c0/d0521f6de28278e06f2d517307b4de6c9bcb3dbdf9a5844bb57a6e4916a180e4136129ccab295c27bd1196ef77757608255afcd7cf927e03baec4479b3df74fc - languageName: node - linkType: hard - "@scure/bip32@npm:1.3.2": version: 1.3.2 resolution: "@scure/bip32@npm:1.3.2" @@ -6475,16 +6124,6 @@ __metadata: languageName: node linkType: hard -"@scure/bip39@npm:1.1.1": - version: 1.1.1 - resolution: "@scure/bip39@npm:1.1.1" - dependencies: - "@noble/hashes": "npm:~1.2.0" - "@scure/base": "npm:~1.1.0" - checksum: 10c0/821dc9d5be8362a32277390526db064860c2216a079ba51d63def9289c2b290599e93681ebbeebf0e93540799eec35784c1dfcf5167d0b280ef148e5023ce01b - languageName: node - linkType: hard - "@scure/bip39@npm:1.2.1": version: 1.2.1 resolution: "@scure/bip39@npm:1.2.1" @@ -6681,19 +6320,6 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/core@npm:5.30.0" - dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/minimal": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c0/6407b9c2a6a56f90c198f5714b3257df24d89d1b4ca6726bd44760d0adabc25798b69fef2c88ccea461c7e79e3c78861aaebfd51fd3cb892aee656c3f7e11801 - languageName: node - linkType: hard - "@sentry/core@npm:7.119.0": version: 7.119.0 resolution: "@sentry/core@npm:7.119.0" @@ -6714,17 +6340,6 @@ __metadata: languageName: node linkType: hard -"@sentry/hub@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/hub@npm:5.30.0" - dependencies: - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c0/386c91d06aa44be0465fc11330d748a113e464d41cd562a9e1d222a682cbcb14e697a3e640953e7a0239997ad8a02b223a0df3d9e1d8816cb823fd3613be3e2f - languageName: node - linkType: hard - "@sentry/hub@npm:7.119.0": version: 7.119.0 resolution: "@sentry/hub@npm:7.119.0" @@ -6760,34 +6375,6 @@ __metadata: languageName: node linkType: hard -"@sentry/minimal@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/minimal@npm:5.30.0" - dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c0/34ec05503de46d01f98c94701475d5d89cc044892c86ccce30e01f62f28344eb23b718e7cf573815e46f30a4ac9da3129bed9b3d20c822938acfb40cbe72437b - languageName: node - linkType: hard - -"@sentry/node@npm:^5.18.1": - version: 5.30.0 - resolution: "@sentry/node@npm:5.30.0" - dependencies: - "@sentry/core": "npm:5.30.0" - "@sentry/hub": "npm:5.30.0" - "@sentry/tracing": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - cookie: "npm:^0.4.1" - https-proxy-agent: "npm:^5.0.0" - lru_map: "npm:^0.3.3" - tslib: "npm:^1.9.3" - checksum: 10c0/c50db7c81ace57cac17692245c2ab3c84a6149183f81d5f2dfd157eaa7b66eb4d6a727dd13a754bb129c96711389eec2944cd94126722ee1d8b11f2b627b830d - languageName: node - linkType: hard - "@sentry/react-native@npm:5.35.0": version: 5.35.0 resolution: "@sentry/react-native@npm:5.35.0" @@ -6841,26 +6428,6 @@ __metadata: languageName: node linkType: hard -"@sentry/tracing@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/tracing@npm:5.30.0" - dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/minimal": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c0/46830265bc54a3203d7d9f0d8d9f2f7d9d2c6a977e07ccdae317fa3ea29c388b904b3bef28f7a0ba9c074845d67feab63c6d3c0ddce9aeb275b6c966253fb415 - languageName: node - linkType: hard - -"@sentry/types@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/types@npm:5.30.0" - checksum: 10c0/99c6e55c0a82c8ca95be2e9dbb35f581b29e4ff7af74b23bc62b690de4e35febfa15868184a2303480ef86babd4fea5273cf3b5ddf4a27685b841a72f13a0c88 - languageName: node - linkType: hard - "@sentry/types@npm:7.119.0": version: 7.119.0 resolution: "@sentry/types@npm:7.119.0" @@ -6875,16 +6442,6 @@ __metadata: languageName: node linkType: hard -"@sentry/utils@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/utils@npm:5.30.0" - dependencies: - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c0/ca8eebfea7ac7db6d16f6c0b8a66ac62587df12a79ce9d0d8393f4d69880bb8d40d438f9810f7fb107a9880fe0d68bbf797b89cbafd113e89a0829eb06b205f8 - languageName: node - linkType: hard - "@sentry/utils@npm:7.119.0": version: 7.119.0 resolution: "@sentry/utils@npm:7.119.0" @@ -7666,13 +7223,6 @@ __metadata: languageName: node linkType: hard -"@types/lru-cache@npm:^5.1.0": - version: 5.1.1 - resolution: "@types/lru-cache@npm:5.1.1" - checksum: 10c0/1f17ec9b202c01a89337cc5528198a690be6b61a6688242125fbfb7fa17770e453e00e4685021abf5ae605860ca0722209faac5c254b780d0104730bb0b9e354 - languageName: node - linkType: hard - "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -7854,16 +7404,6 @@ __metadata: languageName: node linkType: hard -"@types/readable-stream@npm:^2.3.13": - version: 2.3.15 - resolution: "@types/readable-stream@npm:2.3.15" - dependencies: - "@types/node": "npm:*" - safe-buffer: "npm:~5.1.1" - checksum: 10c0/789e0948a8fd2f2cbf880a0f8c95601ac2fd8782a5a8fe653f68fad7fc3a74f44e8559484e331c1ff5d5b00fa467bec97557bb683aa833a3b29a69506f7aee59 - languageName: node - linkType: hard - "@types/scheduler@npm:*": version: 0.23.0 resolution: "@types/scheduler@npm:0.23.0" @@ -9182,8 +8722,6 @@ __metadata: "@ledgerhq/hw-app-eth": "npm:6.39.0" "@ledgerhq/react-native-hw-transport-ble": "npm:6.33.4" "@metamask/eth-sig-util": "npm:7.0.2" - "@nomiclabs/hardhat-ethers": "npm:2.2.3" - "@nomiclabs/hardhat-waffle": "npm:2.0.6" "@notifee/react-native": "npm:7.8.2" "@rainbow-me/provider": "npm:0.1.1" "@rainbow-me/react-native-animated-number": "npm:0.0.2" @@ -9296,7 +8834,6 @@ __metadata: graphql-request: "npm:5.0.0" graphql-tag: "npm:2.11.0" gretchen: "npm:1.5.0" - hardhat: "npm:2.18.1" https-browserify: "npm:0.0.1" husky: "npm:8.0.1" i18n-js: "npm:3.8.0" @@ -9509,21 +9046,6 @@ __metadata: languageName: node linkType: hard -"abstract-level@npm:^1.0.0, abstract-level@npm:^1.0.2, abstract-level@npm:^1.0.3, abstract-level@npm:^1.0.4": - version: 1.0.4 - resolution: "abstract-level@npm:1.0.4" - dependencies: - buffer: "npm:^6.0.3" - catering: "npm:^2.1.0" - is-buffer: "npm:^2.0.5" - level-supports: "npm:^4.0.0" - level-transcoder: "npm:^1.0.1" - module-error: "npm:^1.0.1" - queue-microtask: "npm:^1.2.3" - checksum: 10c0/e89fad8924888b597ca84e6d003a424a670f225fd595ad1f4c2e2d38a5cea3c92877a44f5104cdede1717eb262dd6e7fbf7ef2375569c7395066521170b8d761 - languageName: node - linkType: hard - "abstract-leveldown@npm:2.6.1": version: 2.6.1 resolution: "abstract-leveldown@npm:2.6.1" @@ -9588,13 +9110,6 @@ __metadata: languageName: node linkType: hard -"adm-zip@npm:^0.4.16": - version: 0.4.16 - resolution: "adm-zip@npm:0.4.16" - checksum: 10c0/c56c6e138fd19006155fc716acae14d54e07c267ae19d78c8a8cdca04762bf20170a71a41aa8d8bad2f13b70d4f3e9a191009bafa5280e05a440ee506f871a55 - languageName: node - linkType: hard - "aes-js@npm:3.0.0": version: 3.0.0 resolution: "aes-js@npm:3.0.0" @@ -9699,7 +9214,7 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": +"ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 @@ -10784,13 +10299,6 @@ __metadata: languageName: node linkType: hard -"bigint-crypto-utils@npm:^3.0.23": - version: 3.3.0 - resolution: "bigint-crypto-utils@npm:3.3.0" - checksum: 10c0/7d06fa01d63e8e9513eee629fe8a426993276b1bdca5aefd0eb3188cee7026334d29e801ef6187a5bc6105ebf26e6e79e6fab544a7da769ccf55b913e66d2a14 - languageName: node - linkType: hard - "bignumber.js@npm:9.0.1": version: 9.0.1 resolution: "bignumber.js@npm:9.0.1" @@ -10992,18 +10500,6 @@ __metadata: languageName: node linkType: hard -"browser-level@npm:^1.0.1": - version: 1.0.1 - resolution: "browser-level@npm:1.0.1" - dependencies: - abstract-level: "npm:^1.0.2" - catering: "npm:^2.1.1" - module-error: "npm:^1.0.2" - run-parallel-limit: "npm:^1.1.0" - checksum: 10c0/10f874b05fb06092c4dc3f7b02c1bcff9b01b8eee2a7066837a10c4b0179d40dd9ecef03bfecb9acbd0b61abf67ccd250766ee18b48464cd9a4eeddda1b069b9 - languageName: node - linkType: hard - "browser-process-hrtime@npm:^1.0.0": version: 1.0.0 resolution: "browser-process-hrtime@npm:1.0.0" @@ -11011,13 +10507,6 @@ __metadata: languageName: node linkType: hard -"browser-stdout@npm:^1.3.1": - version: 1.3.1 - resolution: "browser-stdout@npm:1.3.1" - checksum: 10c0/c40e482fd82be872b6ea7b9f7591beafbf6f5ba522fe3dade98ba1573a1c29a11101564993e4eb44e5488be8f44510af072df9a9637c739217eb155ceb639205 - languageName: node - linkType: hard - "browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" @@ -11312,13 +10801,6 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2": - version: 3.1.2 - resolution: "bytes@npm:3.1.2" - checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e - languageName: node - linkType: hard - "cacache@npm:^16.1.0": version: 16.1.3 resolution: "cacache@npm:16.1.3" @@ -11478,20 +10960,6 @@ __metadata: languageName: node linkType: hard -"case@npm:^1.6.3": - version: 1.6.3 - resolution: "case@npm:1.6.3" - checksum: 10c0/43fcbb1dff1c4add94dd2bc98bd923d6616f10bff6959adf686d192c3db7d7ced35410761e1ac94cc4a1f5c41c86406ad79d390805539e421e8a77e553f67223 - languageName: node - linkType: hard - -"catering@npm:^2.1.0, catering@npm:^2.1.1": - version: 2.1.1 - resolution: "catering@npm:2.1.1" - checksum: 10c0/a69f946f82cba85509abcb399759ed4c39d2cc9e33ba35674f242130c1b3c56673da3c3e85804db6898dfd966c395aa128ba484b31c7b906cc2faca6a581e133 - languageName: node - linkType: hard - "centra@npm:^2.7.0": version: 2.7.0 resolution: "centra@npm:2.7.0" @@ -11597,7 +11065,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.4.0, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": +"chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -11731,20 +11199,6 @@ __metadata: languageName: node linkType: hard -"classic-level@npm:^1.2.0": - version: 1.4.1 - resolution: "classic-level@npm:1.4.1" - dependencies: - abstract-level: "npm:^1.0.2" - catering: "npm:^2.1.0" - module-error: "npm:^1.0.1" - napi-macros: "npm:^2.2.2" - node-gyp: "npm:latest" - node-gyp-build: "npm:^4.3.0" - checksum: 10c0/ba769e0b558ab466cef9468f7494b67d8f0139768630ba184d67b33201e67aad274718f3b1b78aaf3c5f5ab4cc526971edf82c7060741031bbad357952e7304d - languageName: node - linkType: hard - "classnames@npm:^2.2.6": version: 2.5.1 resolution: "classnames@npm:2.5.1" @@ -12074,13 +11528,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:3.0.2": - version: 3.0.2 - resolution: "commander@npm:3.0.2" - checksum: 10c0/8a279b4bacde68f03664086260ccb623122d2bdae6f380a41c9e06b646e830372c30a4b88261238550e0ad69d53f7af8883cb705d8237fdd22947e84913b149c - languageName: node - linkType: hard - "commander@npm:^10.0.1": version: 10.0.1 resolution: "commander@npm:10.0.1" @@ -12271,13 +11718,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.1": - version: 0.4.2 - resolution: "cookie@npm:0.4.2" - checksum: 10c0/beab41fbd7c20175e3a2799ba948c1dcc71ef69f23fe14eeeff59fc09f50c517b0f77098db87dbb4c55da802f9d86ee86cdc1cd3efd87760341551838d53fca2 - languageName: node - linkType: hard - "copy-descriptor@npm:^0.1.0": version: 0.1.1 resolution: "copy-descriptor@npm:0.1.1" @@ -12824,7 +12264,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:~4.3.1, debug@npm:~4.3.2": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.5 resolution: "debug@npm:4.3.5" dependencies: @@ -13416,13 +12856,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.2.0": - version: 5.2.0 - resolution: "diff@npm:5.2.0" - checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 - languageName: node - linkType: hard - "diffie-hellman@npm:^5.0.0": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -13836,7 +13269,7 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.0, enquirer@npm:^2.3.5, enquirer@npm:^2.3.6": +"enquirer@npm:^2.3.5, enquirer@npm:^2.3.6": version: 2.4.1 resolution: "enquirer@npm:2.4.1" dependencies: @@ -14556,7 +13989,7 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:0.1.3, ethereum-cryptography@npm:^0.1.3": +"ethereum-cryptography@npm:^0.1.3": version: 0.1.3 resolution: "ethereum-cryptography@npm:0.1.3" dependencies: @@ -14579,18 +14012,6 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:^1.0.3": - version: 1.2.0 - resolution: "ethereum-cryptography@npm:1.2.0" - dependencies: - "@noble/hashes": "npm:1.2.0" - "@noble/secp256k1": "npm:1.7.1" - "@scure/bip32": "npm:1.1.5" - "@scure/bip39": "npm:1.1.1" - checksum: 10c0/93e486a4a8b266dc2f274b69252e751345ef47551163371939b01231afb7b519133e2572b5975bb9cb4cc77ac54ccd36002c7c759a72488abeeaf216e4d55b46 - languageName: node - linkType: hard - "ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2": version: 2.2.1 resolution: "ethereum-cryptography@npm:2.2.1" @@ -14603,17 +14024,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-abi@npm:^0.6.8": - version: 0.6.8 - resolution: "ethereumjs-abi@npm:0.6.8" - dependencies: - bn.js: "npm:^4.11.8" - ethereumjs-util: "npm:^6.0.0" - checksum: 10c0/a7ff1917625e3c812cb3bca6c1231fc0ece282cc7d202d60545a9c31cd379fd751bfed5ff78dae4279cb1ba4d0e8967f9fdd4f135a334a38dbf04e7afd0c4bcf - languageName: node - linkType: hard - -"ethereumjs-util@npm:6.2.1, ethereumjs-util@npm:^6.0.0, ethereumjs-util@npm:^6.2.1": +"ethereumjs-util@npm:6.2.1": version: 6.2.1 resolution: "ethereumjs-util@npm:6.2.1" dependencies: @@ -14657,7 +14068,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:5.7.2, ethers@npm:^5.7.1": +"ethers@npm:5.7.2": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -15416,15 +14827,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^2.1.0": - version: 2.1.0 - resolution: "find-up@npm:2.1.0" - dependencies: - locate-path: "npm:^2.0.0" - checksum: 10c0/c080875c9fe28eb1962f35cbe83c683796a0321899f1eed31a37577800055539815de13d53495049697d3ba313013344f843bb9401dd337a1b832be5edfc6840 - languageName: node - linkType: hard - "find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -15539,7 +14941,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.12.1, follow-redirects@npm:^1.15.6": +"follow-redirects@npm:^1.15.6": version: 1.15.6 resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: @@ -15636,20 +15038,6 @@ __metadata: languageName: node linkType: hard -"fp-ts@npm:1.19.3": - version: 1.19.3 - resolution: "fp-ts@npm:1.19.3" - checksum: 10c0/a016cfc98ad5e61564ab2d53a5379bbb8254642b66df13ced47e8c1d8d507abf4588d8bb43530198dfe1907211d8bae8f112cab52ba0ac6ab055da9168a6e260 - languageName: node - linkType: hard - -"fp-ts@npm:^1.0.0": - version: 1.19.5 - resolution: "fp-ts@npm:1.19.5" - checksum: 10c0/2a330fa1779561307740c26a7255fdffeb1ca2d0c7448d4dc094b477b772b0c8f7da1dfc88569b6f13f6958169b63b5df7361e514535b46b2e215bbf03a3722d - languageName: node - linkType: hard - "fragment-cache@npm:^0.2.1": version: 0.2.1 resolution: "fragment-cache@npm:0.2.1" @@ -15740,19 +15128,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^0.30.0": - version: 0.30.0 - resolution: "fs-extra@npm:0.30.0" - dependencies: - graceful-fs: "npm:^4.1.2" - jsonfile: "npm:^2.1.0" - klaw: "npm:^1.0.0" - path-is-absolute: "npm:^1.0.0" - rimraf: "npm:^2.2.8" - checksum: 10c0/24f3c966018c7bf436bf38ca3a126f1d95bf0f82598302195c4f0c8887767f045dae308f92c53a39cead74631dabbc30fcf1c71dbe96f1f0148f6de8edd114bc - languageName: node - linkType: hard - "fs-extra@npm:^11.0.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -15764,17 +15139,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^7.0.1": - version: 7.0.1 - resolution: "fs-extra@npm:7.0.1" - dependencies: - graceful-fs: "npm:^4.1.2" - jsonfile: "npm:^4.0.0" - universalify: "npm:^0.1.0" - checksum: 10c0/1943bb2150007e3739921b8d13d4109abdc3cc481e53b97b7ea7f77eda1c3c642e27ae49eac3af074e3496ea02fde30f411ef410c760c70a38b92e656e5da784 - languageName: node - linkType: hard - "fs-extra@npm:^8.1.0, fs-extra@npm:~8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -16122,20 +15486,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.0": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^3.0.4" - once: "npm:^1.3.0" - path-is-absolute: "npm:^1.0.0" - checksum: 10c0/478b40e38be5a3d514e64950e1e07e0ac120585add6a37c98d0ed24d72d9127d734d2a125786073c8deb687096e84ae82b641c441a869ada3a9cc91b68978632 - languageName: node - linkType: hard - "glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.2": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -16179,7 +15529,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1, glob@npm:^8.0.3, glob@npm:^8.1.0": +"glob@npm:^8.0.1, glob@npm:^8.0.3": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -16311,7 +15661,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -16421,72 +15771,6 @@ __metadata: languageName: node linkType: hard -"hardhat@npm:2.18.1": - version: 2.18.1 - resolution: "hardhat@npm:2.18.1" - dependencies: - "@ethersproject/abi": "npm:^5.1.2" - "@metamask/eth-sig-util": "npm:^4.0.0" - "@nomicfoundation/ethereumjs-block": "npm:5.0.2" - "@nomicfoundation/ethereumjs-blockchain": "npm:7.0.2" - "@nomicfoundation/ethereumjs-common": "npm:4.0.2" - "@nomicfoundation/ethereumjs-evm": "npm:2.0.2" - "@nomicfoundation/ethereumjs-rlp": "npm:5.0.2" - "@nomicfoundation/ethereumjs-statemanager": "npm:2.0.2" - "@nomicfoundation/ethereumjs-trie": "npm:6.0.2" - "@nomicfoundation/ethereumjs-tx": "npm:5.0.2" - "@nomicfoundation/ethereumjs-util": "npm:9.0.2" - "@nomicfoundation/ethereumjs-vm": "npm:7.0.2" - "@nomicfoundation/solidity-analyzer": "npm:^0.1.0" - "@sentry/node": "npm:^5.18.1" - "@types/bn.js": "npm:^5.1.0" - "@types/lru-cache": "npm:^5.1.0" - adm-zip: "npm:^0.4.16" - aggregate-error: "npm:^3.0.0" - ansi-escapes: "npm:^4.3.0" - chalk: "npm:^2.4.2" - chokidar: "npm:^3.4.0" - ci-info: "npm:^2.0.0" - debug: "npm:^4.1.1" - enquirer: "npm:^2.3.0" - env-paths: "npm:^2.2.0" - ethereum-cryptography: "npm:^1.0.3" - ethereumjs-abi: "npm:^0.6.8" - find-up: "npm:^2.1.0" - fp-ts: "npm:1.19.3" - fs-extra: "npm:^7.0.1" - glob: "npm:7.2.0" - immutable: "npm:^4.0.0-rc.12" - io-ts: "npm:1.10.4" - keccak: "npm:^3.0.2" - lodash: "npm:^4.17.11" - mnemonist: "npm:^0.38.0" - mocha: "npm:^10.0.0" - p-map: "npm:^4.0.0" - raw-body: "npm:^2.4.1" - resolve: "npm:1.17.0" - semver: "npm:^6.3.0" - solc: "npm:0.7.3" - source-map-support: "npm:^0.5.13" - stacktrace-parser: "npm:^0.1.10" - tsort: "npm:0.0.1" - undici: "npm:^5.14.0" - uuid: "npm:^8.3.2" - ws: "npm:^7.4.6" - peerDependencies: - ts-node: "*" - typescript: "*" - peerDependenciesMeta: - ts-node: - optional: true - typescript: - optional: true - bin: - hardhat: internal/cli/bootstrap.js - checksum: 10c0/9d6186ea9f91fe63b46b6ae9009eea2bdf4edbf722c71becb7477f73a6824bf5b227b77107cc3dcc4915a3ce38c68706fd76a3537753923d9443016c8087af85 - languageName: node - linkType: hard - "has-ansi@npm:^2.0.0": version: 2.0.0 resolution: "has-ansi@npm:2.0.0" @@ -16642,7 +15926,7 @@ __metadata: languageName: node linkType: hard -"he@npm:1.2.0, he@npm:^1.2.0": +"he@npm:1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" bin: @@ -16882,7 +16166,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.17": +"iconv-lite@npm:^0.4.17": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -16992,13 +16276,6 @@ __metadata: languageName: node linkType: hard -"immutable@npm:^4.0.0-rc.12": - version: 4.3.6 - resolution: "immutable@npm:4.3.6" - checksum: 10c0/7d0952a768b4fadcee47230ed86dc9505a4517095eceaf5a47e65288571c42400c6e4a2ae21eca4eda957cb7bc50720213135b62cf6a181639111f8acae128c3 - languageName: node - linkType: hard - "import-fresh@npm:^2.0.0": version: 2.0.0 resolution: "import-fresh@npm:2.0.0" @@ -17169,15 +16446,6 @@ __metadata: languageName: node linkType: hard -"io-ts@npm:1.10.4": - version: 1.10.4 - resolution: "io-ts@npm:1.10.4" - dependencies: - fp-ts: "npm:^1.0.0" - checksum: 10c0/9370988a7e17fc23c194115808168ccd1ccf7b7ebe92c39c1cc2fd91c1dc641552a5428bb04fe28c01c826fa4f230e856eb4f7d27c774a1400af3fecf2936ab5 - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -17294,13 +16562,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.5": - version: 2.0.5 - resolution: "is-buffer@npm:2.0.5" - checksum: 10c0/e603f6fced83cf94c53399cff3bda1a9f08e391b872b64a73793b0928be3e5f047f2bcece230edb7632eaea2acdbfcb56c23b33d8a20c820023b230f1485679a - languageName: node - linkType: hard - "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" @@ -18512,13 +17773,6 @@ __metadata: languageName: node linkType: hard -"js-sdsl@npm:^4.1.4": - version: 4.4.2 - resolution: "js-sdsl@npm:4.4.2" - checksum: 10c0/50707728fc31642164f4d83c8087f3750aaa99c450b008b19e236a1f190c9e48f9fc799615c341f9ca2c0803b15ab6f48d92a9cc3e6ffd20065cba7d7e742b92 - languageName: node - linkType: hard - "js-sha256@npm:^0.9.0": version: 0.9.0 resolution: "js-sha256@npm:0.9.0" @@ -18882,7 +18136,7 @@ __metadata: languageName: node linkType: hard -"keccak@npm:^3.0.0, keccak@npm:^3.0.2, keccak@npm:^3.0.3": +"keccak@npm:^3.0.0, keccak@npm:^3.0.3": version: 3.0.4 resolution: "keccak@npm:3.0.4" dependencies: @@ -18967,18 +18221,6 @@ __metadata: languageName: node linkType: hard -"klaw@npm:^1.0.0": - version: 1.3.1 - resolution: "klaw@npm:1.3.1" - dependencies: - graceful-fs: "npm:^4.1.9" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10c0/da994768b02b3843cc994c99bad3cf1c8c67716beb4dd2834133c919e9e9ee788669fbe27d88ab0ad9a3991349c28280afccbde01c2318229b662dd7a05e4728 - languageName: node - linkType: hard - "kleur@npm:^3.0.3": version: 3.0.3 resolution: "kleur@npm:3.0.3" @@ -19074,34 +18316,6 @@ __metadata: languageName: node linkType: hard -"level-supports@npm:^4.0.0": - version: 4.0.1 - resolution: "level-supports@npm:4.0.1" - checksum: 10c0/a94aa591786845d17c9c62ad075ae33e0fce5be714baa6e16305ed14e2d3638d09e724247fa3f63951e36de57ffd168d63e159e79d03944ee648054b8c7c1684 - languageName: node - linkType: hard - -"level-transcoder@npm:^1.0.1": - version: 1.0.1 - resolution: "level-transcoder@npm:1.0.1" - dependencies: - buffer: "npm:^6.0.3" - module-error: "npm:^1.0.1" - checksum: 10c0/25936330676325f22c5143aff5c7fe3f1db156db99f9efb07a2642045c2c6ee565fcbfccbadc0600b3abf8bbe595632cacc3dd334009214069d1857daa57987e - languageName: node - linkType: hard - -"level@npm:^8.0.0": - version: 8.0.1 - resolution: "level@npm:8.0.1" - dependencies: - abstract-level: "npm:^1.0.4" - browser-level: "npm:^1.0.1" - classic-level: "npm:^1.2.0" - checksum: 10c0/52e3c18a3372f22b7c0c96a998a24099454f51952ba2b8f25eabc72b1f7bbc15a3ab480c92c94d3c931be370be5a235b804b16e565b73b22bea52cca4029a625 - languageName: node - linkType: hard - "levelup@npm:^0.18.2": version: 0.18.6 resolution: "levelup@npm:0.18.6" @@ -19363,16 +18577,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^2.0.0": - version: 2.0.0 - resolution: "locate-path@npm:2.0.0" - dependencies: - p-locate: "npm:^2.0.0" - path-exists: "npm:^3.0.0" - checksum: 10c0/24efa0e589be6aa3c469b502f795126b26ab97afa378846cb508174211515633b770aa0ba610cab113caedab8d2a4902b061a08aaed5297c12ab6f5be4df0133 - languageName: node - linkType: hard - "locate-path@npm:^3.0.0": version: 3.0.0 resolution: "locate-path@npm:3.0.0" @@ -19558,13 +18762,6 @@ __metadata: languageName: node linkType: hard -"lru_map@npm:^0.3.3": - version: 0.3.3 - resolution: "lru_map@npm:0.3.3" - checksum: 10c0/d861f14a142a4a74ebf8d3ad57f2e768a5b820db4100ae53eed1a64eb6350912332e6ebc87cb7415ad6d0cd8f3ce6d20beab9a5e6042ccb5996ea0067a220448 - languageName: node - linkType: hard - "ltgt@npm:^2.1.3": version: 2.2.1 resolution: "ltgt@npm:2.2.1" @@ -19757,13 +18954,6 @@ __metadata: languageName: node linkType: hard -"mcl-wasm@npm:^0.7.1": - version: 0.7.9 - resolution: "mcl-wasm@npm:0.7.9" - checksum: 10c0/12acd074621741ac61f4b3d36d72da6317320b5db02734abaaf77c0c7886ced14926de2f637ca9ab70a458419200d7edb8e0a4f9f02c85feb8d5bbbe430e60ad - languageName: node - linkType: hard - "md5-file@npm:^3.2.3": version: 3.2.3 resolution: "md5-file@npm:3.2.3" @@ -19860,24 +19050,6 @@ __metadata: languageName: node linkType: hard -"memory-level@npm:^1.0.0": - version: 1.0.0 - resolution: "memory-level@npm:1.0.0" - dependencies: - abstract-level: "npm:^1.0.0" - functional-red-black-tree: "npm:^1.0.1" - module-error: "npm:^1.0.1" - checksum: 10c0/b926b6ddc43065282c240cd7c0bf44abcfe43d556f6bb3d43d21f5f514b0095abcd8f9ba26b31ffdefa4ce4931afb937a1eaea1f15c45e76d7061086dbcf9148 - languageName: node - linkType: hard - -"memorystream@npm:^0.3.1": - version: 0.3.1 - resolution: "memorystream@npm:0.3.1" - checksum: 10c0/4bd164657711d9747ff5edb0508b2944414da3464b7fe21ac5c67cf35bba975c4b446a0124bd0f9a8be54cfc18faf92e92bd77563a20328b1ccf2ff04e9f39b9 - languageName: node - linkType: hard - "merge-deep@npm:^3.0.2": version: 3.0.3 resolution: "merge-deep@npm:3.0.3" @@ -20312,7 +19484,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0, minimatch@npm:^5.1.6": +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -20550,46 +19722,6 @@ __metadata: languageName: node linkType: hard -"mnemonist@npm:^0.38.0": - version: 0.38.5 - resolution: "mnemonist@npm:0.38.5" - dependencies: - obliterator: "npm:^2.0.0" - checksum: 10c0/a73a2718f88cd12c3b108ecc530619a1b0f2783d479c7f98e7367375102cc3a28811bab384e17eb731553dc8d7ee9d60283d694a9f676af5f306104e75027d4f - languageName: node - linkType: hard - -"mocha@npm:^10.0.0": - version: 10.6.0 - resolution: "mocha@npm:10.6.0" - dependencies: - ansi-colors: "npm:^4.1.3" - browser-stdout: "npm:^1.3.1" - chokidar: "npm:^3.5.3" - debug: "npm:^4.3.5" - diff: "npm:^5.2.0" - escape-string-regexp: "npm:^4.0.0" - find-up: "npm:^5.0.0" - glob: "npm:^8.1.0" - he: "npm:^1.2.0" - js-yaml: "npm:^4.1.0" - log-symbols: "npm:^4.1.0" - minimatch: "npm:^5.1.6" - ms: "npm:^2.1.3" - serialize-javascript: "npm:^6.0.2" - strip-json-comments: "npm:^3.1.1" - supports-color: "npm:^8.1.1" - workerpool: "npm:^6.5.1" - yargs: "npm:^16.2.0" - yargs-parser: "npm:^20.2.9" - yargs-unparser: "npm:^2.0.0" - bin: - _mocha: bin/_mocha - mocha: bin/mocha.js - checksum: 10c0/30b2f810014af6b5701563c6ee6ee78708dcfefc1551801c70018682bc6ca9327a6a27e93c101905a355d130a1ffe1f990975d51459c289bfcb72726ea5f7a50 - languageName: node - linkType: hard - "module-definition@npm:^3.0.0, module-definition@npm:^3.3.0": version: 3.4.0 resolution: "module-definition@npm:3.4.0" @@ -20602,13 +19734,6 @@ __metadata: languageName: node linkType: hard -"module-error@npm:^1.0.1, module-error@npm:^1.0.2": - version: 1.0.2 - resolution: "module-error@npm:1.0.2" - checksum: 10c0/584a43a1bb2720c6c6c771e257a308af4f042a17c17b1472a2c855130a1ad93ba516a82ae7ac2ce2d03062e521cc53c03ec0ce153795d895312d7747fb3bb99b - languageName: node - linkType: hard - "module-lookup-amd@npm:^6.1.0": version: 6.2.0 resolution: "module-lookup-amd@npm:6.2.0" @@ -20664,7 +19789,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -20857,13 +19982,6 @@ __metadata: languageName: node linkType: hard -"napi-macros@npm:^2.2.2": - version: 2.2.2 - resolution: "napi-macros@npm:2.2.2" - checksum: 10c0/cc85daaf82a4f585d30561047cef0f3e702be769b5cf2ffadc6242bc5a1033f6b8269012e54178baf66f022bd18aa9ebb619f1b530cc19c1f9b96f9689affd50 - languageName: node - linkType: hard - "napi-wasm@npm:^1.1.0": version: 1.1.0 resolution: "napi-wasm@npm:1.1.0" @@ -21043,7 +20161,7 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0": +"node-gyp-build@npm:^4.2.0": version: 4.8.1 resolution: "node-gyp-build@npm:4.8.1" bin: @@ -21453,13 +20571,6 @@ __metadata: languageName: node linkType: hard -"obliterator@npm:^2.0.0": - version: 2.0.4 - resolution: "obliterator@npm:2.0.4" - checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40 - languageName: node - linkType: hard - "octal@npm:^1.0.0": version: 1.0.0 resolution: "octal@npm:1.0.0" @@ -21733,15 +20844,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^1.1.0": - version: 1.3.0 - resolution: "p-limit@npm:1.3.0" - dependencies: - p-try: "npm:^1.0.0" - checksum: 10c0/5c1b1d53d180b2c7501efb04b7c817448e10efe1ba46f4783f8951994d5027e4cd88f36ad79af50546682594c4ebd11702ac4b9364c47f8074890e2acad0edee - languageName: node - linkType: hard - "p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -21760,15 +20862,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^2.0.0": - version: 2.0.0 - resolution: "p-locate@npm:2.0.0" - dependencies: - p-limit: "npm:^1.1.0" - checksum: 10c0/82da4be88fb02fd29175e66021610c881938d3cc97c813c71c1a605fac05617d57fd5d3b337494a6106c0edb2a37c860241430851411f1b265108cead34aee67 - languageName: node - linkType: hard - "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -21812,13 +20905,6 @@ __metadata: languageName: node linkType: hard -"p-try@npm:^1.0.0": - version: 1.0.0 - resolution: "p-try@npm:1.0.0" - checksum: 10c0/757ba31de5819502b80c447826fac8be5f16d3cb4fbf9bc8bc4971dba0682e84ac33e4b24176ca7058c69e29f64f34d8d9e9b08e873b7b7bb0aa89d620fa224a - languageName: node - linkType: hard - "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -22074,7 +21160,7 @@ __metadata: languageName: node linkType: hard -"path-parse@npm:^1.0.5, path-parse@npm:^1.0.6, path-parse@npm:^1.0.7": +"path-parse@npm:^1.0.5, path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 @@ -22889,7 +21975,7 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": +"queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 @@ -22952,18 +22038,6 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:^2.4.1": - version: 2.5.2 - resolution: "raw-body@npm:2.5.2" - dependencies: - bytes: "npm:3.1.2" - http-errors: "npm:2.0.0" - iconv-lite: "npm:0.4.24" - unpipe: "npm:1.0.0" - checksum: 10c0/b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4 - languageName: node - linkType: hard - "rc@npm:^1.2.7, rc@npm:~1.2.7": version: 1.2.8 resolution: "rc@npm:1.2.8" @@ -24492,7 +23566,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"require-from-string@npm:^2.0.0, require-from-string@npm:^2.0.2": +"require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2 @@ -24634,15 +23708,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"resolve@npm:1.17.0": - version: 1.17.0 - resolution: "resolve@npm:1.17.0" - dependencies: - path-parse: "npm:^1.0.6" - checksum: 10c0/4e6c76cc1a7b08bff637b092ce035d7901465067915605bc5a23ac0c10fe42ec205fc209d5d5f7a5f27f37ce71d687def7f656bbb003631cd46a8374f55ec73d - languageName: node - linkType: hard - "resolve@npm:1.22.8, resolve@npm:^1.11.1, resolve@npm:^1.14.2, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:^1.22.2, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.8 resolution: "resolve@npm:1.22.8" @@ -24678,15 +23743,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A1.17.0#optional!builtin": - version: 1.17.0 - resolution: "resolve@patch:resolve@npm%3A1.17.0#optional!builtin::version=1.17.0&hash=c3c19d" - dependencies: - path-parse: "npm:^1.0.6" - checksum: 10c0/e072e52be3c3dbfd086761115db4a5136753e7aefc0e665e66e7307ddcd9d6b740274516055c74aee44921625e95993f03570450aa310b8d73b1c9daa056c4cd - languageName: node - linkType: hard - "resolve@patch:resolve@npm%3A1.22.8#optional!builtin, resolve@patch:resolve@npm%3A^1.11.1#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.3#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" @@ -24871,15 +23927,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"run-parallel-limit@npm:^1.1.0": - version: 1.1.0 - resolution: "run-parallel-limit@npm:1.1.0" - dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10c0/9c78eb77e788d0ed803a7e80921412f6f6accfb2006de8c21699d9ebf7696df9cefaa313fe14d6169a3fc9f564b34fe91bfd9948cc3a58e2d24136a2390523ae - languageName: node - linkType: hard - "run-parallel@npm:^1.1.2, run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -24889,13 +23936,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"rustbn.js@npm:~0.2.0": - version: 0.2.0 - resolution: "rustbn.js@npm:0.2.0" - checksum: 10c0/be2d55d4a53465cfd5c7900153cfae54c904f0941acd30191009cf473cacbfcf45082ffd8dc473a354c8e3dcfe2c2bdf5d7ea9cc9b188d892b4aa8d012b94701 - languageName: node - linkType: hard - "rx@npm:^4.1.0": version: 4.1.0 resolution: "rx@npm:4.1.0" @@ -25231,7 +24271,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.1, serialize-javascript@npm:^6.0.2": +"serialize-javascript@npm:^6.0.1": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" dependencies: @@ -25677,25 +24717,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"solc@npm:0.7.3": - version: 0.7.3 - resolution: "solc@npm:0.7.3" - dependencies: - command-exists: "npm:^1.2.8" - commander: "npm:3.0.2" - follow-redirects: "npm:^1.12.1" - fs-extra: "npm:^0.30.0" - js-sha3: "npm:0.8.0" - memorystream: "npm:^0.3.1" - require-from-string: "npm:^2.0.0" - semver: "npm:^5.5.0" - tmp: "npm:0.0.33" - bin: - solcjs: solcjs - checksum: 10c0/28405adfba1f55603dc5b674630383bfbdbfab2d36deba2ff0a90c46cbc346bcabf0ed6175e12ae3c0b751ef082d0405ab42dcc24f88603a446e097a925d7425 - languageName: node - linkType: hard - "sonic-boom@npm:^2.2.1": version: 2.8.0 resolution: "sonic-boom@npm:2.8.0" @@ -25761,7 +24782,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"source-map-support@npm:^0.5.13, source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20, source-map-support@npm:~0.5.21": +"source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20, source-map-support@npm:~0.5.21": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -26423,7 +25444,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": +"supports-color@npm:^8.0.0": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -26857,7 +25878,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"tmp@npm:0.0.33, tmp@npm:^0.0.33": +"tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" dependencies: @@ -27130,7 +26151,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"tslib@npm:1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": +"tslib@npm:1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 @@ -27144,13 +26165,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"tsort@npm:0.0.1": - version: 0.0.1 - resolution: "tsort@npm:0.0.1" - checksum: 10c0/ea3d034ab341dd9282c972710496e98539408d77f1cd476ad0551a9731f40586b65ab917b39745f902bf32037a3161eee3821405f6ab15bcd2ce4cc0a52d1da6 - languageName: node - linkType: hard - "tsutils@npm:3, tsutils@npm:^3.17.1, tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -27501,15 +26515,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"undici@npm:^5.14.0": - version: 5.28.4 - resolution: "undici@npm:5.28.4" - dependencies: - "@fastify/busboy": "npm:^2.0.0" - checksum: 10c0/08d0f2596553aa0a54ca6e8e9c7f45aef7d042c60918564e3a142d449eda165a80196f6ef19ea2ef2e6446959e293095d8e40af1236f0d67223b06afac5ecad7 - languageName: node - linkType: hard - "unenv@npm:^1.9.0": version: 1.10.0 resolution: "unenv@npm:1.10.0" @@ -27648,7 +26653,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"unpipe@npm:1.0.0, unpipe@npm:~1.0.0": +"unpipe@npm:~1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c @@ -28499,13 +27504,6 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"workerpool@npm:^6.5.1": - version: 6.5.1 - resolution: "workerpool@npm:6.5.1" - checksum: 10c0/58e8e969782292cb3a7bfba823f1179a7615250a0cefb4841d5166234db1880a3d0fe83a31dd8d648329ec92c2d0cd1890ad9ec9e53674bb36ca43e9753cdeac - languageName: node - linkType: hard - "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -28657,7 +27655,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.0.0, ws@npm:^7.4.6, ws@npm:^7.5.1": +"ws@npm:^7, ws@npm:^7.0.0, ws@npm:^7.5.1": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -28882,7 +27880,7 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": +"yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72