From c035b9f3519c5cdf9ddf9329b6ce670a1c1dc6b4 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 18 Oct 2024 13:11:45 -0400 Subject: [PATCH 01/45] remove useState that is getting wiped between renders --- src/components/backup/useCreateBackup.ts | 33 ++++++++++++------------ src/hooks/useWalletCloudBackup.ts | 5 ++-- src/model/backup.ts | 4 +-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 92d62c01de2..8d9b6c68ad9 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -38,8 +38,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const [loading, setLoading] = useState('none'); - const [password, setPassword] = useState(''); - const setLoadingStateWithTimeout = useCallback( (state: useCreateBackupStateType, resetInMS = 2500) => { setLoading(state); @@ -49,18 +47,21 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr }, [setLoading] ); - 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 onSuccess = useCallback( + async (password: string) => { + const hasSavedPassword = await getLocalBackupPassword(); + if (!hasSavedPassword && password.trim()) { + await saveLocalBackupPassword(password); + } + analytics.track('Backup Complete', { + category: 'backup', + label: cloudPlatform, + }); + setLoadingStateWithTimeout('success'); + fetchBackups(); + }, + [setLoadingStateWithTimeout, fetchBackups] + ); const onError = useCallback( (msg: string) => { @@ -102,7 +103,7 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr await walletCloudBackup({ onError, - onSuccess, + onSuccess: (password: string) => onSuccess(password), password, walletId, }); @@ -117,7 +118,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr const getPassword = useCallback(async (): Promise => { const password = await getLocalBackupPassword(); if (password) { - setPassword(password); return password; } @@ -126,7 +126,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr nativeScreen: true, step: walletBackupStepTypes.backup_cloud, onSuccess: async (password: string) => { - setPassword(password); resolve(password); }, onCancel: async () => { diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 57b9caac681..75c7e42b8a6 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,4 +1,3 @@ -import { captureException } from '@sentry/react-native'; import lang from 'i18n-js'; import { values } from 'lodash'; import { useCallback, useMemo } from 'react'; @@ -53,7 +52,7 @@ export default function useWalletCloudBackup() { handleNoLatestBackup?: () => void; handlePasswordNotFound?: () => void; onError?: (error: string) => void; - onSuccess?: () => void; + onSuccess?: (password: string) => void; password: string; walletId: string; }): Promise => { @@ -134,7 +133,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}`)); diff --git a/src/model/backup.ts b/src/model/backup.ts index 2eb50a7c297..d1be352281a 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -109,7 +109,7 @@ export async function backupAllWalletsToCloud({ password: BackupPassword; latestBackup: string | null; onError?: (message: string) => void; - onSuccess?: () => void; + onSuccess?: (password: BackupPassword) => void; dispatch: any; }) { let userPIN: string | undefined; @@ -209,7 +209,7 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - onSuccess?.(); + onSuccess?.(password); } catch (error: any) { const userError = getUserError(error); onError?.(userError); From ea2fd0975f1206fd467f259e462ad4b7093aac1d Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Sat, 19 Oct 2024 16:55:21 -0400 Subject: [PATCH 02/45] lots of provider structure changes --- .../backup/AddWalletToCloudBackupStep.tsx | 34 ++-- .../backup/BackupChooseProviderStep.tsx | 30 ++- src/components/backup/BackupCloudStep.tsx | 5 +- src/components/backup/BackupSheet.tsx | 27 ++- src/components/backup/CloudBackupProvider.tsx | 83 +++++--- src/components/backup/useCreateBackup.ts | 96 +++++---- src/handlers/cloudBackup.ts | 4 +- src/navigation/Routes.android.tsx | 9 +- src/navigation/Routes.ios.tsx | 9 +- src/screens/SettingsSheet/SettingsSheet.tsx | 185 +++++++++--------- .../components/Backups/BackUpMenuButton.tsx | 34 ++-- .../components/Backups/ViewWalletBackup.tsx | 26 +-- .../components/Backups/WalletsAndBackup.tsx | 37 ++-- src/screens/SettingsSheet/utils.ts | 4 + 14 files changed, 322 insertions(+), 261 deletions(-) diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx index 62f92a99e2f..3283d51fa82 100644 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ b/src/components/backup/AddWalletToCloudBackupStep.tsx @@ -11,8 +11,9 @@ 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'; +import { useCloudBackupsContext } from './CloudBackupProvider'; +import { BackupTypes } from '@/components/backup/useCreateBackup'; const imageSize = 72; @@ -20,6 +21,8 @@ export default function AddWalletToCloudBackupStep() { const { goBack } = useNavigation(); const { wallets, selectedWallet } = useWallets(); + const { createBackup } = useCloudBackupsContext(); + const walletTypeCount: WalletCountPerType = { phrase: 0, privateKey: 0, @@ -27,20 +30,23 @@ export default function AddWalletToCloudBackupStep() { 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 result = await createBackup({ + type: BackupTypes.Single, + walletId: selectedWallet.id, + navigateToRoute: { + route: Routes.SETTINGS_SHEET, + params: { + screen: Routes.SETTINGS_SECTION_BACKUP, + }, + }, + }); + + if (result) { + goBack(); + } + }, [createBackup, goBack, selectedWallet.id]); const onMaybeLater = useCallback(() => goBack(), [goBack]); @@ -70,7 +76,7 @@ export default function AddWalletToCloudBackupStep() { - potentiallyLoginAndSubmit().then(success => success && goBack())}> + diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupChooseProviderStep.tsx index 38325639704..3bca963c06f 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupChooseProviderStep.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; import * as lang from '@/languages'; import { ImgixImage } from '../images'; @@ -21,6 +20,8 @@ import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, import { WrappedAlert as Alert } from '@/helpers/alert'; import { RainbowError, logger } from '@/logger'; import { Linking } from 'react-native'; +import { CloudBackupState, useCloudBackupsContext } from './CloudBackupProvider'; +import { BackupTypes } from '@/components/backup/useCreateBackup'; const imageSize = 72; @@ -28,21 +29,9 @@ export default function BackupSheetSectionNoProvider() { const { colors } = useTheme(); const { navigate, goBack } = useNavigation(); const { selectedWallet } = useWallets(); - - const { onSubmit, loading } = useCreateBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }); + const { createBackup, backupState } = useCloudBackupsContext(); 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) { @@ -83,7 +72,16 @@ export default function BackupSheetSectionNoProvider() { } } - onSubmit({}); + createBackup({ + type: BackupTypes.Single, + walletId: selectedWallet.id, + navigateToRoute: { + route: Routes.SETTINGS_SHEET, + params: { + screen: Routes.SETTINGS_SECTION_BACKUP, + }, + }, + }); }; const onManualBackup = async () => { @@ -117,7 +115,7 @@ export default function BackupSheetSectionNoProvider() { {/* replace this with BackUpMenuButton */} - + diff --git a/src/components/backup/BackupCloudStep.tsx b/src/components/backup/BackupCloudStep.tsx index e839d9323c0..cb0c4dde868 100644 --- a/src/components/backup/BackupCloudStep.tsx +++ b/src/components/backup/BackupCloudStep.tsx @@ -11,7 +11,7 @@ import { Text } from '@/components/text'; import WalletAndBackup from '@/assets/WalletsAndBackup.png'; import { analytics } from '@/analytics'; import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid } from '@/handlers/cloudBackup'; -import { useDimensions, useMagicAutofocus, useWallets } from '@/hooks'; +import { useDimensions, useMagicAutofocus } from '@/hooks'; import styled from '@/styled-thing'; import { padding } from '@/styles'; import { Box, Inset, Stack } from '@/design-system'; @@ -23,9 +23,6 @@ import { usePasswordValidation } from './usePasswordValidation'; import { TextInput } from 'react-native'; import { useTheme } from '@/theme'; import { useNavigation } from '@/navigation'; -import Routes from '@/navigation/routesNames'; -import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes'; -import walletTypes from '@/helpers/walletTypes'; type BackupCloudStepParams = { BackupCloudStep: { diff --git a/src/components/backup/BackupSheet.tsx b/src/components/backup/BackupSheet.tsx index 5b6a0a4300a..b21487470e2 100644 --- a/src/components/backup/BackupSheet.tsx +++ b/src/components/backup/BackupSheet.tsx @@ -8,7 +8,6 @@ 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: { @@ -40,19 +39,17 @@ export default function BackupSheet() { }, [step]); return ( - - - {({ backgroundColor }) => ( - - {renderStep()} - - )} - - + + {({ backgroundColor }) => ( + + {renderStep()} + + )} + ); } diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx index 377e9d13a83..a8256223a71 100644 --- a/src/components/backup/CloudBackupProvider.tsx +++ b/src/components/backup/CloudBackupProvider.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'; +import React, { PropsWithChildren, createContext, useCallback, useContext, useEffect, useState } from 'react'; import type { BackupUserData, CloudBackups } from '@/model/backup'; import { fetchAllBackups, @@ -9,76 +9,105 @@ import { } from '@/handlers/cloudBackup'; import { RainbowError, logger } from '@/logger'; import { IS_ANDROID } from '@/env'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; type CloudBackupContext = { - isFetching: boolean; + backupState: CloudBackupState; backups: CloudBackups; - fetchBackups: () => Promise; userData: BackupUserData | undefined; + createBackup: ReturnType; }; +export enum CloudBackupState { + Initializing = 'initializing', + Syncing = 'syncing', + Fetching = 'fetching', + FailedToInitialize = 'failed_to_initialize', // Failed to initialize cloud backup + Ready = 'ready', + NotAvailable = 'not_available', // iCloud / Google Drive not available + InProgress = 'in_progress', // Backup in progress + Error = 'error', + Success = 'success', +} + const CloudBackupContext = createContext({} as CloudBackupContext); export function CloudBackupProvider({ children }: PropsWithChildren) { - const [isFetching, setIsFetching] = useState(false); + const [backupState, setBackupState] = useState(CloudBackupState.Initializing); + + const [userData, setUserData] = useState(); const [backups, setBackups] = useState({ files: [], }); - const [userData, setUserData] = useState(); - - const fetchBackups = async () => { + const syncAndFetchBackups = useCallback(async () => { try { - setIsFetching(true); const isAvailable = await isCloudBackupAvailable(); if (!isAvailable) { logger.debug('[CloudBackupProvider]: Cloud backup is not available'); - setIsFetching(false); + setBackupState(CloudBackupState.NotAvailable); return; } if (IS_ANDROID) { const gdata = await getGoogleAccountUserData(); if (!gdata) { + logger.debug('[CloudBackupProvider]: Google account is not available'); + setBackupState(CloudBackupState.NotAvailable); return; } } + setBackupState(CloudBackupState.Syncing); logger.debug('[CloudBackupProvider]: Syncing with cloud'); await syncCloud(); + setBackupState(CloudBackupState.Fetching); logger.debug('[CloudBackupProvider]: Fetching user data'); - const userData = await fetchUserDataFromCloud(); + const [userData, backupFiles] = await Promise.all([fetchUserDataFromCloud(), fetchAllBackups()]); setUserData(userData); + setBackups(backupFiles); + setBackupState(CloudBackupState.Ready); - logger.debug('[CloudBackupProvider]: Fetching all backups'); - const backups = await fetchAllBackups(); - - logger.debug(`[CloudBackupProvider]: Retrieved ${backups.files.length} backup files`); - setBackups(backups); + logger.debug(`[CloudBackupProvider]: Retrieved ${backupFiles.files.length} backup files`); + logger.debug(`[CloudBackupProvider]: Retrieved userData with ${Object.values(userData.wallets).length} wallets`); } catch (e) { logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), { error: e, }); + setBackupState(CloudBackupState.FailedToInitialize); } - setIsFetching(false); - }; + }, [setBackupState]); + + const createBackup = useCreateBackup({ + setBackupState, + backupState, + syncAndFetchBackups, + }); useEffect(() => { - fetchBackups(); - }, []); + syncAndFetchBackups(); - const value = { - isFetching, - backups, - fetchBackups, - userData, - }; + return () => { + setBackupState(CloudBackupState.Initializing); + }; + }, [syncAndFetchBackups]); - return {children}; + return ( + + {children} + + ); } -export function useCloudBackups() { +export function useCloudBackupsContext() { const context = useContext(CloudBackupContext); if (context === null) { throw new Error('useCloudBackups must be used within a CloudBackupProvider'); diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 8d9b6c68ad9..f57a22a39f7 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -1,7 +1,7 @@ /* eslint-disable no-promise-executor-return */ -import { useCallback, useMemo, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; import { backupAllWalletsToCloud, findLatestBackUp, getLocalBackupPassword, saveLocalBackupPassword } from '@/model/backup'; -import { useCloudBackups } from './CloudBackupProvider'; +import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; import { cloudPlatform } from '@/utils/platform'; import { analytics } from '@/analytics'; import { useWalletCloudBackup, useWallets } from '@/hooks'; @@ -9,47 +9,67 @@ import Routes from '@/navigation/routesNames'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { Navigation, useNavigation } from '@/navigation'; import { InteractionManager } from 'react-native'; -import { DelayedAlert } from '../alerts'; +import { DelayedAlert } from '@/components/alerts'; import { useDispatch } from 'react-redux'; import { AllRainbowWallets } from '@/model/wallet'; -type UseCreateBackupProps = { - walletId?: string; +type SingleWalletBackupProps = { + type: BackupTypes.Single; + walletId: string; +}; + +type AllWalletsBackupProps = { + type: BackupTypes.All; + walletId?: undefined; +}; + +type UseCreateBackupProps = (SingleWalletBackupProps | AllWalletsBackupProps) & { navigateToRoute?: { route: string; params?: any; }; }; -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 = ({ + setBackupState, + backupState, + syncAndFetchBackups, +}: { + setBackupState: Dispatch>; + backupState: CloudBackupState; + syncAndFetchBackups: () => Promise; +}) => { 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 setLoadingStateWithTimeout = useCallback( - (state: useCreateBackupStateType, resetInMS = 2500) => { - setLoading(state); + (state: CloudBackupState, failInMs = 10_000) => { + setBackupState(state); setTimeout(() => { - setLoading('none'); - }, resetInMS); + setBackupState(CloudBackupState.Ready); + }, failInMs); }, - [setLoading] + [setBackupState] ); + const onSuccess = useCallback( async (password: string) => { + console.log('onSuccess password: ', password); const hasSavedPassword = await getLocalBackupPassword(); + console.log('hasSavedPassword: ', hasSavedPassword); if (!hasSavedPassword && password.trim()) { await saveLocalBackupPassword(password); } @@ -57,31 +77,31 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr category: 'backup', label: cloudPlatform, }); - setLoadingStateWithTimeout('success'); - fetchBackups(); + setLoadingStateWithTimeout(CloudBackupState.Success); + syncAndFetchBackups(); }, - [setLoadingStateWithTimeout, fetchBackups] + [setLoadingStateWithTimeout, syncAndFetchBackups] ); const onError = useCallback( (msg: string) => { InteractionManager.runAfterInteractions(async () => { DelayedAlert({ title: msg }, 500); - setLoadingStateWithTimeout('error', 5000); + setLoadingStateWithTimeout(CloudBackupState.Error); }); }, [setLoadingStateWithTimeout] ); const onConfirmBackup = useCallback( - async ({ password, type }: { password: string; type: BackupTypes }) => { + async ({ password, type, walletId, navigateToRoute }: ConfirmBackupProps) => { analytics.track('Tapped "Confirm Backup"'); - setLoading('loading'); + setBackupState(CloudBackupState.InProgress); if (type === BackupTypes.All) { if (!wallets) { onError('Error loading wallets. Please try again.'); - setLoading('error'); + setBackupState(CloudBackupState.Error); return; } backupAllWalletsToCloud({ @@ -97,7 +117,7 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr if (!walletId) { onError('Wallet not found. Please try again.'); - setLoading('error'); + setBackupState(CloudBackupState.Error); return; } @@ -112,11 +132,12 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr navigate(navigateToRoute.route, navigateToRoute.params || {}); } }, - [walletId, walletCloudBackup, onError, onSuccess, navigateToRoute, wallets, latestBackup, dispatch, navigate] + [setBackupState, walletCloudBackup, onError, wallets, latestBackup, onSuccess, dispatch, navigate] ); - const getPassword = useCallback(async (): Promise => { + const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { const password = await getLocalBackupPassword(); + console.log('getLocalBackupPassword result: ', password); if (password) { return password; } @@ -126,31 +147,36 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr nativeScreen: true, step: walletBackupStepTypes.backup_cloud, onSuccess: async (password: string) => { - resolve(password); + return resolve(password); }, onCancel: async () => { - resolve(null); + return resolve(null); }, - walletId, + ...props, }); }); - }, [walletId]); + }, []); + + const createBackup = useCallback( + async (props: UseCreateBackupProps) => { + if (backupState !== CloudBackupState.Ready) { + return false; + } - const onSubmit = useCallback( - async ({ type = BackupTypes.Single }: { type?: BackupTypes }) => { - const password = await getPassword(); + const password = await getPassword(props); + console.log('result of getPassword: ', password); if (password) { onConfirmBackup({ password, - type, + ...props, }); return true; } - setLoadingStateWithTimeout('error'); + setLoadingStateWithTimeout(CloudBackupState.Ready); return false; }, - [getPassword, onConfirmBackup, setLoadingStateWithTimeout] + [backupState, getPassword, onConfirmBackup, setLoadingStateWithTimeout] ); - return { onSuccess, onError, onSubmit, loading }; + return createBackup; }; diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index 1eb3f5be795..50f72f83b22 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -6,7 +6,7 @@ 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 { BackupUserData, CloudBackups } from '@/model/backup'; const REMOTE_BACKUP_WALLET_DIR = 'rainbow.me/wallet-backups'; const USERDATA_FILE = 'UserData.json'; const encryptor = new AesEncryptor(); @@ -207,7 +207,7 @@ export async function backupUserDataIntoCloud(data: any) { return encryptAndSaveDataToCloud(data, password, filename); } -export async function fetchUserDataFromCloud() { +export async function fetchUserDataFromCloud(): Promise { const filename = USERDATA_FILE; const password = RAINBOW_MASTER_KEY; diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index e66ca433ef5..354593a9f6e 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -92,6 +92,7 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; +import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -274,9 +275,11 @@ function AuthNavigator() { const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - + + + + + )); diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 1ea1c9553ae..06ce996c640 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -106,6 +106,7 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; +import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -297,9 +298,11 @@ function NativeStackNavigator() { const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - + + + + + )); 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..79cf9821874 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,15 +5,16 @@ import MenuItem from '../MenuItem'; import Spinner from '@/components/Spinner'; import { FloatingEmojis } from '@/components/floating-emojis'; import { useDimensions } from '@/hooks'; +import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; export const BackUpMenuItem = ({ icon = '􀊯', - loading, + backupState, onPress, title, }: { icon?: string; - loading: useCreateBackupStateType; + backupState: CloudBackupState; title: string; onPress: () => void; }) => { @@ -23,17 +23,17 @@ export const BackUpMenuItem = ({ const [emojiTrigger, setEmojiTrigger] = useState void)>(null); useEffect(() => { - if (loading === 'success') { + if (backupState === 'success') { for (let i = 0; i < 20; i++) { setTimeout(() => { emojiTrigger?.(); }, 100 * i); } } - }, [emojiTrigger, loading]); + }, [emojiTrigger, backupState]); const accentColor = useMemo(() => { - switch (loading) { + switch (backupState) { case 'success': return colors.green; case 'error': @@ -41,30 +41,30 @@ export const BackUpMenuItem = ({ 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 ( <> @@ -87,7 +87,7 @@ export const BackUpMenuItem = ({ testID={'backup-now-button'} hasSfSymbol leftComponent={ - loading === 'loading' ? ( + backupState === CloudBackupState.InProgress ? ( ) : ( diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 6ea5ddbc87a..d66beded7f1 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -40,21 +40,20 @@ import { login, } from '@/handlers/cloudBackup'; import { logger, RainbowError } from '@/logger'; -import { captureException } from '@sentry/react-native'; import { RainbowAccount, createWallet } from '@/model/wallet'; import { PROFILES, useExperimentalFlag } from '@/config'; import showWalletErrorAlert from '@/helpers/support'; import { IS_ANDROID, 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 { useCloudBackupsContext } 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 { BackupTypes } from '@/components/backup/useCreateBackup'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -127,7 +126,7 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }: const ViewWalletBackup = () => { const { params } = useRoute>(); - const { backups } = useCloudBackups(); + const { createBackup, backups, backupState } = useCloudBackupsContext(); const { walletId, title: incomingTitle } = params; const creatingWallet = useRef(); const { isDamaged, wallets } = useWallets(); @@ -184,9 +183,6 @@ const ViewWalletBackup = () => { const { navigate } = useNavigation(); const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); - const { onSubmit, loading } = useCreateBackup({ - walletId, - }); const backupWalletsToCloud = useCallback(async () => { if (IS_ANDROID) { @@ -195,7 +191,10 @@ const ViewWalletBackup = () => { getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { if (accountDetails) { - return onSubmit({}); + return createBackup({ + walletId, + type: BackupTypes.Single, + }); } Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); }); @@ -226,8 +225,11 @@ const ViewWalletBackup = () => { } } - onSubmit({}); - }, [onSubmit]); + return createBackup({ + walletId, + type: BackupTypes.Single, + }); + }, [createBackup, walletId]); const onNavigateToSecretWarning = useCallback(() => { navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, { @@ -441,7 +443,7 @@ const ViewWalletBackup = () => { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - loading={loading} + backupState={backupState} onPress={backupWalletsToCloud} /> @@ -462,7 +464,7 @@ const ViewWalletBackup = () => { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - loading={loading} + backupState={backupState} onPress={backupWalletsToCloud} /> diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 9823fd2555f..38a44a84205 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -16,7 +16,7 @@ import { abbreviations } from '@/utils'; import { addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; import MenuHeader from '../MenuHeader'; -import { checkWalletsForBackupStatus } from '../../utils'; +import { checkWalletsForBackupStatus, hasManuallyBackedUpWallet } from '../../utils'; import { Inline, Text, Box, Stack } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; import { useTheme } from '@/theme'; @@ -30,16 +30,15 @@ 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 { BackupTypes } 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 { useCloudBackupsContext } 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'; type WalletPillProps = { account: RainbowAccount; @@ -95,15 +94,11 @@ export const WalletsAndBackup = () => { const { navigate } = useNavigation(); const { wallets } = useWallets(); const profilesEnabled = useExperimentalFlag(PROFILES); - const { backups } = useCloudBackups(); const dispatch = useDispatch(); + const { backups, backupState, createBackup } = useCloudBackupsContext(); const initializeWallet = useInitializeWallet(); - const { onSubmit, loading } = useCreateBackup({ - walletId: undefined, // NOTE: This is not used when backing up All wallets - }); - const { manageCloudBackups } = useManageCloudBackups(); const walletTypeCount: WalletCountPerType = { @@ -111,7 +106,9 @@ export const WalletsAndBackup = () => { privateKey: 0, }; - const { allBackedUp, backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const hasManualBackup = useMemo(() => hasManuallyBackedUpWallet(wallets || {}), [wallets]); + + const { allBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const { visibleWallets, lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); @@ -173,7 +170,7 @@ export const WalletsAndBackup = () => { getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { if (accountDetails) { - return onSubmit({ type: BackupTypes.All }); + return createBackup({ type: BackupTypes.All }); } Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); }); @@ -206,8 +203,8 @@ export const WalletsAndBackup = () => { } } - onSubmit({ type: BackupTypes.All }); - }, [onSubmit]); + createBackup({ type: BackupTypes.All }); + }, [createBackup]); const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { @@ -263,8 +260,10 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); + const backupView = backups.files.length ? WalletBackupTypes.cloud : hasManualBackup ? WalletBackupTypes.manual : undefined; + const renderView = useCallback(() => { - switch (backupProvider) { + switch (backupView) { default: case undefined: { return ( @@ -297,7 +296,7 @@ export const WalletsAndBackup = () => { @@ -471,7 +470,7 @@ export const WalletsAndBackup = () => { cloudPlatformName: cloudPlatform, })} icon="􀎽" - loading={loading} + backupState={backupState} onPress={backupAllNonBackedUpWalletsTocloud} /> @@ -661,7 +660,7 @@ export const WalletsAndBackup = () => { > @@ -671,8 +670,8 @@ export const WalletsAndBackup = () => { } } }, [ - backupProvider, - loading, + backupView, + backupState, backupAllNonBackedUpWalletsTocloud, sortedWallets, onCreateNewSecretPhrase, diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 08fa3e03e22..eb3ae8f8027 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -21,6 +21,10 @@ export const capitalizeFirstLetter = (str: string) => { return str.charAt(0).toUpperCase() + str.slice(1); }; +export const hasManuallyBackedUpWallet = (wallets: WalletsByKey) => { + return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual); +}; + export const checkUserDataForBackupProvider = (userData?: BackupUserData): { backupProvider: string | undefined } => { let backupProvider: string | undefined = undefined; From 4afed74f6750cd3ca9b6893bcd544239e0c1e8b8 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 6 Nov 2024 15:05:40 -0500 Subject: [PATCH 03/45] lots more progress on restructuring and code sharing --- src/components/backup/CloudBackupProvider.tsx | 28 +- .../secret-display/SecretDisplaySection.tsx | 7 +- src/handlers/cloudBackup.ts | 11 +- src/handlers/walletReadyEvents.ts | 9 +- src/hooks/useImportingWallet.ts | 11 +- src/model/wallet.ts | 1 - .../components/Backups/ViewCloudBackups.tsx | 196 ++++++------- .../components/Backups/ViewWalletBackup.tsx | 93 ++----- .../components/Backups/WalletsAndBackup.tsx | 259 +++++++----------- .../components/SettingsSection.tsx | 15 +- .../SettingsSheet/useVisibleWallets.ts | 78 ++---- src/screens/SettingsSheet/utils.ts | 141 +++------- src/screens/WalletScreen/index.tsx | 9 +- 13 files changed, 338 insertions(+), 520 deletions(-) diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx index a8256223a71..d546b3447bc 100644 --- a/src/components/backup/CloudBackupProvider.tsx +++ b/src/components/backup/CloudBackupProvider.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren, createContext, useCallback, useContext, useEffect, useState } from 'react'; -import type { BackupUserData, CloudBackups } from '@/model/backup'; +import type { Backup, BackupUserData, CloudBackups } from '@/model/backup'; import { fetchAllBackups, fetchUserDataFromCloud, @@ -10,11 +10,17 @@ import { import { RainbowError, logger } from '@/logger'; import { IS_ANDROID } from '@/env'; import { useCreateBackup } from '@/components/backup/useCreateBackup'; +import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; +import { useWallets } from '@/hooks'; type CloudBackupContext = { + provider: string | undefined; + setProvider: (provider: string | undefined) => void; backupState: CloudBackupState; backups: CloudBackups; userData: BackupUserData | undefined; + mostRecentBackup: Backup | undefined; createBackup: ReturnType; }; @@ -33,6 +39,7 @@ export enum CloudBackupState { const CloudBackupContext = createContext({} as CloudBackupContext); export function CloudBackupProvider({ children }: PropsWithChildren) { + const { wallets } = useWallets(); const [backupState, setBackupState] = useState(CloudBackupState.Initializing); const [userData, setUserData] = useState(); @@ -40,6 +47,9 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { files: [], }); + const [mostRecentBackup, setMostRecentBackup] = useState(undefined); + const [provider, setProvider] = useState(undefined); + const syncAndFetchBackups = useCallback(async () => { try { const isAvailable = await isCloudBackupAvailable(); @@ -67,17 +77,26 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { const [userData, backupFiles] = await Promise.all([fetchUserDataFromCloud(), fetchAllBackups()]); setUserData(userData); setBackups(backupFiles); - setBackupState(CloudBackupState.Ready); + // if the user has any cloud backups, set the provider to cloud + if (backupFiles.files.length > 0) { + setProvider(walletBackupTypes.cloud); + setMostRecentBackup(getMostRecentCloudBackup(backupFiles.files)); + } else if (hasManuallyBackedUpWallet(wallets)) { + // if the user has manually backed up wallets, set the provider to manual + setProvider(walletBackupTypes.manual); + } // else it'll remain undefined logger.debug(`[CloudBackupProvider]: Retrieved ${backupFiles.files.length} backup files`); logger.debug(`[CloudBackupProvider]: Retrieved userData with ${Object.values(userData.wallets).length} wallets`); + + setBackupState(CloudBackupState.Ready); } catch (e) { logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), { error: e, }); setBackupState(CloudBackupState.FailedToInitialize); } - }, [setBackupState]); + }, [wallets]); const createBackup = useCreateBackup({ setBackupState, @@ -96,9 +115,12 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { return ( diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx index 0ef93ba05e6..dc0da60210a 100644 --- a/src/components/secret-display/SecretDisplaySection.tsx +++ b/src/components/secret-display/SecretDisplaySection.tsx @@ -25,6 +25,7 @@ import { useNavigation } from '@/navigation'; import { ImgixImage } from '../images'; import RoutesWithPlatformDifferences from '@/navigation/routesNames'; import { Source } from 'react-native-fast-image'; +import { useCloudBackupsContext } from '../backup/CloudBackupProvider'; const MIN_HEIGHT = 740; @@ -63,6 +64,7 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const { colors } = useTheme(); const { params } = useRoute>(); const { selectedWallet, wallets } = useWallets(); + const { provider, setProvider } = useCloudBackupsContext(); const { onManuallyBackupWalletId } = useWalletManualBackup(); const { navigate } = useNavigation(); @@ -124,9 +126,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const handleConfirmSaved = useCallback(() => { if (backupType === WalletBackupTypes.manual) { onManuallyBackupWalletId(walletId); + if (!provider) { + setProvider(WalletBackupTypes.manual); + } navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP); } - }, [backupType, walletId, onManuallyBackupWalletId, navigate]); + }, [backupType, onManuallyBackupWalletId, walletId, provider, navigate, setProvider]); const getIconForBackupType = useCallback(() => { if (isBackingUp) { diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index 50f72f83b22..e5b03815a1a 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -6,9 +6,9 @@ import RNFS from 'react-native-fs'; import AesEncryptor from '../handlers/aesEncryption'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { BackupUserData, CloudBackups } from '@/model/backup'; +import { Backup, BackupUserData, 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,10 +65,15 @@ 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: Backup) => file.name !== USERDATA_FILE) || [], + }; } export async function encryptAndSaveDataToCloud(data: any, password: any, filename: any) { diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 1cfa62be144..e6d52085615 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -13,7 +13,6 @@ 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'; @@ -26,7 +25,7 @@ export const runKeychainIntegrityChecks = async () => { } }; -export const runWalletBackupStatusChecks = () => { +export const runWalletBackupStatusChecks = (provider: string | undefined) => { const { selected, wallets, @@ -38,8 +37,6 @@ export const runWalletBackupStatusChecks = () => { // 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 ( @@ -63,9 +60,9 @@ export const runWalletBackupStatusChecks = () => { // 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) { + if (provider === walletBackupTypes.cloud) { stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { + } else if (provider === walletBackupTypes.manual) { stepType = WalletBackupStepTypes.backup_now_manually; } diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index e78fa43a2e8..2fa49276c0d 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -29,9 +29,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 '@/chains/types'; +import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -52,6 +52,8 @@ export default function useImportingWallet({ showImportModal = true } = {}) { const { updateWalletENSAvatars } = useWalletENSAvatar(); const profilesEnabled = useExperimentalFlag(PROFILES); + const { provider } = useCloudBackupsContext(); + const inputRef = useRef(null); const { handleFocus } = useMagicAutofocus(inputRef); @@ -346,12 +348,10 @@ export default function useImportingWallet({ showImportModal = true } = {}) { isValidBluetoothDeviceId(input) ) ) { - const { backupProvider } = checkWalletsForBackupStatus(wallets); - let stepType: string = WalletBackupStepTypes.no_provider; - if (backupProvider === walletBackupTypes.cloud) { + if (provider === walletBackupTypes.cloud) { stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (backupProvider === walletBackupTypes.manual) { + } else if (provider === walletBackupTypes.manual) { stepType = WalletBackupStepTypes.backup_now_manually; } @@ -414,6 +414,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { showImportModal, profilesEnabled, dangerouslyGetParent, + provider, ]); return { diff --git a/src/model/wallet.ts b/src/model/wallet.ts index bb4ed3e6df0..1f7d833d014 100644 --- a/src/model/wallet.ts +++ b/src/model/wallet.ts @@ -51,7 +51,6 @@ import { Signer } from '@ethersproject/abstract-signer'; import { sanitizeTypedData } from '@/utils/signingUtils'; import { ExecuteFnParamsWithoutFn, performanceTracking, Screen } from '@/state/performance/performance'; import { Network } from '@/chains/types'; -import { WalletBalanceResult } from '@/hooks/useWalletBalances'; export type EthereumPrivateKey = string; type EthereumMnemonic = string; diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index 1842d3fae2a..8eda2caaca8 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -10,14 +10,13 @@ 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 Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; import { cloudPlatform } from '@/utils/platform'; import { useTheme } from '@/theme'; +import { useCloudBackupsContext, CloudBackupState } from '@/components/backup/CloudBackupProvider'; const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({ color: colors.blueGreyDark, @@ -32,40 +31,7 @@ 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 { backupState, backups, mostRecentBackup } = useCloudBackupsContext(); const onSelectCloudBackup = useCallback( async (selectedBackup: Backup) => { @@ -77,81 +43,101 @@ const ViewCloudBackups = () => { [navigate] ); - return ( + const renderNoBackupsState = () => ( - - {!isFetching && !cloudBackups.length && ( - - } /> - - )} + + } /> + + + ); - {!isFetching && cloudBackups.length && ( - <> - {mostRecentBackup && ( - - } - onPress={() => onSelectCloudBackup(mostRecentBackup)} - size={52} - width="full" - titleComponent={} + 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 && ( + } /> + )} + + + ); - - {cloudBackups.map( - backup => - backup.name !== mostRecentBackup?.name && ( - onSelectCloudBackup(backup)} - size={52} - width="full" - titleComponent={ - - } - /> - ) - )} + const renderBackupsList = () => ( + <> + {renderMostRecentBackup()} + {renderOlderBackups()} + + ); - {cloudBackups.length === 1 && ( - } - /> - )} - - - )} + const isLoading = + backupState === CloudBackupState.Initializing || backupState === CloudBackupState.Syncing || backupState === CloudBackupState.Fetching; + + if (isLoading) { + return ( + + {android ? : } + + {i18n.t(i18n.l.back_up.cloud.fetching_backups, { + cloudPlatformName: cloudPlatform, + })} + + + ); + } - {isFetching && ( - - {android ? : } - { - - {i18n.t(i18n.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - } - - )} - - + return ( + <> + {backupState === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()} + {backupState === 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 c671c89f2d7..ec16c005ac8 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -46,12 +46,9 @@ import showWalletErrorAlert from '@/helpers/support'; import { IS_ANDROID, IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { BackUpMenuItem } from './BackUpMenuButton'; -import { checkWalletsForBackupStatus } from '../../utils'; import { useCloudBackupsContext } 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 { BackupTypes } from '@/components/backup/useCreateBackup'; @@ -126,7 +123,7 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }: const ViewWalletBackup = () => { const { params } = useRoute>(); - const { createBackup, backups, backupState } = useCloudBackupsContext(); + const { createBackup, backupState, provider, mostRecentBackup } = useCloudBackupsContext(); const { walletId, title: incomingTitle } = params; const creatingWallet = useRef(); const { isDamaged, wallets } = useWallets(); @@ -135,48 +132,6 @@ const ViewWalletBackup = () => { 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; @@ -397,14 +352,14 @@ const ViewWalletBackup = () => { paddingBottom={{ custom: 24 }} iconComponent={ } titleComponent={} labelComponent={ - {backupProvider === walletBackupTypes.cloud && ( + {provider === walletBackupTypes.cloud && ( { })} /> )} - {backupProvider !== walletBackupTypes.cloud && ( + {provider !== walletBackupTypes.cloud && ( { /> - {backupProvider === walletBackupTypes.cloud && ( - + - - + } + > + + + )} - {backupProvider !== walletBackupTypes.cloud && ( + {provider !== walletBackupTypes.cloud && ( { const { wallets } = useWallets(); const profilesEnabled = useExperimentalFlag(PROFILES); const dispatch = useDispatch(); - const { backups, backupState, createBackup } = useCloudBackupsContext(); + const { backups, backupState, createBackup, provider, mostRecentBackup } = useCloudBackupsContext(); const initializeWallet = useInitializeWallet(); @@ -106,54 +105,15 @@ export const WalletsAndBackup = () => { privateKey: 0, }; - const hasManualBackup = useMemo(() => hasManuallyBackedUpWallet(wallets || {}), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets), [wallets]); - const { allBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); - - 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, @@ -260,10 +220,10 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); - const backupView = backups.files.length ? WalletBackupTypes.cloud : hasManualBackup ? WalletBackupTypes.manual : undefined; + console.log({ provider }); const renderView = useCallback(() => { - switch (backupView) { + switch (provider) { default: case undefined: { return ( @@ -293,21 +253,23 @@ export const WalletsAndBackup = () => { /> - - - + + + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { return ( - + { {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)} + leftComponent={} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - + {addresses.map(address => ( + ))} } @@ -360,6 +322,7 @@ export const WalletsAndBackup = () => { ); })} + { titleComponent={} /> - - - } - onPress={onViewCloudBackups} - size={52} - titleComponent={ - - } - /> - } - onPress={manageCloudBackups} - size={52} - titleComponent={ - - } - /> - ); @@ -452,37 +384,37 @@ export const WalletsAndBackup = () => { /> - + - - + } + > + + + - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, imported, addresses, ...rest }) => { + console.log({ name, ...rest }); + return ( - + { {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)} + leftComponent={} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - + {addresses.map(address => ( + ))} } @@ -580,12 +512,12 @@ export const WalletsAndBackup = () => { case WalletBackupTypes.manual: { return ( - {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => { + {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { return ( - + { {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)} + leftComponent={} + onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} /> - {accounts.map(account => ( - + {addresses.map(address => ( + ))} } @@ -644,33 +576,35 @@ 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)} + - - } - > - - + } + > + + + ); } } }, [ - backupView, + provider, backupState, backupAllNonBackedUpWalletsTocloud, sortedWallets, @@ -681,7 +615,6 @@ export const WalletsAndBackup = () => { onNavigateToWalletView, allBackedUp, mostRecentBackup, - lastBackupDate, onPressLearnMoreAboutCloudBackups, ]); diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 9fae44a89eb..d083178c5f1 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 { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; +import { capitalize } from 'lodash'; interface SettingsSectionProps { onCloseModal: () => void; @@ -59,10 +61,11 @@ const SettingsSection = ({ const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS); const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS); + const { provider } = useCloudBackupsContext(); + const { isDarkMode, setTheme, colorScheme } = useTheme(); const onSendFeedback = useSendFeedback(); - const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); const onPressReview = useCallback(async () => { if (ios) { @@ -85,7 +88,7 @@ const SettingsSection = ({ const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []); - const { allBackedUp, canBeBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp, canBeBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets), [wallets]); const themeMenuConfig = useMemo(() => { return { @@ -160,12 +163,12 @@ const SettingsSection = ({ return undefined; } - if (backupProvider === walletBackupTypes.cloud) { + if (provider === walletBackupTypes.cloud) { return CloudBackupWarningIcon; } return BackupWarningIcon; - }, [allBackedUp, backupProvider]); + }, [allBackedUp, provider]); return ( }> @@ -215,7 +218,7 @@ const SettingsSection = ({ } - 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..ec19e9a96de 100644 --- a/src/screens/SettingsSheet/useVisibleWallets.ts +++ b/src/screens/SettingsSheet/useVisibleWallets.ts @@ -4,6 +4,7 @@ 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 { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; type WalletByKey = { [key: string]: RainbowWallet; @@ -19,20 +20,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: @@ -44,55 +31,30 @@ export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: ? i18n.t(i18n.l.back_up.private_key_plural, { privateKeyNumber: walletTypeCount.privateKey }) : i18n.t(i18n.l.back_up.private_key_singluar); default: + console.log({ type, walletTypeCount }); return ''; } }; -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), + }; + }); }; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index eb3ae8f8027..cfbb12d20d0 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -1,122 +1,67 @@ import WalletBackupTypes from '@/helpers/walletBackupTypes'; import WalletTypes from '@/helpers/walletTypes'; -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 { useWallets } from '@/hooks'; +import { isEmpty } from 'lodash'; +import { Backup, parseTimestampFromFilename } from '@/model/backup'; +import { IS_ANDROID } from '@/env'; 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: WalletsByKey) => { +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']): WalletBackupStatus => { + if (!wallets || isEmpty(wallets)) { return { allBackedUp: false, areBackedUp: false, canBeBackedUp: false, - backupProvider: undefined, }; + } + + 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; +export const getMostRecentCloudBackup = (backups: Backup[]) => { + const cloudBackups = backups.sort((a, b) => { + return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); + }); - 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; + return cloudBackups.reduce( + (prev, current) => { + if (!current) { + return prev; } - } - - if (!wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) { - allBackedUp = false; - } - - if ( - !wallets[key].backedUp && - wallets[key].type !== WalletTypes.readOnly && - wallets[key].type !== WalletTypes.bluetooth && - !wallets[key].imported - ) { - areBackedUp = false; - } - - if (wallets[key].type !== WalletTypes.bluetooth && wallets[key].type !== WalletTypes.readOnly) { - canBeBackedUp = true; - } - }); - return { - allBackedUp, - areBackedUp, - canBeBackedUp, - backupProvider, - }; -}; -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; -}; + if (!prev) { + return current; + } -export const fetchBackupPasswordAndNavigate = async () => { - const password = await getLocalBackupPassword(); + const prevTimestamp = new Date(prev.lastModified).getTime(); + const currentTimestamp = new Date(current.lastModified).getTime(); + if (currentTimestamp > prevTimestamp) { + return current; + } - return new Promise(resolve => { - return Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_cloud, - password, - onSuccess: async (password: string) => { - resolve(password); - }, - }); - }); + return prev; + }, + undefined as Backup | undefined + ); }; diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 026acd32b4e..a526209a642 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -29,6 +29,7 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { RootStackParamList } from '@/navigation/types'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/Routes'; +import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; function WalletScreen() { const { params } = useRoute>(); @@ -37,6 +38,7 @@ function WalletScreen() { const [initialized, setInitialized] = useState(!!params?.initialized); const initializeWallet = useInitializeWallet(); const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings(); + const { provider } = useCloudBackupsContext(); const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData(); @@ -71,11 +73,16 @@ function WalletScreen() { } }, [initializeWallet, initialized, params, setParams]); + useEffect(() => { + if (provider) { + runWalletBackupStatusChecks(provider); + } + }, [provider]); + useEffect(() => { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); - runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); From fb913bc28e65bf67948ff6bb0657012f1cf30437 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 6 Nov 2024 18:56:30 -0500 Subject: [PATCH 04/45] more code quality improvements --- src/components/backup/ChooseBackupStep.tsx | 164 ++++++++---------- src/components/backup/CloudBackupProvider.tsx | 11 +- src/components/backup/RestoreCloudStep.tsx | 7 +- src/components/backup/useCreateBackup.ts | 2 +- src/hooks/useCloudBackups.ts | 71 -------- src/hooks/useWalletCloudBackup.ts | 7 +- src/languages/en_US.json | 2 + src/model/backup.ts | 1 + .../components/Backups/ViewCloudBackups.tsx | 99 ++++++----- src/screens/SettingsSheet/utils.ts | 16 +- 10 files changed, 166 insertions(+), 214 deletions(-) delete mode 100644 src/hooks/useCloudBackups.ts diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index 2f7b68cedf6..1865105158f 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -6,10 +6,10 @@ 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 { Box, Stack } from '@/design-system'; import { RouteProp, useRoute } from '@react-navigation/native'; 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'; @@ -20,12 +20,12 @@ import { RestoreSheetParams } from '@/screens/RestoreSheet'; 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 { CloudBackupState, useCloudBackupsContext } from './CloudBackupProvider'; +import { titleForBackupState } from '@/screens/SettingsSheet/utils'; const Title = styled(RNText).attrs({ align: 'left', @@ -58,45 +58,15 @@ export function ChooseBackupStep() { } = useRoute>(); const { colors } = useTheme(); - const { isFetching, backups, userData, step, fetchBackups } = useCloudBackups(); + const { backupState, backups, userData, mostRecentBackup, syncAndFetchBackups } = useCloudBackupsContext(); + + const isLoading = + backupState === CloudBackupState.Initializing || backupState === CloudBackupState.Syncing || backupState === CloudBackupState.Fetching; 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) => { navigate(Routes.RESTORE_CLOUD_SHEET, { @@ -132,7 +102,7 @@ export function ChooseBackupStep() { - {!isFetching && step === CloudBackupStep.FAILED && ( + {backupState === CloudBackupState.FailedToInitialize && ( } /> )} - {!isFetching && !cloudBackups.length && step !== CloudBackupStep.FAILED && ( + {backupState === CloudBackupState.Ready && backups.files.length === 0 && ( } /> @@ -162,67 +132,81 @@ export function ChooseBackupStep() { )} - {!isFetching && cloudBackups.length > 0 && ( + {backupState === 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={syncAndFetchBackups} + titleComponent={} /> - )} - + + )} - {isFetching && ( - + {isLoading && ( + {android ? : } - - {lang.t(lang.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - + {titleForBackupState[backupState]} + )} diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx index d546b3447bc..cb994731bd9 100644 --- a/src/components/backup/CloudBackupProvider.tsx +++ b/src/components/backup/CloudBackupProvider.tsx @@ -13,6 +13,9 @@ import { useCreateBackup } from '@/components/backup/useCreateBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; import { useWallets } from '@/hooks'; +import { Semaphore } from 'async-mutex'; + +const semaphore = new Semaphore(1); type CloudBackupContext = { provider: string | undefined; @@ -22,6 +25,7 @@ type CloudBackupContext = { userData: BackupUserData | undefined; mostRecentBackup: Backup | undefined; createBackup: ReturnType; + syncAndFetchBackups: () => Promise; }; export enum CloudBackupState { @@ -50,7 +54,7 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { const [mostRecentBackup, setMostRecentBackup] = useState(undefined); const [provider, setProvider] = useState(undefined); - const syncAndFetchBackups = useCallback(async () => { + const syncAndPullFiles = useCallback(async () => { try { const isAvailable = await isCloudBackupAvailable(); if (!isAvailable) { @@ -98,6 +102,10 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { } }, [wallets]); + const syncAndFetchBackups = useCallback(async () => { + return semaphore.runExclusive(syncAndPullFiles); + }, [syncAndPullFiles]); + const createBackup = useCreateBackup({ setBackupState, backupState, @@ -122,6 +130,7 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { userData, mostRecentBackup, createBackup, + syncAndFetchBackups, }} > {children} diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index e8bd83aa7a3..a5f13ec590a 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -36,7 +36,7 @@ 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 { useCloudBackupsContext } from './CloudBackupProvider'; const Title = styled(Text).attrs({ size: 'big', @@ -83,7 +83,7 @@ type RestoreCloudStepParams = { export default function RestoreCloudStep() { const { params } = useRoute>(); - const { userData } = useCloudBackups(); + const { userData } = useCloudBackupsContext(); const { selectedBackup } = params; const { isDarkMode } = useTheme(); @@ -91,7 +91,7 @@ export default function RestoreCloudStep() { const dispatch = useDispatch(); const { width: deviceWidth, height: deviceHeight } = useDimensions(); - const { replace, navigate, getState: dangerouslyGetState, goBack } = useNavigation(); + const { replace, navigate, getState: dangerouslyGetState } = useNavigation(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); const [password, setPassword] = useState(''); @@ -140,6 +140,7 @@ export default function RestoreCloudStep() { if (status === RestoreCloudBackupResultStates.success) { // Store it in the keychain in case it was missing const hasSavedPassword = await getLocalBackupPassword(); + console.log({ hasSavedPassword }); if (!hasSavedPassword) { await saveLocalBackupPassword(password); } diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index f57a22a39f7..a83a8baf0ae 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -123,7 +123,7 @@ export const useCreateBackup = ({ await walletCloudBackup({ onError, - onSuccess: (password: string) => onSuccess(password), + onSuccess, password, walletId, }); 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/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 75c7e42b8a6..63b2f427c96 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -16,6 +16,7 @@ import { getSupportedBiometryType } from '@/keychain'; import { IS_ANDROID } from '@/env'; import { authenticateWithPIN } from '@/handlers/authentication'; import * as i18n from '@/languages'; +import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; export function getUserError(e: Error) { switch (e.message) { @@ -102,17 +103,17 @@ export default function useWalletCloudBackup() { let updatedBackupFile = null; try { if (!latestBackup) { - logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${wallets![walletId]}`); + logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${(wallets || {})[walletId]}`); updatedBackupFile = await backupWalletToCloud({ password, - wallet: wallets![walletId], + wallet: (wallets || {})[walletId], userPIN, }); } else { logger.debug(`[useWalletCloudBackup]: adding wallet to ${cloudPlatform} backup: ${wallets![walletId]}`); updatedBackupFile = await addWalletToCloudBackup({ password, - wallet: wallets![walletId], + wallet: (wallets || {})[walletId], filename: latestBackup, userPIN, }); diff --git a/src/languages/en_US.json b/src/languages/en_US.json index d966a6040e9..28616fb883f 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -115,6 +115,8 @@ "no_backups": "No backups found", "failed_to_fetch_backups": "Failed to fetch backups", "retry": "Retry", + "refresh": "Refresh", + "syncing_cloud_store": "Syncing user data from %{cloudPlatformName}", "fetching_backups": "Retrieving backups from %{cloudPlatformName}", "back_up_to_platform": "Back up to %{cloudPlatformName}", "restore_from_platform": "Restore from %{cloudPlatformName}", diff --git a/src/model/backup.ts b/src/model/backup.ts index d1be352281a..89935e3f515 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -337,6 +337,7 @@ export function findLatestBackUp(wallets: AllRainbowWallets | null): string | nu } }); } + return filename; } diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index 8eda2caaca8..ca77fdd9f90 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -7,16 +7,16 @@ import MenuContainer from '../MenuContainer'; import MenuItem from '../MenuItem'; import { Backup, parseTimestampFromFilename } from '@/model/backup'; import { format } from 'date-fns'; -import { Stack } from '@/design-system'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import { Centered } from '@/components/layout'; +import { Centered, Page } from '@/components/layout'; import Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; -import { cloudPlatform } from '@/utils/platform'; import { useTheme } from '@/theme'; import { useCloudBackupsContext, CloudBackupState } from '@/components/backup/CloudBackupProvider'; +import { titleForBackupState } from '../../utils'; +import { Box } from '@/design-system'; const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({ color: colors.blueGreyDark, @@ -31,7 +31,7 @@ const ViewCloudBackups = () => { const { navigate } = useNavigation(); const { colors } = useTheme(); - const { backupState, backups, mostRecentBackup } = useCloudBackupsContext(); + const { backupState, backups, mostRecentBackup, syncAndFetchBackups } = useCloudBackupsContext(); const onSelectCloudBackup = useCallback( async (selectedBackup: Backup) => { @@ -44,11 +44,11 @@ const ViewCloudBackups = () => { ); const renderNoBackupsState = () => ( - + <> } /> - + ); const renderMostRecentBackup = () => { @@ -57,7 +57,7 @@ const ViewCloudBackups = () => { } return ( - + { 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 => ( + <> + + + {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={} /> - ))} - {backups.files.length === 1 && ( - } /> - )} + )} + + + + + } + /> - + ); const renderBackupsList = () => ( @@ -122,22 +137,18 @@ const ViewCloudBackups = () => { if (isLoading) { return ( - + {android ? : } - - {i18n.t(i18n.l.back_up.cloud.fetching_backups, { - cloudPlatformName: cloudPlatform, - })} - - + {titleForBackupState[backupState]} + ); } return ( - <> + {backupState === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()} {backupState === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()} - + ); }; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index cfbb12d20d0..3d8bacd7cc9 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -3,7 +3,9 @@ import WalletTypes from '@/helpers/walletTypes'; import { useWallets } from '@/hooks'; import { isEmpty } from 'lodash'; import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { IS_ANDROID } from '@/env'; +import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; +import * as i18n from '@/languages'; +import { cloudPlatform } from '@/utils/platform'; type WalletBackupStatus = { allBackedUp: boolean; @@ -65,3 +67,15 @@ export const getMostRecentCloudBackup = (backups: Backup[]) => { undefined as Backup | undefined ); }; + +export const titleForBackupState = { + [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, + }), +}; From afc42bffce94bae3e8f58064b632bb7586fc941a Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 8 Nov 2024 14:46:38 -0500 Subject: [PATCH 05/45] prevent App.tsx from re-rendering unless it needs to --- src/App.tsx | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d59b7ed7296..a8f83cb098e 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,7 +9,7 @@ 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'; @@ -22,7 +22,7 @@ import * as keychain from '@/model/keychain'; import { Navigation } from '@/navigation'; import { PersistQueryClientProvider, persistOptions, queryClient } from '@/react-query'; import store, { AppDispatch, type AppState } from '@/redux/store'; -import { MainThemeProvider, useTheme } from '@/theme/ThemeContext'; +import { MainThemeProvider } from '@/theme/ThemeContext'; import { addressKey } from '@/utils/keychainConstants'; import { SharedValuesProvider } from '@/helpers/SharedValuesContext'; import { InitialRouteContext } from '@/navigation/initialRoute'; @@ -42,7 +42,6 @@ import { Address } from 'viem'; import { IS_ANDROID, IS_DEV } from '@/env'; import { prefetchDefaultFavorites } from '@/resources/favorites'; import Routes from '@/navigation/Routes'; -import { BackendNetworks } from '@/components/BackendNetworks'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -75,7 +74,6 @@ function App({ walletReady }: AppProps) { {initialRoute && ( - )} @@ -83,14 +81,25 @@ 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); From 694662637bbf44466309b75bfdc3968b7b530fee Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 8 Nov 2024 14:57:31 -0500 Subject: [PATCH 06/45] fix password not being pulled from local password keychain and add loading portal back in --- src/components/backup/RestoreCloudStep.tsx | 192 +++++++++++---------- 1 file changed, 100 insertions(+), 92 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index a5f13ec590a..36d946b18f3 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -7,7 +7,6 @@ import { KeyboardArea } from 'react-native-keyboard-area'; import { Backup, - fetchBackupPassword, getLocalBackupPassword, restoreCloudBackup, RestoreCloudBackupResultStates, @@ -19,9 +18,15 @@ import { Text } from '../text'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { useDimensions, useInitializeWallet } from '@/hooks'; +import { useDimensions, useInitializeWallet, useWallets } from '@/hooks'; import { useNavigation } from '@/navigation'; -import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets'; +import { + addressSetSelected, + setAllWalletsWithIdsAsBackedUp, + setIsWalletLoading, + walletsLoadState, + walletsSetSelected, +} from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { padding } from '@/styles'; @@ -37,6 +42,7 @@ import { RestoreSheetParams } from '@/screens/RestoreSheet'; import { Source } from 'react-native-fast-image'; import { useTheme } from '@/theme'; import { useCloudBackupsContext } from './CloudBackupProvider'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; const Title = styled(Text).attrs({ size: 'big', @@ -83,30 +89,29 @@ type RestoreCloudStepParams = { export default function RestoreCloudStep() { const { params } = useRoute>(); - const { userData } = useCloudBackupsContext(); + const { userData, password, setPassword } = useCloudBackupsContext(); const { selectedBackup } = params; const { isDarkMode } = useTheme(); - const [loading, setLoading] = useState(false); const dispatch = useDispatch(); const { width: deviceWidth, height: deviceHeight } = useDimensions(); const { replace, navigate, getState: dangerouslyGetState } = useNavigation(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); - const [password, setPassword] = useState(''); const passwordRef = useRef(null); const initializeWallet = useInitializeWallet(); + const { isWalletLoading } = useWallets(); useEffect(() => { const fetchPasswordIfPossible = async () => { - const pwd = await fetchBackupPassword(); + const pwd = await getLocalBackupPassword(); if (pwd) { setPassword(pwd); } }; fetchPasswordIfPossible(); - }, []); + }, [setPassword]); useEffect(() => { let passwordIsValid = false; @@ -117,13 +122,18 @@ export default function RestoreCloudStep() { setValidPassword(passwordIsValid); }, [incorrectPassword, password]); - const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { - setPassword(inputText); - setIncorrectPassword(false); - }, []); + const onPasswordChange = useCallback( + ({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { + setPassword(inputText); + setIncorrectPassword(false); + }, + [setPassword] + ); 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(); + try { if (!selectedBackup.name) { throw new Error('No backup file selected'); @@ -131,91 +141,89 @@ export default function RestoreCloudStep() { const prevWalletsState = await dispatch(walletsLoadState()); - const status = await restoreCloudBackup({ - password, - userData, - nameOfSelectedBackupFile: selectedBackup.name, - }); - - if (status === RestoreCloudBackupResultStates.success) { - // Store it in the keychain in case it was missing - const hasSavedPassword = await getLocalBackupPassword(); - console.log({ hasSavedPassword }); - if (!hasSavedPassword) { - await saveLocalBackupPassword(password); - } - - InteractionManager.runAfterInteractions(async () => { - const newWalletsState = await dispatch(walletsLoadState()); - let filename = selectedBackup.name; - if (IS_ANDROID && filename) { - filename = normalizeAndroidBackupFilename(filename); + await Promise.all([ + dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)), + restoreCloudBackup({ + password: pwd, + userData, + nameOfSelectedBackupFile: selectedBackup.name, + }), + ]).then(async ([_, status]) => { + if (status === RestoreCloudBackupResultStates.success) { + // Store it in the keychain in case it was missing + const hasSavedPassword = await getLocalBackupPassword(); + if (!hasSavedPassword) { + await saveLocalBackupPassword(pwd); } - logger.debug('[RestoreCloudStep]: Done updating backup state'); - // NOTE: Marking the restored wallets as backed up - // @ts-expect-error TypeScript doesn't play nicely with Redux types here - const walletIdsToUpdate = Object.keys(newWalletsState || {}).filter(walletId => !(prevWalletsState || {})[walletId]); - - logger.debug('[RestoreCloudStep]: Updating backup state of wallets with ids', { - walletIds: JSON.stringify(walletIdsToUpdate), - }); - logger.debug('[RestoreCloudStep]: Selected backup name', { - fileName: selectedBackup.name, - }); - - await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, walletBackupTypes.cloud, filename)); - - const oldCloudIds: string[] = []; - const oldManualIds: string[] = []; - // NOTE: Looping over previous wallets and restoring backup state of that wallet - Object.values(prevWalletsState || {}).forEach(wallet => { - // NOTE: This handles cloud and manual backups - if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { - oldCloudIds.push(wallet.id); - } else if (wallet.backedUp && wallet.backupType === walletBackupTypes.manual) { - oldManualIds.push(wallet.id); + InteractionManager.runAfterInteractions(async () => { + const newWalletsState = await dispatch(walletsLoadState()); + let filename = selectedBackup.name; + if (IS_ANDROID && filename) { + filename = normalizeAndroidBackupFilename(filename); } - }); - await dispatch(setAllWalletsWithIdsAsBackedUp(oldCloudIds, walletBackupTypes.cloud, filename)); - await dispatch(setAllWalletsWithIdsAsBackedUp(oldManualIds, walletBackupTypes.manual, filename)); - - const walletKeys = Object.keys(newWalletsState || {}); - // @ts-expect-error TypeScript doesn't play nicely with Redux types here - const firstWallet = walletKeys.length > 0 ? (newWalletsState || {})[walletKeys[0]] : undefined; - const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; - const p1 = dispatch(walletsSetSelected(firstWallet)); - 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, + logger.debug('[RestoreCloudStep]: Done updating backup state'); + // NOTE: Marking the restored wallets as backed up + // @ts-expect-error TypeScript doesn't play nicely with Redux types here + const walletIdsToUpdate = Object.keys(newWalletsState || {}).filter(walletId => !(prevWalletsState || {})[walletId]); + + logger.debug('[RestoreCloudStep]: Updating backup state of wallets with ids', { + walletIds: JSON.stringify(walletIdsToUpdate), + }); + logger.debug('[RestoreCloudStep]: Selected backup name', { + fileName: selectedBackup.name, + }); + + await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, walletBackupTypes.cloud, filename)); + + const oldCloudIds: string[] = []; + const oldManualIds: string[] = []; + // NOTE: Looping over previous wallets and restoring backup state of that wallet + Object.values(prevWalletsState || {}).forEach(wallet => { + // NOTE: This handles cloud and manual backups + if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { + oldCloudIds.push(wallet.id); + } else if (wallet.backedUp && wallet.backupType === walletBackupTypes.manual) { + oldManualIds.push(wallet.id); + } + }); + + await dispatch(setAllWalletsWithIdsAsBackedUp(oldCloudIds, walletBackupTypes.cloud, filename)); + await dispatch(setAllWalletsWithIdsAsBackedUp(oldManualIds, walletBackupTypes.manual, filename)); + + const walletKeys = Object.keys(newWalletsState || {}); + // @ts-expect-error TypeScript doesn't play nicely with Redux types here + const firstWallet = walletKeys.length > 0 ? (newWalletsState || {})[walletKeys[0]] : undefined; + const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; + const p1 = dispatch(walletsSetSelected(firstWallet)); + 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); - }); - } else { - switch (status) { - case RestoreCloudBackupResultStates.incorrectPassword: - setIncorrectPassword(true); - break; - case RestoreCloudBackupResultStates.incorrectPinCode: - Alert.alert(lang.t('back_up.restore_cloud.incorrect_pin_code')); - break; - default: - Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); - break; + } else { + switch (status) { + case RestoreCloudBackupResultStates.incorrectPassword: + setIncorrectPassword(true); + break; + case RestoreCloudBackupResultStates.incorrectPinCode: + Alert.alert(lang.t('back_up.restore_cloud.incorrect_pin_code')); + break; + default: + Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); + break; + } } - } + }); } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } - - setLoading(false); - }, [selectedBackup.name, password, userData, dispatch, initializeWallet, dangerouslyGetState, navigate, replace]); + }, [selectedBackup.name, dispatch, password, userData, initializeWallet, dangerouslyGetState, navigate, replace]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); @@ -249,7 +257,7 @@ export default function RestoreCloudStep() { Date: Fri, 8 Nov 2024 14:58:55 -0500 Subject: [PATCH 07/45] misc backups improvements --- .vscode/settings.json | 3 ++- src/components/backup/CloudBackupProvider.tsx | 10 ++++++++-- src/components/backup/useCreateBackup.ts | 4 ---- src/handlers/cloudBackup.ts | 6 ++++++ src/helpers/walletLoadingStates.ts | 10 ++++++++++ src/hooks/useWallets.ts | 2 +- src/model/backup.ts | 8 ++++++++ src/redux/wallets.ts | 12 ++++++++++++ .../components/Backups/WalletsAndBackup.tsx | 6 +----- src/screens/SettingsSheet/useVisibleWallets.ts | 1 - src/screens/WalletScreen/index.tsx | 2 ++ 11 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 src/helpers/walletLoadingStates.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4f4c6f3528c..d3d6528619b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ "ios/Pods/Headers/Private/RCT-Folly/folly/lang", "ios/Pods/Headers/Public/Flipper-Folly/folly/lang", "ios/Pods/Headers/Public/RCT-Folly/folly/lang" - ] + ], + "typescript.tsdk": "node_modules/typescript/lib" } \ No newline at end of file diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx index cb994731bd9..4f5a897b9f2 100644 --- a/src/components/backup/CloudBackupProvider.tsx +++ b/src/components/backup/CloudBackupProvider.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { Dispatch, PropsWithChildren, SetStateAction, createContext, useCallback, useContext, useEffect, useState } from 'react'; import type { Backup, BackupUserData, CloudBackups } from '@/model/backup'; import { fetchAllBackups, @@ -19,11 +19,13 @@ const semaphore = new Semaphore(1); type CloudBackupContext = { provider: string | undefined; - setProvider: (provider: string | undefined) => void; + setProvider: Dispatch>; backupState: CloudBackupState; backups: CloudBackups; userData: BackupUserData | undefined; mostRecentBackup: Backup | undefined; + password: string; + setPassword: Dispatch>; createBackup: ReturnType; syncAndFetchBackups: () => Promise; }; @@ -51,6 +53,8 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { files: [], }); + const [password, setPassword] = useState(''); + const [mostRecentBackup, setMostRecentBackup] = useState(undefined); const [provider, setProvider] = useState(undefined); @@ -129,6 +133,8 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { backups, userData, mostRecentBackup, + password, + setPassword, createBackup, syncAndFetchBackups, }} diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index a83a8baf0ae..37730a62513 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -67,9 +67,7 @@ export const useCreateBackup = ({ const onSuccess = useCallback( async (password: string) => { - console.log('onSuccess password: ', password); const hasSavedPassword = await getLocalBackupPassword(); - console.log('hasSavedPassword: ', hasSavedPassword); if (!hasSavedPassword && password.trim()) { await saveLocalBackupPassword(password); } @@ -137,7 +135,6 @@ export const useCreateBackup = ({ const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { const password = await getLocalBackupPassword(); - console.log('getLocalBackupPassword result: ', password); if (password) { return password; } @@ -164,7 +161,6 @@ export const useCreateBackup = ({ } const password = await getPassword(props); - console.log('result of getPassword: ', password); if (password) { onConfirmBackup({ password, diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index e5b03815a1a..c2e38ef3889 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -159,6 +159,8 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n targetPath: REMOTE_BACKUP_WALLET_DIR, }); + console.log({ backups }); + if (!backups || !backups.files || !backups.files.length) { const error = new Error(CLOUD_BACKUP_ERRORS.NO_BACKUPS_FOUND); throw error; @@ -175,6 +177,8 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n }); } + console.log({ document }); + if (!document) { logger.error(new RainbowError('[cloudBackup]: No backup found with that name!'), { filename, @@ -188,6 +192,8 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n } const encryptedData = ios ? await getICloudDocument(filename) : await getGoogleDriveDocument(document.id); + console.log({ filename, encryptedData }); + if (encryptedData) { logger.debug(`[cloudBackup]: Got cloud document ${filename}`); const backedUpDataStringified = await encryptor.decrypt(backupPassword, encryptedData); diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts new file mode 100644 index 00000000000..44e2d94c039 --- /dev/null +++ b/src/helpers/walletLoadingStates.ts @@ -0,0 +1,10 @@ +export const WalletLoadingStates = { + BACKING_UP_WALLET: 'Backing up...', + CREATING_WALLET: 'Creating wallet...', + FETCHING_PASSWORD: 'Fetching Password...', + IMPORTING_WALLET: 'Importing...', + IMPORTING_WALLET_SILENTLY: '', + RESTORING_WALLET: 'Restoring...', +} as const; + +export type WalletLoadingState = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts index 38363886917..7194f701fe5 100644 --- a/src/hooks/useWallets.ts +++ b/src/hooks/useWallets.ts @@ -7,7 +7,7 @@ import { AppState } from '@/redux/store'; const walletSelector = createSelector( ({ wallets: { isWalletLoading, selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({ isWalletLoading, - selectedWallet: selected as any, + selectedWallet: selected, walletNames, wallets, }), diff --git a/src/model/backup.ts b/src/model/backup.ts index 89935e3f515..4b981dbd06a 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -379,12 +379,16 @@ export async function restoreCloudBackup({ try { // 1 - sanitize filename to remove extra things we don't care about const filename = sanitizeFilename(nameOfSelectedBackupFile); + console.log({ filename }); if (!filename) { + console.log('no filename'); return RestoreCloudBackupResultStates.failedWhenRestoring; } // 2 - retrieve that backup data const data = await getDataFromCloud(password, filename); + console.log({ data }); if (!data) { + console.log('no data'); return RestoreCloudBackupResultStates.incorrectPassword; } @@ -392,6 +396,8 @@ export async function restoreCloudBackup({ ...data.secrets, }; + console.log({ dataToRestore }); + // ANDROID ONLY - pin auth if biometrics are disabled let userPIN: string | undefined; const hasBiometricsEnabled = await kc.getSupportedBiometryType(); @@ -507,6 +513,8 @@ async function restoreSpecificBackupIntoKeychain(backedUpData: BackedUpData, use secretPhraseOrOldAndroidBackupPrivateKey = parsedValue.seedphrase; } + console.log({ secretPhraseOrOldAndroidBackupPrivateKey }); + if (!secretPhraseOrOldAndroidBackupPrivateKey) { continue; } diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index deb49a5ea9b..9fd1fcf52e4 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -241,6 +241,18 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di }); }; +/** + * Updates the wallet loading state. + * + * @param val The new loading state. + */ +export const setIsWalletLoading = (val: WalletsState['isWalletLoading']) => (dispatch: Dispatch) => { + dispatch({ + payload: val, + type: WALLETS_SET_IS_LOADING, + }); +}; + /** * Marks all wallets with passed ids as backed-up * using a specified method and file in storage diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index edb7c1550d5..7680d4f3d78 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -220,8 +220,6 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); - console.log({ provider }); - const renderView = useCallback(() => { switch (provider) { default: @@ -407,9 +405,7 @@ export const WalletsAndBackup = () => { - {sortedWallets.map(({ id, name, backedUp, imported, addresses, ...rest }) => { - console.log({ name, ...rest }); - + {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { return ( >(); @@ -118,6 +119,7 @@ function WalletScreen() { + {/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */} From fd16a13a7c29d1c27f8a75e8df382c0ee0b7d72c Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 8 Nov 2024 15:01:08 -0500 Subject: [PATCH 08/45] Update .vscode/settings.json --- .vscode/settings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d3d6528619b..4f4c6f3528c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,5 @@ "ios/Pods/Headers/Private/RCT-Folly/folly/lang", "ios/Pods/Headers/Public/Flipper-Folly/folly/lang", "ios/Pods/Headers/Public/RCT-Folly/folly/lang" - ], - "typescript.tsdk": "node_modules/typescript/lib" + ] } \ No newline at end of file From 00d1bff583044f91262d2b848ebebc2f0cae3b8f Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 8 Nov 2024 15:18:28 -0500 Subject: [PATCH 09/45] cleanup --- src/components/backup/RestoreCloudStep.tsx | 2 + src/handlers/cloudBackup.ts | 6 -- src/model/backup.ts | 76 ---------------------- 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 36d946b18f3..aa68dd805c7 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -222,6 +222,8 @@ export default function RestoreCloudStep() { }); } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); + } finally { + dispatch(setIsWalletLoading(null)); } }, [selectedBackup.name, dispatch, password, userData, initializeWallet, dangerouslyGetState, navigate, replace]); diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index c2e38ef3889..e5b03815a1a 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -159,8 +159,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n targetPath: REMOTE_BACKUP_WALLET_DIR, }); - console.log({ backups }); - if (!backups || !backups.files || !backups.files.length) { const error = new Error(CLOUD_BACKUP_ERRORS.NO_BACKUPS_FOUND); throw error; @@ -177,8 +175,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n }); } - console.log({ document }); - if (!document) { logger.error(new RainbowError('[cloudBackup]: No backup found with that name!'), { filename, @@ -192,8 +188,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n } const encryptedData = ios ? await getICloudDocument(filename) : await getGoogleDriveDocument(document.id); - console.log({ filename, encryptedData }); - if (encryptedData) { logger.debug(`[cloudBackup]: Got cloud document ${filename}`); const backedUpDataStringified = await encryptor.decrypt(backupPassword, encryptedData); diff --git a/src/model/backup.ts b/src/model/backup.ts index 4b981dbd06a..03c2044f14c 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -379,16 +379,12 @@ export async function restoreCloudBackup({ try { // 1 - sanitize filename to remove extra things we don't care about const filename = sanitizeFilename(nameOfSelectedBackupFile); - console.log({ filename }); if (!filename) { - console.log('no filename'); return RestoreCloudBackupResultStates.failedWhenRestoring; } // 2 - retrieve that backup data const data = await getDataFromCloud(password, filename); - console.log({ data }); if (!data) { - console.log('no data'); return RestoreCloudBackupResultStates.incorrectPassword; } @@ -396,8 +392,6 @@ export async function restoreCloudBackup({ ...data.secrets, }; - console.log({ dataToRestore }); - // ANDROID ONLY - pin auth if biometrics are disabled let userPIN: string | undefined; const hasBiometricsEnabled = await kc.getSupportedBiometryType(); @@ -513,8 +507,6 @@ async function restoreSpecificBackupIntoKeychain(backedUpData: BackedUpData, use secretPhraseOrOldAndroidBackupPrivateKey = parsedValue.seedphrase; } - console.log({ secretPhraseOrOldAndroidBackupPrivateKey }); - if (!secretPhraseOrOldAndroidBackupPrivateKey) { continue; } @@ -534,74 +526,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; From 5420054c1e52920bde0b1aaf23128e8ecab7696f Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Sun, 10 Nov 2024 21:04:26 -0500 Subject: [PATCH 10/45] fix lint --- .../backup/AddWalletToCloudBackupStep.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx index 3283d51fa82..20db9daa517 100644 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ b/src/components/backup/AddWalletToCloudBackupStep.tsx @@ -9,7 +9,6 @@ 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 { login } from '@/handlers/cloudBackup'; import { useCloudBackupsContext } from './CloudBackupProvider'; @@ -19,16 +18,9 @@ const imageSize = 72; export default function AddWalletToCloudBackupStep() { const { goBack } = useNavigation(); - const { wallets, selectedWallet } = useWallets(); + const { selectedWallet } = useWallets(); - const { createBackup } = useCloudBackupsContext(); - - const walletTypeCount: WalletCountPerType = { - phrase: 0, - privateKey: 0, - }; - - const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount }); + const { createBackup, mostRecentBackup } = useCloudBackupsContext(); const potentiallyLoginAndSubmit = useCallback(async () => { await login(); @@ -111,13 +103,13 @@ export default function AddWalletToCloudBackupStep() { - {lastBackupDate && ( + {mostRecentBackup && ( {lang.t(lang.l.back_up.cloud.latest_backup, { - date: format(lastBackupDate, "M/d/yy 'at' h:mm a"), + date: format(new Date(mostRecentBackup.lastModified), "M/d/yy 'at' h:mm a"), })} From 5784cb280633ea353faf728dddf1e6d652f00295 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 14 Nov 2024 11:11:54 -0500 Subject: [PATCH 11/45] fix overlay causing top leve re-renders --- src/App.tsx | 5 ++- src/components/PortalConsumer.js | 10 ++--- src/components/backup/RestoreCloudStep.tsx | 11 ++--- src/react-native-cool-modals/Portal.js | 50 ---------------------- src/react-native-cool-modals/Portal.tsx | 41 ++++++++++++++++++ src/state/portal/portal.ts | 15 +++++++ 6 files changed, 68 insertions(+), 64 deletions(-) delete mode 100644 src/react-native-cool-modals/Portal.js create mode 100644 src/react-native-cool-modals/Portal.tsx create mode 100644 src/state/portal/portal.ts diff --git a/src/App.tsx b/src/App.tsx index a8f83cb098e..6256687fcdd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -69,7 +69,7 @@ function App({ walletReady }: AppProps) { }, []); return ( - + <> {initialRoute && ( @@ -81,8 +81,9 @@ function App({ walletReady }: AppProps) { + - + ); } diff --git a/src/components/PortalConsumer.js b/src/components/PortalConsumer.js index 351b2271f02..7644dff645b 100644 --- a/src/components/PortalConsumer.js +++ b/src/components/PortalConsumer.js @@ -2,17 +2,17 @@ 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'; +import { portalStore } from '@/state/portal/portal'; export default function PortalConsumer() { const { isWalletLoading } = useWallets(); - const { setComponent, hide } = usePortal(); + useEffect(() => { if (isWalletLoading) { - setComponent(, true); + portalStore.getState().setComponent(, true); } - return hide; - }, [hide, isWalletLoading, setComponent]); + return portalStore.getState().hide; + }, [isWalletLoading]); return null; } diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index aa68dd805c7..ce8dac3af65 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -19,7 +19,7 @@ import { WrappedAlert as Alert } from '@/helpers/alert'; import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { useDimensions, useInitializeWallet, useWallets } from '@/hooks'; -import { useNavigation } from '@/navigation'; +import { Navigation, useNavigation } from '@/navigation'; import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, @@ -89,6 +89,7 @@ type RestoreCloudStepParams = { export default function RestoreCloudStep() { const { params } = useRoute>(); + const { goBack } = useNavigation(); const { userData, password, setPassword } = useCloudBackupsContext(); const { selectedBackup } = params; @@ -200,11 +201,6 @@ 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, - }); }); } else { switch (status) { @@ -224,8 +220,9 @@ export default function RestoreCloudStep() { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { dispatch(setIsWalletLoading(null)); + Navigation.handleAction(Routes.WALLET_SCREEN, {}, dangerouslyGetState()?.index === 1); } - }, [selectedBackup.name, dispatch, password, userData, initializeWallet, dangerouslyGetState, navigate, replace]); + }, [selectedBackup.name, dispatch, password, userData, initializeWallet, dangerouslyGetState]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); 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..8ab9a0d2c3f --- /dev/null +++ b/src/react-native-cool-modals/Portal.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { IS_IOS } from '@/env'; +import { portalStore } from '@/state/portal/portal'; +import { Platform, requireNativeComponent, StyleSheet, View } from 'react-native'; + +const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View; +const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => children : View; + +export function Portal() { + const { blockTouches, Component } = portalStore(state => ({ + blockTouches: state.blockTouches, + Component: state.Component, + })); + + return ( + + + {Component} + + + ); +} + +const sx = StyleSheet.create({ + wrapper: { + zIndex: Number.MAX_SAFE_INTEGER, + ...StyleSheet.absoluteFillObject, + }, +}); diff --git a/src/state/portal/portal.ts b/src/state/portal/portal.ts new file mode 100644 index 00000000000..51e0f96ccba --- /dev/null +++ b/src/state/portal/portal.ts @@ -0,0 +1,15 @@ +import { createRainbowStore } from '../internal/createRainbowStore'; + +type PortalState = { + blockTouches: boolean; + Component: JSX.Element | null; + hide: () => void; + setComponent: (Component: JSX.Element, blockTouches?: boolean) => void; +}; + +export const portalStore = createRainbowStore(set => ({ + blockTouches: false, + Component: null, + hide: () => set({ blockTouches: false, Component: null }), + setComponent: (Component: JSX.Element, blockTouches?: boolean) => set({ blockTouches, Component }), +})); From 0c81b5f67e5685f10449c362dc1716ca6a7550f4 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 14 Nov 2024 14:15:05 -0500 Subject: [PATCH 12/45] nav back to wallet screen on both new user restore / existing restore --- src/components/backup/ChooseBackupStep.tsx | 2 +- src/components/backup/CloudBackupProvider.tsx | 6 ++-- src/components/backup/RestoreCloudStep.tsx | 30 ++++++++++++++----- src/screens/RestoreSheet.tsx | 2 +- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index 1865105158f..fdcab9e6e3d 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -203,7 +203,7 @@ export function ChooseBackupStep() { )} {isLoading && ( - + {android ? : } {titleForBackupState[backupState]} diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx index 4f5a897b9f2..a7f50a44657 100644 --- a/src/components/backup/CloudBackupProvider.tsx +++ b/src/components/backup/CloudBackupProvider.tsx @@ -13,9 +13,9 @@ import { useCreateBackup } from '@/components/backup/useCreateBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; import { useWallets } from '@/hooks'; -import { Semaphore } from 'async-mutex'; +import { Mutex } from 'async-mutex'; -const semaphore = new Semaphore(1); +const mutex = new Mutex(); type CloudBackupContext = { provider: string | undefined; @@ -107,7 +107,7 @@ export function CloudBackupProvider({ children }: PropsWithChildren) { }, [wallets]); const syncAndFetchBackups = useCallback(async () => { - return semaphore.runExclusive(syncAndPullFiles); + return mutex.runExclusive(syncAndPullFiles); }, [syncAndPullFiles]); const createBackup = useCreateBackup({ diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index ce8dac3af65..815207a8e4e 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -43,6 +43,7 @@ import { Source } from 'react-native-fast-image'; import { useTheme } from '@/theme'; import { useCloudBackupsContext } from './CloudBackupProvider'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { isEmpty } from 'lodash'; const Title = styled(Text).attrs({ size: 'big', @@ -88,16 +89,20 @@ type RestoreCloudStepParams = { export default function RestoreCloudStep() { const { params } = useRoute>(); - - const { goBack } = useNavigation(); const { userData, password, setPassword } = useCloudBackupsContext(); const { selectedBackup } = params; const { isDarkMode } = useTheme(); + 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 } = useNavigation(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); const passwordRef = useRef(null); @@ -135,13 +140,13 @@ export default function RestoreCloudStep() { // NOTE: Localizing password to prevent an empty string from being saved if we re-render const pwd = password.trim(); + const prevWalletsState = await dispatch(walletsLoadState()); + try { if (!selectedBackup.name) { throw new Error('No backup file selected'); } - const prevWalletsState = await dispatch(walletsLoadState()); - await Promise.all([ dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)), restoreCloudBackup({ @@ -219,10 +224,21 @@ export default function RestoreCloudStep() { } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { + onRestoreSuccess(); dispatch(setIsWalletLoading(null)); - Navigation.handleAction(Routes.WALLET_SCREEN, {}, dangerouslyGetState()?.index === 1); + if (isEmpty(prevWalletsState)) { + Navigation.handleAction( + Routes.SWIPE_LAYOUT, + { + screen: Routes.WALLET_SCREEN, + }, + false + ); + } else { + Navigation.handleAction(Routes.WALLET_SCREEN, {}); + } } - }, [selectedBackup.name, dispatch, password, userData, initializeWallet, dangerouslyGetState]); + }, [password, dispatch, selectedBackup.name, userData, initializeWallet, onRestoreSuccess]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); 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'; From 1fadaf5e35f83f09e3eb0a345dd285791553c90d Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 15 Nov 2024 20:08:22 -0500 Subject: [PATCH 13/45] convert cloud backup provider to zustand store, remove userData as it is not storing proper data, and create new backup every time the user wants to backup --- src/App.tsx | 2 + .../backup/AddWalletToCloudBackupStep.tsx | 9 +- .../backup/BackupChooseProviderStep.tsx | 11 +- src/components/backup/ChooseBackupStep.tsx | 52 +++-- src/components/backup/CloudBackupProvider.tsx | 153 --------------- src/components/backup/RestoreCloudStep.tsx | 180 +++++++++--------- src/components/backup/useCreateBackup.ts | 61 +++--- .../secret-display/SecretDisplaySection.tsx | 13 +- src/handlers/cloudBackup.ts | 20 +- src/handlers/walletReadyEvents.ts | 5 +- src/hooks/useImportingWallet.ts | 12 +- src/hooks/useWalletCloudBackup.ts | 31 +-- src/languages/en_US.json | 4 +- src/model/backup.ts | 113 +++-------- src/navigation/Routes.android.tsx | 9 +- src/navigation/Routes.ios.tsx | 9 +- src/redux/wallets.ts | 113 +---------- src/screens/AddWalletSheet.tsx | 30 +-- .../components/Backups/BackUpMenuButton.tsx | 5 +- .../components/Backups/ViewCloudBackups.tsx | 21 +- .../components/Backups/ViewWalletBackup.tsx | 69 +++---- .../components/Backups/WalletsAndBackup.tsx | 84 ++++++-- .../components/GoogleAccountSection.tsx | 5 +- .../SettingsSheet/components/MenuHeader.tsx | 6 +- .../components/SettingsSection.tsx | 10 +- src/screens/SettingsSheet/utils.ts | 4 +- src/screens/WalletScreen/index.tsx | 9 +- src/state/backups/backups.ts | 161 ++++++++++++++++ src/state/sync/BackupsSync.tsx | 12 ++ 29 files changed, 539 insertions(+), 674 deletions(-) delete mode 100644 src/components/backup/CloudBackupProvider.tsx create mode 100644 src/state/backups/backups.ts create mode 100644 src/state/sync/BackupsSync.tsx diff --git a/src/App.tsx b/src/App.tsx index 6256687fcdd..80a5f7140a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,7 @@ import { Address } from 'viem'; import { IS_ANDROID, IS_DEV } from '@/env'; import { prefetchDefaultFavorites } from '@/resources/favorites'; import Routes from '@/navigation/Routes'; +import { BackupsSync } from '@/state/sync/BackupsSync'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -83,6 +84,7 @@ function App({ walletReady }: AppProps) { + ); } diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx index 20db9daa517..545762e5db0 100644 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ b/src/components/backup/AddWalletToCloudBackupStep.tsx @@ -11,16 +11,19 @@ import { useNavigation } from '@/navigation'; import { useWallets } from '@/hooks'; import { format } from 'date-fns'; import { login } from '@/handlers/cloudBackup'; -import { useCloudBackupsContext } from './CloudBackupProvider'; -import { BackupTypes } from '@/components/backup/useCreateBackup'; +import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore } from '@/state/backups/backups'; const imageSize = 72; export default function AddWalletToCloudBackupStep() { const { goBack } = useNavigation(); const { selectedWallet } = useWallets(); + const createBackup = useCreateBackup(); - const { createBackup, mostRecentBackup } = useCloudBackupsContext(); + const { mostRecentBackup } = backupsStore(state => ({ + mostRecentBackup: state.mostRecentBackup, + })); const potentiallyLoginAndSubmit = useCallback(async () => { await login(); diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupChooseProviderStep.tsx index 3bca963c06f..f7334ea438d 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupChooseProviderStep.tsx @@ -20,8 +20,8 @@ import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, import { WrappedAlert as Alert } from '@/helpers/alert'; import { RainbowError, logger } from '@/logger'; import { Linking } from 'react-native'; -import { CloudBackupState, useCloudBackupsContext } from './CloudBackupProvider'; -import { BackupTypes } from '@/components/backup/useCreateBackup'; +import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; const imageSize = 72; @@ -29,7 +29,10 @@ export default function BackupSheetSectionNoProvider() { const { colors } = useTheme(); const { navigate, goBack } = useNavigation(); const { selectedWallet } = useWallets(); - const { createBackup, backupState } = useCloudBackupsContext(); + const createBackup = useCreateBackup(); + const { status } = backupsStore(state => ({ + status: state.status, + })); const onCloudBackup = async () => { // NOTE: On Android we need to make sure the user is signed into a Google account before trying to backup @@ -115,7 +118,7 @@ export default function BackupSheetSectionNoProvider() { {/* replace this with BackUpMenuButton */} - + diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index fdcab9e6e3d..234e672a978 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -7,7 +7,6 @@ import { useNavigation } from '@/navigation'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; import { Box, Stack } from '@/design-system'; -import { RouteProp, useRoute } from '@react-navigation/native'; import { sharedCoolModalTopOffset } from '@/navigation/config'; import { ImgixImage } from '@/components/images'; import MenuContainer from '@/screens/SettingsSheet/components/MenuContainer'; @@ -16,7 +15,6 @@ 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 { Source } from 'react-native-fast-image'; import { IS_ANDROID } from '@/env'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -24,7 +22,7 @@ import { Page } from '@/components/layout'; import Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; import { useTheme } from '@/theme'; -import { CloudBackupState, useCloudBackupsContext } from './CloudBackupProvider'; +import { backupsStore, CloudBackupState, LoadingStates } from '@/state/backups/backups'; import { titleForBackupState } from '@/screens/SettingsSheet/utils'; const Title = styled(RNText).attrs({ @@ -53,15 +51,15 @@ const Masthead = styled(Box).attrs({ }); export function ChooseBackupStep() { - const { - params: { fromSettings }, - } = useRoute>(); const { colors } = useTheme(); - const { backupState, backups, userData, mostRecentBackup, syncAndFetchBackups } = useCloudBackupsContext(); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); - const isLoading = - backupState === CloudBackupState.Initializing || backupState === CloudBackupState.Syncing || backupState === CloudBackupState.Fetching; + const isLoading = LoadingStates.includes(status); const { top } = useSafeAreaInsets(); const { height: deviceHeight } = useDimensions(); @@ -70,13 +68,10 @@ export function ChooseBackupStep() { const onSelectCloudBackup = useCallback( (selectedBackup: Backup) => { navigate(Routes.RESTORE_CLOUD_SHEET, { - backups, - userData, selectedBackup, - fromSettings, }); }, - [navigate, userData, backups, fromSettings] + [navigate] ); const height = IS_ANDROID ? deviceHeight - top : deviceHeight - sharedCoolModalTopOffset - 48; @@ -102,7 +97,7 @@ export function ChooseBackupStep() { - {backupState === CloudBackupState.FailedToInitialize && ( + {status === CloudBackupState.FailedToInitialize && ( backupsStore.getState().syncAndFetchBackups()} titleComponent={} /> )} - {backupState === CloudBackupState.Ready && backups.files.length === 0 && ( + {status === CloudBackupState.Ready && backups.files.length === 0 && ( + + + } + /> + + + - } /> + backupsStore.getState().syncAndFetchBackups()} + titleComponent={} + /> )} - {backupState === CloudBackupState.Ready && backups.files.length > 0 && ( + {status === CloudBackupState.Ready && backups.files.length > 0 && ( {mostRecentBackup && ( @@ -194,7 +204,7 @@ export function ChooseBackupStep() { backupsStore.getState().syncAndFetchBackups()} titleComponent={} /> @@ -203,9 +213,9 @@ export function ChooseBackupStep() { )} {isLoading && ( - + {android ? : } - {titleForBackupState[backupState]} + {titleForBackupState[status]} )} diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx deleted file mode 100644 index a7f50a44657..00000000000 --- a/src/components/backup/CloudBackupProvider.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React, { Dispatch, PropsWithChildren, SetStateAction, createContext, useCallback, useContext, useEffect, useState } from 'react'; -import type { Backup, BackupUserData, CloudBackups } from '@/model/backup'; -import { - fetchAllBackups, - fetchUserDataFromCloud, - getGoogleAccountUserData, - isCloudBackupAvailable, - syncCloud, -} from '@/handlers/cloudBackup'; -import { RainbowError, logger } from '@/logger'; -import { IS_ANDROID } from '@/env'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; -import { useWallets } from '@/hooks'; -import { Mutex } from 'async-mutex'; - -const mutex = new Mutex(); - -type CloudBackupContext = { - provider: string | undefined; - setProvider: Dispatch>; - backupState: CloudBackupState; - backups: CloudBackups; - userData: BackupUserData | undefined; - mostRecentBackup: Backup | undefined; - password: string; - setPassword: Dispatch>; - createBackup: ReturnType; - syncAndFetchBackups: () => Promise; -}; - -export enum CloudBackupState { - Initializing = 'initializing', - Syncing = 'syncing', - Fetching = 'fetching', - FailedToInitialize = 'failed_to_initialize', // Failed to initialize cloud backup - Ready = 'ready', - NotAvailable = 'not_available', // iCloud / Google Drive not available - InProgress = 'in_progress', // Backup in progress - Error = 'error', - Success = 'success', -} - -const CloudBackupContext = createContext({} as CloudBackupContext); - -export function CloudBackupProvider({ children }: PropsWithChildren) { - const { wallets } = useWallets(); - const [backupState, setBackupState] = useState(CloudBackupState.Initializing); - - const [userData, setUserData] = useState(); - const [backups, setBackups] = useState({ - files: [], - }); - - const [password, setPassword] = useState(''); - - const [mostRecentBackup, setMostRecentBackup] = useState(undefined); - const [provider, setProvider] = useState(undefined); - - const syncAndPullFiles = useCallback(async () => { - try { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[CloudBackupProvider]: Cloud backup is not available'); - setBackupState(CloudBackupState.NotAvailable); - return; - } - - if (IS_ANDROID) { - const gdata = await getGoogleAccountUserData(); - if (!gdata) { - logger.debug('[CloudBackupProvider]: Google account is not available'); - setBackupState(CloudBackupState.NotAvailable); - return; - } - } - - setBackupState(CloudBackupState.Syncing); - logger.debug('[CloudBackupProvider]: Syncing with cloud'); - await syncCloud(); - - setBackupState(CloudBackupState.Fetching); - logger.debug('[CloudBackupProvider]: Fetching user data'); - const [userData, backupFiles] = await Promise.all([fetchUserDataFromCloud(), fetchAllBackups()]); - setUserData(userData); - setBackups(backupFiles); - // if the user has any cloud backups, set the provider to cloud - if (backupFiles.files.length > 0) { - setProvider(walletBackupTypes.cloud); - setMostRecentBackup(getMostRecentCloudBackup(backupFiles.files)); - } else if (hasManuallyBackedUpWallet(wallets)) { - // if the user has manually backed up wallets, set the provider to manual - setProvider(walletBackupTypes.manual); - } // else it'll remain undefined - - logger.debug(`[CloudBackupProvider]: Retrieved ${backupFiles.files.length} backup files`); - logger.debug(`[CloudBackupProvider]: Retrieved userData with ${Object.values(userData.wallets).length} wallets`); - - setBackupState(CloudBackupState.Ready); - } catch (e) { - logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), { - error: e, - }); - setBackupState(CloudBackupState.FailedToInitialize); - } - }, [wallets]); - - const syncAndFetchBackups = useCallback(async () => { - return mutex.runExclusive(syncAndPullFiles); - }, [syncAndPullFiles]); - - const createBackup = useCreateBackup({ - setBackupState, - backupState, - syncAndFetchBackups, - }); - - useEffect(() => { - syncAndFetchBackups(); - - return () => { - setBackupState(CloudBackupState.Initializing); - }; - }, [syncAndFetchBackups]); - - return ( - - {children} - - ); -} - -export function useCloudBackupsContext() { - 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 815207a8e4e..3d8e826d48a 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -41,9 +41,9 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { RestoreSheetParams } from '@/screens/RestoreSheet'; import { Source } from 'react-native-fast-image'; import { useTheme } from '@/theme'; -import { useCloudBackupsContext } from './CloudBackupProvider'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { isEmpty } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; const Title = styled(Text).attrs({ size: 'big', @@ -89,7 +89,9 @@ type RestoreCloudStepParams = { export default function RestoreCloudStep() { const { params } = useRoute>(); - const { userData, password, setPassword } = useCloudBackupsContext(); + const { password } = backupsStore(state => ({ + password: state.password, + })); const { selectedBackup } = params; const { isDarkMode } = useTheme(); @@ -113,11 +115,11 @@ export default function RestoreCloudStep() { const fetchPasswordIfPossible = async () => { const pwd = await getLocalBackupPassword(); if (pwd) { - setPassword(pwd); + backupsStore.getState().setPassword(pwd); } }; fetchPasswordIfPossible(); - }, [setPassword]); + }, []); useEffect(() => { let passwordIsValid = false; @@ -128,17 +130,15 @@ export default function RestoreCloudStep() { setValidPassword(passwordIsValid); }, [incorrectPassword, password]); - const onPasswordChange = useCallback( - ({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { - setPassword(inputText); - setIncorrectPassword(false); - }, - [setPassword] - ); + const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => { + backupsStore.getState().setPassword(inputText); + setIncorrectPassword(false); + }, []); const onSubmit = useCallback(async () => { // 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()); @@ -147,98 +147,94 @@ export default function RestoreCloudStep() { throw new Error('No backup file selected'); } - await Promise.all([ - dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)), - restoreCloudBackup({ - password: pwd, - userData, - nameOfSelectedBackupFile: selectedBackup.name, - }), - ]).then(async ([_, status]) => { - if (status === RestoreCloudBackupResultStates.success) { - // Store it in the keychain in case it was missing - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { - await saveLocalBackupPassword(pwd); + dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)); + const status = await restoreCloudBackup({ + password: pwd, + backupFilename: selectedBackup.name, + }); + if (status === RestoreCloudBackupResultStates.success) { + // Store it in the keychain in case it was missing + const hasSavedPassword = await getLocalBackupPassword(); + if (!hasSavedPassword) { + await saveLocalBackupPassword(pwd); + } + + InteractionManager.runAfterInteractions(async () => { + const newWalletsState = await dispatch(walletsLoadState()); + if (IS_ANDROID && filename) { + filename = normalizeAndroidBackupFilename(filename); } - InteractionManager.runAfterInteractions(async () => { - const newWalletsState = await dispatch(walletsLoadState()); - let filename = selectedBackup.name; - if (IS_ANDROID && filename) { - filename = normalizeAndroidBackupFilename(filename); - } + logger.debug('[RestoreCloudStep]: Done updating backup state'); + // NOTE: Marking the restored wallets as backed up + // @ts-expect-error TypeScript doesn't play nicely with Redux types here + const walletIdsToUpdate = Object.keys(newWalletsState || {}).filter(walletId => !(prevWalletsState || {})[walletId]); - logger.debug('[RestoreCloudStep]: Done updating backup state'); - // NOTE: Marking the restored wallets as backed up - // @ts-expect-error TypeScript doesn't play nicely with Redux types here - const walletIdsToUpdate = Object.keys(newWalletsState || {}).filter(walletId => !(prevWalletsState || {})[walletId]); - - logger.debug('[RestoreCloudStep]: Updating backup state of wallets with ids', { - walletIds: JSON.stringify(walletIdsToUpdate), - }); - logger.debug('[RestoreCloudStep]: Selected backup name', { - fileName: selectedBackup.name, - }); - - await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, walletBackupTypes.cloud, filename)); - - const oldCloudIds: string[] = []; - const oldManualIds: string[] = []; - // NOTE: Looping over previous wallets and restoring backup state of that wallet - Object.values(prevWalletsState || {}).forEach(wallet => { - // NOTE: This handles cloud and manual backups - if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { - oldCloudIds.push(wallet.id); - } else if (wallet.backedUp && wallet.backupType === walletBackupTypes.manual) { - oldManualIds.push(wallet.id); - } - }); - - await dispatch(setAllWalletsWithIdsAsBackedUp(oldCloudIds, walletBackupTypes.cloud, filename)); - await dispatch(setAllWalletsWithIdsAsBackedUp(oldManualIds, walletBackupTypes.manual, filename)); - - const walletKeys = Object.keys(newWalletsState || {}); - // @ts-expect-error TypeScript doesn't play nicely with Redux types here - const firstWallet = walletKeys.length > 0 ? (newWalletsState || {})[walletKeys[0]] : undefined; - const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; - const p1 = dispatch(walletsSetSelected(firstWallet)); - const p2 = dispatch(addressSetSelected(firstAddress)); - await Promise.all([p1, p2]); - await initializeWallet(null, null, null, false, false, null, true, null); + logger.debug('[RestoreCloudStep]: Updating backup state of wallets with ids', { + walletIds: JSON.stringify(walletIdsToUpdate), + }); + logger.debug('[RestoreCloudStep]: Selected backup name', { + fileName: selectedBackup.name, + }); + + await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, walletBackupTypes.cloud, filename)); + + const oldCloudIds: string[] = []; + const oldManualIds: string[] = []; + // NOTE: Looping over previous wallets and restoring backup state of that wallet + Object.values(prevWalletsState || {}).forEach(wallet => { + // NOTE: This handles cloud and manual backups + if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) { + oldCloudIds.push(wallet.id); + } else if (wallet.backedUp && wallet.backupType === walletBackupTypes.manual) { + oldManualIds.push(wallet.id); + } }); + + await dispatch(setAllWalletsWithIdsAsBackedUp(oldCloudIds, walletBackupTypes.cloud, filename)); + await dispatch(setAllWalletsWithIdsAsBackedUp(oldManualIds, walletBackupTypes.manual, filename)); + + const walletKeys = Object.keys(newWalletsState || {}); + // @ts-expect-error TypeScript doesn't play nicely with Redux types here + const firstWallet = walletKeys.length > 0 ? (newWalletsState || {})[walletKeys[0]] : undefined; + const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; + const p1 = dispatch(walletsSetSelected(firstWallet)); + const p2 = dispatch(addressSetSelected(firstAddress)); + await Promise.all([p1, p2]); + await initializeWallet(null, null, null, false, false, null, true, null); + }); + + onRestoreSuccess(); + if (isEmpty(prevWalletsState)) { + Navigation.handleAction( + Routes.SWIPE_LAYOUT, + { + screen: Routes.WALLET_SCREEN, + }, + false + ); } else { - switch (status) { - case RestoreCloudBackupResultStates.incorrectPassword: - setIncorrectPassword(true); - break; - case RestoreCloudBackupResultStates.incorrectPinCode: - Alert.alert(lang.t('back_up.restore_cloud.incorrect_pin_code')); - break; - default: - Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); - break; - } + Navigation.handleAction(Routes.WALLET_SCREEN, {}); } - }); + } else { + switch (status) { + case RestoreCloudBackupResultStates.incorrectPassword: + setIncorrectPassword(true); + break; + case RestoreCloudBackupResultStates.incorrectPinCode: + Alert.alert(lang.t('back_up.restore_cloud.incorrect_pin_code')); + break; + default: + Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); + break; + } + } } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { - onRestoreSuccess(); dispatch(setIsWalletLoading(null)); - if (isEmpty(prevWalletsState)) { - Navigation.handleAction( - Routes.SWIPE_LAYOUT, - { - screen: Routes.WALLET_SCREEN, - }, - false - ); - } else { - Navigation.handleAction(Routes.WALLET_SCREEN, {}); - } } - }, [password, dispatch, selectedBackup.name, userData, initializeWallet, onRestoreSuccess]); + }, [password, dispatch, selectedBackup.name, initializeWallet, onRestoreSuccess]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 37730a62513..a7ba6b0c465 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -1,7 +1,7 @@ /* eslint-disable no-promise-executor-return */ -import { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; -import { backupAllWalletsToCloud, findLatestBackUp, getLocalBackupPassword, saveLocalBackupPassword } from '@/model/backup'; -import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; +import { useCallback } from 'react'; +import { backupAllWalletsToCloud, getLocalBackupPassword, saveLocalBackupPassword } from '@/model/backup'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { cloudPlatform } from '@/utils/platform'; import { analytics } from '@/analytics'; import { useWalletCloudBackup, useWallets } from '@/hooks'; @@ -39,30 +39,29 @@ export enum BackupTypes { All = 'all', } -export const useCreateBackup = ({ - setBackupState, - backupState, - syncAndFetchBackups, -}: { - setBackupState: Dispatch>; - backupState: CloudBackupState; - syncAndFetchBackups: () => Promise; -}) => { +export const useCreateBackup = () => { const dispatch = useDispatch(); const { navigate } = useNavigation(); const walletCloudBackup = useWalletCloudBackup(); const { wallets } = useWallets(); - const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]); const setLoadingStateWithTimeout = useCallback( - (state: CloudBackupState, failInMs = 10_000) => { - setBackupState(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(() => { - setBackupState(CloudBackupState.Ready); + const currentState = backupsStore.getState().status; + if (currentState === state) { + backupsStore.getState().setStatus(CloudBackupState.Ready); + } }, failInMs); }, - [setBackupState] + [] ); const onSuccess = useCallback( @@ -75,17 +74,20 @@ export const useCreateBackup = ({ category: 'backup', label: cloudPlatform, }); - setLoadingStateWithTimeout(CloudBackupState.Success); - syncAndFetchBackups(); + setLoadingStateWithTimeout({ + state: CloudBackupState.Success, + outOfSync: true, + }); + backupsStore.getState().syncAndFetchBackups(); }, - [setLoadingStateWithTimeout, syncAndFetchBackups] + [setLoadingStateWithTimeout] ); const onError = useCallback( (msg: string) => { InteractionManager.runAfterInteractions(async () => { DelayedAlert({ title: msg }, 500); - setLoadingStateWithTimeout(CloudBackupState.Error); + setLoadingStateWithTimeout({ state: CloudBackupState.Error }); }); }, [setLoadingStateWithTimeout] @@ -94,18 +96,17 @@ export const useCreateBackup = ({ const onConfirmBackup = useCallback( async ({ password, type, walletId, navigateToRoute }: ConfirmBackupProps) => { analytics.track('Tapped "Confirm Backup"'); - setBackupState(CloudBackupState.InProgress); + backupsStore.getState().setStatus(CloudBackupState.InProgress); if (type === BackupTypes.All) { if (!wallets) { onError('Error loading wallets. Please try again.'); - setBackupState(CloudBackupState.Error); + backupsStore.getState().setStatus(CloudBackupState.Error); return; } backupAllWalletsToCloud({ wallets: wallets as AllRainbowWallets, password, - latestBackup, onError, onSuccess, dispatch, @@ -115,7 +116,7 @@ export const useCreateBackup = ({ if (!walletId) { onError('Wallet not found. Please try again.'); - setBackupState(CloudBackupState.Error); + backupsStore.getState().setStatus(CloudBackupState.Error); return; } @@ -130,7 +131,7 @@ export const useCreateBackup = ({ navigate(navigateToRoute.route, navigateToRoute.params || {}); } }, - [setBackupState, walletCloudBackup, onError, wallets, latestBackup, onSuccess, dispatch, navigate] + [walletCloudBackup, onError, wallets, onSuccess, dispatch, navigate] ); const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { @@ -156,7 +157,7 @@ export const useCreateBackup = ({ const createBackup = useCallback( async (props: UseCreateBackupProps) => { - if (backupState !== CloudBackupState.Ready) { + if (backupsStore.getState().status !== CloudBackupState.Ready) { return false; } @@ -168,10 +169,12 @@ export const useCreateBackup = ({ }); return true; } - setLoadingStateWithTimeout(CloudBackupState.Ready); + setLoadingStateWithTimeout({ + state: CloudBackupState.Ready, + }); return false; }, - [backupState, getPassword, onConfirmBackup, setLoadingStateWithTimeout] + [getPassword, onConfirmBackup, setLoadingStateWithTimeout] ); return createBackup; diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx index dc0da60210a..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,7 +24,7 @@ import { useNavigation } from '@/navigation'; import { ImgixImage } from '../images'; import RoutesWithPlatformDifferences from '@/navigation/routesNames'; import { Source } from 'react-native-fast-image'; -import { useCloudBackupsContext } from '../backup/CloudBackupProvider'; +import { backupsStore } from '@/state/backups/backups'; const MIN_HEIGHT = 740; @@ -64,7 +63,9 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const { colors } = useTheme(); const { params } = useRoute>(); const { selectedWallet, wallets } = useWallets(); - const { provider, setProvider } = useCloudBackupsContext(); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); const { onManuallyBackupWalletId } = useWalletManualBackup(); const { navigate } = useNavigation(); @@ -126,12 +127,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }: const handleConfirmSaved = useCallback(() => { if (backupType === WalletBackupTypes.manual) { onManuallyBackupWalletId(walletId); - if (!provider) { - setProvider(WalletBackupTypes.manual); + if (!backupProvider) { + backupsStore.getState().setBackupProvider(WalletBackupTypes.manual); } navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP); } - }, [backupType, onManuallyBackupWalletId, walletId, provider, navigate, setProvider]); + }, [backupType, onManuallyBackupWalletId, walletId, backupProvider, navigate]); const getIconForBackupType = useCallback(() => { if (isBackingUp) { diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index e5b03815a1a..f3dd195c140 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 { Backup, BackupUserData, CloudBackups } from '@/model/backup'; +import { Backup, CloudBackups } from '@/model/backup'; + const REMOTE_BACKUP_WALLET_DIR = 'rainbow.me/wallet-backups'; export const USERDATA_FILE = 'UserData.json'; + const encryptor = new AesEncryptor(); export const CLOUD_BACKUP_ERRORS = { @@ -76,7 +77,7 @@ export async function fetchAllBackups(): Promise { }; } -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)); @@ -206,19 +207,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(): Promise { - 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/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index e6d52085615..7fbfe3fc105 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -15,6 +15,7 @@ import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { InteractionManager } from 'react-native'; +import { backupsStore } from '@/state/backups/backups'; const BACKUP_SHEET_DELAY_MS = 3000; @@ -25,7 +26,7 @@ export const runKeychainIntegrityChecks = async () => { } }; -export const runWalletBackupStatusChecks = (provider: string | undefined) => { +export const runWalletBackupStatusChecks = () => { const { selected, wallets, @@ -57,6 +58,8 @@ export const runWalletBackupStatusChecks = (provider: string | undefined) => { hasSelectedWallet, }); + const provider = backupsStore.getState().backupProvider; + // if one of them is selected, show the default BackupSheet if (selected && hasSelectedWallet && IS_TESTING !== 'true') { let stepType: string = WalletBackupStepTypes.no_provider; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index 2fa49276c0d..96f08578a73 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -31,7 +31,7 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { ChainId } from '@/chains/types'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; +import { backupsStore } from '@/state/backups/backups'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -52,7 +52,9 @@ export default function useImportingWallet({ showImportModal = true } = {}) { const { updateWalletENSAvatars } = useWalletENSAvatar(); const profilesEnabled = useExperimentalFlag(PROFILES); - const { provider } = useCloudBackupsContext(); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); const inputRef = useRef(null); @@ -349,9 +351,9 @@ export default function useImportingWallet({ showImportModal = true } = {}) { ) ) { let stepType: string = WalletBackupStepTypes.no_provider; - if (provider === walletBackupTypes.cloud) { + if (backupProvider === walletBackupTypes.cloud) { stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (provider === walletBackupTypes.manual) { + } else if (backupProvider === walletBackupTypes.manual) { stepType = WalletBackupStepTypes.backup_now_manually; } @@ -414,7 +416,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { showImportModal, profilesEnabled, dangerouslyGetParent, - provider, + backupProvider, ]); return { diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 63b2f427c96..12c2e23d872 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,9 +1,9 @@ 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'; @@ -16,7 +16,6 @@ import { getSupportedBiometryType } from '@/keychain'; import { IS_ANDROID } from '@/env'; import { authenticateWithPIN } from '@/handlers/authentication'; import * as i18n from '@/languages'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; export function getUserError(e: Error) { switch (e.message) { @@ -41,7 +40,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 ({ @@ -101,23 +99,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); @@ -148,7 +137,7 @@ export default function useWalletCloudBackup() { return false; }, - [dispatch, latestBackup, wallets] + [dispatch, wallets] ); return walletCloudBackup; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index fb454c734af..902997e943f 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -116,7 +116,7 @@ "failed_to_fetch_backups": "Failed to fetch backups", "retry": "Retry", "refresh": "Refresh", - "syncing_cloud_store": "Syncing user data from %{cloudPlatformName}", + "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}", @@ -139,7 +139,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", diff --git a/src/model/backup.ts b/src/model/backup.ts index 03c2044f14c..9c2f4ce88b0 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -4,12 +4,11 @@ import { captureException } from '@sentry/react-native'; import { endsWith } from 'lodash'; import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup'; 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'; @@ -100,14 +99,12 @@ 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?: (password: BackupPassword) => void; dispatch: any; @@ -158,48 +155,22 @@ export async function backupAllWalletsToCloud({ }); 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); + updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); const walletIdsToUpdate = Object.keys(wallets); await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, WalletBackupTypes.cloud, updatedBackupFile)); @@ -251,9 +222,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,26 +298,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', @@ -369,16 +326,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; } @@ -403,26 +358,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) { diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 5cacf83eb4e..79bfbd90c29 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -90,7 +90,6 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; -import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -270,11 +269,9 @@ function AuthNavigator() { const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - - - + + + )); diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 238d87f33e0..459561484d3 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -102,7 +102,6 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; -import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -284,11 +283,9 @@ function NativeStackNavigator() { const AppContainerWithAnalytics = React.forwardRef, { onReady: () => void }>((props, ref) => ( - - - - - + + + )); diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index 9fd1fcf52e4..8f592fd42c9 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'; @@ -288,17 +286,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; - } - } }; /** @@ -308,15 +295,9 @@ 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 }; @@ -332,98 +313,6 @@ export const setWalletBackedUp = 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])); - } }; /** diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 6021f7ad295..8ed3d4bcc8c 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -8,8 +8,7 @@ import * as i18n from '@/languages'; import { HARDWARE_WALLETS, PROFILES, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; import { InteractionManager, Linking } from 'react-native'; -import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; -import WalletBackupTypes from '@/helpers/walletBackupTypes'; +import { createAccountForWallet, setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { createWallet } from '@/model/wallet'; import WalletTypes from '@/helpers/walletTypes'; import { logger, RainbowError } from '@/logger'; @@ -18,10 +17,8 @@ import CreateNewWallet from '@/assets/CreateNewWallet.png'; import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; -import { captureException } from '@sentry/react-native'; import { useDispatch } from 'react-redux'; import { - backupUserDataIntoCloud, getGoogleAccountUserData, GoogleDriveUserData, isCloudBackupAvailable, @@ -34,6 +31,7 @@ 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'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -84,6 +82,8 @@ export const AddWalletSheet = () => { }, onCloseModal: async (args: any) => { if (args) { + dispatch(setIsWalletLoading(WalletLoadingStates.CREATING_WALLET)); + const name = args?.name ?? ''; const color = args?.color ?? null; // Check if the selected wallet is the primary @@ -114,31 +114,19 @@ 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 + // TODO: Make sure the new wallet is marked as not backed up } else { + // If doesn't exist, we need to create a new wallet await createWallet({ color, name, clearCallbackOnStartCreation: true, }); await dispatch(walletsLoadState(profilesEnabled)); - // @ts-ignore + // @ts-expect-error - needs refactor to object params await initializeWallet(); } } catch (e) { @@ -150,6 +138,8 @@ export const AddWalletSheet = () => { showWalletErrorAlert(); }, 1000); } + } finally { + dispatch(setIsWalletLoading(null)); } } creatingWallet.current = false; diff --git a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx index 79cf9821874..fa38313f32f 100644 --- a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx +++ b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx @@ -5,18 +5,20 @@ import MenuItem from '../MenuItem'; import Spinner from '@/components/Spinner'; import { FloatingEmojis } from '@/components/floating-emojis'; import { useDimensions } from '@/hooks'; -import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; +import { CloudBackupState } from '@/state/backups/backups'; export const BackUpMenuItem = ({ icon = '􀊯', backupState, onPress, title, + disabled, }: { icon?: string; backupState: CloudBackupState; title: string; onPress: () => void; + disabled?: boolean; }) => { const { colors } = useTheme(); const { width: deviceWidth } = useDimensions(); @@ -86,6 +88,7 @@ export const BackUpMenuItem = ({ diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index ca77fdd9f90..b765a2d16c3 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -10,11 +10,11 @@ import { format } from 'date-fns'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; -import { Centered, Page } from '@/components/layout'; +import { Page } from '@/components/layout'; import Spinner from '@/components/Spinner'; import ActivityIndicator from '@/components/ActivityIndicator'; import { useTheme } from '@/theme'; -import { useCloudBackupsContext, CloudBackupState } from '@/components/backup/CloudBackupProvider'; +import { CloudBackupState, LoadingStates, backupsStore } from '@/state/backups/backups'; import { titleForBackupState } from '../../utils'; import { Box } from '@/design-system'; @@ -31,7 +31,11 @@ const ViewCloudBackups = () => { const { navigate } = useNavigation(); const { colors } = useTheme(); - const { backupState, backups, mostRecentBackup, syncAndFetchBackups } = useCloudBackupsContext(); + const { status, backups, mostRecentBackup } = backupsStore(state => ({ + status: state.status, + backups: state.backups, + mostRecentBackup: state.mostRecentBackup, + })); const onSelectCloudBackup = useCallback( async (selectedBackup: Backup) => { @@ -118,7 +122,7 @@ const ViewCloudBackups = () => { backupsStore.getState().syncAndFetchBackups()} titleComponent={} /> @@ -132,22 +136,21 @@ const ViewCloudBackups = () => { ); - const isLoading = - backupState === CloudBackupState.Initializing || backupState === CloudBackupState.Syncing || backupState === CloudBackupState.Fetching; + const isLoading = LoadingStates.includes(status); if (isLoading) { return ( {android ? : } - {titleForBackupState[backupState]} + {titleForBackupState[status]} ); } return ( - {backupState === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()} - {backupState === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()} + {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 ec16c005ac8..626e44d2297 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -31,26 +31,21 @@ import { SETTINGS_BACKUP_ROUTES } from './routes'; import { analyticsV2 } from '@/analytics'; import { InteractionManager, Linking } 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, setIsWalletLoading } from '@/redux/wallets'; +import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; import { logger, RainbowError } from '@/logger'; -import { RainbowAccount, createWallet } from '@/model/wallet'; +import { RainbowAccount } from '@/model/wallet'; import { PROFILES, useExperimentalFlag } from '@/config'; import showWalletErrorAlert from '@/helpers/support'; import { IS_ANDROID, IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { BackUpMenuItem } from './BackUpMenuButton'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { BackupTypes } from '@/components/backup/useCreateBackup'; +import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { backupsStore } from '@/state/backups/backups'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -123,11 +118,18 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }: const ViewWalletBackup = () => { const { params } = useRoute>(); - const { createBackup, backupState, provider, mostRecentBackup } = useCloudBackupsContext(); + 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]; + + console.log(JSON.stringify(wallet, null, 2)); const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); const profilesEnabled = useExperimentalFlag(PROFILES); @@ -223,38 +225,19 @@ const ViewWalletBackup = () => { }, onCloseModal: async (args: any) => { if (args) { + dispatch(setIsWalletLoading(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)); + 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)); - // @ts-expect-error - no params - await initializeWallet(); + // TODO: mark the newly created wallet as not backed up } } catch (e) { logger.error(new RainbowError(`[ViewWalletBackup]: Error while trying to add account`), { @@ -265,6 +248,8 @@ const ViewWalletBackup = () => { showWalletErrorAlert(); }, 1000); } + } finally { + dispatch(setIsWalletLoading(null)); } } creatingWallet.current = false; @@ -352,14 +337,14 @@ const ViewWalletBackup = () => { paddingBottom={{ custom: 24 }} iconComponent={ } titleComponent={} labelComponent={ - {provider === walletBackupTypes.cloud && ( + {backupProvider === walletBackupTypes.cloud && ( { })} /> )} - {provider !== walletBackupTypes.cloud && ( + {backupProvider !== walletBackupTypes.cloud && ( { /> - {provider === walletBackupTypes.cloud && ( + {backupProvider === walletBackupTypes.cloud && ( { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - backupState={backupState} + backupState={status} onPress={backupWalletsToCloud} /> )} - {provider !== walletBackupTypes.cloud && ( + {backupProvider !== walletBackupTypes.cloud && ( { title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, { cloudPlatformName: cloudPlatform, })} - backupState={backupState} + backupState={status} onPress={backupWalletsToCloud} /> diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 7680d4f3d78..aa5faf7503b 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -15,7 +15,7 @@ import { useNavigation } from '@/navigation'; import { abbreviations } from '@/utils'; import { addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; -import MenuHeader from '../MenuHeader'; +import MenuHeader, { StatusType } from '../MenuHeader'; import { checkLocalWalletsForBackupStatus } from '../../utils'; import { Inline, Text, Box, Stack } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; @@ -27,17 +27,18 @@ 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 { setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { BackupTypes } from '@/components/backup/useCreateBackup'; +import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { Linking } from 'react-native'; +import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; type WalletPillProps = { account: RainbowAccount; @@ -94,7 +95,14 @@ export const WalletsAndBackup = () => { const { wallets } = useWallets(); const profilesEnabled = useExperimentalFlag(PROFILES); const dispatch = useDispatch(); - const { backups, backupState, createBackup, provider, mostRecentBackup } = useCloudBackupsContext(); + + 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(); @@ -163,8 +171,12 @@ export const WalletsAndBackup = () => { } } + if (status !== CloudBackupState.Ready) { + return; + } + createBackup({ type: BackupTypes.All }); - }, [createBackup]); + }, [createBackup, status]); const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { @@ -180,6 +192,8 @@ export const WalletsAndBackup = () => { onCloseModal: async ({ name }: { name: string }) => { const nameValue = name.trim() !== '' ? name.trim() : ''; try { + dispatch(setIsWalletLoading(WalletLoadingStates.CREATING_WALLET)); + await createWallet({ color: null, name: nameValue, @@ -194,6 +208,8 @@ export const WalletsAndBackup = () => { logger.error(new RainbowError(`[WalletsAndBackup]: Failed to create new secret phrase`), { error: err, }); + } finally { + dispatch(setIsWalletLoading(null)); } }, }); @@ -220,8 +236,36 @@ export const WalletsAndBackup = () => { [navigate, wallets] ); + const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: 'Not Enabled', + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: 'Syncing', + }; + } + + if (!allBackedUp) { + return { + status: 'out-of-date', + text: 'Out of Date', + }; + } + + return { + status: 'up-to-date', + text: 'Up to date', + }; + }, [status, allBackedUp]); + const renderView = useCallback(() => { - switch (provider) { + switch (backupProvider) { default: case undefined: { return ( @@ -255,7 +299,8 @@ export const WalletsAndBackup = () => { @@ -345,12 +390,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={ - - } + statusComponent={} labelComponent={ allBackedUp ? ( { cloudPlatformName: cloudPlatform, })} icon="􀎽" - backupState={backupState} + backupState={status} + disabled={status !== CloudBackupState.Ready} onPress={backupAllNonBackedUpWalletsTocloud} /> @@ -589,8 +630,9 @@ export const WalletsAndBackup = () => { > @@ -600,17 +642,19 @@ export const WalletsAndBackup = () => { } } }, [ - provider, - backupState, + backupProvider, + status, backupAllNonBackedUpWalletsTocloud, sortedWallets, onCreateNewSecretPhrase, - onViewCloudBackups, - manageCloudBackups, navigate, onNavigateToWalletView, allBackedUp, + iconStatusType, + text, mostRecentBackup, + onViewCloudBackups, + manageCloudBackups, onPressLearnMoreAboutCloudBackups, ]); diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx index b415e1d4d30..6bf2e0160e0 100644 --- a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx +++ b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx @@ -3,7 +3,7 @@ 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 { clearAllWalletsBackupStatus } from '@/redux/wallets'; import { useDispatch } from 'react-redux'; import Menu from './Menu'; import MenuItem from './MenuItem'; @@ -61,7 +61,8 @@ export const GoogleAccountSection: React.FC = () => { const loginToGoogleDrive = async () => { setLoading(true); - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); + + // TODO: We need to update the wallet backup status here somehow... try { const accountDetails = await getGoogleAccountUserData(); setAccountDetails(accountDetails ?? undefined); diff --git a/src/screens/SettingsSheet/components/MenuHeader.tsx b/src/screens/SettingsSheet/components/MenuHeader.tsx index fe3ee059881..344fc516f01 100644 --- a/src/screens/SettingsSheet/components/MenuHeader.tsx +++ b/src/screens/SettingsSheet/components/MenuHeader.tsx @@ -64,7 +64,7 @@ const Selection = ({ children }: SelectionProps) => ( ); -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 d083178c5f1..19a73f10071 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -31,8 +31,8 @@ import { SettingsExternalURLs } from '../constants'; import { checkLocalWalletsForBackupStatus } from '../utils'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { Box } from '@/design-system'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; import { capitalize } from 'lodash'; +import { backupsStore } from '@/state/backups/backups'; interface SettingsSectionProps { onCloseModal: () => void; @@ -61,7 +61,9 @@ const SettingsSection = ({ const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS); const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS); - const { provider } = useCloudBackupsContext(); + const { backupProvider } = backupsStore(state => ({ + backupProvider: state.backupProvider, + })); const { isDarkMode, setTheme, colorScheme } = useTheme(); @@ -163,12 +165,12 @@ const SettingsSection = ({ return undefined; } - if (provider === walletBackupTypes.cloud) { + if (backupProvider === walletBackupTypes.cloud) { return CloudBackupWarningIcon; } return BackupWarningIcon; - }, [allBackedUp, provider]); + }, [allBackedUp, backupProvider]); return ( }> diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 3d8bacd7cc9..47ecea30fc5 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -3,9 +3,9 @@ import WalletTypes from '@/helpers/walletTypes'; import { useWallets } from '@/hooks'; import { isEmpty } from 'lodash'; import { Backup, parseTimestampFromFilename } from '@/model/backup'; -import { CloudBackupState } from '@/components/backup/CloudBackupProvider'; import * as i18n from '@/languages'; import { cloudPlatform } from '@/utils/platform'; +import { CloudBackupState } from '@/state/backups/backups'; type WalletBackupStatus = { allBackedUp: boolean; @@ -68,7 +68,7 @@ export const getMostRecentCloudBackup = (backups: Backup[]) => { ); }; -export const titleForBackupState = { +export const titleForBackupState: Partial> = { [CloudBackupState.Initializing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, { cloudPlatformName: cloudPlatform, }), diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 2eb3449577c..03ddf4442c8 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -29,7 +29,6 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { RootStackParamList } from '@/navigation/types'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/Routes'; -import { useCloudBackupsContext } from '@/components/backup/CloudBackupProvider'; import { BackendNetworks } from '@/components/BackendNetworks'; function WalletScreen() { @@ -39,8 +38,6 @@ function WalletScreen() { const [initialized, setInitialized] = useState(!!params?.initialized); const initializeWallet = useInitializeWallet(); const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings(); - const { provider } = useCloudBackupsContext(); - const loadAccountLateData = useLoadAccountLateData(); const loadGlobalLateData = useLoadGlobalLateData(); const insets = useSafeAreaInsets(); @@ -75,10 +72,8 @@ function WalletScreen() { }, [initializeWallet, initialized, params, setParams]); useEffect(() => { - if (provider) { - runWalletBackupStatusChecks(provider); - } - }, [provider]); + runWalletBackupStatusChecks(); + }, []); useEffect(() => { if (walletReady) { diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts new file mode 100644 index 00000000000..e39c625d219 --- /dev/null +++ b/src/state/backups/backups.ts @@ -0,0 +1,161 @@ +import { Backup, CloudBackups } from '@/model/backup'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { IS_ANDROID } from '@/env'; +import { fetchAllBackups, getGoogleAccountUserData, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup'; +import { RainbowError, logger } from '@/logger'; +import walletBackupTypes from '@/helpers/walletBackupTypes'; +import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/SettingsSheet/utils'; +import { Mutex } from 'async-mutex'; +import store from '@/redux/store'; + +const sleep = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); + }); + +const mutex = new Mutex(); + +export enum CloudBackupState { + Initializing = 'initializing', + Syncing = 'syncing', + Fetching = 'fetching', + FailedToInitialize = 'failed_to_initialize', + Ready = 'ready', + NotAvailable = 'not_available', + InProgress = 'in_progress', + Error = 'error', + Success = 'success', +} + +export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching]; + +interface BackupsStore { + backupProvider: string | undefined; + setBackupProvider: (backupProvider: string | undefined) => void; + + status: CloudBackupState; + setStatus: (status: CloudBackupState) => void; + + backups: CloudBackups; + setBackups: (backups: CloudBackups) => void; + + mostRecentBackup: Backup | undefined; + setMostRecentBackup: (backup: Backup | undefined) => void; + + password: string; + setPassword: (password: string) => void; + + syncAndFetchBackups: (retryOnFailure?: boolean) => Promise<{ + success: boolean; + retry?: boolean; + }>; +} + +const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching]; + +export const backupsStore = createRainbowStore((set, get) => ({ + 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) => { + const { status } = get(); + const syncAndPullFiles = async (): Promise<{ success: boolean; retry?: boolean }> => { + try { + const isAvailable = await isCloudBackupAvailable(); + if (!isAvailable) { + logger.debug('[backupsStore]: Cloud backup is not available'); + set({ status: CloudBackupState.NotAvailable }); + return { + success: false, + retry: false, + }; + } + + if (IS_ANDROID) { + const gdata = await getGoogleAccountUserData(); + if (!gdata) { + logger.debug('[backupsStore]: Google account is not available'); + set({ status: CloudBackupState.NotAvailable }); + 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 }); + } + + 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 syncAndPullFiles(); + releaser(); + logger.debug('[backupsStore]: Released mutex'); + if (retry) { + await sleep(5_000); + return get().syncAndFetchBackups(retryOnFailure); + } + return { success, retry }; + }, +})); 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); From a9948afc4f58158131b1e48dbdaaaa0fc57f49cc Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 10:29:56 -0500 Subject: [PATCH 14/45] lots of cleanup --- .../backup/AddWalletToCloudBackupStep.tsx | 25 +++---- .../backup/BackupChooseProviderStep.tsx | 68 ++++--------------- src/components/backup/ChooseBackupStep.tsx | 4 +- src/components/backup/RestoreCloudStep.tsx | 5 +- src/components/backup/useCreateBackup.ts | 30 ++------ src/hooks/useManageCloudBackups.ts | 6 +- src/model/backup.ts | 67 ++++++++++++++++-- src/screens/AddWalletSheet.tsx | 56 ++------------- .../components/Backups/ViewWalletBackup.tsx | 59 +++------------- .../components/Backups/WalletsAndBackup.tsx | 57 ++-------------- src/screens/SettingsSheet/utils.ts | 35 +++++----- src/state/backups/backups.ts | 6 +- 12 files changed, 143 insertions(+), 275 deletions(-) diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx index 545762e5db0..24bfc56bf44 100644 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ b/src/components/backup/AddWalletToCloudBackupStep.tsx @@ -10,9 +10,9 @@ import Routes from '@/navigation/routesNames'; import { useNavigation } from '@/navigation'; import { useWallets } from '@/hooks'; import { format } from 'date-fns'; -import { login } from '@/handlers/cloudBackup'; -import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { backupsStore } from '@/state/backups/backups'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; const imageSize = 72; @@ -26,16 +26,17 @@ export default function AddWalletToCloudBackupStep() { })); const potentiallyLoginAndSubmit = useCallback(async () => { - await login(); - const result = await createBackup({ - type: BackupTypes.Single, - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, + const result = await executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId: selectedWallet.id, + navigateToRoute: { + route: Routes.SETTINGS_SHEET, + params: { + screen: Routes.SETTINGS_SECTION_BACKUP, + }, + }, + }), }); if (result) { diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupChooseProviderStep.tsx index f7334ea438d..c576626c541 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupChooseProviderStep.tsx @@ -15,13 +15,9 @@ import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backu import { useWallets } from '@/hooks'; 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 { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; const imageSize = 72; @@ -34,56 +30,18 @@ export default function BackupSheetSectionNoProvider() { status: state.status, })); - const onCloudBackup = async () => { - // 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), + const onCloudBackup = () => { + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId: selectedWallet.id, + navigateToRoute: { + route: Routes.SETTINGS_SHEET, + params: { + screen: Routes.SETTINGS_SECTION_BACKUP, }, - { - style: 'cancel', - text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), - }, - ] - ); - return; - } - } - - createBackup({ - type: BackupTypes.Single, - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, + }, + }), }); }; diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx index 234e672a978..d08d4cdb0e2 100644 --- a/src/components/backup/ChooseBackupStep.tsx +++ b/src/components/backup/ChooseBackupStep.tsx @@ -14,7 +14,7 @@ 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 { 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'; @@ -66,7 +66,7 @@ export function ChooseBackupStep() { const { navigate } = useNavigation(); const onSelectCloudBackup = useCallback( - (selectedBackup: Backup) => { + (selectedBackup: BackupFile) => { navigate(Routes.RESTORE_CLOUD_SHEET, { selectedBackup, }); diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 3d8e826d48a..8c7949397a9 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -6,7 +6,7 @@ import WalletAndBackup from '@/assets/WalletsAndBackup.png'; import { KeyboardArea } from 'react-native-keyboard-area'; import { - Backup, + BackupFile, getLocalBackupPassword, restoreCloudBackup, RestoreCloudBackupResultStates, @@ -83,7 +83,7 @@ const KeyboardSizeView = styled(KeyboardArea)({ type RestoreCloudStepParams = { RestoreSheet: { - selectedBackup: Backup; + selectedBackup: BackupFile; }; }; @@ -205,6 +205,7 @@ export default function RestoreCloudStep() { }); onRestoreSuccess(); + backupsStore.getState().setPassword(''); if (isEmpty(prevWalletsState)) { Navigation.handleAction( Routes.SWIPE_LAYOUT, diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index a7ba6b0c465..a3c002b6803 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -13,20 +13,11 @@ import { DelayedAlert } from '@/components/alerts'; import { useDispatch } from 'react-redux'; import { AllRainbowWallets } from '@/model/wallet'; -type SingleWalletBackupProps = { - type: BackupTypes.Single; - walletId: string; -}; - -type AllWalletsBackupProps = { - type: BackupTypes.All; - walletId?: undefined; -}; - -type UseCreateBackupProps = (SingleWalletBackupProps | AllWalletsBackupProps) & { +type UseCreateBackupProps = { + walletId?: string; navigateToRoute?: { route: string; - params?: any; + params?: Record; }; }; @@ -34,11 +25,6 @@ type ConfirmBackupProps = { password: string; } & UseCreateBackupProps; -export enum BackupTypes { - Single = 'single', - All = 'all', -} - export const useCreateBackup = () => { const dispatch = useDispatch(); const { navigate } = useNavigation(); @@ -94,11 +80,11 @@ export const useCreateBackup = () => { ); const onConfirmBackup = useCallback( - async ({ password, type, walletId, navigateToRoute }: ConfirmBackupProps) => { + async ({ password, walletId, navigateToRoute }: ConfirmBackupProps) => { analytics.track('Tapped "Confirm Backup"'); backupsStore.getState().setStatus(CloudBackupState.InProgress); - if (type === BackupTypes.All) { + if (typeof walletId === 'undefined') { if (!wallets) { onError('Error loading wallets. Please try again.'); backupsStore.getState().setStatus(CloudBackupState.Error); @@ -114,12 +100,6 @@ export const useCreateBackup = () => { return; } - if (!walletId) { - onError('Wallet not found. Please try again.'); - backupsStore.getState().setStatus(CloudBackupState.Error); - return; - } - await walletCloudBackup({ onError, onSuccess, diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index 141f26b7f4e..526d75ad26a 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -4,11 +4,12 @@ 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 { 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 } from '@/state/backups/backups'; export default function useManageCloudBackups() { const dispatch = useDispatch(); @@ -49,9 +50,10 @@ export default function useManageCloudBackups() { }; const loginToGoogleDrive = async () => { - await dispatch(updateWalletBackupStatusesBasedOnCloudUserData()); + // TODO: Figure out how to update the backup status based on the new account? try { const accountDetails = await getGoogleAccountUserData(); + backupsStore.getState().syncAndFetchBackups(); setAccountDetails(accountDetails ?? undefined); } catch (error) { logger.error(new RainbowError(`[useManageCloudBackups]: Logging into Google Drive failed.`), { diff --git a/src/model/backup.ts b/src/model/backup.ts index 9c2f4ce88b0..294e51636e3 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -1,10 +1,17 @@ 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, +} from '@/handlers/cloudBackup'; import WalletBackupTypes from '../helpers/walletBackupTypes'; -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'; @@ -24,15 +31,17 @@ import { clearAllStorages } from './mmkv'; import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { getRemoteConfig } from './remoteConfig'; +import { WrappedAlert as Alert } from '@/helpers/alert'; + 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; @@ -62,6 +71,54 @@ interface BackedUpData { export interface BackupUserData { wallets: AllRainbowWallets; } +type MaybePromise = T | Promise; + +export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: { fn: () => MaybePromise; logout?: boolean }) => { + if (IS_ANDROID) { + try { + if (logout) { + await logoutFromGoogleDrive(); + } + await login(); + const userData = await getGoogleAccountUserData(); + if (!userData) { + Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); + return; + } + // execute the function + 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)); + } + } 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; + } + + // execute the function + return await fn(); + } +}; async function extractSecretsForWallet(wallet: RainbowWallet) { const allKeys = await keychain.loadAllKeys(); diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 8ed3d4bcc8c..93954ee7266 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -7,7 +7,7 @@ import React, { useRef } from 'react'; import * as i18n from '@/languages'; import { HARDWARE_WALLETS, PROFILES, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; -import { InteractionManager, Linking } from 'react-native'; +import { InteractionManager } from 'react-native'; import { createAccountForWallet, setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { createWallet } from '@/model/wallet'; import WalletTypes from '@/helpers/walletTypes'; @@ -18,20 +18,12 @@ import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; import { useDispatch } from 'react-redux'; -import { - 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'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -188,47 +180,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/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 626e44d2297..6f5c9243cb7 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -29,23 +29,22 @@ 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, setIsWalletLoading } from '@/redux/wallets'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; import { logger, RainbowError } from '@/logger'; import { RainbowAccount } from '@/model/wallet'; import { PROFILES, useExperimentalFlag } from '@/config'; import showWalletErrorAlert from '@/helpers/support'; -import { IS_ANDROID, IS_IOS } from '@/env'; +import { IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { backupsStore } from '@/state/backups/backups'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -128,8 +127,6 @@ const ViewWalletBackup = () => { const creatingWallet = useRef(); const { isDamaged, wallets } = useWallets(); const wallet = wallets?.[walletId]; - - console.log(JSON.stringify(wallet, null, 2)); const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); const profilesEnabled = useExperimentalFlag(PROFILES); @@ -142,49 +139,11 @@ const ViewWalletBackup = () => { const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom); const backupWalletsToCloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return createBackup({ - walletId, - type: BackupTypes.Single, - }); - } - 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; - } - } - - return createBackup({ - walletId, - type: BackupTypes.Single, + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId, + }), }); }, [createBackup, walletId]); diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index aa5faf7503b..3e3e6fa082f 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -29,16 +29,14 @@ import { PROFILES, useExperimentalFlag } from '@/config'; import { useDispatch } from 'react-redux'; import { setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; -import { IS_ANDROID, IS_IOS } from '@/env'; -import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup'; +import { IS_IOS } from '@/env'; +import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; import { backupsStore, CloudBackupState } from '@/state/backups/backups'; -import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup'; -import { WrappedAlert as Alert } from '@/helpers/alert'; -import { Linking } from 'react-native'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; type WalletPillProps = { account: RainbowAccount; @@ -132,51 +130,10 @@ export const WalletsAndBackup = () => { }, [visibleWallets]); const backupAllNonBackedUpWalletsTocloud = useCallback(async () => { - if (IS_ANDROID) { - try { - await login(); - - getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => { - if (accountDetails) { - return createBackup({ type: BackupTypes.All }); - } - 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; - } - } - - if (status !== CloudBackupState.Ready) { - return; - } - - createBackup({ type: BackupTypes.All }); - }, [createBackup, status]); + executeFnIfCloudBackupAvailable({ + fn: () => createBackup({}), + }); + }, [createBackup]); const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 47ecea30fc5..58c0162f242 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -2,7 +2,7 @@ import WalletBackupTypes from '@/helpers/walletBackupTypes'; import WalletTypes from '@/helpers/walletTypes'; import { useWallets } from '@/hooks'; import { isEmpty } from 'lodash'; -import { Backup, parseTimestampFromFilename } from '@/model/backup'; +import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import * as i18n from '@/languages'; import { cloudPlatform } from '@/utils/platform'; import { CloudBackupState } from '@/state/backups/backups'; @@ -41,31 +41,28 @@ export const checkLocalWalletsForBackupStatus = (wallets: ReturnType { +export const getMostRecentCloudBackup = (backups: BackupFile[]) => { const cloudBackups = backups.sort((a, b) => { return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name); }); - return cloudBackups.reduce( - (prev, current) => { - if (!current) { - return prev; - } + return cloudBackups.reduce((prev, current) => { + if (!current) { + return prev; + } - if (!prev) { - return current; - } + if (!prev) { + return current; + } - const prevTimestamp = new Date(prev.lastModified).getTime(); - const currentTimestamp = new Date(current.lastModified).getTime(); - if (currentTimestamp > prevTimestamp) { - 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 - ); + return prev; + }, cloudBackups[0]); }; export const titleForBackupState: Partial> = { diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index e39c625d219..3148fa7482a 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -1,4 +1,4 @@ -import { Backup, CloudBackups } from '@/model/backup'; +import { BackupFile, CloudBackups } from '@/model/backup'; import { createRainbowStore } from '../internal/createRainbowStore'; import { IS_ANDROID } from '@/env'; import { fetchAllBackups, getGoogleAccountUserData, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup'; @@ -39,8 +39,8 @@ interface BackupsStore { backups: CloudBackups; setBackups: (backups: CloudBackups) => void; - mostRecentBackup: Backup | undefined; - setMostRecentBackup: (backup: Backup | undefined) => void; + mostRecentBackup: BackupFile | undefined; + setMostRecentBackup: (backup: BackupFile | undefined) => void; password: string; setPassword: (password: string) => void; From 5c5a848e27df53cc443a60ac0de35108bace7b6b Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 11:22:14 -0500 Subject: [PATCH 15/45] add icon to indicate wrogn backup password --- src/components/backup/RestoreCloudStep.tsx | 5 +--- src/components/backup/useCreateBackup.ts | 3 +- src/components/fields/PasswordField.tsx | 32 +++++++++++++++++++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 8c7949397a9..33b8f27df25 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -241,9 +241,6 @@ export default function RestoreCloudStep() { validPassword && onSubmit(); }, [onSubmit, validPassword]); - const isPasswordValid = - (password !== '' && password.length < cloudBackupPasswordMinLength && !passwordRef?.current?.isFocused()) || incorrectPassword; - return ( @@ -270,7 +267,7 @@ export default function RestoreCloudStep() { { return; } backupAllWalletsToCloud({ - wallets: wallets as AllRainbowWallets, + wallets, password, onError, onSuccess, 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 && } ); From 6d7b6faa95154ac7043c2d98f762c9e45bb02768 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 11:23:05 -0500 Subject: [PATCH 16/45] simplify prompt to backup selected wallet --- src/handlers/walletReadyEvents.ts | 54 +++++++------------------------ 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 7fbfe3fc105..8239f182976 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,7 +5,7 @@ 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'; @@ -16,8 +15,7 @@ import { logger } from '@/logger'; import walletBackupTypes from '@/helpers/walletBackupTypes'; import { InteractionManager } from 'react-native'; import { backupsStore } from '@/state/backups/backups'; - -const BACKUP_SHEET_DELAY_MS = 3000; +import { IS_TEST } from '@/env'; export const runKeychainIntegrityChecks = async () => { const keychainIntegrityState = await getKeychainIntegrityState(); @@ -27,41 +25,13 @@ 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 { selected } = store.getState().wallets; + if (!selected || IS_TEST) return; - 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 - ); - }); - - if (!rainbowWalletsNotBackedUp.length) return; - - logger.debug('[walletReadyEvents]: there is a rainbow wallet not backed up'); - - const hasSelectedWallet = rainbowWalletsNotBackedUp.find(notBackedUpWallet => notBackedUpWallet.id === selected!.id); - logger.debug('[walletReadyEvents]: rainbow wallet not backed up that is selected?', { - hasSelectedWallet, - }); - - const provider = backupsStore.getState().backupProvider; - - // if one of them is selected, show the default BackupSheet - if (selected && hasSelectedWallet && IS_TESTING !== 'true') { + const isSelectedWalletBackedUp = selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly; + if (!isSelectedWalletBackedUp) { + logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); + const provider = backupsStore.getState().backupProvider; let stepType: string = WalletBackupStepTypes.no_provider; if (provider === walletBackupTypes.cloud) { stepType = WalletBackupStepTypes.backup_now_to_cloud; @@ -69,17 +39,15 @@ export const runWalletBackupStatusChecks = () => { stepType = WalletBackupStepTypes.backup_now_manually; } - setTimeout(() => { - logger.debug(`[walletReadyEvents]: showing ${stepType} backup sheet for selected wallet`); + InteractionManager.runAfterInteractions(() => { + logger.debug(`[walletReadyEvents]: BackupSheet: showing ${stepType} for selected wallet`); triggerOnSwipeLayout(() => Navigation.handleAction(Routes.BACKUP_SHEET, { step: stepType, }) ); - }, BACKUP_SHEET_DELAY_MS); - return; + }); } - return; }; export const runFeatureUnlockChecks = async (): Promise => { From 982391940935e2600bbf00aa78ccfa77b88fed8e Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 13:26:41 -0500 Subject: [PATCH 17/45] more manual backup to cloud backup transitions --- src/components/backup/RestoreCloudStep.tsx | 4 +- .../remote-promo-sheet/runChecks.ts | 9 +- src/handlers/cloudBackup.ts | 5 +- src/handlers/walletReadyEvents.ts | 15 +- src/hooks/useImportingWallet.ts | 2 +- src/hooks/useInitializeWallet.ts | 4 +- src/hooks/useWalletCloudBackup.ts | 75 +++++--- src/model/backup.ts | 50 ++++-- src/redux/wallets.ts | 170 +++++++++--------- src/screens/AddWalletSheet.tsx | 5 +- .../components/Backups/ViewCloudBackups.tsx | 4 +- .../components/Backups/ViewWalletBackup.tsx | 121 ++++++------- .../components/Backups/WalletsAndBackup.tsx | 7 +- 13 files changed, 257 insertions(+), 214 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 33b8f27df25..5768b79e8d4 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -16,7 +16,7 @@ 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, useWallets } from '@/hooks'; import { Navigation, useNavigation } from '@/navigation'; @@ -283,7 +283,7 @@ export default function RestoreCloudStep() { { }); return { - files: files?.files?.filter((file: Backup) => file.name !== USERDATA_FILE) || [], + files: files?.files?.filter((file: BackupFile) => file.name !== USERDATA_FILE) || [], }; } @@ -106,6 +106,7 @@ export async function encryptAndSaveDataToCloud(data: Record, p 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( diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 8239f182976..38cef55fefe 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -75,19 +75,18 @@ 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(); + const showingFeatureUnlock = await runFeatureUnlockChecks(); if (!showingFeatureUnlock) { - await runLocalCampaignChecks(); + return await runLocalCampaignChecks(); } + return false; }; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index 96f08578a73..5d4b134bc50 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -295,7 +295,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { image, true ); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); handleSetImporting(false); } else { const previousWalletCount = keys(wallets).length; diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts index b46134e68a8..bf7348f4791 100644 --- a/src/hooks/useInitializeWallet.ts +++ b/src/hooks/useInitializeWallet.ts @@ -64,7 +64,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'); @@ -90,7 +90,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/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 12c2e23d872..3ef3d819ac6 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -9,7 +9,7 @@ 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'; @@ -55,32 +55,53 @@ export default function useWalletCloudBackup() { 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'), - }, - ]); - return false; + 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( + 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'), + }, + ] + ); + return false; + } } // For Android devices without biometrics enabled, we need to ask for PIN diff --git a/src/model/backup.ts b/src/model/backup.ts index 294e51636e3..7a1efac021d 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -11,6 +11,7 @@ import { login, logoutFromGoogleDrive, } from '@/handlers/cloudBackup'; +import { Alert as NativeAlert } from '@/components/alerts'; import WalletBackupTypes from '../helpers/walletBackupTypes'; import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants'; import * as keychain from '@/model/keychain'; @@ -30,8 +31,8 @@ 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'; const { DeviceUUID } = NativeModules; const encryptor = new AesEncryptor(); @@ -62,6 +63,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 { @@ -164,7 +186,7 @@ export async function backupAllWalletsToCloud({ password: BackupPassword; onError?: (message: string) => void; onSuccess?: (password: BackupPassword) => void; - dispatch: any; + dispatch: AppDispatch; }) { let userPIN: string | undefined; const hasBiometricsEnabled = await kc.getSupportedBiometryType(); @@ -211,7 +233,7 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - let updatedBackupFile: any = null; + let updatedBackupFile: string | null = null; const data = { createdAt: now, @@ -238,15 +260,17 @@ export async function backupAllWalletsToCloud({ }); onSuccess?.(password); - } 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, - }); + } 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, + }); + } } } @@ -620,7 +644,7 @@ export async function getDeviceUUID(): Promise { } const FailureAlert = () => - Alert({ + NativeAlert({ buttons: [ { style: 'cancel', diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index 8f592fd42c9..f55da2f26c1 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -28,6 +28,8 @@ import { AppGetState, AppState } from './store'; import { fetchReverseRecord } from '@/handlers/ens'; import { lightModeThemeColors } from '@/styles'; import { RainbowError, logger } from '@/logger'; +import { parseTimestampFromBackupFile } from '@/model/backup'; +import { WalletLoadingState } from '@/helpers/walletLoadingStates'; // -- Types ---------------------------------------- // @@ -38,7 +40,7 @@ interface WalletsState { /** * The current loading state of the wallet. */ - isWalletLoading: any; + isWalletLoading: WalletLoadingState | null; /** * The currently selected wallet. @@ -128,90 +130,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. @@ -262,21 +262,21 @@ export const setIsWalletLoading = (val: WalletsState['isWalletLoading']) => (dis * @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, }; @@ -301,16 +301,20 @@ export const setWalletBackedUp = 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])); } }; diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 93954ee7266..69350fac4e0 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -5,7 +5,7 @@ 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 } from 'react-native'; import { createAccountForWallet, setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; @@ -43,7 +43,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(); @@ -117,7 +116,7 @@ export const AddWalletSheet = () => { name, clearCallbackOnStartCreation: true, }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); // @ts-expect-error - needs refactor to object params await initializeWallet(); } diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx index b765a2d16c3..90cbdddeff3 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx @@ -5,7 +5,7 @@ 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 { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; @@ -38,7 +38,7 @@ const ViewCloudBackups = () => { })); const onSelectCloudBackup = useCallback( - async (selectedBackup: Backup) => { + async (selectedBackup: BackupFile) => { navigate(Routes.BACKUP_SHEET, { step: walletBackupStepTypes.restore_from_backup, selectedBackup, diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 6f5c9243cb7..4d5e16873af 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -34,7 +34,6 @@ import { useDispatch } from 'react-redux'; import { createAccountForWallet, setIsWalletLoading } from '@/redux/wallets'; import { logger, RainbowError } from '@/logger'; import { RainbowAccount } from '@/model/wallet'; -import { PROFILES, useExperimentalFlag } from '@/config'; import showWalletErrorAlert from '@/helpers/support'; import { IS_IOS } from '@/env'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -129,10 +128,8 @@ const ViewWalletBackup = () => { const wallet = wallets?.[walletId]; const dispatch = useDispatch(); const initializeWallet = useInitializeWallet(); - const profilesEnabled = useExperimentalFlag(PROFILES); const isSecretPhrase = WalletTypes.mnemonic === wallet?.type; - const title = wallet?.type === WalletTypes.privateKey ? wallet?.addresses[0].label : incomingTitle; const { navigate } = useNavigation(); @@ -226,7 +223,7 @@ const ViewWalletBackup = () => { error: e, }); } - }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, profilesEnabled, wallet]); + }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, wallet]); const handleCopyAddress = React.useCallback( (address: string) => { @@ -323,31 +320,24 @@ const ViewWalletBackup = () => { /> - {backupProvider === walletBackupTypes.cloud && ( - - - - - - )} - - {backupProvider !== walletBackupTypes.cloud && ( - + + + } @@ -356,16 +346,8 @@ const ViewWalletBackup = () => { titleComponent={} testID={'back-up-manually'} /> - - )} + )} @@ -412,20 +394,37 @@ const ViewWalletBackup = () => { )} - - } - onPress={onNavigateToSecretWarning} - size={52} - titleComponent={ - + + - } - /> - + + + )} + + + + } + onPress={onNavigateToSecretWarning} + size={52} + titleComponent={ + + } + /> + + @@ -460,15 +459,17 @@ const ViewWalletBackup = () => { {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 3e3e6fa082f..3001de0429d 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -91,7 +91,6 @@ const getAccountSectionHeight = (numAccounts: number) => { export const WalletsAndBackup = () => { const { navigate } = useNavigation(); const { wallets } = useWallets(); - const profilesEnabled = useExperimentalFlag(PROFILES); const dispatch = useDispatch(); const createBackup = useCreateBackup(); @@ -157,7 +156,7 @@ export const WalletsAndBackup = () => { clearCallbackOnStartCreation: true, }); - await dispatch(walletsLoadState(profilesEnabled)); + await dispatch(walletsLoadState()); // @ts-expect-error - no params await initializeWallet(); @@ -170,7 +169,7 @@ export const WalletsAndBackup = () => { } }, }); - }, [dispatch, initializeWallet, navigate, profilesEnabled, walletTypeCount.phrase]); + }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]); const onPressLearnMoreAboutCloudBackups = useCallback(() => { navigate(Routes.LEARN_WEB_VIEW_SCREEN, { @@ -233,7 +232,7 @@ export const WalletsAndBackup = () => { paddingTop={{ custom: 8 }} iconComponent={} titleComponent={} - statusComponent={} + statusComponent={} labelComponent={ Date: Mon, 18 Nov 2024 13:29:43 -0500 Subject: [PATCH 18/45] Update src/components/backup/RestoreCloudStep.tsx --- src/components/backup/RestoreCloudStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 5768b79e8d4..186f318c18b 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -150,7 +150,7 @@ export default function RestoreCloudStep() { dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)); const status = await restoreCloudBackup({ password: pwd, - backupFilename: selectedBackup.name, + backupFilename: filename, }); if (status === RestoreCloudBackupResultStates.success) { // Store it in the keychain in case it was missing From 1f42ab96394c7ae7dc08c303199d1220eab299e8 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 14:05:05 -0500 Subject: [PATCH 19/45] reset backup provider to undefined if no condition is met --- src/hooks/useManageCloudBackups.ts | 3 +-- src/state/backups/backups.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index 526d75ad26a..1a50c8b079d 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -50,7 +50,6 @@ export default function useManageCloudBackups() { }; const loginToGoogleDrive = async () => { - // TODO: Figure out how to update the backup status based on the new account? try { const accountDetails = await getGoogleAccountUserData(); backupsStore.getState().syncAndFetchBackups(); @@ -96,7 +95,7 @@ export default function useManageCloudBackups() { if (_buttonIndex === 1 && IS_ANDROID) { logoutFromGoogleDrive(); setAccountDetails(undefined); - removeBackupStateFromAllWallets().then(() => loginToGoogleDrive()); + loginToGoogleDrive(); } } ); diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index 3148fa7482a..74e34ec3cf3 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -115,6 +115,8 @@ export const backupsStore = createRainbowStore((set, get) => ({ }); } else if (hasManuallyBackedUpWallet(wallets)) { set({ backupProvider: walletBackupTypes.manual }); + } else { + set({ backupProvider: undefined }); } logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`); From 466f3dca1843d7db6070bfcf336b3acafb226aeb Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 14:41:54 -0500 Subject: [PATCH 20/45] adjust logic for displaying backed up status on Android when switching accounts --- .../components/Backups/ViewWalletBackup.tsx | 16 +++++++++---- .../components/Backups/WalletsAndBackup.tsx | 17 ++++++++------ .../components/GoogleAccountSection.tsx | 16 +++---------- src/screens/SettingsSheet/utils.ts | 23 ++++++++++++++++++- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 4d5e16873af..f98e6b73297 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -44,6 +44,7 @@ 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'; type ViewWalletBackupParams = { ViewWalletBackup: { walletId: string; title: string; imported?: boolean }; @@ -131,6 +132,11 @@ const ViewWalletBackup = () => { 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); @@ -285,7 +291,7 @@ const ViewWalletBackup = () => { return ( - {!wallet?.backedUp && ( + {!isBackedUp && ( <> { )} - {wallet?.backedUp && ( + {isBackedUp && ( <> { paddingBottom={{ custom: 24 }} iconComponent={ } titleComponent={ { { - {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( { } > - {!backedUp && } + {!isBackedUp && } {imported && } { /> } - leftComponent={} + leftComponent={} onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} @@ -505,7 +507,8 @@ export const WalletsAndBackup = () => { case WalletBackupTypes.manual: { return ( - {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { + {sortedWallets.map(({ id, name, backedUp, backupType, backupFile, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupType, backupFile }); return ( { } > - {!backedUp && } + {!isBackedUp && } {imported && } { /> } - leftComponent={} + leftComponent={} onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx index 6bf2e0160e0..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 } 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,11 +52,10 @@ export const GoogleAccountSection: React.FC = () => { const loginToGoogleDrive = async () => { setLoading(true); - - // TODO: We need to update the wallet backup status here somehow... 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/utils.ts b/src/screens/SettingsSheet/utils.ts index 58c0162f242..bd25ae11223 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -5,7 +5,10 @@ import { isEmpty } from 'lodash'; import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; import * as i18n from '@/languages'; import { cloudPlatform } from '@/utils/platform'; -import { CloudBackupState } from '@/state/backups/backups'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; +import { RainbowWallet } from '@/model/wallet'; +import { IS_IOS } from '@/env'; +import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; type WalletBackupStatus = { allBackedUp: boolean; @@ -76,3 +79,21 @@ export const titleForBackupState: Partial> = { cloudPlatformName: cloudPlatform, }), }; + +export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { + if (!backupType || !backedUp || !backupFile) { + return false; + } + + if (IS_IOS || backupType === WalletBackupTypes.manual) { + return backedUp; + } + + // 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)); +}; From 461ee79451fa0f3c23ceb7d2576efcf7e7492e73 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 18 Nov 2024 17:17:09 -0500 Subject: [PATCH 21/45] more android tweaks --- src/handlers/cloudBackup.ts | 2 +- src/hooks/useManageCloudBackups.ts | 21 +++++++++++-- src/model/backup.ts | 4 ++- src/react-native-cool-modals/Portal.tsx | 3 +- .../components/Backups/WalletsAndBackup.tsx | 27 ++++++++++++---- src/screens/SettingsSheet/utils.ts | 31 +++++++++++++++++-- src/state/backups/backups.ts | 2 +- 7 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts index 1a7b2a286d3..14347c42a75 100644 --- a/src/handlers/cloudBackup.ts +++ b/src/handlers/cloudBackup.ts @@ -73,7 +73,7 @@ export async function fetchAllBackups(): Promise { }); return { - files: files?.files?.filter((file: BackupFile) => file.name !== USERDATA_FILE) || [], + files: files?.files?.filter((file: BackupFile) => normalizeAndroidBackupFilename(file.name) !== USERDATA_FILE) || [], }; } diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index 1a50c8b079d..d5e6bc73f58 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -3,13 +3,19 @@ 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 { + 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 } from '@/state/backups/backups'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; export default function useManageCloudBackups() { const dispatch = useDispatch(); @@ -49,8 +55,19 @@ 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 () => { try { + await login(); const accountDetails = await getGoogleAccountUserData(); backupsStore.getState().syncAndFetchBackups(); setAccountDetails(accountDetails ?? undefined); diff --git a/src/model/backup.ts b/src/model/backup.ts index 7a1efac021d..8bef43d6347 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -10,6 +10,7 @@ import { getGoogleAccountUserData, login, logoutFromGoogleDrive, + normalizeAndroidBackupFilename, } from '@/handlers/cloudBackup'; import { Alert as NativeAlert } from '@/components/alerts'; import WalletBackupTypes from '../helpers/walletBackupTypes'; @@ -53,8 +54,9 @@ export interface BackupFile { } export const parseTimestampFromFilename = (filename: string) => { + const name = normalizeAndroidBackupFilename(filename); return Number( - filename + name .replace('.backup_', '') .replace('backup_', '') .replace('.json', '') diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx index 8ab9a0d2c3f..4fadb8753ef 100644 --- a/src/react-native-cool-modals/Portal.tsx +++ b/src/react-native-cool-modals/Portal.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { IS_IOS } from '@/env'; import { portalStore } from '@/state/portal/portal'; -import { Platform, requireNativeComponent, StyleSheet, View } from 'react-native'; +import { requireNativeComponent, StyleSheet, View } from 'react-native'; const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View; const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => children : View; @@ -35,7 +35,6 @@ export function Portal() { const sx = StyleSheet.create({ wrapper: { - zIndex: Number.MAX_SAFE_INTEGER, ...StyleSheet.absoluteFillObject, }, }); diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 8d09b71c94c..57c61ecedcd 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -25,7 +25,6 @@ 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 { setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; @@ -193,6 +192,20 @@ export const WalletsAndBackup = () => { ); const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => { + if (!backupProvider) { + if (!allBackedUp) { + return { + status: 'out-of-date', + text: 'Out of Date', + }; + } + + return { + status: 'up-to-date', + text: 'Up to date', + }; + } + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { return { status: 'not-enabled', @@ -218,7 +231,7 @@ export const WalletsAndBackup = () => { status: 'up-to-date', text: 'Up to date', }; - }, [status, allBackedUp]); + }, [backupProvider, status, allBackedUp]); const renderView = useCallback(() => { switch (backupProvider) { @@ -256,14 +269,15 @@ export const WalletsAndBackup = () => { - {sortedWallets.map(({ id, name, backedUp, imported, addresses }) => { + {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { + const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + return ( { } > - {!backedUp && ( + {!isBackedUp && ( { /> } - leftComponent={} + leftComponent={} onPress={() => onNavigateToWalletView(id, name)} size={60} titleComponent={} @@ -404,6 +418,7 @@ export const WalletsAndBackup = () => { {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); + console.log('isBackedUp', isBackedUp); return ( diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index bd25ae11223..b92b5a8c4e3 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -7,7 +7,7 @@ import * as i18n from '@/languages'; import { cloudPlatform } from '@/utils/platform'; import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { RainbowWallet } from '@/model/wallet'; -import { IS_IOS } from '@/env'; +import { IS_ANDROID, IS_IOS } from '@/env'; import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; type WalletBackupStatus = { @@ -30,6 +30,26 @@ export const checkLocalWalletsForBackupStatus = (wallets: ReturnType( + (acc, wallet) => { + const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; + const hasBackupFile = backupFiles.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; @@ -81,7 +101,12 @@ export const titleForBackupState: Partial> = { }; export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { - if (!backupType || !backedUp || !backupFile) { + console.log({ + backupType, + backedUp, + backupFile, + }); + if (!backupType || !backupFile) { return false; } @@ -89,6 +114,8 @@ export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backup return backedUp; } + console.log('backupFile', backupFile); + // NOTE: For Android, we also need to check if the current google account has the matching backup file if (!backupFile) { return false; diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index 74e34ec3cf3..a54a6e759fc 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -87,7 +87,7 @@ export const backupsStore = createRainbowStore((set, get) => ({ const gdata = await getGoogleAccountUserData(); if (!gdata) { logger.debug('[backupsStore]: Google account is not available'); - set({ status: CloudBackupState.NotAvailable }); + set({ status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); return { success: false, retry: false, From 053231c7d68191789fe9fd6b90ab9c13b03f233f Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 25 Nov 2024 11:35:44 -0600 Subject: [PATCH 22/45] more android fixes --- src/components/PortalConsumer.js | 2 +- src/components/backup/RestoreCloudStep.tsx | 12 +++- src/model/backup.ts | 8 ++- src/react-native-cool-modals/Portal.tsx | 4 ++ src/screens/AddWalletSheet.tsx | 2 + .../components/Backups/ViewWalletBackup.tsx | 55 +++++++++---------- .../components/Backups/WalletsAndBackup.tsx | 46 +++++++++++++--- .../components/SettingsSection.tsx | 5 +- .../SettingsSheet/useVisibleWallets.ts | 5 +- src/screens/SettingsSheet/utils.ts | 18 +++--- src/state/backups/backups.ts | 4 +- 11 files changed, 102 insertions(+), 59 deletions(-) diff --git a/src/components/PortalConsumer.js b/src/components/PortalConsumer.js index 7644dff645b..9be593ede2c 100644 --- a/src/components/PortalConsumer.js +++ b/src/components/PortalConsumer.js @@ -9,7 +9,7 @@ export default function PortalConsumer() { useEffect(() => { if (isWalletLoading) { - portalStore.getState().setComponent(, true); + portalStore.getState().setComponent(); } return portalStore.getState().hide; }, [isWalletLoading]); diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 186f318c18b..c7c83062078 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -22,6 +22,8 @@ import { useDimensions, useInitializeWallet, useWallets } from '@/hooks'; import { Navigation, useNavigation } from '@/navigation'; import { addressSetSelected, + fetchWalletENSAvatars, + fetchWalletNames, setAllWalletsWithIdsAsBackedUp, setIsWalletLoading, walletsLoadState, @@ -44,6 +46,7 @@ import { useTheme } from '@/theme'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { isEmpty } from 'lodash'; import { backupsStore } from '@/state/backups/backups'; +import { useExperimentalFlag, PROFILES } from '@/config'; const Title = styled(Text).attrs({ size: 'big', @@ -104,6 +107,7 @@ export default function RestoreCloudStep() { }, [canGoBack, goBack]); const dispatch = useDispatch(); + const profilesEnabled = useExperimentalFlag(PROFILES); const { width: deviceWidth, height: deviceHeight } = useDimensions(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); @@ -205,6 +209,9 @@ export default function RestoreCloudStep() { }); onRestoreSuccess(); + const getWalletNames = dispatch(fetchWalletNames()); + const getWalletENSAvatars = profilesEnabled ? dispatch(fetchWalletENSAvatars()) : null; + Promise.all([getWalletNames, getWalletENSAvatars]); backupsStore.getState().setPassword(''); if (isEmpty(prevWalletsState)) { Navigation.handleAction( @@ -212,7 +219,7 @@ export default function RestoreCloudStep() { { screen: Routes.WALLET_SCREEN, }, - false + true ); } else { Navigation.handleAction(Routes.WALLET_SCREEN, {}); @@ -233,9 +240,10 @@ export default function RestoreCloudStep() { } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { + console.log('here'); dispatch(setIsWalletLoading(null)); } - }, [password, dispatch, selectedBackup.name, initializeWallet, onRestoreSuccess]); + }, [password, selectedBackup.name, dispatch, onRestoreSuccess, profilesEnabled, initializeWallet]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit(); diff --git a/src/model/backup.ts b/src/model/backup.ts index 8bef43d6347..79c20b9e521 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -34,6 +34,7 @@ import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { getRemoteConfig } from './remoteConfig'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { AppDispatch } from '@/redux/store'; +import { backupsStore } from '@/state/backups/backups'; const { DeviceUUID } = NativeModules; const encryptor = new AesEncryptor(); @@ -103,7 +104,12 @@ export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: if (logout) { await logoutFromGoogleDrive(); } - await login(); + + 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)); diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx index 4fadb8753ef..19c09a0aa3d 100644 --- a/src/react-native-cool-modals/Portal.tsx +++ b/src/react-native-cool-modals/Portal.tsx @@ -12,6 +12,10 @@ export function Portal() { Component: state.Component, })); + if (!Component) { + return null; + } + return ( { await dispatch(createAccountForWallet(wallet.id, color, name)); // @ts-expect-error - no params await initializeWallet(); - - // TODO: mark the newly created wallet as not backed up } } catch (e) { logger.error(new RainbowError(`[ViewWalletBackup]: Error while trying to add account`), { @@ -436,32 +434,33 @@ const ViewWalletBackup = () => { {wallet?.addresses .filter(a => a.visible) - .map((account: RainbowAccount) => ( - - } - labelComponent={ - account.label.endsWith('.eth') || account.label !== '' ? ( - - ) : null - } - titleComponent={ - - } - rightComponent={} - /> - - ))} + .map((account: RainbowAccount) => { + console.log({ + address: account.address, + label: account.label, + }); + 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 && ( diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 57c61ecedcd..e11f66fad82 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -28,7 +28,7 @@ import { RainbowAccount, createWallet } from '@/model/wallet'; import { useDispatch } from 'react-redux'; import { setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; -import { IS_IOS } from '@/env'; +import { IS_ANDROID, IS_IOS } from '@/env'; import { useCreateBackup } from '@/components/backup/useCreateBackup'; import { BackUpMenuItem } from './BackUpMenuButton'; import { format } from 'date-fns'; @@ -109,7 +109,7 @@ export const WalletsAndBackup = () => { privateKey: 0, }; - const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); const visibleWallets = useVisibleWallets({ wallets, walletTypeCount }); @@ -127,12 +127,28 @@ export const WalletsAndBackup = () => { ]; }, [visibleWallets]); - const backupAllNonBackedUpWalletsTocloud = useCallback(async () => { + 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; + } + } + return createBackup({}); + }, + }); + }, [createBackup, wallets]); + const onViewCloudBackups = useCallback(async () => { navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, { backups, @@ -193,6 +209,20 @@ export const WalletsAndBackup = () => { const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => { if (!backupProvider) { + if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { + return { + status: 'not-enabled', + text: 'Not Enabled', + }; + } + + if (status !== CloudBackupState.Ready) { + return { + status: 'out-of-sync', + text: 'Syncing', + }; + } + if (!allBackedUp) { return { status: 'out-of-date', @@ -269,7 +299,7 @@ export const WalletsAndBackup = () => { @@ -418,7 +448,6 @@ export const WalletsAndBackup = () => { {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => { const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType }); - console.log('isBackedUp', isBackedUp); return ( @@ -617,16 +646,17 @@ export const WalletsAndBackup = () => { } }, [ backupProvider, + iconStatusType, + text, status, - backupAllNonBackedUpWalletsTocloud, + enableCloudBackups, sortedWallets, onCreateNewSecretPhrase, navigate, onNavigateToWalletView, allBackedUp, - iconStatusType, - text, mostRecentBackup, + backupAllNonBackedUpWalletsTocloud, onViewCloudBackups, manageCloudBackups, onPressLearnMoreAboutCloudBackups, diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 19a73f10071..811950e1565 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -61,8 +61,9 @@ const SettingsSection = ({ const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS); const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS); - const { backupProvider } = backupsStore(state => ({ + const { backupProvider, backups } = backupsStore(state => ({ backupProvider: state.backupProvider, + backups: state.backups, })); const { isDarkMode, setTheme, colorScheme } = useTheme(); @@ -90,7 +91,7 @@ const SettingsSection = ({ const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []); - const { allBackedUp, canBeBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets), [wallets]); + const { allBackedUp, canBeBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); const themeMenuConfig = useMemo(() => { return { diff --git a/src/screens/SettingsSheet/useVisibleWallets.ts b/src/screens/SettingsSheet/useVisibleWallets.ts index 824feb0bb26..e7c5e353858 100644 --- a/src/screens/SettingsSheet/useVisibleWallets.ts +++ b/src/screens/SettingsSheet/useVisibleWallets.ts @@ -1,10 +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 { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; +import { RainbowWallet } from '@/model/wallet'; type WalletByKey = { [key: string]: RainbowWallet; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index b92b5a8c4e3..395c068c21f 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -2,7 +2,7 @@ import WalletBackupTypes from '@/helpers/walletBackupTypes'; import WalletTypes from '@/helpers/walletTypes'; import { useWallets } from '@/hooks'; import { isEmpty } from 'lodash'; -import { BackupFile, parseTimestampFromFilename } from '@/model/backup'; +import { BackupFile, CloudBackups, parseTimestampFromFilename } from '@/model/backup'; import * as i18n from '@/languages'; import { cloudPlatform } from '@/utils/platform'; import { backupsStore, CloudBackupState } from '@/state/backups/backups'; @@ -21,7 +21,10 @@ export const hasManuallyBackedUpWallet = (wallets: ReturnType return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual); }; -export const checkLocalWalletsForBackupStatus = (wallets: ReturnType['wallets']): WalletBackupStatus => { +export const checkLocalWalletsForBackupStatus = ( + wallets: ReturnType['wallets'], + backups: CloudBackups +): WalletBackupStatus => { if (!wallets || isEmpty(wallets)) { return { allBackedUp: false, @@ -32,11 +35,10 @@ export const checkLocalWalletsForBackupStatus = (wallets: ReturnType( (acc, wallet) => { const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth; - const hasBackupFile = backupFiles.files.some( + const hasBackupFile = backups.files.some( file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(wallet.backupFile ?? '') ); @@ -101,11 +103,7 @@ export const titleForBackupState: Partial> = { }; export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { - console.log({ - backupType, - backedUp, - backupFile, - }); + console.log({ backupType, backedUp, backupFile }); if (!backupType || !backupFile) { return false; } @@ -114,8 +112,6 @@ export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backup return backedUp; } - console.log('backupFile', backupFile); - // NOTE: For Android, we also need to check if the current google account has the matching backup file if (!backupFile) { return false; diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index a54a6e759fc..c455b715e63 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -76,7 +76,7 @@ export const backupsStore = createRainbowStore((set, get) => ({ const isAvailable = await isCloudBackupAvailable(); if (!isAvailable) { logger.debug('[backupsStore]: Cloud backup is not available'); - set({ status: CloudBackupState.NotAvailable }); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); return { success: false, retry: false, @@ -87,7 +87,7 @@ export const backupsStore = createRainbowStore((set, get) => ({ const gdata = await getGoogleAccountUserData(); if (!gdata) { logger.debug('[backupsStore]: Google account is not available'); - set({ status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); + set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); return { success: false, retry: false, From e1569fa40df39cfd1a55c4a1722e57a290ecaf0d Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 25 Nov 2024 12:00:38 -0600 Subject: [PATCH 23/45] cleanup logs, prevent consolidatedTransactions when no address, and prevent sends to damaged wallets --- src/components/backup/RestoreCloudStep.tsx | 9 ++++----- .../transactions/consolidatedTransactions.ts | 1 + src/screens/SendSheet.tsx | 14 +++++++++++++- .../components/Backups/ViewWalletBackup.tsx | 6 ------ src/screens/SettingsSheet/utils.ts | 1 - 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index c7c83062078..d78efe43bcc 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -204,14 +204,14 @@ export default function RestoreCloudStep() { const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; const p1 = dispatch(walletsSetSelected(firstWallet)); const p2 = dispatch(addressSetSelected(firstAddress)); - await Promise.all([p1, p2]); + const p3 = dispatch(fetchWalletNames()); + const p4 = profilesEnabled ? dispatch(fetchWalletENSAvatars()) : null; + + await Promise.all([p1, p2, p3, p4]); await initializeWallet(null, null, null, false, false, null, true, null); }); onRestoreSuccess(); - const getWalletNames = dispatch(fetchWalletNames()); - const getWalletENSAvatars = profilesEnabled ? dispatch(fetchWalletENSAvatars()) : null; - Promise.all([getWalletNames, getWalletENSAvatars]); backupsStore.getState().setPassword(''); if (isEmpty(prevWalletsState)) { Navigation.handleAction( @@ -240,7 +240,6 @@ export default function RestoreCloudStep() { } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { - console.log('here'); dispatch(setIsWalletLoading(null)); } }, [password, selectedBackup.name, dispatch, onRestoreSuccess, profilesEnabled, initializeWallet]); diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts index 616af75da8e..a04d10bb7fa 100644 --- a/src/resources/transactions/consolidatedTransactions.ts +++ b/src/resources/transactions/consolidatedTransactions.ts @@ -133,6 +133,7 @@ export function useConsolidatedTransactions( keepPreviousData: true, getNextPageParam: lastPage => lastPage?.nextPage, refetchInterval: CONSOLIDATED_TRANSACTIONS_INTERVAL, + enabled: !!address, retry: 3, } ); diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index eca3f922fa2..d4c817e5a93 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -51,7 +51,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'; @@ -70,6 +70,7 @@ 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; @@ -96,6 +97,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; } diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 0963cec823d..3433ded65a9 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -435,14 +435,8 @@ const ViewWalletBackup = () => { {wallet?.addresses .filter(a => a.visible) .map((account: RainbowAccount) => { - console.log({ - address: account.address, - label: account.label, - }); 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) ?? ''; diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 395c068c21f..0bac0b75f2d 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -103,7 +103,6 @@ export const titleForBackupState: Partial> = { }; export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => { - console.log({ backupType, backedUp, backupFile }); if (!backupType || !backupFile) { return false; } From 176cac6d3fe0c3b6a5faadf10170f21107ea8462 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 25 Nov 2024 12:10:11 -0600 Subject: [PATCH 24/45] prevent backups on wallets which are damaged --- src/components/backup/useCreateBackup.ts | 14 ++++++++++++-- src/hooks/useWalletCloudBackup.ts | 15 ++++++++++----- src/languages/en_US.json | 3 ++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 5b02988e0c2..61202534389 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -11,6 +11,7 @@ import { Navigation, useNavigation } from '@/navigation'; import { InteractionManager } from 'react-native'; import { DelayedAlert } from '@/components/alerts'; import { useDispatch } from 'react-redux'; +import * as i18n from '@/languages'; type UseCreateBackupProps = { walletId?: string; @@ -85,12 +86,21 @@ export const useCreateBackup = () => { if (typeof walletId === 'undefined') { if (!wallets) { - onError('Error loading wallets. Please try again.'); + 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)); + backupsStore.getState().setStatus(CloudBackupState.Error); + return; + } + backupAllWalletsToCloud({ - wallets, + wallets: validWallets, password, onError, onSuccess, diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 3ef3d819ac6..5e95b736e60 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -1,4 +1,3 @@ -import lang from 'i18n-js'; import { values } from 'lodash'; import { useCallback } from 'react'; import { Linking } from 'react-native'; @@ -77,8 +76,8 @@ export default function useWalletCloudBackup() { category: 'backup', }); Alert.alert( - lang.t('modal.back_up.alerts.cloud_not_enabled.label'), - lang.t('modal.back_up.alerts.cloud_not_enabled.description'), + 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: () => { @@ -87,7 +86,7 @@ export default function useWalletCloudBackup() { category: 'backup', }); }, - text: lang.t('modal.back_up.alerts.cloud_not_enabled.show_me'), + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me), }, { onPress: () => { @@ -96,7 +95,7 @@ export default function useWalletCloudBackup() { }); }, style: 'cancel', - text: lang.t('modal.back_up.alerts.cloud_not_enabled.no_thanks'), + text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks), }, ] ); @@ -104,6 +103,12 @@ export default function useWalletCloudBackup() { } } + const wallet = wallets?.[walletId]; + if (wallet?.damaged) { + onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet)); + return false; + } + // For Android devices without biometrics enabled, we need to ask for PIN let userPIN: string | undefined; const hasBiometricsEnabled = await getSupportedBiometryType(); diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 902997e943f..467e95d67cc 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": { From 11f079f07a564fd70ac8863e3485f1805b82902e Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 25 Nov 2024 12:39:43 -0600 Subject: [PATCH 25/45] prevent portal overlay on pin authentication screen --- src/App.tsx | 4 ---- src/hooks/useActiveRoute.ts | 16 ++++++++++++++++ src/navigation/Routes.android.tsx | 6 ++++++ src/navigation/Routes.ios.tsx | 6 ++++++ src/react-native-cool-modals/Portal.tsx | 11 ++++++++--- 5 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useActiveRoute.ts diff --git a/src/App.tsx b/src/App.tsx index 80a5f7140a2..3d4cc1d7cc2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,6 @@ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-cont import { enableScreens } from 'react-native-screens'; 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'; @@ -26,7 +25,6 @@ import { MainThemeProvider } from '@/theme/ThemeContext'; import { addressKey } from '@/utils/keychainConstants'; 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, securelyHashWalletAddress } from '@/analytics/utils'; @@ -82,8 +80,6 @@ function App({ walletReady }: AppProps) { - - ); 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/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 79bfbd90c29..5fdf407299a 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -90,6 +90,8 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; +import PortalConsumer from '@/components/PortalConsumer'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -272,6 +274,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 459561484d3..64b293f2378 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -102,6 +102,8 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; +import PortalConsumer from '@/components/PortalConsumer'; +import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; const Stack = createStackNavigator(); const NativeStack = createNativeStackNavigator(); @@ -286,6 +288,10 @@ const AppContainerWithAnalytics = React.forwardRef + + {/* NOTE: Internally, these use some navigational checks */} + + )); diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx index 19c09a0aa3d..97ab83052da 100644 --- a/src/react-native-cool-modals/Portal.tsx +++ b/src/react-native-cool-modals/Portal.tsx @@ -1,18 +1,23 @@ -import React from 'react'; -import { IS_IOS } from '@/env'; +import React, { useEffect, useState } from 'react'; +import { IS_ANDROID, IS_IOS } from '@/env'; import { portalStore } from '@/state/portal/portal'; import { requireNativeComponent, StyleSheet, View } from 'react-native'; +import Routes from '@/navigation/routesNames'; +import { Navigation, useNavigation } from '@/navigation'; +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 } = portalStore(state => ({ blockTouches: state.blockTouches, Component: state.Component, })); - if (!Component) { + if (!Component || (activeRoute === Routes.PIN_AUTHENTICATION_SCREEN && !IS_IOS)) { return null; } From 25bb9c2e69aba906a45b86ebcbeea5781c826e5c Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 25 Nov 2024 15:54:46 -0600 Subject: [PATCH 26/45] prevent copying address and more improvements to disabling backups if damaged wallet --- .../profile-header/ProfileActionButtonsRow.tsx | 16 ++++++++++------ src/components/backup/useCreateBackup.ts | 12 ++++++++---- src/hooks/useWalletCloudBackup.ts | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx index 686f8e692a8..e2bafa7ad1e 100644 --- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileActionButtonsRow.tsx @@ -69,7 +69,7 @@ export function ProfileActionButtonsRow() { - + @@ -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/backup/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index 61202534389..08cc736ed5a 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -12,6 +12,7 @@ import { InteractionManager } from 'react-native'; import { DelayedAlert } from '@/components/alerts'; import { useDispatch } from 'react-redux'; import * as i18n from '@/languages'; +import showWalletErrorAlert from '@/helpers/support'; type UseCreateBackupProps = { walletId?: string; @@ -70,9 +71,13 @@ export const useCreateBackup = () => { ); const onError = useCallback( - (msg: string) => { + (msg: string, isDamaged?: boolean) => { InteractionManager.runAfterInteractions(async () => { - DelayedAlert({ title: msg }, 500); + if (isDamaged) { + showWalletErrorAlert(); + } else { + DelayedAlert({ title: msg }, 500); + } setLoadingStateWithTimeout({ state: CloudBackupState.Error }); }); }, @@ -92,9 +97,8 @@ export const useCreateBackup = () => { } 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)); + onError(i18n.t(i18n.l.back_up.errors.no_keys_found), true); backupsStore.getState().setStatus(CloudBackupState.Error); return; } diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index 5e95b736e60..ffc339d690d 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -105,7 +105,7 @@ export default function useWalletCloudBackup() { const wallet = wallets?.[walletId]; if (wallet?.damaged) { - onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet)); + onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet), true); return false; } From 664a03576f6d52fcd22ea38a371c5f7220cc91ca Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Tue, 26 Nov 2024 09:02:51 -0600 Subject: [PATCH 27/45] i18n wallet loading states --- src/helpers/walletLoadingStates.ts | 12 ++++++------ src/languages/en_US.json | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts index 44e2d94c039..7ed8ceef3c2 100644 --- a/src/helpers/walletLoadingStates.ts +++ b/src/helpers/walletLoadingStates.ts @@ -1,10 +1,10 @@ +import * as i18n from '@/languages'; + export const WalletLoadingStates = { - BACKING_UP_WALLET: 'Backing up...', - CREATING_WALLET: 'Creating wallet...', - FETCHING_PASSWORD: 'Fetching Password...', - IMPORTING_WALLET: 'Importing...', - IMPORTING_WALLET_SILENTLY: '', - RESTORING_WALLET: 'Restoring...', + 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 WalletLoadingState = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 467e95d67cc..0e2e8dcceb9 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1193,6 +1193,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...", From f550f6825cc80d8dd731b36e66cdccf2313a1317 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Tue, 26 Nov 2024 09:04:58 -0600 Subject: [PATCH 28/45] fix lint --- src/hooks/useWalletCloudBackup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts index ffc339d690d..cb5d6350a5e 100644 --- a/src/hooks/useWalletCloudBackup.ts +++ b/src/hooks/useWalletCloudBackup.ts @@ -49,7 +49,7 @@ export default function useWalletCloudBackup() { }: { handleNoLatestBackup?: () => void; handlePasswordNotFound?: () => void; - onError?: (error: string) => void; + onError?: (error: string, isDamaged?: boolean) => void; onSuccess?: (password: string) => void; password: string; walletId: string; From 3ccb2fb98c46de105f1fd3ca159699d9a190a544 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Tue, 26 Nov 2024 09:15:37 -0600 Subject: [PATCH 29/45] fix lint --- src/hooks/useUpdateEmoji.ts | 4 ++-- src/navigation/HardwareWalletTxNavigator.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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); From 380103f283a32c11c1d72e7e843aebb97eeadceb Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 27 Nov 2024 11:53:19 -0600 Subject: [PATCH 30/45] possibly fix the enable cloud backups on android --- src/model/backup.ts | 1 + .../SettingsSheet/components/Backups/WalletsAndBackup.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/model/backup.ts b/src/model/backup.ts index 79c20b9e521..3b1ac3d5818 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -110,6 +110,7 @@ export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: await login(); await backupsStore.getState().syncAndFetchBackups(); } + const userData = await getGoogleAccountUserData(); if (!userData) { Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found)); diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index e11f66fad82..481bb2c5e39 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -146,6 +146,7 @@ export const WalletsAndBackup = () => { } return createBackup({}); }, + logout: true, }); }, [createBackup, wallets]); From d0fd2e360fa37b3952a0b7bd07a5b392a969c609 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 27 Nov 2024 15:34:45 -0500 Subject: [PATCH 31/45] fix button not being disabled and confetti (#6281) --- src/components/AbsolutePortal.tsx | 6 +- .../floating-emojis/FloatingEmojis.js | 156 ------------------ .../floating-emojis/FloatingEmojis.tsx | 151 +++++++++++++++++ .../floating-emojis/GravityEmoji.tsx | 2 + .../components/Backups/BackUpMenuButton.tsx | 77 +++++---- .../components/Backups/WalletsAndBackup.tsx | 16 +- 6 files changed, 207 insertions(+), 201 deletions(-) delete mode 100644 src/components/floating-emojis/FloatingEmojis.js create mode 100644 src/components/floating-emojis/FloatingEmojis.tsx diff --git a/src/components/AbsolutePortal.tsx b/src/components/AbsolutePortal.tsx index b578234bd0a..809aec392f5 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(() => { @@ -33,7 +33,7 @@ export const AbsolutePortalRoot = () => { }, []); return ( - + {nodes} ); 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/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx index fa38313f32f..ba33ae5da99 100644 --- a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx +++ b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx @@ -25,7 +25,7 @@ export const BackUpMenuItem = ({ const [emojiTrigger, setEmojiTrigger] = useState void)>(null); useEffect(() => { - if (backupState === 'success') { + if (backupState === CloudBackupState.Success) { for (let i = 0; i < 20; i++) { setTimeout(() => { emojiTrigger?.(); @@ -36,9 +36,9 @@ export const BackUpMenuItem = ({ const accentColor = useMemo(() => { switch (backupState) { - case 'success': + case CloudBackupState.Success: return colors.green; - case 'error': + case CloudBackupState.Error: return colors.red; default: return undefined; @@ -69,42 +69,39 @@ export const BackUpMenuItem = ({ }, [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/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 481bb2c5e39..09146c92734 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -36,6 +36,7 @@ import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; import { backupsStore, CloudBackupState } from '@/state/backups/backups'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; type WalletPillProps = { account: RainbowAccount; @@ -264,6 +265,10 @@ export const WalletsAndBackup = () => { }; }, [backupProvider, status, allBackedUp]); + const isCloudBackupDisabled = useMemo(() => { + return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable; + }, [status]); + const renderView = useCallback(() => { switch (backupProvider) { default: @@ -300,6 +305,7 @@ export const WalletsAndBackup = () => { @@ -439,7 +445,7 @@ export const WalletsAndBackup = () => { })} icon="􀎽" backupState={status} - disabled={status !== CloudBackupState.Ready} + disabled={isCloudBackupDisabled} onPress={backupAllNonBackedUpWalletsTocloud} /> @@ -650,6 +656,7 @@ export const WalletsAndBackup = () => { iconStatusType, text, status, + isCloudBackupDisabled, enableCloudBackups, sortedWallets, onCreateNewSecretPhrase, @@ -663,7 +670,12 @@ export const WalletsAndBackup = () => { onPressLearnMoreAboutCloudBackups, ]); - return {renderView()}; + return ( + + + {renderView()} + + ); }; export default WalletsAndBackup; From 11fbc3c2ec0559e143e9cbc201bcd5c344d42308 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Tue, 3 Dec 2024 15:15:56 -0500 Subject: [PATCH 32/45] [1297] - Prevent a few faceID prompts during restore / backup flow (#6282) * 3 faceID requests down to 2 during backup creation process * prevent a couple faceIDs during the restore process * save local backup password if it's not the same password * fix portal not blocking touches --- src/components/PortalConsumer.js | 18 -------- src/components/WalletLoadingListener.tsx | 17 ++++++++ src/components/backup/RestoreCloudStep.tsx | 43 +++++++++++++------ src/components/backup/useCreateBackup.ts | 22 +++++----- src/helpers/walletLoadingStates.ts | 2 +- src/hooks/useWallets.ts | 9 ++-- src/model/backup.ts | 18 +++----- src/navigation/Routes.android.tsx | 4 +- src/navigation/Routes.ios.tsx | 4 +- src/react-native-cool-modals/Portal.tsx | 29 +++++-------- src/redux/wallets.ts | 30 ------------- src/screens/AddWalletSheet.tsx | 13 +++--- .../components/Backups/ViewWalletBackup.tsx | 11 +++-- .../components/Backups/WalletsAndBackup.tsx | 11 +++-- src/state/backups/backups.ts | 6 +++ src/state/portal/portal.ts | 15 ------- src/state/walletLoading/walletLoading.ts | 18 ++++++++ 17 files changed, 132 insertions(+), 138 deletions(-) delete mode 100644 src/components/PortalConsumer.js create mode 100644 src/components/WalletLoadingListener.tsx delete mode 100644 src/state/portal/portal.ts create mode 100644 src/state/walletLoading/walletLoading.ts diff --git a/src/components/PortalConsumer.js b/src/components/PortalConsumer.js deleted file mode 100644 index 9be593ede2c..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 { portalStore } from '@/state/portal/portal'; - -export default function PortalConsumer() { - const { isWalletLoading } = useWallets(); - - useEffect(() => { - if (isWalletLoading) { - portalStore.getState().setComponent(); - } - return portalStore.getState().hide; - }, [isWalletLoading]); - - return null; -} 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/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index d78efe43bcc..9d9d9c71552 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -18,14 +18,13 @@ import { Text } from '../text'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup'; import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { useDimensions, useInitializeWallet, useWallets } from '@/hooks'; +import { useDimensions, useInitializeWallet } from '@/hooks'; import { Navigation, useNavigation } from '@/navigation'; import { addressSetSelected, fetchWalletENSAvatars, fetchWalletNames, setAllWalletsWithIdsAsBackedUp, - setIsWalletLoading, walletsLoadState, walletsSetSelected, } from '@/redux/wallets'; @@ -42,11 +41,17 @@ 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 { ThemeContextProps, useTheme } from '@/theme'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { isEmpty } from 'lodash'; import { backupsStore } from '@/state/backups/backups'; import { useExperimentalFlag, PROFILES } from '@/config'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; + +type ComponentProps = { + theme: ThemeContextProps; + color: ThemeContextProps['colors'][keyof ThemeContextProps['colors']]; +}; const Title = styled(Text).attrs({ size: 'big', @@ -55,7 +60,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', @@ -63,7 +68,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), @@ -81,7 +86,7 @@ const Masthead = styled(Box).attrs({ }); const KeyboardSizeView = styled(KeyboardArea)({ - backgroundColor: ({ theme: { colors } }: any) => colors.transparent, + backgroundColor: ({ theme: { colors } }: ComponentProps) => colors.transparent, }); type RestoreCloudStepParams = { @@ -96,6 +101,8 @@ export default function RestoreCloudStep() { password: state.password, })); + const loadingState = walletLoadingStore(state => state.loadingState); + const { selectedBackup } = params; const { isDarkMode } = useTheme(); const { canGoBack, goBack } = useNavigation(); @@ -113,12 +120,12 @@ export default function RestoreCloudStep() { const [incorrectPassword, setIncorrectPassword] = useState(false); const passwordRef = useRef(null); const initializeWallet = useInitializeWallet(); - const { isWalletLoading } = useWallets(); useEffect(() => { const fetchPasswordIfPossible = async () => { const pwd = await getLocalBackupPassword(); if (pwd) { + backupsStore.getState().setStoredPassword(pwd); backupsStore.getState().setPassword(pwd); } }; @@ -151,18 +158,24 @@ export default function RestoreCloudStep() { throw new Error('No backup file selected'); } - dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET)); + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.RESTORING_WALLET, + }); const status = await restoreCloudBackup({ password: pwd, backupFilename: filename, }); if (status === RestoreCloudBackupResultStates.success) { // Store it in the keychain in case it was missing - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword) { + 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()); if (IS_ANDROID && filename) { @@ -240,7 +253,9 @@ export default function RestoreCloudStep() { } catch (e) { Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring')); } finally { - dispatch(setIsWalletLoading(null)); + walletLoadingStore.setState({ + loadingState: null, + }); } }, [password, selectedBackup.name, dispatch, onRestoreSuccess, profilesEnabled, initializeWallet]); @@ -273,7 +288,7 @@ export default function RestoreCloudStep() { { const onSuccess = useCallback( async (password: string) => { - const hasSavedPassword = await getLocalBackupPassword(); - if (!hasSavedPassword && password.trim()) { + 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, @@ -130,6 +131,7 @@ export const useCreateBackup = () => { const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => { const password = await getLocalBackupPassword(); if (password) { + backupsStore.getState().setStoredPassword(password); return password; } @@ -155,17 +157,17 @@ export const useCreateBackup = () => { } const password = await getPassword(props); - if (password) { - onConfirmBackup({ - password, - ...props, + if (!password) { + setLoadingStateWithTimeout({ + state: CloudBackupState.Ready, }); - return true; + return false; } - setLoadingStateWithTimeout({ - state: CloudBackupState.Ready, + onConfirmBackup({ + password, + ...props, }); - return false; + return true; }, [getPassword, onConfirmBackup, setLoadingStateWithTimeout] ); diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts index 7ed8ceef3c2..a9cdd674d2e 100644 --- a/src/helpers/walletLoadingStates.ts +++ b/src/helpers/walletLoadingStates.ts @@ -7,4 +7,4 @@ export const WalletLoadingStates = { RESTORING_WALLET: i18n.t('loading.restoring'), } as const; -export type WalletLoadingState = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; +export type WalletLoadingStates = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates]; diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts index 7194f701fe5..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, + ({ 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/model/backup.ts b/src/model/backup.ts index 3b1ac3d5818..f9305464411 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -152,7 +152,7 @@ export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: }; 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 }; @@ -215,7 +215,7 @@ export async function backupAllWalletsToCloud({ * 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; @@ -596,13 +596,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(); @@ -611,7 +607,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); } @@ -624,7 +620,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) { diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 5fdf407299a..9c27245b419 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -90,7 +90,7 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; -import PortalConsumer from '@/components/PortalConsumer'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; const Stack = createStackNavigator(); @@ -277,7 +277,7 @@ const AppContainerWithAnalytics = React.forwardRef - + )); diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx index 64b293f2378..790ff87b37c 100644 --- a/src/navigation/Routes.ios.tsx +++ b/src/navigation/Routes.ios.tsx @@ -102,7 +102,7 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel'; import { ClaimClaimablePanel } from '@/screens/claimables/ClaimClaimablePanel'; import { RootStackParamList } from './types'; -import PortalConsumer from '@/components/PortalConsumer'; +import WalletLoadingListener from '@/components/WalletLoadingListener'; import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; const Stack = createStackNavigator(); @@ -291,7 +291,7 @@ const AppContainerWithAnalytics = React.forwardRef - + )); diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx index 97ab83052da..dd2830ee0b4 100644 --- a/src/react-native-cool-modals/Portal.tsx +++ b/src/react-native-cool-modals/Portal.tsx @@ -1,9 +1,8 @@ -import React, { useEffect, useState } from 'react'; -import { IS_ANDROID, IS_IOS } from '@/env'; -import { portalStore } from '@/state/portal/portal'; +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 { Navigation, useNavigation } from '@/navigation'; import { useActiveRoute } from '@/hooks/useActiveRoute'; const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View; @@ -12,7 +11,7 @@ const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => childr export function Portal() { const activeRoute = useActiveRoute(); - const { blockTouches, Component } = portalStore(state => ({ + const { blockTouches, Component } = walletLoadingStore(state => ({ blockTouches: state.blockTouches, Component: state.Component, })); @@ -21,19 +20,19 @@ export function Portal() { return null; } + console.log('blockTouches', blockTouches); + return ( {Component} @@ -41,9 +40,3 @@ export function Portal() { ); } - -const sx = StyleSheet.create({ - wrapper: { - ...StyleSheet.absoluteFillObject, - }, -}); diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts index f55da2f26c1..d17f8b4c0d8 100644 --- a/src/redux/wallets.ts +++ b/src/redux/wallets.ts @@ -29,7 +29,6 @@ import { fetchReverseRecord } from '@/handlers/ens'; import { lightModeThemeColors } from '@/styles'; import { RainbowError, logger } from '@/logger'; import { parseTimestampFromBackupFile } from '@/model/backup'; -import { WalletLoadingState } from '@/helpers/walletLoadingStates'; // -- Types ---------------------------------------- // @@ -37,11 +36,6 @@ import { WalletLoadingState } from '@/helpers/walletLoadingStates'; * The current state of the `wallets` reducer. */ interface WalletsState { - /** - * The current loading state of the wallet. - */ - isWalletLoading: WalletLoadingState | null; - /** * 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. */ @@ -239,18 +224,6 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di }); }; -/** - * Updates the wallet loading state. - * - * @param val The new loading state. - */ -export const setIsWalletLoading = (val: WalletsState['isWalletLoading']) => (dispatch: Dispatch) => { - dispatch({ - payload: val, - type: WALLETS_SET_IS_LOADING, - }); -}; - /** * Marks all wallets with passed ids as backed-up * using a specified method and file in storage @@ -611,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/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index a49a19ee0d6..314cacfd9d5 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -8,7 +8,7 @@ import * as i18n from '@/languages'; import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; import { InteractionManager } from 'react-native'; -import { createAccountForWallet, setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; +import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; import { createWallet } from '@/model/wallet'; import WalletTypes from '@/helpers/walletTypes'; import { logger, RainbowError } from '@/logger'; @@ -24,8 +24,7 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { useInitializeWallet, useWallets } from '@/hooks'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { executeFnIfCloudBackupAvailable } from '@/model/backup'; -import { IS_ANDROID } from '@/env'; -import { backupsStore } from '@/state/backups/backups'; +import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -75,7 +74,9 @@ export const AddWalletSheet = () => { }, onCloseModal: async (args: any) => { if (args) { - dispatch(setIsWalletLoading(WalletLoadingStates.CREATING_WALLET)); + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); const name = args?.name ?? ''; const color = args?.color ?? null; @@ -132,7 +133,9 @@ export const AddWalletSheet = () => { }, 1000); } } finally { - dispatch(setIsWalletLoading(null)); + walletLoadingStore.setState({ + loadingState: null, + }); } } creatingWallet.current = false; diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx index 3433ded65a9..9fddd15964d 100644 --- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx @@ -31,7 +31,7 @@ import { SETTINGS_BACKUP_ROUTES } from './routes'; import { analyticsV2 } from '@/analytics'; import { InteractionManager } from 'react-native'; import { useDispatch } from 'react-redux'; -import { createAccountForWallet, setIsWalletLoading } from '@/redux/wallets'; +import { createAccountForWallet } from '@/redux/wallets'; import { logger, RainbowError } from '@/logger'; import { RainbowAccount } from '@/model/wallet'; import showWalletErrorAlert from '@/helpers/support'; @@ -45,6 +45,7 @@ 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 }; @@ -187,7 +188,9 @@ const ViewWalletBackup = () => { }, onCloseModal: async (args: any) => { if (args) { - dispatch(setIsWalletLoading(WalletLoadingStates.CREATING_WALLET)); + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); const name = args?.name ?? ''; const color = args?.color ?? null; @@ -209,7 +212,9 @@ const ViewWalletBackup = () => { }, 1000); } } finally { - dispatch(setIsWalletLoading(null)); + walletLoadingStore.setState({ + loadingState: null, + }); } } creatingWallet.current = false; diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 09146c92734..8883c01977d 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -26,7 +26,7 @@ import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets'; import { SETTINGS_BACKUP_ROUTES } from './routes'; import { RainbowAccount, createWallet } from '@/model/wallet'; import { useDispatch } from 'react-redux'; -import { setIsWalletLoading, walletsLoadState } from '@/redux/wallets'; +import { walletsLoadState } from '@/redux/wallets'; import { RainbowError, logger } from '@/logger'; import { IS_ANDROID, IS_IOS } from '@/env'; import { useCreateBackup } from '@/components/backup/useCreateBackup'; @@ -36,6 +36,7 @@ import { removeFirstEmojiFromString } from '@/helpers/emojiHandler'; 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'; type WalletPillProps = { @@ -165,7 +166,9 @@ export const WalletsAndBackup = () => { onCloseModal: async ({ name }: { name: string }) => { const nameValue = name.trim() !== '' ? name.trim() : ''; try { - dispatch(setIsWalletLoading(WalletLoadingStates.CREATING_WALLET)); + walletLoadingStore.setState({ + loadingState: WalletLoadingStates.CREATING_WALLET, + }); await createWallet({ color: null, @@ -182,7 +185,9 @@ export const WalletsAndBackup = () => { error: err, }); } finally { - dispatch(setIsWalletLoading(null)); + walletLoadingStore.setState({ + loadingState: null, + }); } }, }); diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index c455b715e63..d060a1b4e16 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -30,6 +30,9 @@ export enum CloudBackupState { export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching]; interface BackupsStore { + storedPassword: string; + setStoredPassword: (storedPassword: string) => void; + backupProvider: string | undefined; setBackupProvider: (backupProvider: string | undefined) => void; @@ -54,6 +57,9 @@ interface BackupsStore { const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching]; export const backupsStore = createRainbowStore((set, get) => ({ + storedPassword: '', + setStoredPassword: storedPassword => set({ storedPassword }), + backupProvider: undefined, setBackupProvider: provider => set({ backupProvider: provider }), diff --git a/src/state/portal/portal.ts b/src/state/portal/portal.ts deleted file mode 100644 index 51e0f96ccba..00000000000 --- a/src/state/portal/portal.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createRainbowStore } from '../internal/createRainbowStore'; - -type PortalState = { - blockTouches: boolean; - Component: JSX.Element | null; - hide: () => void; - setComponent: (Component: JSX.Element, blockTouches?: boolean) => void; -}; - -export const portalStore = createRainbowStore(set => ({ - blockTouches: false, - Component: null, - hide: () => set({ blockTouches: false, Component: null }), - setComponent: (Component: JSX.Element, blockTouches?: boolean) => set({ blockTouches, Component }), -})); 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 }), +})); From 109ca083cdb2ecb1628d6ffa881dd70ceff36ee3 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 09:43:56 -0500 Subject: [PATCH 33/45] fix backup prompt seemingly being stuck (#6289) --- .../backup/AddWalletToCloudBackupStep.tsx | 125 ------------------ src/components/backup/BackupManuallyStep.tsx | 100 -------------- src/components/backup/BackupSheet.tsx | 14 +- ...roviderStep.tsx => BackupWalletPrompt.tsx} | 76 +++++++---- src/components/backup/useCreateBackup.ts | 1 - src/handlers/walletReadyEvents.ts | 17 +-- src/helpers/walletBackupStepTypes.ts | 2 +- src/hooks/useImportingWallet.ts | 15 +-- src/languages/en_US.json | 6 + src/model/backup.ts | 13 +- src/navigation/Routes.android.tsx | 2 +- src/navigation/config.tsx | 4 +- .../components/Backups/WalletsAndBackup.tsx | 24 ++-- .../components/MenuContainer.tsx | 4 +- src/screens/WalletScreen/index.tsx | 5 +- 15 files changed, 102 insertions(+), 306 deletions(-) delete mode 100644 src/components/backup/AddWalletToCloudBackupStep.tsx delete mode 100644 src/components/backup/BackupManuallyStep.tsx rename src/components/backup/{BackupChooseProviderStep.tsx => BackupWalletPrompt.tsx} (73%) diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx deleted file mode 100644 index 24bfc56bf44..00000000000 --- a/src/components/backup/AddWalletToCloudBackupStep.tsx +++ /dev/null @@ -1,125 +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 { format } from 'date-fns'; -import { useCreateBackup } from '@/components/backup/useCreateBackup'; -import { backupsStore } from '@/state/backups/backups'; -import { executeFnIfCloudBackupAvailable } from '@/model/backup'; - -const imageSize = 72; - -export default function AddWalletToCloudBackupStep() { - const { goBack } = useNavigation(); - const { selectedWallet } = useWallets(); - const createBackup = useCreateBackup(); - - const { mostRecentBackup } = backupsStore(state => ({ - mostRecentBackup: state.mostRecentBackup, - })); - - const potentiallyLoginAndSubmit = useCallback(async () => { - const result = await executeFnIfCloudBackupAvailable({ - fn: () => - createBackup({ - walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, - }), - }); - - if (result) { - goBack(); - } - }, [createBackup, goBack, selectedWallet.id]); - - const onMaybeLater = useCallback(() => goBack(), [goBack]); - - return ( - - - - - - {lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)} - - - - - - - - - - - - - - 􀎽{' '} - {lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, { - cloudPlatform, - })} - - - - - - - - - - - - - - - - {lang.t(lang.l.back_up.cloud.mayber_later)} - - - - - - - - - - - {mostRecentBackup && ( - - - - - {lang.t(lang.l.back_up.cloud.latest_backup, { - date: format(new Date(mostRecentBackup.lastModified), "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 b21487470e2..12e15c60190 100644 --- a/src/components/backup/BackupSheet.tsx +++ b/src/components/backup/BackupSheet.tsx @@ -2,11 +2,9 @@ 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'; type BackupSheetParams = { @@ -20,21 +18,17 @@ 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]); diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupWalletPrompt.tsx similarity index 73% rename from src/components/backup/BackupChooseProviderStep.tsx rename to src/components/backup/BackupWalletPrompt.tsx index c576626c541..77071534f48 100644 --- a/src/components/backup/BackupChooseProviderStep.tsx +++ b/src/components/backup/BackupWalletPrompt.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +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'; @@ -13,11 +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 { 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; @@ -30,24 +32,26 @@ export default function BackupSheetSectionNoProvider() { status: state.status, })); - const onCloudBackup = () => { + 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, + }); + executeFnIfCloudBackupAvailable({ fn: () => createBackup({ walletId: selectedWallet.id, - navigateToRoute: { - route: Routes.SETTINGS_SHEET, - params: { - screen: Routes.SETTINGS_SECTION_BACKUP, - }, - }, }), + 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; @@ -61,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)} @@ -75,8 +104,7 @@ export default function BackupSheetSectionNoProvider() { - {/* replace this with BackUpMenuButton */} - + @@ -92,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, })} @@ -151,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/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index fef87e43de3..e227b1173c4 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -155,7 +155,6 @@ export const useCreateBackup = () => { if (backupsStore.getState().status !== CloudBackupState.Ready) { return false; } - const password = await getPassword(props); if (!password) { setLoadingStateWithTimeout({ diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 38cef55fefe..8d3e17e9494 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -12,9 +12,7 @@ import store from '@/redux/store'; import { checkKeychainIntegrity } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; -import walletBackupTypes from '@/helpers/walletBackupTypes'; import { InteractionManager } from 'react-native'; -import { backupsStore } from '@/state/backups/backups'; import { IS_TEST } from '@/env'; export const runKeychainIntegrityChecks = async () => { @@ -28,22 +26,15 @@ export const runWalletBackupStatusChecks = () => { const { selected } = store.getState().wallets; if (!selected || IS_TEST) return; - const isSelectedWalletBackedUp = selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly; - if (!isSelectedWalletBackedUp) { + const selectedWalletNeedsBackedUp = !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly; + if (selectedWalletNeedsBackedUp) { logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); - const provider = backupsStore.getState().backupProvider; - let stepType: string = WalletBackupStepTypes.no_provider; - if (provider === walletBackupTypes.cloud) { - stepType = WalletBackupStepTypes.backup_now_to_cloud; - } else if (provider === walletBackupTypes.manual) { - stepType = WalletBackupStepTypes.backup_now_manually; - } InteractionManager.runAfterInteractions(() => { - logger.debug(`[walletReadyEvents]: BackupSheet: showing ${stepType} for selected wallet`); + logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); triggerOnSwipeLayout(() => Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, + step: WalletBackupStepTypes.backup_prompt, }) ); }); 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/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index 5d4b134bc50..8ef855195dc 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 walletBackupTypes from '@/helpers/walletBackupTypes'; import { ChainId } from '@/chains/types'; import { backupsStore } from '@/state/backups/backups'; +import { IS_TEST } from '@/env'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -350,17 +349,11 @@ export default function useImportingWallet({ showImportModal = true } = {}) { isValidBluetoothDeviceId(input) ) ) { - 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' && + if (!IS_TEST) { Navigation.handleAction(Routes.BACKUP_SHEET, { - step: stepType, + step: WalletBackupStepTypes.backup_prompt, }); + } } }, 1000); diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 0e2e8dcceb9..cfdcded7d00 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -148,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", diff --git a/src/model/backup.ts b/src/model/backup.ts index f9305464411..caaecd71c1e 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -34,7 +34,7 @@ import walletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import { getRemoteConfig } from './remoteConfig'; import { WrappedAlert as Alert } from '@/helpers/alert'; import { AppDispatch } from '@/redux/store'; -import { backupsStore } from '@/state/backups/backups'; +import { backupsStore, CloudBackupState } from '@/state/backups/backups'; const { DeviceUUID } = NativeModules; const encryptor = new AesEncryptor(); @@ -99,6 +99,8 @@ export interface BackupUserData { 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) { @@ -114,15 +116,20 @@ export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: 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(); @@ -143,10 +150,12 @@ export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: }, ] ); + 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(); } }; diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 9c27245b419..5c21eac49f0 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -214,7 +214,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; } diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 4de26c91662..70965f013eb 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; } diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 8883c01977d..c0ae83c9f5d 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -1,5 +1,5 @@ /* 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'; @@ -38,6 +38,7 @@ import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { executeFnIfCloudBackupAvailable } from '@/model/backup'; import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; +import { ScrollView } from 'react-native'; type WalletPillProps = { account: RainbowAccount; @@ -94,6 +95,8 @@ export const WalletsAndBackup = () => { const { wallets } = useWallets(); const dispatch = useDispatch(); + const scrollviewRef = useRef(null); + const createBackup = useCreateBackup(); const { status, backupProvider, backups, mostRecentBackup } = backupsStore(state => ({ status: state.status, @@ -188,6 +191,7 @@ export const WalletsAndBackup = () => { walletLoadingStore.setState({ loadingState: null, }); + scrollviewRef.current?.scrollTo({ y: 0, animated: true }); } }, }); @@ -219,54 +223,54 @@ export const WalletsAndBackup = () => { if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) { return { status: 'not-enabled', - text: 'Not Enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), }; } if (status !== CloudBackupState.Ready) { return { status: 'out-of-sync', - text: 'Syncing', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), }; } if (!allBackedUp) { return { status: 'out-of-date', - text: 'Out of Date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), }; } return { status: 'up-to-date', - text: '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: 'Not Enabled', + text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled), }; } if (status !== CloudBackupState.Ready) { return { status: 'out-of-sync', - text: 'Syncing', + text: i18n.t(i18n.l.back_up.cloud.statuses.syncing), }; } if (!allBackedUp) { return { status: 'out-of-date', - text: 'Out of Date', + text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date), }; } return { status: 'up-to-date', - text: 'Up to date', + text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date), }; }, [backupProvider, status, allBackedUp]); @@ -676,7 +680,7 @@ export const WalletsAndBackup = () => { ]); return ( - + {renderView()} 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 { - runWalletBackupStatusChecks(); - }, []); - useEffect(() => { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); + runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); From 7dfd7e231c53fc3da2ff5ab80670018940a3f0a1 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 09:44:26 -0500 Subject: [PATCH 34/45] [1297] - Fix infinite sync (#6280) * add syncing timeout 10s * fix timeout --- src/state/backups/backups.ts | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index d060a1b4e16..ef1abf3ab23 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -8,11 +8,6 @@ import { getMostRecentCloudBackup, hasManuallyBackedUpWallet } from '@/screens/S import { Mutex } from 'async-mutex'; import store from '@/redux/store'; -const sleep = (ms: number) => - new Promise(resolve => { - setTimeout(resolve, ms); - }); - const mutex = new Mutex(); export enum CloudBackupState { @@ -27,6 +22,9 @@ export enum CloudBackupState { Success = 'success', } +const DEFAULT_TIMEOUT = 10_000; +const MAX_RETRIES = 3; + export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching]; interface BackupsStore { @@ -48,7 +46,10 @@ interface BackupsStore { password: string; setPassword: (password: string) => void; - syncAndFetchBackups: (retryOnFailure?: boolean) => Promise<{ + syncAndFetchBackups: ( + retryOnFailure?: boolean, + retryCount?: number + ) => Promise<{ success: boolean; retry?: boolean; }>; @@ -75,8 +76,15 @@ export const backupsStore = createRainbowStore((set, get) => ({ password: '', setPassword: password => set({ password }), - syncAndFetchBackups: async (retryOnFailure = true) => { + 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(); @@ -157,13 +165,18 @@ export const backupsStore = createRainbowStore((set, get) => ({ const releaser = await mutex.acquire(); logger.debug('[backupsStore]: Acquired mutex'); - const { success, retry } = await syncAndPullFiles(); + const { success, retry } = await Promise.race([syncAndPullFiles(), timeoutPromise]); releaser(); logger.debug('[backupsStore]: Released mutex'); - if (retry) { - await sleep(5_000); - return get().syncAndFetchBackups(retryOnFailure); + 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 }; }, })); From 64d075b13ff9632471fa588bfefdcc2c21da9f18 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 09:53:51 -0500 Subject: [PATCH 35/45] fix: code review changes --- src/screens/SettingsSheet/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts index 0bac0b75f2d..0fb1d26faff 100644 --- a/src/screens/SettingsSheet/utils.ts +++ b/src/screens/SettingsSheet/utils.ts @@ -45,7 +45,7 @@ export const checkLocalWalletsForBackupStatus = ( return { allBackedUp: acc.allBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), areBackedUp: acc.areBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible), - canBeBackedUp: acc.canBeBackedUp || isBackupEligible, + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, }; }, { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } @@ -59,7 +59,7 @@ export const checkLocalWalletsForBackupStatus = ( return { allBackedUp: acc.allBackedUp && (wallet.backedUp || !isBackupEligible), areBackedUp: acc.areBackedUp && (wallet.backedUp || !isBackupEligible || wallet.imported), - canBeBackedUp: acc.canBeBackedUp || isBackupEligible, + canBeBackedUp: acc.canBeBackedUp && isBackupEligible, }; }, { allBackedUp: true, areBackedUp: true, canBeBackedUp: false } From 48c346a605319596b50f20f7d2e7b3464acd0364 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 12:10:11 -0500 Subject: [PATCH 36/45] prevent backup prompt from firing until backups status is synced --- src/handlers/walletReadyEvents.ts | 33 ++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 8d3e17e9494..3320d4250c2 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -14,6 +14,7 @@ import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; import { InteractionManager } from 'react-native'; import { IS_TEST } from '@/env'; +import { backupsStore, LoadingStates } from '@/state/backups/backups'; export const runKeychainIntegrityChecks = async () => { const keychainIntegrityState = await getKeychainIntegrityState(); @@ -22,6 +23,28 @@ export const runKeychainIntegrityChecks = async () => { } }; +const delay = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); + }); + +const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { + const { status } = backupsStore.getState(); + if (LoadingStates.includes(status)) { + await delay(1000); + return promptForBackupOnceReadyOrNotAvailable(); + } + + InteractionManager.runAfterInteractions(() => { + logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); + triggerOnSwipeLayout(() => + Navigation.handleAction(Routes.BACKUP_SHEET, { + step: WalletBackupStepTypes.backup_prompt, + }) + ); + }); +}; + export const runWalletBackupStatusChecks = () => { const { selected } = store.getState().wallets; if (!selected || IS_TEST) return; @@ -29,15 +52,7 @@ export const runWalletBackupStatusChecks = () => { const selectedWalletNeedsBackedUp = !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly; if (selectedWalletNeedsBackedUp) { logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); - - InteractionManager.runAfterInteractions(() => { - logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); - triggerOnSwipeLayout(() => - Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_prompt, - }) - ); - }); + promptForBackupOnceReadyOrNotAvailable(); } }; From 515691bd04b8b784153aa16988a065282e3f6abb Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 14:10:42 -0500 Subject: [PATCH 37/45] replace inline flex with flatlist 3 column --- .../components/Inline/Inline.tsx | 2 +- src/screens/AddWalletSheet.tsx | 1 - .../components/Backups/WalletsAndBackup.tsx | 69 ++++++++++++------- 3 files changed, 45 insertions(+), 27 deletions(-) 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/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 314cacfd9d5..92712d4241d 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -111,7 +111,6 @@ export const AddWalletSheet = () => { await dispatch(createAccountForWallet(primaryWalletKey, color, name)); // @ts-ignore await initializeWallet(); - // TODO: Make sure the new wallet is marked as not backed up } else { // If doesn't exist, we need to create a new wallet await createWallet({ diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index c0ae83c9f5d..89db4984bbb 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, useRef } from 'react'; +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { cloudPlatform } from '@/utils/platform'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; @@ -17,7 +16,7 @@ import { addressHashedEmoji } from '@/utils/profileUtils'; import * as i18n from '@/languages'; import MenuHeader, { StatusType } from '../MenuHeader'; import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils'; -import { Inline, Text, Box, Stack } from '@/design-system'; +import { Inline, Text, Box, Stack, Bleed } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; import { useTheme } from '@/theme'; import Routes from '@/navigation/routesNames'; @@ -38,13 +37,14 @@ import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { executeFnIfCloudBackupAvailable } from '@/model/backup'; import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; -import { ScrollView } from 'react-native'; +import { FlatList, ScrollView } from 'react-native'; type WalletPillProps = { account: RainbowAccount; }; const WalletPill = ({ account }: WalletPillProps) => { + const [width, setWidth] = useState(undefined); const label = useMemo(() => removeFirstEmojiFromString(account.label), [account.label]); const { data: ENSAvatar } = useENSAvatar(label); @@ -52,12 +52,18 @@ const WalletPill = ({ account }: WalletPillProps) => { const accountImage = addressHashedEmoji(account.address); + useLayoutEffect(() => { + if (width) { + setWidth(width - 8); + } + }, [width]); + return ( { ); }; +const basePadding = 16; +const rowHeight = 36; + const getAccountSectionHeight = (numAccounts: number) => { - const basePadding = 16; - const rowHeight = 36; const rows = Math.ceil(Math.max(1, numAccounts) / 3); const paddingBetween = (rows - 1) * 4; @@ -328,7 +335,7 @@ export const WalletsAndBackup = () => { { > {!isBackedUp && ( @@ -367,15 +374,18 @@ export const WalletsAndBackup = () => { titleComponent={} /> - {addresses.map(address => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + /> } /> @@ -508,11 +518,17 @@ export const WalletsAndBackup = () => { disabled width="full" titleComponent={ - - {addresses.map(address => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + /> } /> @@ -610,11 +626,14 @@ export const WalletsAndBackup = () => { size={getAccountSectionHeight(addresses.length)} disabled titleComponent={ - - {addresses.map(address => ( - - ))} - + } + keyExtractor={item => item.address} + numColumns={3} + /> } /> From ee330bebaad177f8b8d0ce9d887c5b6a47c298a2 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 15:29:18 -0500 Subject: [PATCH 38/45] fix non-visible wallets from appearing in backup section wallet address breakdown (#6297) --- src/model/backup.ts | 6 +--- .../components/SettingsSection.tsx | 30 +++++++++---------- .../SettingsSheet/useVisibleWallets.ts | 1 + 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/model/backup.ts b/src/model/backup.ts index caaecd71c1e..c838796664d 100644 --- a/src/model/backup.ts +++ b/src/model/backup.ts @@ -220,8 +220,6 @@ 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 kc.getAllKeys(); @@ -251,8 +249,6 @@ export async function backupAllWalletsToCloud({ label: cloudPlatform, }); - let updatedBackupFile: string | null = null; - const data = { createdAt: now, secrets: {}, @@ -267,7 +263,7 @@ export async function backupAllWalletsToCloud({ }); await Promise.all(promises); - updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); + const updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`); const walletIdsToUpdate = Object.keys(wallets); await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, WalletBackupTypes.cloud, updatedBackupFile)); diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx index 811950e1565..095b88cbb85 100644 --- a/src/screens/SettingsSheet/components/SettingsSection.tsx +++ b/src/screens/SettingsSheet/components/SettingsSection.tsx @@ -91,7 +91,7 @@ const SettingsSection = ({ const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []); - const { allBackedUp, canBeBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); + const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]); const themeMenuConfig = useMemo(() => { return { @@ -176,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 && ( address.visible), }; }); }; From 869a1fe5a78eb740bebc8f20fc6f93e17f7fe88e Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 16:40:21 -0500 Subject: [PATCH 39/45] properly space items and calculate width based on device --- .../components/Backups/WalletsAndBackup.tsx | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 89db4984bbb..42283f42896 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef } from 'react'; import { cloudPlatform } from '@/utils/platform'; import Menu from '../Menu'; import MenuContainer from '../MenuContainer'; @@ -11,12 +11,12 @@ 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, { StatusType } from '../MenuHeader'; import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils'; -import { Inline, Text, Box, Stack, Bleed } from '@/design-system'; +import { Inline, Text, Box, Stack } from '@/design-system'; import { ContactAvatar } from '@/components/contacts'; import { useTheme } from '@/theme'; import Routes from '@/navigation/routesNames'; @@ -43,8 +43,22 @@ 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 [width, setWidth] = useState(undefined); const label = useMemo(() => removeFirstEmojiFromString(account.label), [account.label]); const { data: ENSAvatar } = useENSAvatar(label); @@ -52,12 +66,6 @@ const WalletPill = ({ account }: WalletPillProps) => { const accountImage = addressHashedEmoji(account.address); - useLayoutEffect(() => { - if (width) { - setWidth(width - 8); - } - }, [width]); - return ( { paddingLeft={{ custom: 4 }} paddingRight={{ custom: 8 }} padding={{ custom: 4 }} + width={{ custom: accountsItemWidth }} > {ENSAvatar?.imageUrl ? ( @@ -87,16 +96,6 @@ const WalletPill = ({ account }: WalletPillProps) => { ); }; -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; -}; - export const WalletsAndBackup = () => { const { navigate } = useNavigation(); const { wallets } = useWallets(); @@ -385,6 +384,7 @@ export const WalletsAndBackup = () => { renderItem={({ item }) => } keyExtractor={item => item.address} numColumns={3} + scrollEnabled={false} /> } /> @@ -520,14 +520,12 @@ export const WalletsAndBackup = () => { titleComponent={ } keyExtractor={item => item.address} numColumns={3} + scrollEnabled={false} /> } /> From f9249f8e599920373b68ae239f308763b45f9ad7 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 4 Dec 2024 16:47:14 -0500 Subject: [PATCH 40/45] fix inconsistencies with keys --- .../components/Backups/WalletsAndBackup.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index 42283f42896..74ec4a4e969 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -376,11 +376,12 @@ export const WalletsAndBackup = () => { key={`${id}-accounts`} size={getAccountSectionHeight(addresses.length)} disabled + width="full" titleComponent={ } keyExtractor={item => item.address} numColumns={3} @@ -479,7 +480,7 @@ export const WalletsAndBackup = () => { { } > - {!isBackedUp && } + {!isBackedUp && ( + + )} {imported && } { titleComponent={} /> { { } > - {!isBackedUp && } + {!isBackedUp && ( + + )} {imported && } { titleComponent={} /> } keyExtractor={item => item.address} numColumns={3} + scrollEnabled={false} /> } /> From 308437ad38d329b279d69d7f0fc6b9ef4e5c3a52 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 6 Dec 2024 13:27:49 -0500 Subject: [PATCH 41/45] fix lint --- src/components/ExchangeTokenRow.tsx | 5 +---- .../FastComponents/FastCurrencySelectionRow.tsx | 2 -- .../RecyclerAssetList2/profile-header/ProfileNameRow.tsx | 1 - src/languages/en_US.json | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/ExchangeTokenRow.tsx b/src/components/ExchangeTokenRow.tsx index 2b70fe2d35a..91027de2bb0 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, @@ -101,10 +100,8 @@ export default React.memo(function ExchangeTokenRow({ {isInfoButtonVisible && } {showFavoriteButton && (IS_IOS ? ( - // @ts-ignore (onNewEmoji.current = newOnNewEmoji)} /> diff --git a/src/languages/en_US.json b/src/languages/en_US.json index cfdcded7d00..a5bddec015d 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -152,7 +152,7 @@ "not_enabled": "Not Enabled", "syncing": "Syncing", "out_of_date": "Out of Date", - "up_to_date":"Up to Date" + "up_to_date": "Up to Date" }, "password": { "a_password_youll_remember_part_one": "This password is", From 74949919f97618b87242796e3e31a95bed04473c Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 6 Dec 2024 15:37:53 -0500 Subject: [PATCH 42/45] add backup prompt to checks to avoid stacking sheets --- .../remote-promo-sheet/runChecks.ts | 4 +- src/handlers/walletReadyEvents.ts | 42 +++++++++++-------- src/screens/WalletScreen/index.tsx | 2 - 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/components/remote-promo-sheet/runChecks.ts b/src/components/remote-promo-sheet/runChecks.ts index d25902adcfa..9167de41182 100644 --- a/src/components/remote-promo-sheet/runChecks.ts +++ b/src/components/remote-promo-sheet/runChecks.ts @@ -1,5 +1,5 @@ import { IS_TEST } from '@/env'; -import { runFeatureAndLocalCampaignChecks } from '@/handlers/walletReadyEvents'; +import { runFeaturesLocalCampaignAndBackupChecks } from '@/handlers/walletReadyEvents'; import { logger } from '@/logger'; import { checkForRemotePromoSheet } from '@/components/remote-promo-sheet/checkForRemotePromoSheet'; import { useCallback, useEffect } from 'react'; @@ -18,7 +18,7 @@ export const useRunChecks = ({ runChecksOnMount = true, walletReady }: { runChec return; } - if (await runFeatureAndLocalCampaignChecks()) return; + if (await runFeaturesLocalCampaignAndBackupChecks()) return; if (!remotePromoSheets) { logger.debug('[useRunChecks]: remote promo sheets is disabled'); diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index 3320d4250c2..b62749da519 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -12,7 +12,6 @@ import store from '@/redux/store'; import { checkKeychainIntegrity } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; -import { InteractionManager } from 'react-native'; import { IS_TEST } from '@/env'; import { backupsStore, LoadingStates } from '@/state/backups/backups'; @@ -28,32 +27,33 @@ const delay = (ms: number) => setTimeout(resolve, ms); }); -const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { +const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { const { status } = backupsStore.getState(); if (LoadingStates.includes(status)) { await delay(1000); return promptForBackupOnceReadyOrNotAvailable(); } - InteractionManager.runAfterInteractions(() => { - logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); - triggerOnSwipeLayout(() => - Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_prompt, - }) - ); - }); + logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); + triggerOnSwipeLayout(() => + Navigation.handleAction(Routes.BACKUP_SHEET, { + step: WalletBackupStepTypes.backup_prompt, + }) + ); + return true; }; -export const runWalletBackupStatusChecks = () => { +export const runWalletBackupStatusChecks = async (): Promise => { const { selected } = store.getState().wallets; - if (!selected || IS_TEST) return; + if (!selected || IS_TEST) return false; - const selectedWalletNeedsBackedUp = !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly; + const selectedWalletNeedsBackedUp = + !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly && selected.type !== WalletTypes.bluetooth; if (selectedWalletNeedsBackedUp) { logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet'); - promptForBackupOnceReadyOrNotAvailable(); + return promptForBackupOnceReadyOrNotAvailable(); } + return false; }; export const runFeatureUnlockChecks = async (): Promise => { @@ -89,10 +89,16 @@ export const runFeatureUnlockChecks = async (): Promise => { return false; }; -export const runFeatureAndLocalCampaignChecks = async () => { - const showingFeatureUnlock = await runFeatureUnlockChecks(); - if (!showingFeatureUnlock) { - return 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/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index 7880a6d300a..3d0c12ceca1 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -25,7 +25,6 @@ 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'; @@ -137,7 +136,6 @@ function WalletScreen() { if (walletReady) { loadAccountLateData(); loadGlobalLateData(); - runWalletBackupStatusChecks(); } }, [loadAccountLateData, loadGlobalLateData, walletReady]); From 1b4e81f087f7a7fc7f4114d10f5755fe9ef4c530 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 6 Dec 2024 15:51:26 -0500 Subject: [PATCH 43/45] add authentication in order to allow backup deletion --- src/hooks/useManageCloudBackups.ts | 38 ++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts index d5e6bc73f58..323fd1d62db 100644 --- a/src/hooks/useManageCloudBackups.ts +++ b/src/hooks/useManageCloudBackups.ts @@ -16,6 +16,8 @@ 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(); @@ -96,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; + } + } + + // 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')); + 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')); + } } } ); From 1b149e980bbc43fdaf495e4c1abf90abed7251a5 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 19 Dec 2024 19:42:05 -0500 Subject: [PATCH 44/45] fix botched merge --- src/components/backup/RestoreCloudStep.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 9d9d9c71552..071971e9bb5 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -20,14 +20,7 @@ import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/ha import walletBackupTypes from '@/helpers/walletBackupTypes'; import { useDimensions, useInitializeWallet } from '@/hooks'; import { Navigation, useNavigation } from '@/navigation'; -import { - addressSetSelected, - fetchWalletENSAvatars, - fetchWalletNames, - setAllWalletsWithIdsAsBackedUp, - walletsLoadState, - walletsSetSelected, -} from '@/redux/wallets'; +import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import styled from '@/styled-thing'; import { padding } from '@/styles'; @@ -217,10 +210,7 @@ export default function RestoreCloudStep() { const firstAddress = firstWallet ? (firstWallet.addresses || [])[0].address : undefined; const p1 = dispatch(walletsSetSelected(firstWallet)); const p2 = dispatch(addressSetSelected(firstAddress)); - const p3 = dispatch(fetchWalletNames()); - const p4 = profilesEnabled ? dispatch(fetchWalletENSAvatars()) : null; - - await Promise.all([p1, p2, p3, p4]); + await Promise.all([p1, p2]); await initializeWallet(null, null, null, false, false, null, true, null); }); From ff305c6abf1e7955d830d72ba9e87683ba1e4930 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 19 Dec 2024 19:43:15 -0500 Subject: [PATCH 45/45] remove unused imports --- src/components/backup/RestoreCloudStep.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx index 071971e9bb5..ce0774f2ec3 100644 --- a/src/components/backup/RestoreCloudStep.tsx +++ b/src/components/backup/RestoreCloudStep.tsx @@ -38,7 +38,6 @@ import { ThemeContextProps, useTheme } from '@/theme'; import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { isEmpty } from 'lodash'; import { backupsStore } from '@/state/backups/backups'; -import { useExperimentalFlag, PROFILES } from '@/config'; import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; type ComponentProps = { @@ -107,7 +106,6 @@ export default function RestoreCloudStep() { }, [canGoBack, goBack]); const dispatch = useDispatch(); - const profilesEnabled = useExperimentalFlag(PROFILES); const { width: deviceWidth, height: deviceHeight } = useDimensions(); const [validPassword, setValidPassword] = useState(false); const [incorrectPassword, setIncorrectPassword] = useState(false); @@ -247,7 +245,7 @@ export default function RestoreCloudStep() { loadingState: null, }); } - }, [password, selectedBackup.name, dispatch, onRestoreSuccess, profilesEnabled, initializeWallet]); + }, [password, selectedBackup.name, dispatch, onRestoreSuccess, initializeWallet]); const onPasswordSubmit = useCallback(() => { validPassword && onSubmit();