diff --git a/src/App.tsx b/src/App.tsx
index d59b7ed7296..80a5f7140a2 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,7 @@ 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';
+import { BackupsSync } from '@/state/sync/BackupsSync';
if (IS_DEV) {
reactNativeDisableYellowBox && LogBox.ignoreAllLogs();
@@ -70,12 +70,11 @@ function App({ walletReady }: AppProps) {
}, []);
return (
-
+ <>
{initialRoute && (
-
)}
@@ -83,14 +82,27 @@ function App({ walletReady }: AppProps) {
-
-
+
+
+
+ >
);
}
-const AppWithRedux = connect(state => ({
- walletReady: state.appState.walletReady,
-}))(App);
+const AppWithRedux = connect(
+ state => ({
+ walletReady: state.appState.walletReady,
+ }),
+ null,
+ null,
+ {
+ areStatesEqual: (next, prev) => {
+ // Only update if walletReady actually changed
+ return next.appState.walletReady === prev.appState.walletReady;
+ },
+ areOwnPropsEqual: shallowEqual,
+ }
+)(memo(App));
function Root() {
const [initializing, setInitializing] = useState(true);
diff --git a/src/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/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx
index 62f92a99e2f..24bfc56bf44 100644
--- a/src/components/backup/AddWalletToCloudBackupStep.tsx
+++ b/src/components/backup/AddWalletToCloudBackupStep.tsx
@@ -9,38 +9,40 @@ import { ButtonPressAnimation } from '../animations';
import Routes from '@/navigation/routesNames';
import { useNavigation } from '@/navigation';
import { useWallets } from '@/hooks';
-import { WalletCountPerType, useVisibleWallets } from '@/screens/SettingsSheet/useVisibleWallets';
import { format } from 'date-fns';
-import { useCreateBackup } from './useCreateBackup';
-import { login } from '@/handlers/cloudBackup';
+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 { wallets, selectedWallet } = useWallets();
+ const { selectedWallet } = useWallets();
+ const createBackup = useCreateBackup();
- const walletTypeCount: WalletCountPerType = {
- phrase: 0,
- privateKey: 0,
- };
-
- const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount });
-
- const { onSubmit } = useCreateBackup({
- walletId: selectedWallet.id,
- navigateToRoute: {
- route: Routes.SETTINGS_SHEET,
- params: {
- screen: Routes.SETTINGS_SECTION_BACKUP,
- },
- },
- });
+ const { mostRecentBackup } = backupsStore(state => ({
+ mostRecentBackup: state.mostRecentBackup,
+ }));
const potentiallyLoginAndSubmit = useCallback(async () => {
- await login();
- return onSubmit({});
- }, [onSubmit]);
+ 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]);
@@ -70,7 +72,7 @@ export default function AddWalletToCloudBackupStep() {
- potentiallyLoginAndSubmit().then(success => success && goBack())}>
+
@@ -105,13 +107,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"),
})}
diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupChooseProviderStep.tsx
index 38325639704..c576626c541 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';
@@ -16,11 +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 { useCreateBackup } from '@/components/backup/useCreateBackup';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
const imageSize = 72;
@@ -28,62 +25,24 @@ export default function BackupSheetSectionNoProvider() {
const { colors } = useTheme();
const { navigate, goBack } = useNavigation();
const { selectedWallet } = useWallets();
+ const createBackup = useCreateBackup();
+ const { status } = backupsStore(state => ({
+ status: state.status,
+ }));
- const { onSubmit, loading } = useCreateBackup({
- walletId: selectedWallet.id,
- navigateToRoute: {
- route: Routes.SETTINGS_SHEET,
- params: {
- screen: Routes.SETTINGS_SECTION_BACKUP,
- },
- },
- });
-
- const onCloudBackup = async () => {
- if (loading !== 'none') {
- return;
- }
- // NOTE: On Android we need to make sure the user is signed into a Google account before trying to backup
- // otherwise we'll fake backup and it's confusing...
- if (IS_ANDROID) {
- try {
- await login();
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (!accountDetails) {
- Alert.alert(lang.t(lang.l.back_up.errors.no_account_found));
- return;
- }
- });
- } catch (e) {
- logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), {
- error: e,
- });
- Alert.alert(lang.t(lang.l.back_up.errors.no_account_found));
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.label),
- lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
+ const onCloudBackup = () => {
+ executeFnIfCloudBackupAvailable({
+ fn: () =>
+ createBackup({
+ walletId: selectedWallet.id,
+ navigateToRoute: {
+ route: Routes.SETTINGS_SHEET,
+ params: {
+ screen: Routes.SETTINGS_SECTION_BACKUP,
},
- ]
- );
- return;
- }
- }
-
- onSubmit({});
+ },
+ }),
+ });
};
const onManualBackup = async () => {
@@ -117,7 +76,7 @@ export default function BackupSheetSectionNoProvider() {
{/* replace this with BackUpMenuButton */}
-
+
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/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx
index 2f7b68cedf6..d08d4cdb0e2 100644
--- a/src/components/backup/ChooseBackupStep.tsx
+++ b/src/components/backup/ChooseBackupStep.tsx
@@ -6,26 +6,24 @@ import { useDimensions } from '@/hooks';
import { useNavigation } from '@/navigation';
import styled from '@/styled-thing';
import { margin, padding } from '@/styles';
-import { Box, Stack, Text } from '@/design-system';
-import { RouteProp, useRoute } from '@react-navigation/native';
+import { Box, Stack } from '@/design-system';
import { sharedCoolModalTopOffset } from '@/navigation/config';
-import { ImgixImage } from '../images';
+import { ImgixImage } from '@/components/images';
import MenuContainer from '@/screens/SettingsSheet/components/MenuContainer';
import Menu from '@/screens/SettingsSheet/components/Menu';
import { format } from 'date-fns';
import MenuItem from '@/screens/SettingsSheet/components/MenuItem';
import Routes from '@/navigation/routesNames';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { RestoreSheetParams } from '@/screens/RestoreSheet';
+import { BackupFile, parseTimestampFromFilename } from '@/model/backup';
import { Source } from 'react-native-fast-image';
import { IS_ANDROID } from '@/env';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import useCloudBackups, { CloudBackupStep } from '@/hooks/useCloudBackups';
-import { Centered } from '../layout';
-import { cloudPlatform } from '@/utils/platform';
-import Spinner from '../Spinner';
-import ActivityIndicator from '../ActivityIndicator';
+import { Page } from '@/components/layout';
+import Spinner from '@/components/Spinner';
+import ActivityIndicator from '@/components/ActivityIndicator';
import { useTheme } from '@/theme';
+import { backupsStore, CloudBackupState, LoadingStates } from '@/state/backups/backups';
+import { titleForBackupState } from '@/screens/SettingsSheet/utils';
const Title = styled(RNText).attrs({
align: 'left',
@@ -53,60 +51,27 @@ const Masthead = styled(Box).attrs({
});
export function ChooseBackupStep() {
- const {
- params: { fromSettings },
- } = useRoute>();
const { colors } = useTheme();
- const { isFetching, backups, userData, step, fetchBackups } = useCloudBackups();
+ const { status, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
+
+ const isLoading = LoadingStates.includes(status);
const { top } = useSafeAreaInsets();
const { height: deviceHeight } = useDimensions();
const { navigate } = useNavigation();
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
-
const onSelectCloudBackup = useCallback(
- (selectedBackup: Backup) => {
+ (selectedBackup: BackupFile) => {
navigate(Routes.RESTORE_CLOUD_SHEET, {
- backups,
- userData,
selectedBackup,
- fromSettings,
});
},
- [navigate, userData, backups, fromSettings]
+ [navigate]
);
const height = IS_ANDROID ? deviceHeight - top : deviceHeight - sharedCoolModalTopOffset - 48;
@@ -132,7 +97,7 @@ export function ChooseBackupStep() {
- {!isFetching && step === CloudBackupStep.FAILED && (
+ {status === CloudBackupState.FailedToInitialize && (
)}
- {!isFetching && !cloudBackups.length && step !== CloudBackupStep.FAILED && (
+ {status === CloudBackupState.Ready && backups.files.length === 0 && (
+
+
+
+
)}
- {!isFetching && cloudBackups.length > 0 && (
+ {status === CloudBackupState.Ready && backups.files.length > 0 && (
{mostRecentBackup && (
-
+
+
+
)}
-
+
)}
- {isFetching && (
-
+ {isLoading && (
+
{android ? : }
-
- {lang.t(lang.l.back_up.cloud.fetching_backups, {
- cloudPlatformName: cloudPlatform,
- })}
-
-
+ {titleForBackupState[status]}
+
)}
diff --git a/src/components/backup/CloudBackupProvider.tsx b/src/components/backup/CloudBackupProvider.tsx
deleted file mode 100644
index 377e9d13a83..00000000000
--- a/src/components/backup/CloudBackupProvider.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import React, { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
-import type { BackupUserData, CloudBackups } from '@/model/backup';
-import {
- fetchAllBackups,
- fetchUserDataFromCloud,
- getGoogleAccountUserData,
- isCloudBackupAvailable,
- syncCloud,
-} from '@/handlers/cloudBackup';
-import { RainbowError, logger } from '@/logger';
-import { IS_ANDROID } from '@/env';
-
-type CloudBackupContext = {
- isFetching: boolean;
- backups: CloudBackups;
- fetchBackups: () => Promise;
- userData: BackupUserData | undefined;
-};
-
-const CloudBackupContext = createContext({} as CloudBackupContext);
-
-export function CloudBackupProvider({ children }: PropsWithChildren) {
- const [isFetching, setIsFetching] = useState(false);
- const [backups, setBackups] = useState({
- files: [],
- });
-
- const [userData, setUserData] = useState();
-
- const fetchBackups = async () => {
- try {
- setIsFetching(true);
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- logger.debug('[CloudBackupProvider]: Cloud backup is not available');
- setIsFetching(false);
- return;
- }
-
- if (IS_ANDROID) {
- const gdata = await getGoogleAccountUserData();
- if (!gdata) {
- return;
- }
- }
-
- logger.debug('[CloudBackupProvider]: Syncing with cloud');
- await syncCloud();
-
- logger.debug('[CloudBackupProvider]: Fetching user data');
- const userData = await fetchUserDataFromCloud();
- setUserData(userData);
-
- logger.debug('[CloudBackupProvider]: Fetching all backups');
- const backups = await fetchAllBackups();
-
- logger.debug(`[CloudBackupProvider]: Retrieved ${backups.files.length} backup files`);
- setBackups(backups);
- } catch (e) {
- logger.error(new RainbowError('[CloudBackupProvider]: Failed to fetch all backups'), {
- error: e,
- });
- }
- setIsFetching(false);
- };
-
- useEffect(() => {
- fetchBackups();
- }, []);
-
- const value = {
- isFetching,
- backups,
- fetchBackups,
- userData,
- };
-
- return {children};
-}
-
-export function useCloudBackups() {
- const context = useContext(CloudBackupContext);
- if (context === null) {
- throw new Error('useCloudBackups must be used within a CloudBackupProvider');
- }
- return context;
-}
diff --git a/src/components/backup/RestoreCloudStep.tsx b/src/components/backup/RestoreCloudStep.tsx
index e8bd83aa7a3..186f318c18b 100644
--- a/src/components/backup/RestoreCloudStep.tsx
+++ b/src/components/backup/RestoreCloudStep.tsx
@@ -6,8 +6,7 @@ import WalletAndBackup from '@/assets/WalletsAndBackup.png';
import { KeyboardArea } from 'react-native-keyboard-area';
import {
- Backup,
- fetchBackupPassword,
+ BackupFile,
getLocalBackupPassword,
restoreCloudBackup,
RestoreCloudBackupResultStates,
@@ -17,11 +16,17 @@ import { cloudPlatform } from '@/utils/platform';
import { PasswordField } from '../fields';
import { Text } from '../text';
import { WrappedAlert as Alert } from '@/helpers/alert';
-import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
+import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
import walletBackupTypes from '@/helpers/walletBackupTypes';
-import { useDimensions, useInitializeWallet } from '@/hooks';
-import { useNavigation } from '@/navigation';
-import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets';
+import { useDimensions, useInitializeWallet, useWallets } from '@/hooks';
+import { Navigation, useNavigation } from '@/navigation';
+import {
+ addressSetSelected,
+ setAllWalletsWithIdsAsBackedUp,
+ setIsWalletLoading,
+ walletsLoadState,
+ walletsSetSelected,
+} from '@/redux/wallets';
import Routes from '@/navigation/routesNames';
import styled from '@/styled-thing';
import { padding } from '@/styles';
@@ -36,7 +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 useCloudBackups from '@/hooks/useCloudBackups';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { isEmpty } from 'lodash';
+import { backupsStore } from '@/state/backups/backups';
const Title = styled(Text).attrs({
size: 'big',
@@ -76,33 +83,39 @@ const KeyboardSizeView = styled(KeyboardArea)({
type RestoreCloudStepParams = {
RestoreSheet: {
- selectedBackup: Backup;
+ selectedBackup: BackupFile;
};
};
export default function RestoreCloudStep() {
const { params } = useRoute>();
-
- const { userData } = useCloudBackups();
+ const { password } = backupsStore(state => ({
+ password: state.password,
+ }));
const { selectedBackup } = params;
const { isDarkMode } = useTheme();
- const [loading, setLoading] = useState(false);
+ const { canGoBack, goBack } = useNavigation();
+
+ const onRestoreSuccess = useCallback(() => {
+ while (canGoBack()) {
+ goBack();
+ }
+ }, [canGoBack, goBack]);
const dispatch = useDispatch();
const { width: deviceWidth, height: deviceHeight } = useDimensions();
- const { replace, navigate, getState: dangerouslyGetState, goBack } = useNavigation();
const [validPassword, setValidPassword] = useState(false);
const [incorrectPassword, setIncorrectPassword] = useState(false);
- const [password, setPassword] = useState('');
const passwordRef = useRef(null);
const initializeWallet = useInitializeWallet();
+ const { isWalletLoading } = useWallets();
useEffect(() => {
const fetchPasswordIfPossible = async () => {
- const pwd = await fetchBackupPassword();
+ const pwd = await getLocalBackupPassword();
if (pwd) {
- setPassword(pwd);
+ backupsStore.getState().setPassword(pwd);
}
};
fetchPasswordIfPossible();
@@ -118,35 +131,36 @@ export default function RestoreCloudStep() {
}, [incorrectPassword, password]);
const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => {
- setPassword(inputText);
+ backupsStore.getState().setPassword(inputText);
setIncorrectPassword(false);
}, []);
const onSubmit = useCallback(async () => {
- setLoading(true);
+ // NOTE: Localizing password to prevent an empty string from being saved if we re-render
+ const pwd = password.trim();
+ let filename = selectedBackup.name;
+
+ const prevWalletsState = await dispatch(walletsLoadState());
+
try {
if (!selectedBackup.name) {
throw new Error('No backup file selected');
}
- const prevWalletsState = await dispatch(walletsLoadState());
-
+ dispatch(setIsWalletLoading(WalletLoadingStates.RESTORING_WALLET));
const status = await restoreCloudBackup({
- password,
- userData,
- nameOfSelectedBackupFile: selectedBackup.name,
+ password: pwd,
+ backupFilename: filename,
});
-
if (status === RestoreCloudBackupResultStates.success) {
// Store it in the keychain in case it was missing
const hasSavedPassword = await getLocalBackupPassword();
if (!hasSavedPassword) {
- await saveLocalBackupPassword(password);
+ await saveLocalBackupPassword(pwd);
}
InteractionManager.runAfterInteractions(async () => {
const newWalletsState = await dispatch(walletsLoadState());
- let filename = selectedBackup.name;
if (IS_ANDROID && filename) {
filename = normalizeAndroidBackupFilename(filename);
}
@@ -188,14 +202,21 @@ export default function RestoreCloudStep() {
const p2 = dispatch(addressSetSelected(firstAddress));
await Promise.all([p1, p2]);
await initializeWallet(null, null, null, false, false, null, true, null);
-
- const operation = dangerouslyGetState()?.index === 1 ? navigate : replace;
- operation(Routes.SWIPE_LAYOUT, {
- screen: Routes.WALLET_SCREEN,
- });
-
- setLoading(false);
});
+
+ onRestoreSuccess();
+ backupsStore.getState().setPassword('');
+ if (isEmpty(prevWalletsState)) {
+ Navigation.handleAction(
+ Routes.SWIPE_LAYOUT,
+ {
+ screen: Routes.WALLET_SCREEN,
+ },
+ false
+ );
+ } else {
+ Navigation.handleAction(Routes.WALLET_SCREEN, {});
+ }
} else {
switch (status) {
case RestoreCloudBackupResultStates.incorrectPassword:
@@ -211,18 +232,15 @@ export default function RestoreCloudStep() {
}
} catch (e) {
Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring'));
+ } finally {
+ dispatch(setIsWalletLoading(null));
}
-
- setLoading(false);
- }, [selectedBackup.name, password, userData, dispatch, initializeWallet, dangerouslyGetState, navigate, replace]);
+ }, [password, dispatch, selectedBackup.name, initializeWallet, onRestoreSuccess]);
const onPasswordSubmit = useCallback(() => {
validPassword && onSubmit();
}, [onSubmit, validPassword]);
- const isPasswordValid =
- (password !== '' && password.length < cloudBackupPasswordMinLength && !passwordRef?.current?.isFocused()) || incorrectPassword;
-
return (
@@ -248,8 +266,8 @@ export default function RestoreCloudStep() {
;
};
};
-export type useCreateBackupStateType = 'none' | 'loading' | 'success' | 'error';
+type ConfirmBackupProps = {
+ password: string;
+} & UseCreateBackupProps;
-export enum BackupTypes {
- Single = 'single',
- All = 'all',
-}
-
-export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupProps) => {
+export const useCreateBackup = () => {
const dispatch = useDispatch();
const { navigate } = useNavigation();
- const { fetchBackups } = useCloudBackups();
const walletCloudBackup = useWalletCloudBackup();
const { wallets } = useWallets();
- const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]);
- const [loading, setLoading] = useState('none');
-
- const [password, setPassword] = useState('');
const setLoadingStateWithTimeout = useCallback(
- (state: useCreateBackupStateType, resetInMS = 2500) => {
- setLoading(state);
+ ({ state, outOfSync = false, failInMs = 10_000 }: { state: CloudBackupState; outOfSync?: boolean; failInMs?: number }) => {
+ backupsStore.getState().setStatus(state);
+ if (outOfSync) {
+ setTimeout(() => {
+ backupsStore.getState().setStatus(CloudBackupState.Syncing);
+ }, 1_000);
+ }
setTimeout(() => {
- setLoading('none');
- }, resetInMS);
+ const currentState = backupsStore.getState().status;
+ if (currentState === state) {
+ backupsStore.getState().setStatus(CloudBackupState.Ready);
+ }
+ }, failInMs);
},
- [setLoading]
+ []
+ );
+
+ const onSuccess = useCallback(
+ async (password: string) => {
+ const hasSavedPassword = await getLocalBackupPassword();
+ if (!hasSavedPassword && password.trim()) {
+ await saveLocalBackupPassword(password);
+ }
+ analytics.track('Backup Complete', {
+ category: 'backup',
+ label: cloudPlatform,
+ });
+ setLoadingStateWithTimeout({
+ state: CloudBackupState.Success,
+ outOfSync: true,
+ });
+ backupsStore.getState().syncAndFetchBackups();
+ },
+ [setLoadingStateWithTimeout]
);
- const onSuccess = useCallback(async () => {
- const hasSavedPassword = await getLocalBackupPassword();
- if (!hasSavedPassword) {
- await saveLocalBackupPassword(password);
- }
- analytics.track('Backup Complete', {
- category: 'backup',
- label: cloudPlatform,
- });
- setLoadingStateWithTimeout('success');
- fetchBackups();
- }, [setLoadingStateWithTimeout, fetchBackups, password]);
const onError = useCallback(
(msg: string) => {
InteractionManager.runAfterInteractions(async () => {
DelayedAlert({ title: msg }, 500);
- setLoadingStateWithTimeout('error', 5000);
+ setLoadingStateWithTimeout({ state: CloudBackupState.Error });
});
},
[setLoadingStateWithTimeout]
);
const onConfirmBackup = useCallback(
- async ({ password, type }: { password: string; type: BackupTypes }) => {
+ async ({ password, walletId, navigateToRoute }: ConfirmBackupProps) => {
analytics.track('Tapped "Confirm Backup"');
- setLoading('loading');
+ backupsStore.getState().setStatus(CloudBackupState.InProgress);
- if (type === BackupTypes.All) {
+ if (typeof walletId === 'undefined') {
if (!wallets) {
onError('Error loading wallets. Please try again.');
- setLoading('error');
+ backupsStore.getState().setStatus(CloudBackupState.Error);
return;
}
backupAllWalletsToCloud({
- wallets: wallets as AllRainbowWallets,
+ wallets,
password,
- latestBackup,
onError,
onSuccess,
dispatch,
@@ -94,12 +99,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
return;
}
- if (!walletId) {
- onError('Wallet not found. Please try again.');
- setLoading('error');
- return;
- }
-
await walletCloudBackup({
onError,
onSuccess,
@@ -111,13 +110,12 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
navigate(navigateToRoute.route, navigateToRoute.params || {});
}
},
- [walletId, walletCloudBackup, onError, onSuccess, navigateToRoute, wallets, latestBackup, dispatch, navigate]
+ [walletCloudBackup, onError, wallets, onSuccess, dispatch, navigate]
);
- const getPassword = useCallback(async (): Promise => {
+ const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => {
const password = await getLocalBackupPassword();
if (password) {
- setPassword(password);
return password;
}
@@ -126,32 +124,37 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
nativeScreen: true,
step: walletBackupStepTypes.backup_cloud,
onSuccess: async (password: string) => {
- setPassword(password);
- resolve(password);
+ return resolve(password);
},
onCancel: async () => {
- resolve(null);
+ return resolve(null);
},
- walletId,
+ ...props,
});
});
- }, [walletId]);
+ }, []);
- const onSubmit = useCallback(
- async ({ type = BackupTypes.Single }: { type?: BackupTypes }) => {
- const password = await getPassword();
+ const createBackup = useCallback(
+ async (props: UseCreateBackupProps) => {
+ if (backupsStore.getState().status !== CloudBackupState.Ready) {
+ return false;
+ }
+
+ const password = await getPassword(props);
if (password) {
onConfirmBackup({
password,
- type,
+ ...props,
});
return true;
}
- setLoadingStateWithTimeout('error');
+ setLoadingStateWithTimeout({
+ state: CloudBackupState.Ready,
+ });
return false;
},
[getPassword, onConfirmBackup, setLoadingStateWithTimeout]
);
- return { onSuccess, onError, onSubmit, loading };
+ return createBackup;
};
diff --git a/src/components/fields/PasswordField.tsx b/src/components/fields/PasswordField.tsx
index 0925b29862c..6d28e81e802 100644
--- a/src/components/fields/PasswordField.tsx
+++ b/src/components/fields/PasswordField.tsx
@@ -1,14 +1,37 @@
import React, { forwardRef, useCallback, Ref } from 'react';
-import { useTheme } from '../../theme/ThemeContext';
+import { ThemeContextProps, useTheme } from '../../theme/ThemeContext';
import { Input } from '../inputs';
import { cloudBackupPasswordMinLength } from '@/handlers/cloudBackup';
import { useDimensions } from '@/hooks';
import styled from '@/styled-thing';
-import { padding } from '@/styles';
+import { padding, position } from '@/styles';
import ShadowStack from '@/react-native-shadow-stack';
import { Box } from '@/design-system';
import { TextInput, TextInputProps, View } from 'react-native';
import { IS_IOS, IS_ANDROID } from '@/env';
+import { Icon } from '../icons';
+
+const FieldAccessoryBadgeSize = 22;
+const FieldAccessoryBadgeWrapper = styled(ShadowStack).attrs(
+ ({ theme: { colors, isDarkMode }, color }: { theme: ThemeContextProps; color: string }) => ({
+ ...position.sizeAsObject(FieldAccessoryBadgeSize),
+ borderRadius: FieldAccessoryBadgeSize,
+ shadows: [[0, 4, 12, isDarkMode ? colors.shadow : color, isDarkMode ? 0.1 : 0.4]],
+ })
+)({
+ marginBottom: 12,
+ position: 'absolute',
+ right: 12,
+ top: 12,
+});
+
+function FieldAccessoryBadge({ color, name }: { color: string; name: string }) {
+ return (
+
+
+
+ );
+}
const Container = styled(Box)({
width: '100%',
@@ -53,9 +76,9 @@ interface PasswordFieldProps extends TextInputProps {
}
const PasswordField = forwardRef(
- ({ password, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => {
+ ({ password, isInvalid, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => {
const { width: deviceWidth } = useDimensions();
- const { isDarkMode } = useTheme();
+ const { isDarkMode, colors } = useTheme();
const handleFocus = useCallback(() => {
if (ref && 'current' in ref && ref.current) {
@@ -67,6 +90,7 @@ const PasswordField = forwardRef(
+ {isInvalid && }
);
diff --git a/src/components/remote-promo-sheet/runChecks.ts b/src/components/remote-promo-sheet/runChecks.ts
index f83170eecce..d25902adcfa 100644
--- a/src/components/remote-promo-sheet/runChecks.ts
+++ b/src/components/remote-promo-sheet/runChecks.ts
@@ -1,7 +1,6 @@
import { IS_TEST } from '@/env';
-import { runFeatureUnlockChecks } from '@/handlers/walletReadyEvents';
+import { runFeatureAndLocalCampaignChecks } from '@/handlers/walletReadyEvents';
import { logger } from '@/logger';
-import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks';
import { checkForRemotePromoSheet } from '@/components/remote-promo-sheet/checkForRemotePromoSheet';
import { useCallback, useEffect } from 'react';
import { InteractionManager } from 'react-native';
@@ -19,11 +18,7 @@ export const useRunChecks = ({ runChecksOnMount = true, walletReady }: { runChec
return;
}
- const showedFeatureUnlock = await runFeatureUnlockChecks();
- if (showedFeatureUnlock) return;
-
- const showedLocalPromo = await runLocalCampaignChecks();
- if (showedLocalPromo) return;
+ if (await runFeatureAndLocalCampaignChecks()) return;
if (!remotePromoSheets) {
logger.debug('[useRunChecks]: remote promo sheets is disabled');
diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx
index 0ef93ba05e6..3cd37f05611 100644
--- a/src/components/secret-display/SecretDisplaySection.tsx
+++ b/src/components/secret-display/SecretDisplaySection.tsx
@@ -1,5 +1,4 @@
import { RouteProp, useRoute } from '@react-navigation/native';
-import { captureException } from '@sentry/react-native';
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { createdWithBiometricError, identifyWalletType, loadPrivateKey, loadSeedPhraseAndMigrateIfNeeded } from '@/model/wallet';
import ActivityIndicator from '../ActivityIndicator';
@@ -25,6 +24,7 @@ import { useNavigation } from '@/navigation';
import { ImgixImage } from '../images';
import RoutesWithPlatformDifferences from '@/navigation/routesNames';
import { Source } from 'react-native-fast-image';
+import { backupsStore } from '@/state/backups/backups';
const MIN_HEIGHT = 740;
@@ -63,6 +63,9 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }:
const { colors } = useTheme();
const { params } = useRoute>();
const { selectedWallet, wallets } = useWallets();
+ const { backupProvider } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ }));
const { onManuallyBackupWalletId } = useWalletManualBackup();
const { navigate } = useNavigation();
@@ -124,9 +127,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }:
const handleConfirmSaved = useCallback(() => {
if (backupType === WalletBackupTypes.manual) {
onManuallyBackupWalletId(walletId);
+ if (!backupProvider) {
+ backupsStore.getState().setBackupProvider(WalletBackupTypes.manual);
+ }
navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP);
}
- }, [backupType, walletId, onManuallyBackupWalletId, navigate]);
+ }, [backupType, onManuallyBackupWalletId, walletId, backupProvider, navigate]);
const getIconForBackupType = useCallback(() => {
if (isBackingUp) {
diff --git a/src/handlers/cloudBackup.ts b/src/handlers/cloudBackup.ts
index 1eb3f5be795..14347c42a75 100644
--- a/src/handlers/cloudBackup.ts
+++ b/src/handlers/cloudBackup.ts
@@ -1,14 +1,15 @@
import { sortBy } from 'lodash';
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message
import RNCloudFs from 'react-native-cloud-fs';
-import { RAINBOW_MASTER_KEY } from 'react-native-dotenv';
import RNFS from 'react-native-fs';
import AesEncryptor from '../handlers/aesEncryption';
import { logger, RainbowError } from '@/logger';
import { IS_ANDROID, IS_IOS } from '@/env';
-import { CloudBackups } from '@/model/backup';
+import { BackupFile, CloudBackups } from '@/model/backup';
+
const REMOTE_BACKUP_WALLET_DIR = 'rainbow.me/wallet-backups';
-const USERDATA_FILE = 'UserData.json';
+export const USERDATA_FILE = 'UserData.json';
+
const encryptor = new AesEncryptor();
export const CLOUD_BACKUP_ERRORS = {
@@ -65,13 +66,18 @@ export async function fetchAllBackups(): Promise {
if (android) {
await RNCloudFs.loginIfNeeded();
}
- return RNCloudFs.listFiles({
+
+ const files = await RNCloudFs.listFiles({
scope: 'hidden',
targetPath: REMOTE_BACKUP_WALLET_DIR,
});
+
+ return {
+ files: files?.files?.filter((file: BackupFile) => normalizeAndroidBackupFilename(file.name) !== USERDATA_FILE) || [],
+ };
}
-export async function encryptAndSaveDataToCloud(data: any, password: any, filename: any) {
+export async function encryptAndSaveDataToCloud(data: Record, password: string, filename: string) {
// Encrypt the data
try {
const encryptedData = await encryptor.encrypt(password, JSON.stringify(data));
@@ -100,6 +106,7 @@ export async function encryptAndSaveDataToCloud(data: any, password: any, filena
scope,
sourcePath: sourceUri,
targetPath: destinationPath,
+ update: true,
});
// Now we need to verify the file has been stored in the cloud
const exists = await RNCloudFs.fileExists(
@@ -201,19 +208,6 @@ export async function getDataFromCloud(backupPassword: any, filename: string | n
throw error;
}
-export async function backupUserDataIntoCloud(data: any) {
- const filename = USERDATA_FILE;
- const password = RAINBOW_MASTER_KEY;
- return encryptAndSaveDataToCloud(data, password, filename);
-}
-
-export async function fetchUserDataFromCloud() {
- const filename = USERDATA_FILE;
- const password = RAINBOW_MASTER_KEY;
-
- return getDataFromCloud(password, filename);
-}
-
export const cloudBackupPasswordMinLength = 8;
export function isCloudBackupPasswordValid(password: any) {
diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts
index 1cfa62be144..38cef55fefe 100644
--- a/src/handlers/walletReadyEvents.ts
+++ b/src/handlers/walletReadyEvents.ts
@@ -1,4 +1,3 @@
-import { IS_TESTING } from 'react-native-dotenv';
import { triggerOnSwipeLayout } from '../navigation/onNavigationStateChange';
import { getKeychainIntegrityState } from './localstorage/globalSettings';
import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks';
@@ -6,18 +5,17 @@ import { EthereumAddress } from '@/entities';
import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes';
import WalletTypes from '@/helpers/walletTypes';
import { featureUnlockChecks } from '@/featuresToUnlock';
-import { AllRainbowWallets, RainbowAccount, RainbowWallet } from '@/model/wallet';
+import { AllRainbowWallets, RainbowAccount } from '@/model/wallet';
import { Navigation } from '@/navigation';
import store from '@/redux/store';
import { checkKeychainIntegrity } from '@/redux/wallets';
import Routes from '@/navigation/routesNames';
import { logger } from '@/logger';
-import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { InteractionManager } from 'react-native';
-
-const BACKUP_SHEET_DELAY_MS = 3000;
+import { backupsStore } from '@/state/backups/backups';
+import { IS_TEST } from '@/env';
export const runKeychainIntegrityChecks = async () => {
const keychainIntegrityState = await getKeychainIntegrityState();
@@ -27,59 +25,29 @@ 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 { backupProvider } = checkWalletsForBackupStatus(wallets);
-
- const rainbowWalletsNotBackedUp = Object.values(wallets).filter(wallet => {
- const hasVisibleAccount = wallet.addresses?.find((account: RainbowAccount) => account.visible);
- return (
- !wallet.imported &&
- !!hasVisibleAccount &&
- wallet.type !== WalletTypes.readOnly &&
- wallet.type !== WalletTypes.bluetooth &&
- !wallet.backedUp
- );
- });
-
- 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,
- });
-
- // 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 (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;
}
- 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 => {
@@ -107,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/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/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/useImportingWallet.ts b/src/hooks/useImportingWallet.ts
index e78fa43a2e8..5d4b134bc50 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 { backupsStore } from '@/state/backups/backups';
export default function useImportingWallet({ showImportModal = true } = {}) {
const { accountAddress } = useAccountSettings();
@@ -52,6 +52,10 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
const { updateWalletENSAvatars } = useWalletENSAvatar();
const profilesEnabled = useExperimentalFlag(PROFILES);
+ const { backupProvider } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ }));
+
const inputRef = useRef(null);
const { handleFocus } = useMagicAutofocus(inputRef);
@@ -291,7 +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;
@@ -346,8 +350,6 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
isValidBluetoothDeviceId(input)
)
) {
- const { backupProvider } = checkWalletsForBackupStatus(wallets);
-
let stepType: string = WalletBackupStepTypes.no_provider;
if (backupProvider === walletBackupTypes.cloud) {
stepType = WalletBackupStepTypes.backup_now_to_cloud;
@@ -414,6 +416,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
showImportModal,
profilesEnabled,
dangerouslyGetParent,
+ backupProvider,
]);
return {
diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts
index 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/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts
index 141f26b7f4e..d5e6bc73f58 100644
--- a/src/hooks/useManageCloudBackups.ts
+++ b/src/hooks/useManageCloudBackups.ts
@@ -3,12 +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 { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets';
+import {
+ GoogleDriveUserData,
+ getGoogleAccountUserData,
+ deleteAllBackups,
+ logoutFromGoogleDrive as logout,
+ login,
+} from '@/handlers/cloudBackup';
+import { clearAllWalletsBackupStatus } from '@/redux/wallets';
import { showActionSheetWithOptions } from '@/utils';
import { IS_ANDROID } from '@/env';
import { RainbowError, logger } from '@/logger';
import * as i18n from '@/languages';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
export default function useManageCloudBackups() {
const dispatch = useDispatch();
@@ -48,10 +55,21 @@ export default function useManageCloudBackups() {
await dispatch(clearAllWalletsBackupStatus());
};
+ const logoutFromGoogleDrive = async () => {
+ await logout();
+ backupsStore.setState({
+ backupProvider: undefined,
+ backups: { files: [] },
+ mostRecentBackup: undefined,
+ status: CloudBackupState.NotAvailable,
+ });
+ };
+
const loginToGoogleDrive = async () => {
- await dispatch(updateWalletBackupStatusesBasedOnCloudUserData());
try {
+ await login();
const accountDetails = await getGoogleAccountUserData();
+ backupsStore.getState().syncAndFetchBackups();
setAccountDetails(accountDetails ?? undefined);
} catch (error) {
logger.error(new RainbowError(`[useManageCloudBackups]: Logging into Google Drive failed.`), {
@@ -94,7 +112,7 @@ export default function useManageCloudBackups() {
if (_buttonIndex === 1 && IS_ANDROID) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets().then(() => loginToGoogleDrive());
+ loginToGoogleDrive();
}
}
);
diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts
index 57b9caac681..3ef3d819ac6 100644
--- a/src/hooks/useWalletCloudBackup.ts
+++ b/src/hooks/useWalletCloudBackup.ts
@@ -1,16 +1,15 @@
-import { captureException } from '@sentry/react-native';
import lang from 'i18n-js';
import { values } from 'lodash';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { Linking } from 'react-native';
import { useDispatch } from 'react-redux';
-import { addWalletToCloudBackup, backupWalletToCloud, findLatestBackUp } from '../model/backup';
+import { backupWalletToCloud } from '../model/backup';
import { setWalletBackedUp } from '../redux/wallets';
import { cloudPlatform } from '../utils/platform';
import useWallets from './useWallets';
import { WrappedAlert as Alert } from '@/helpers/alert';
import { analytics } from '@/analytics';
-import { CLOUD_BACKUP_ERRORS, isCloudBackupAvailable } from '@/handlers/cloudBackup';
+import { CLOUD_BACKUP_ERRORS, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
import WalletBackupTypes from '@/helpers/walletBackupTypes';
import { logger, RainbowError } from '@/logger';
import { getSupportedBiometryType } from '@/keychain';
@@ -41,7 +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 ({
@@ -53,36 +51,57 @@ export default function useWalletCloudBackup() {
handleNoLatestBackup?: () => void;
handlePasswordNotFound?: () => void;
onError?: (error: string) => void;
- onSuccess?: () => void;
+ onSuccess?: (password: string) => void;
password: string;
walletId: string;
}): Promise => {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- analytics.track('iCloud not enabled', {
- category: 'backup',
- });
- Alert.alert(lang.t('modal.back_up.alerts.cloud_not_enabled.label'), lang.t('modal.back_up.alerts.cloud_not_enabled.description'), [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- analytics.track('View how to Enable iCloud', {
- category: 'backup',
- });
- },
- text: lang.t('modal.back_up.alerts.cloud_not_enabled.show_me'),
- },
- {
- onPress: () => {
- analytics.track('Ignore how to enable iCloud', {
- category: 'backup',
- });
- },
- style: 'cancel',
- text: lang.t('modal.back_up.alerts.cloud_not_enabled.no_thanks'),
- },
- ]);
- 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
@@ -101,23 +120,14 @@ export default function useWalletCloudBackup() {
logger.debug('[useWalletCloudBackup]: password fetched correctly');
let updatedBackupFile = null;
+
try {
- if (!latestBackup) {
- logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${wallets![walletId]}`);
- updatedBackupFile = await backupWalletToCloud({
- password,
- wallet: wallets![walletId],
- userPIN,
- });
- } else {
- logger.debug(`[useWalletCloudBackup]: adding wallet to ${cloudPlatform} backup: ${wallets![walletId]}`);
- updatedBackupFile = await addWalletToCloudBackup({
- password,
- wallet: wallets![walletId],
- filename: latestBackup,
- userPIN,
- });
- }
+ logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${(wallets || {})[walletId]}`);
+ updatedBackupFile = await backupWalletToCloud({
+ password,
+ wallet: (wallets || {})[walletId],
+ userPIN,
+ });
} catch (e: any) {
const userError = getUserError(e);
!!onError && onError(userError);
@@ -134,7 +144,7 @@ export default function useWalletCloudBackup() {
logger.debug('[useWalletCloudBackup]: backup completed!');
await dispatch(setWalletBackedUp(walletId, WalletBackupTypes.cloud, updatedBackupFile));
logger.debug('[useWalletCloudBackup]: backup saved everywhere!');
- !!onSuccess && onSuccess();
+ !!onSuccess && onSuccess(password);
return true;
} catch (e) {
logger.error(new RainbowError(`[useWalletCloudBackup]: error while trying to save wallet backup state: ${e}`));
@@ -148,7 +158,7 @@ export default function useWalletCloudBackup() {
return false;
},
- [dispatch, latestBackup, wallets]
+ [dispatch, wallets]
);
return walletCloudBackup;
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/languages/en_US.json b/src/languages/en_US.json
index e49299f792b..902997e943f 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 to %{cloudPlatformName}",
"fetching_backups": "Retrieving backups from %{cloudPlatformName}",
"back_up_to_platform": "Back up to %{cloudPlatformName}",
"restore_from_platform": "Restore from %{cloudPlatformName}",
@@ -137,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 2eb50a7c297..8bef43d6347 100644
--- a/src/model/backup.ts
+++ b/src/model/backup.ts
@@ -1,15 +1,23 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
-import { NativeModules } from 'react-native';
+import { NativeModules, Linking } from 'react-native';
import { captureException } from '@sentry/react-native';
import { endsWith } from 'lodash';
-import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup';
+import {
+ CLOUD_BACKUP_ERRORS,
+ encryptAndSaveDataToCloud,
+ getDataFromCloud,
+ isCloudBackupAvailable,
+ getGoogleAccountUserData,
+ login,
+ logoutFromGoogleDrive,
+ normalizeAndroidBackupFilename,
+} from '@/handlers/cloudBackup';
+import { Alert as NativeAlert } from '@/components/alerts';
import WalletBackupTypes from '../helpers/walletBackupTypes';
-import WalletTypes from '../helpers/walletTypes';
-import { Alert } from '@/components/alerts';
import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants';
import * as keychain from '@/model/keychain';
import * as kc from '@/keychain';
-import { AllRainbowWallets, allWalletsVersion, createWallet, RainbowWallet } from './wallet';
+import { AllRainbowWallets, createWallet, RainbowWallet } from './wallet';
import { analytics } from '@/analytics';
import { logger, RainbowError } from '@/logger';
import { IS_ANDROID, IS_DEV } from '@/env';
@@ -24,16 +32,18 @@ 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();
const PIN_REGEX = /^\d{4}$/;
export interface CloudBackups {
- files: Backup[];
+ files: BackupFile[];
}
-export interface Backup {
+export interface BackupFile {
isDirectory: boolean;
isFile: boolean;
lastModified: string;
@@ -44,8 +54,9 @@ export interface Backup {
}
export const parseTimestampFromFilename = (filename: string) => {
+ const name = normalizeAndroidBackupFilename(filename);
return Number(
- filename
+ name
.replace('.backup_', '')
.replace('backup_', '')
.replace('.json', '')
@@ -54,6 +65,27 @@ export const parseTimestampFromFilename = (filename: string) => {
);
};
+/**
+ * Parse the timestamp from a backup file name
+ * @param filename - The name of the backup file backup_${now}.json
+ * @returns The timestamp as a number
+ */
+export const parseTimestampFromBackupFile = (filename: string | null): number | undefined => {
+ if (!filename) {
+ return;
+ }
+ const match = filename.match(/backup_(\d+)\.json/);
+ if (!match) {
+ return;
+ }
+
+ if (Number.isNaN(Number(match[1]))) {
+ return;
+ }
+
+ return Number(match[1]);
+};
+
type BackupPassword = string;
interface BackedUpData {
@@ -63,6 +95,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();
@@ -100,17 +180,15 @@ async function extractSecretsForWallet(wallet: RainbowWallet) {
export async function backupAllWalletsToCloud({
wallets,
password,
- latestBackup,
onError,
onSuccess,
dispatch,
}: {
wallets: AllRainbowWallets;
password: BackupPassword;
- latestBackup: string | null;
onError?: (message: string) => void;
- onSuccess?: () => void;
- dispatch: any;
+ onSuccess?: (password: BackupPassword) => void;
+ dispatch: AppDispatch;
}) {
let userPIN: string | undefined;
const hasBiometricsEnabled = await kc.getSupportedBiometryType();
@@ -157,49 +235,23 @@ export async function backupAllWalletsToCloud({
label: cloudPlatform,
});
- let updatedBackupFile: any = null;
- if (!latestBackup) {
- const data = {
- createdAt: now,
- secrets: {},
- };
- const promises = Object.entries(allSecrets).map(async ([username, password]) => {
- const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN);
-
- data.secrets = {
- ...data.secrets,
- ...processedNewSecrets,
- };
- });
+ let updatedBackupFile: string | null = null;
- 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));
@@ -209,16 +261,18 @@ export async function backupAllWalletsToCloud({
label: cloudPlatform,
});
- onSuccess?.();
- } catch (error: any) {
- const userError = getUserError(error);
- onError?.(userError);
- captureException(error);
- analytics.track(`Error backing up all wallets to ${cloudPlatform}`, {
- category: 'backup',
- error: userError,
- label: cloudPlatform,
- });
+ onSuccess?.(password);
+ } catch (error) {
+ if (error instanceof Error) {
+ const userError = getUserError(error);
+ onError?.(userError);
+ captureException(error);
+ analytics.track(`Error backing up all wallets to ${cloudPlatform}`, {
+ category: 'backup',
+ error: userError,
+ label: cloudPlatform,
+ });
+ }
}
}
@@ -251,9 +305,15 @@ export async function addWalletToCloudBackup({
wallet: RainbowWallet;
filename: string;
userPIN?: string;
-}): Promise {
- // @ts-ignore
+}): Promise {
const backup = await getDataFromCloud(password, filename);
+ if (!backup) {
+ logger.error(new RainbowError('[backup]: Unable to get backup data for filename'), {
+ filename,
+ });
+ return null;
+ }
+
const now = Date.now();
const newSecretsToBeAddedToBackup = await extractSecretsForWallet(wallet);
const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded(newSecretsToBeAddedToBackup, userPIN);
@@ -321,25 +381,6 @@ export async function decryptAllPinEncryptedSecretsIfNeeded(secrets: Record {
- // Check if there's a wallet backed up
- if (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) {
- // If there is one, let's grab the latest backup
- if (!latestBackup || Number(wallet.backupDate) > latestBackup) {
- filename = wallet.backupFile;
- latestBackup = Number(wallet.backupDate);
- }
- }
- });
- }
- return filename;
-}
-
export const RestoreCloudBackupResultStates = {
success: 'success',
failedWhenRestoring: 'failedWhenRestoring',
@@ -368,16 +409,14 @@ const sanitizeFilename = (filename: string) => {
*/
export async function restoreCloudBackup({
password,
- userData,
- nameOfSelectedBackupFile,
+ backupFilename,
}: {
password: BackupPassword;
- userData: BackupUserData | undefined;
- nameOfSelectedBackupFile: string;
+ backupFilename: string;
}): Promise {
try {
// 1 - sanitize filename to remove extra things we don't care about
- const filename = sanitizeFilename(nameOfSelectedBackupFile);
+ const filename = sanitizeFilename(backupFilename);
if (!filename) {
return RestoreCloudBackupResultStates.failedWhenRestoring;
}
@@ -402,26 +441,6 @@ export async function restoreCloudBackup({
}
}
- if (userData) {
- // Restore only wallets that were backed up in cloud
- // or wallets that are read-only
- const walletsToRestore: AllRainbowWallets = {};
- Object.values(userData?.wallets ?? {}).forEach(wallet => {
- if (
- (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) ||
- wallet.type === WalletTypes.readOnly
- ) {
- walletsToRestore[wallet.id] = wallet;
- }
- });
-
- // All wallets
- dataToRestore[allWalletsKey] = {
- version: allWalletsVersion,
- wallets: walletsToRestore,
- };
- }
-
const restoredSuccessfully = await restoreSpecificBackupIntoKeychain(dataToRestore, userPIN);
return restoredSuccessfully ? RestoreCloudBackupResultStates.success : RestoreCloudBackupResultStates.failedWhenRestoring;
} catch (error) {
@@ -525,74 +544,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;
@@ -695,7 +646,7 @@ export async function getDeviceUUID(): Promise {
}
const FailureAlert = () =>
- Alert({
+ NativeAlert({
buttons: [
{
style: 'cancel',
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..4fadb8753ef
--- /dev/null
+++ b/src/react-native-cool-modals/Portal.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { IS_IOS } from '@/env';
+import { portalStore } from '@/state/portal/portal';
+import { 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: {
+ ...StyleSheet.absoluteFillObject,
+ },
+});
diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts
index deb49a5ea9b..f55da2f26c1 100644
--- a/src/redux/wallets.ts
+++ b/src/redux/wallets.ts
@@ -3,10 +3,8 @@ import { toChecksumAddress } from 'ethereumjs-util';
import { isEmpty, keys } from 'lodash';
import { Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
-import { backupUserDataIntoCloud, fetchUserDataFromCloud } from '../handlers/cloudBackup';
import { saveKeychainIntegrityState } from '../handlers/localstorage/globalSettings';
import { getWalletNames, saveWalletNames } from '../handlers/localstorage/walletNames';
-import WalletBackupTypes from '../helpers/walletBackupTypes';
import WalletTypes from '../helpers/walletTypes';
import { fetchENSAvatar } from '../hooks/useENSAvatar';
import { hasKey } from '../model/keychain';
@@ -30,6 +28,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 ---------------------------------------- //
@@ -40,7 +40,7 @@ interface WalletsState {
/**
* The current loading state of the wallet.
*/
- isWalletLoading: any;
+ isWalletLoading: WalletLoadingState | null;
/**
* The currently selected wallet.
@@ -130,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.
@@ -241,6 +239,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
@@ -252,21 +262,21 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di
* @param updateUserMetadata Whether to update user metadata.
*/
export const setAllWalletsWithIdsAsBackedUp =
- (
- walletIds: RainbowWallet['id'][],
- method: RainbowWallet['backupType'],
- backupFile: RainbowWallet['backupFile'] = null,
- updateUserMetadata = true
- ) =>
+ (walletIds: RainbowWallet['id'][], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) =>
async (dispatch: ThunkDispatch, getState: AppGetState) => {
const { wallets, selected } = getState().wallets;
const newWallets = { ...wallets };
+ let backupDate = Date.now();
+ if (backupFile) {
+ backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now();
+ }
+
walletIds.forEach(walletId => {
newWallets[walletId] = {
...newWallets[walletId],
backedUp: true,
- backupDate: Date.now(),
+ backupDate,
backupFile,
backupType: method,
};
@@ -276,17 +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;
- }
- }
};
/**
@@ -296,122 +295,28 @@ export const setAllWalletsWithIdsAsBackedUp =
* @param walletId The ID of the wallet to modify.
* @param method The backup type used.
* @param backupFile The backup file, if present.
- * @param updateUserMetadata Whether to update user metadata.
*/
export const setWalletBackedUp =
- (
- walletId: RainbowWallet['id'],
- method: RainbowWallet['backupType'],
- backupFile: RainbowWallet['backupFile'] = null,
- updateUserMetadata = true
- ) =>
+ (walletId: RainbowWallet['id'], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) =>
async (dispatch: ThunkDispatch, getState: AppGetState) => {
const { wallets, selected } = getState().wallets;
const newWallets = { ...wallets };
+ let backupDate = Date.now();
+ if (backupFile) {
+ backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now();
+ }
newWallets[walletId] = {
...newWallets[walletId],
backedUp: true,
- backupDate: Date.now(),
+ backupDate,
backupFile,
backupType: method,
};
await dispatch(walletsUpdate(newWallets));
- if (selected!.id === walletId) {
+ if (selected?.id === walletId) {
await dispatch(walletsSetSelected(newWallets[walletId]));
}
-
- if (method === WalletBackupTypes.cloud && updateUserMetadata) {
- try {
- await backupUserDataIntoCloud({ wallets: newWallets });
- } catch (e) {
- logger.error(new RainbowError('[redux/wallets]: Saving wallet UserData to cloud failed.'), {
- message: (e as Error)?.message,
- });
- throw e;
- }
- }
- };
-
-/**
- * Grabs user data stored in the cloud and based on this data marks wallets
- * as backed up or not
- */
-export const updateWalletBackupStatusesBasedOnCloudUserData =
- () => async (dispatch: ThunkDispatch, getState: AppGetState) => {
- const { wallets, selected } = getState().wallets;
- const newWallets = { ...wallets };
-
- let currentUserData: { wallets: { [p: string]: RainbowWallet } } | undefined;
- try {
- currentUserData = await fetchUserDataFromCloud();
- } catch (error) {
- logger.error(new RainbowError('[redux/wallets]: There was an error when trying to update wallet backup statuses'), {
- error: (error as Error).message,
- });
- return;
- }
- if (currentUserData === undefined) {
- return;
- }
-
- // build hashmap of address to wallet based on backup metadata
- const addressToWalletLookup = new Map();
- Object.values(currentUserData.wallets).forEach(wallet => {
- wallet.addresses?.forEach(account => {
- addressToWalletLookup.set(account.address, wallet);
- });
- });
-
- /*
- marking wallet as already backed up if all addresses are backed up properly
- and linked to the same wallet
-
- we assume it's not backed up if:
- * we don't have an address in the backup metadata
- * we have an address in the backup metadata, but it's linked to multiple
- wallet ids (should never happen, but that's a sanity check)
- */
- Object.values(newWallets).forEach(wallet => {
- const localWalletId = wallet.id;
-
- let relatedCloudWalletId: string | null = null;
- for (const account of wallet.addresses || []) {
- const walletDataForCurrentAddress = addressToWalletLookup.get(account.address);
- if (!walletDataForCurrentAddress) {
- return;
- }
- if (relatedCloudWalletId === null) {
- relatedCloudWalletId = walletDataForCurrentAddress.id;
- } else if (relatedCloudWalletId !== walletDataForCurrentAddress.id) {
- logger.warn(
- '[redux/wallets]: Wallet address is linked to multiple or different accounts in the cloud backup metadata. It could mean that there is an issue with the cloud backup metadata.'
- );
- return;
- }
- }
-
- if (relatedCloudWalletId === null) {
- return;
- }
-
- // update only if we checked the wallet is actually backed up
- const cloudBackupData = currentUserData?.wallets[relatedCloudWalletId];
- if (cloudBackupData) {
- newWallets[localWalletId] = {
- ...newWallets[localWalletId],
- backedUp: cloudBackupData.backedUp,
- backupDate: cloudBackupData.backupDate,
- backupFile: cloudBackupData.backupFile,
- backupType: cloudBackupData.backupType,
- };
- }
- });
-
- await dispatch(walletsUpdate(newWallets));
- if (selected?.id) {
- await dispatch(walletsSetSelected(newWallets[selected.id]));
- }
};
/**
diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx
index 6021f7ad295..69350fac4e0 100644
--- a/src/screens/AddWalletSheet.tsx
+++ b/src/screens/AddWalletSheet.tsx
@@ -5,11 +5,10 @@ import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import React, { useRef } from 'react';
import * as i18n from '@/languages';
-import { HARDWARE_WALLETS, PROFILES, useExperimentalFlag } from '@/config';
+import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config';
import { analytics, analyticsV2 } from '@/analytics';
-import { InteractionManager, Linking } from 'react-native';
-import { createAccountForWallet, walletsLoadState } from '@/redux/wallets';
-import WalletBackupTypes from '@/helpers/walletBackupTypes';
+import { InteractionManager } from 'react-native';
+import { createAccountForWallet, setIsWalletLoading, walletsLoadState } from '@/redux/wallets';
import { createWallet } from '@/model/wallet';
import WalletTypes from '@/helpers/walletTypes';
import { logger, RainbowError } from '@/logger';
@@ -18,22 +17,13 @@ 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,
- 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;
@@ -53,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();
@@ -84,6 +73,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 +105,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
+ await dispatch(walletsLoadState());
+ // @ts-expect-error - needs refactor to object params
await initializeWallet();
}
} catch (e) {
@@ -150,6 +129,8 @@ export const AddWalletSheet = () => {
showWalletErrorAlert();
}, 1000);
}
+ } finally {
+ dispatch(setIsWalletLoading(null));
}
}
creatingWallet.current = false;
@@ -198,47 +179,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/RestoreSheet.tsx b/src/screens/RestoreSheet.tsx
index 4a3e324bb65..f8186c86341 100644
--- a/src/screens/RestoreSheet.tsx
+++ b/src/screens/RestoreSheet.tsx
@@ -1,5 +1,5 @@
import { RouteProp, useRoute } from '@react-navigation/native';
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import RestoreCloudStep from '../components/backup/RestoreCloudStep';
import ChooseBackupStep from '@/components/backup/ChooseBackupStep';
import Routes from '@/navigation/routesNames';
diff --git a/src/screens/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..fa38313f32f 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,34 +5,37 @@ import MenuItem from '../MenuItem';
import Spinner from '@/components/Spinner';
import { FloatingEmojis } from '@/components/floating-emojis';
import { useDimensions } from '@/hooks';
+import { CloudBackupState } from '@/state/backups/backups';
export const BackUpMenuItem = ({
icon = '',
- loading,
+ backupState,
onPress,
title,
+ disabled,
}: {
icon?: string;
- loading: useCreateBackupStateType;
+ backupState: CloudBackupState;
title: string;
onPress: () => void;
+ disabled?: boolean;
}) => {
const { colors } = useTheme();
const { width: deviceWidth } = useDimensions();
const [emojiTrigger, setEmojiTrigger] = useState void)>(null);
useEffect(() => {
- if (loading === 'success') {
+ if (backupState === '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 +43,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 (
<>
@@ -86,8 +88,9 @@ export const BackUpMenuItem = ({
) : (
diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
index 1842d3fae2a..90cbdddeff3 100644
--- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
+++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
@@ -5,19 +5,18 @@ import { Text as RNText } from '@/components/text';
import Menu from '../Menu';
import MenuContainer from '../MenuContainer';
import MenuItem from '../MenuItem';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
+import { BackupFile, parseTimestampFromFilename } from '@/model/backup';
import { format } from 'date-fns';
-import { Stack } from '@/design-system';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
-import { IS_ANDROID } from '@/env';
import walletBackupStepTypes from '@/helpers/walletBackupStepTypes';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { Centered } from '@/components/layout';
+import { Page } from '@/components/layout';
import Spinner from '@/components/Spinner';
import ActivityIndicator from '@/components/ActivityIndicator';
-import { cloudPlatform } from '@/utils/platform';
import { useTheme } from '@/theme';
+import { CloudBackupState, LoadingStates, backupsStore } from '@/state/backups/backups';
+import { titleForBackupState } from '../../utils';
+import { Box } from '@/design-system';
const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({
color: colors.blueGreyDark,
@@ -32,43 +31,14 @@ const ViewCloudBackups = () => {
const { navigate } = useNavigation();
const { colors } = useTheme();
- const { isFetching, backups } = useCloudBackups();
-
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
+ const { status, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
const onSelectCloudBackup = useCallback(
- async (selectedBackup: Backup) => {
+ async (selectedBackup: BackupFile) => {
navigate(Routes.BACKUP_SHEET, {
step: walletBackupStepTypes.restore_from_backup,
selectedBackup,
@@ -77,80 +47,110 @@ const ViewCloudBackups = () => {
[navigate]
);
- return (
-
-
- {!isFetching && !cloudBackups.length && (
-
- } />
-
- )}
+ const renderNoBackupsState = () => (
+ <>
+
+ } />
+
+ >
+ );
+
+ const renderMostRecentBackup = () => {
+ if (!mostRecentBackup) {
+ return null;
+ }
+
+ return (
+
+
+ }
+ onPress={() => onSelectCloudBackup(mostRecentBackup)}
+ size={52}
+ width="full"
+ titleComponent={}
+ />
+
+
+ );
+ };
+
+ const renderOlderBackups = () => (
+ <>
+
+
+ {backups.files
+ .filter(backup => backup.name !== mostRecentBackup?.name)
+ .sort((a, b) => {
+ const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime();
+ const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime();
+ return timestampB - timestampA;
+ })
+ .map(backup => (
+
+
+
+
+ backupsStore.getState().syncAndFetchBackups()}
+ titleComponent={}
+ />
+
+ >
+ );
- {!isFetching && cloudBackups.length && (
- <>
- {mostRecentBackup && (
-
- }
- onPress={() => onSelectCloudBackup(mostRecentBackup)}
- size={52}
- width="full"
- titleComponent={}
- />
-
- )}
+ const renderBackupsList = () => (
+ <>
+ {renderMostRecentBackup()}
+ {renderOlderBackups()}
+ >
+ );
-
- {cloudBackups.map(
- backup =>
- backup.name !== mostRecentBackup?.name && (
- onSelectCloudBackup(backup)}
- size={52}
- width="full"
- titleComponent={
-
- }
- />
- )
- )}
+ const isLoading = LoadingStates.includes(status);
- {cloudBackups.length === 1 && (
- }
- />
- )}
-
- >
- )}
+ if (isLoading) {
+ return (
+
+ {android ? : }
+ {titleForBackupState[status]}
+
+ );
+ }
- {isFetching && (
-
- {android ? : }
- {
-
- {i18n.t(i18n.l.back_up.cloud.fetching_backups, {
- cloudPlatformName: cloudPlatform,
- })}
-
- }
-
- )}
-
+ return (
+
+ {status === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()}
+ {status === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()}
);
};
diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
index d085c3f62fd..f98e6b73297 100644
--- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
+++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
@@ -29,31 +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, walletsLoadState } from '@/redux/wallets';
-import {
- GoogleDriveUserData,
- backupUserDataIntoCloud,
- getGoogleAccountUserData,
- isCloudBackupAvailable,
- login,
-} from '@/handlers/cloudBackup';
+import { createAccountForWallet, setIsWalletLoading } from '@/redux/wallets';
import { logger, RainbowError } from '@/logger';
-import { RainbowAccount, createWallet } from '@/model/wallet';
-import { PROFILES, useExperimentalFlag } from '@/config';
+import { RainbowAccount } from '@/model/wallet';
import showWalletErrorAlert from '@/helpers/support';
-import { IS_ANDROID, IS_IOS } from '@/env';
+import { IS_IOS } from '@/env';
import ImageAvatar from '@/components/contacts/ImageAvatar';
-import { useCreateBackup } from '@/components/backup/useCreateBackup';
import { BackUpMenuItem } from './BackUpMenuButton';
-import { checkWalletsForBackupStatus } from '../../utils';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets';
import { format } from 'date-fns';
import { removeFirstEmojiFromString } from '@/helpers/emojiHandler';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { WrappedAlert as Alert } from '@/helpers/alert';
+import { useCreateBackup } from '@/components/backup/useCreateBackup';
+import { backupsStore } from '@/state/backups/backups';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
+import { isWalletBackedUpForCurrentAccount } from '../../utils';
type ViewWalletBackupParams = {
ViewWalletBackup: { walletId: string; title: string; imported?: boolean };
@@ -126,107 +117,38 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }:
const ViewWalletBackup = () => {
const { params } = useRoute>();
- const { backups } = useCloudBackups();
+ const createBackup = useCreateBackup();
+ const { status, backupProvider, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backupProvider: state.backupProvider,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
const { walletId, title: incomingTitle } = params;
const creatingWallet = useRef();
const { isDamaged, wallets } = useWallets();
const wallet = wallets?.[walletId];
const dispatch = useDispatch();
const initializeWallet = useInitializeWallet();
- const profilesEnabled = useExperimentalFlag(PROFILES);
-
- const walletTypeCount: WalletCountPerType = {
- phrase: 0,
- privateKey: 0,
- };
-
- const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount });
-
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
-
- const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
const isSecretPhrase = WalletTypes.mnemonic === wallet?.type;
-
const title = wallet?.type === WalletTypes.privateKey ? wallet?.addresses[0].label : incomingTitle;
+ const isBackedUp = isWalletBackedUpForCurrentAccount({
+ backupType: wallet?.backupType,
+ backedUp: wallet?.backedUp,
+ backupFile: wallet?.backupFile,
+ });
const { navigate } = useNavigation();
const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom);
- const { onSubmit, loading } = useCreateBackup({
- walletId,
- });
const backupWalletsToCloud = useCallback(async () => {
- if (IS_ANDROID) {
- try {
- await login();
-
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (accountDetails) {
- return onSubmit({});
- }
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- });
- } catch (e) {
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- logger.error(new RainbowError(`[ViewWalletBackup]: Logging into Google Drive failed`), { error: e });
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- }
-
- onSubmit({});
- }, [onSubmit]);
+ executeFnIfCloudBackupAvailable({
+ fn: () =>
+ createBackup({
+ walletId,
+ }),
+ });
+ }, [createBackup, walletId]);
const onNavigateToSecretWarning = useCallback(() => {
navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, {
@@ -265,38 +187,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`), {
@@ -307,6 +210,8 @@ const ViewWalletBackup = () => {
showWalletErrorAlert();
}, 1000);
}
+ } finally {
+ dispatch(setIsWalletLoading(null));
}
}
creatingWallet.current = false;
@@ -324,7 +229,7 @@ const ViewWalletBackup = () => {
error: e,
});
}
- }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, profilesEnabled, wallet]);
+ }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, wallet]);
const handleCopyAddress = React.useCallback(
(address: string) => {
@@ -386,7 +291,7 @@ const ViewWalletBackup = () => {
return (
- {!wallet?.backedUp && (
+ {!isBackedUp && (
<>
{
/>
- {backupProvider === walletBackupTypes.cloud && (
+
{
title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, {
cloudPlatformName: cloudPlatform,
})}
- loading={loading}
+ backupState={status}
onPress={backupWalletsToCloud}
/>
-
- )}
-
- {backupProvider !== walletBackupTypes.cloud && (
-
}
@@ -456,20 +352,12 @@ const ViewWalletBackup = () => {
titleComponent={}
testID={'back-up-manually'}
/>
-
- )}
+
>
)}
- {wallet?.backedUp && (
+ {isBackedUp && (
<>
{
paddingBottom={{ custom: 24 }}
iconComponent={
}
titleComponent={
{
{
>
)}
-
- }
- onPress={onNavigateToSecretWarning}
- size={52}
- titleComponent={
-
+
+
- }
- />
-
+
+
+ )}
+
+
+
+ }
+ onPress={onNavigateToSecretWarning}
+ size={52}
+ titleComponent={
+
+ }
+ />
+
+
@@ -560,15 +465,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 9823fd2555f..57c61ecedcd 100644
--- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
+++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
@@ -15,8 +15,8 @@ import { useNavigation } from '@/navigation';
import { abbreviations } from '@/utils';
import { addressHashedEmoji } from '@/utils/profileUtils';
import * as i18n from '@/languages';
-import MenuHeader from '../MenuHeader';
-import { checkWalletsForBackupStatus } from '../../utils';
+import MenuHeader, { StatusType } from '../MenuHeader';
+import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils';
import { Inline, Text, Box, Stack } from '@/design-system';
import { ContactAvatar } from '@/components/contacts';
import { useTheme } from '@/theme';
@@ -25,21 +25,17 @@ import { backupsCard } from '@/components/cards/utils/constants';
import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets';
import { SETTINGS_BACKUP_ROUTES } from './routes';
import { RainbowAccount, createWallet } from '@/model/wallet';
-import { PROFILES, useExperimentalFlag } from '@/config';
import { useDispatch } from 'react-redux';
-import { walletsLoadState } from '@/redux/wallets';
+import { 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 { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
-import { WrappedAlert as Alert } from '@/helpers/alert';
-import { Linking } from 'react-native';
-import { noop } from 'lodash';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
type WalletPillProps = {
account: RainbowAccount;
@@ -94,15 +90,17 @@ const getAccountSectionHeight = (numAccounts: number) => {
export const WalletsAndBackup = () => {
const { navigate } = useNavigation();
const { wallets } = useWallets();
- const profilesEnabled = useExperimentalFlag(PROFILES);
- const { backups } = useCloudBackups();
const dispatch = useDispatch();
- const initializeWallet = useInitializeWallet();
+ const createBackup = useCreateBackup();
+ const { status, backupProvider, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backupProvider: state.backupProvider,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
- const { onSubmit, loading } = useCreateBackup({
- walletId: undefined, // NOTE: This is not used when backing up All wallets
- });
+ const initializeWallet = useInitializeWallet();
const { manageCloudBackups } = useManageCloudBackups();
@@ -111,52 +109,15 @@ export const WalletsAndBackup = () => {
privateKey: 0,
};
- const { allBackedUp, backupProvider } = 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;
- }
+ const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets), [wallets]);
- 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,
@@ -167,47 +128,10 @@ export const WalletsAndBackup = () => {
}, [visibleWallets]);
const backupAllNonBackedUpWalletsTocloud = useCallback(async () => {
- if (IS_ANDROID) {
- try {
- await login();
-
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (accountDetails) {
- return onSubmit({ type: BackupTypes.All });
- }
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- });
- } catch (e) {
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- logger.error(new RainbowError(`[WalletsAndBackup]: Logging into Google Drive failed`), {
- error: e,
- });
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- }
-
- onSubmit({ type: BackupTypes.All });
- }, [onSubmit]);
+ executeFnIfCloudBackupAvailable({
+ fn: () => createBackup({}),
+ });
+ }, [createBackup]);
const onViewCloudBackups = useCallback(async () => {
navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, {
@@ -223,13 +147,15 @@ 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,
clearCallbackOnStartCreation: true,
});
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(walletsLoadState());
// @ts-expect-error - no params
await initializeWallet();
@@ -237,10 +163,12 @@ export const WalletsAndBackup = () => {
logger.error(new RainbowError(`[WalletsAndBackup]: Failed to create new secret phrase`), {
error: err,
});
+ } finally {
+ dispatch(setIsWalletLoading(null));
}
},
});
- }, [dispatch, initializeWallet, navigate, profilesEnabled, walletTypeCount.phrase]);
+ }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]);
const onPressLearnMoreAboutCloudBackups = useCallback(() => {
navigate(Routes.LEARN_WEB_VIEW_SCREEN, {
@@ -263,6 +191,48 @@ export const WalletsAndBackup = () => {
[navigate, wallets]
);
+ 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',
+ 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',
+ };
+ }, [backupProvider, status, allBackedUp]);
+
const renderView = useCallback(() => {
switch (backupProvider) {
default:
@@ -275,7 +245,7 @@ export const WalletsAndBackup = () => {
paddingTop={{ custom: 8 }}
iconComponent={}
titleComponent={}
- statusComponent={}
+ statusComponent={}
labelComponent={
{
/>
-
-
-
+
+
+
+
+
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType });
+
return (
-
+
{
}
>
- {!backedUp && (
+ {!isBackedUp && (
{
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
+ {addresses.map(address => (
+
))}
}
@@ -361,6 +335,7 @@ export const WalletsAndBackup = () => {
);
})}
+
{
titleComponent={}
/>
-
-
- }
- onPress={onViewCloudBackups}
- size={52}
- titleComponent={
-
- }
- />
- }
- onPress={manageCloudBackups}
- size={52}
- titleComponent={
-
- }
- />
-
);
@@ -416,12 +360,7 @@ export const WalletsAndBackup = () => {
paddingTop={{ custom: 8 }}
iconComponent={}
titleComponent={}
- statusComponent={
-
- }
+ statusComponent={}
labelComponent={
allBackedUp ? (
{
/>
-
+
-
-
+ }
+ >
+
+
+
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType });
+ console.log('isBackedUp', isBackedUp);
+
return (
-
+
{
}
>
- {!backedUp && }
+ {!isBackedUp && }
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
+ {addresses.map(address => (
+
))}
}
@@ -581,12 +522,13 @@ export const WalletsAndBackup = () => {
case WalletBackupTypes.manual: {
return (
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupType, backupFile, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupType, backupFile });
return (
-
+
{
}
>
- {!backedUp && }
+ {!isBackedUp && }
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
+ {addresses.map(address => (
+
))}
}
@@ -645,26 +587,29 @@ export const WalletsAndBackup = () => {
/>
-
- {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, {
- cloudPlatform,
- })}
+
+
+ {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, {
+ cloudPlatform,
+ })}
-
- {' '}
- {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)}
+
+ {' '}
+ {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)}
+
-
- }
- >
-
-
+ }
+ >
+
+
+
);
@@ -672,17 +617,18 @@ export const WalletsAndBackup = () => {
}
}, [
backupProvider,
- loading,
+ status,
backupAllNonBackedUpWalletsTocloud,
sortedWallets,
onCreateNewSecretPhrase,
- onViewCloudBackups,
- manageCloudBackups,
navigate,
onNavigateToWalletView,
allBackedUp,
+ iconStatusType,
+ text,
mostRecentBackup,
- lastBackupDate,
+ onViewCloudBackups,
+ manageCloudBackups,
onPressLearnMoreAboutCloudBackups,
]);
diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
index b415e1d4d30..10e28e6ebc6 100644
--- a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
+++ b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
@@ -3,14 +3,12 @@ import { getGoogleAccountUserData, GoogleDriveUserData, logoutFromGoogleDrive }
import ImageAvatar from '@/components/contacts/ImageAvatar';
import { showActionSheetWithOptions } from '@/utils';
import * as i18n from '@/languages';
-import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets';
-import { useDispatch } from 'react-redux';
import Menu from './Menu';
import MenuItem from './MenuItem';
import { logger, RainbowError } from '@/logger';
+import { backupsStore } from '@/state/backups/backups';
export const GoogleAccountSection: React.FC = () => {
- const dispatch = useDispatch();
const [accountDetails, setAccountDetails] = useState(undefined);
const [loading, setLoading] = useState(true);
@@ -29,12 +27,6 @@ export const GoogleAccountSection: React.FC = () => {
});
}, []);
- const removeBackupStateFromAllWallets = async () => {
- setLoading(true);
- await dispatch(clearAllWalletsBackupStatus());
- setLoading(false);
- };
-
const onGoogleAccountPress = () => {
showActionSheetWithOptions(
{
@@ -49,11 +41,10 @@ export const GoogleAccountSection: React.FC = () => {
if (buttonIndex === 0) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets().then(() => loginToGoogleDrive());
+ loginToGoogleDrive();
} else if (buttonIndex === 1) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets();
}
}
);
@@ -61,10 +52,10 @@ export const GoogleAccountSection: React.FC = () => {
const loginToGoogleDrive = async () => {
setLoading(true);
- await dispatch(updateWalletBackupStatusesBasedOnCloudUserData());
try {
const accountDetails = await getGoogleAccountUserData();
setAccountDetails(accountDetails ?? undefined);
+ backupsStore.getState().syncAndFetchBackups();
} catch (error) {
logger.error(new RainbowError(`[GoogleAccountSection]: Logging into Google Drive failed`), {
error: (error as Error).message,
diff --git a/src/screens/SettingsSheet/components/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 9fae44a89eb..19a73f10071 100644
--- a/src/screens/SettingsSheet/components/SettingsSection.tsx
+++ b/src/screens/SettingsSheet/components/SettingsSection.tsx
@@ -28,9 +28,11 @@ import { showActionSheetWithOptions } from '@/utils';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';
import { SettingsExternalURLs } from '../constants';
-import { capitalizeFirstLetter, checkWalletsForBackupStatus } from '../utils';
+import { checkLocalWalletsForBackupStatus } from '../utils';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { Box } from '@/design-system';
+import { capitalize } from 'lodash';
+import { backupsStore } from '@/state/backups/backups';
interface SettingsSectionProps {
onCloseModal: () => void;
@@ -59,10 +61,13 @@ const SettingsSection = ({
const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS);
const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS);
+ const { backupProvider } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ }));
+
const { isDarkMode, setTheme, colorScheme } = useTheme();
const onSendFeedback = useSendFeedback();
- const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
const onPressReview = useCallback(async () => {
if (ios) {
@@ -85,7 +90,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 {
@@ -215,7 +220,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..824feb0bb26 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:
@@ -48,51 +35,25 @@ export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount:
}
};
-const isWalletGroupNamed = (wallet: RainbowWallet) => wallet.name && wallet.name.trim() !== '' && wallet.name !== DEFAULT_WALLET_NAME;
-
-export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): UseVisibleWalletReturnType => {
- const [lastBackupDate, setLastBackupDate] = useState(undefined);
-
+export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): RainbowWallet[] => {
if (!wallets) {
- return {
- visibleWallets: [],
- lastBackupDate,
- };
+ return [];
}
- return {
- visibleWallets: Object.keys(wallets)
- .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth)
- .map(key => {
- const wallet = wallets[key];
- const visibleAccounts = (wallet.addresses || []).filter(a => a.visible);
- const totalAccounts = visibleAccounts.length;
-
- if (
- wallet.backedUp &&
- wallet.backupDate &&
- wallet.backupType === walletBackupTypes.cloud &&
- (!lastBackupDate || Number(wallet.backupDate) > lastBackupDate)
- ) {
- setLastBackupDate(Number(wallet.backupDate));
- }
-
- if (wallet.type === WalletTypes.mnemonic) {
- walletTypeCount.phrase += 1;
- } else if (wallet.type === WalletTypes.privateKey) {
- walletTypeCount.privateKey += 1;
- }
-
- return {
- ...wallet,
- name: isWalletGroupNamed(wallet) ? wallet.name : getTitleForWalletType(wallet.type, walletTypeCount),
- isBackedUp: wallet.backedUp,
- accounts: visibleAccounts,
- key,
- label: wallet.name,
- numAccounts: totalAccounts,
- };
- }),
- lastBackupDate,
- };
+ return Object.keys(wallets)
+ .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth)
+ .map(key => {
+ const wallet = wallets[key];
+
+ if (wallet.type === WalletTypes.mnemonic) {
+ walletTypeCount.phrase += 1;
+ } else if (wallet.type === WalletTypes.privateKey) {
+ walletTypeCount.privateKey += 1;
+ }
+
+ return {
+ ...wallet,
+ name: getTitleForWalletType(wallet.type, walletTypeCount),
+ };
+ });
};
diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts
index 08fa3e03e22..b92b5a8c4e3 100644
--- a/src/screens/SettingsSheet/utils.ts
+++ b/src/screens/SettingsSheet/utils.ts
@@ -1,118 +1,126 @@
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 * as i18n from '@/languages';
+import { cloudPlatform } from '@/utils/platform';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
import { RainbowWallet } from '@/model/wallet';
-import { Navigation } from '@/navigation';
-import { BackupUserData, getLocalBackupPassword } from '@/model/backup';
-import Routes from '@/navigation/routesNames';
-import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes';
-
-type WalletsByKey = {
- [key: string]: RainbowWallet;
-};
+import { IS_ANDROID, IS_IOS } from '@/env';
+import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
type WalletBackupStatus = {
allBackedUp: boolean;
areBackedUp: boolean;
canBeBackedUp: boolean;
- backupProvider: string | undefined;
-};
-
-export const capitalizeFirstLetter = (str: string) => {
- return str.charAt(0).toUpperCase() + str.slice(1);
};
-export const 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 hasManuallyBackedUpWallet = (wallets: ReturnType['wallets']) => {
+ if (!wallets) return false;
+ return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual);
};
-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,
};
+ }
+
+ // FOR ANDROID, we need to check if the current google account also has the backup file
+ if (IS_ANDROID) {
+ const backupFiles = backupsStore.getState().backups;
+ return Object.values(wallets).reduce(
+ (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;
+
+ return {
+ allBackedUp: acc.allBackedUp && (wallet.backedUp || !isBackupEligible),
+ areBackedUp: acc.areBackedUp && (wallet.backedUp || !isBackupEligible || wallet.imported),
+ canBeBackedUp: acc.canBeBackedUp || isBackupEligible,
+ };
+ },
+ { allBackedUp: true, areBackedUp: true, canBeBackedUp: false }
+ );
+};
- let backupProvider: string | undefined = undefined;
- let areBackedUp = true;
- let canBeBackedUp = false;
- let allBackedUp = true;
-
- Object.keys(wallets).forEach(key => {
- if (wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) {
- if (wallets[key].backupType === WalletBackupTypes.cloud) {
- backupProvider = WalletBackupTypes.cloud;
- } else if (backupProvider !== WalletBackupTypes.cloud && wallets[key].backupType === WalletBackupTypes.manual) {
- backupProvider = WalletBackupTypes.manual;
- }
- }
+export const getMostRecentCloudBackup = (backups: BackupFile[]) => {
+ const cloudBackups = backups.sort((a, b) => {
+ return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
+ });
- if (!wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) {
- allBackedUp = false;
+ return cloudBackups.reduce((prev, current) => {
+ if (!current) {
+ return prev;
}
- if (
- !wallets[key].backedUp &&
- wallets[key].type !== WalletTypes.readOnly &&
- wallets[key].type !== WalletTypes.bluetooth &&
- !wallets[key].imported
- ) {
- areBackedUp = false;
+ if (!prev) {
+ return current;
}
- if (wallets[key].type !== WalletTypes.bluetooth && wallets[key].type !== WalletTypes.readOnly) {
- canBeBackedUp = true;
+ const prevTimestamp = new Date(prev.lastModified).getTime();
+ const currentTimestamp = new Date(current.lastModified).getTime();
+ if (currentTimestamp > prevTimestamp) {
+ return current;
}
- });
- return {
- allBackedUp,
- areBackedUp,
- canBeBackedUp,
- backupProvider,
- };
-};
-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;
+ return prev;
+ }, cloudBackups[0]);
};
-export const fetchBackupPasswordAndNavigate = async () => {
- const password = await getLocalBackupPassword();
+export const titleForBackupState: Partial> = {
+ [CloudBackupState.Initializing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, {
+ cloudPlatformName: cloudPlatform,
+ }),
+ [CloudBackupState.Syncing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, {
+ cloudPlatformName: cloudPlatform,
+ }),
+ [CloudBackupState.Fetching]: i18n.t(i18n.l.back_up.cloud.fetching_backups, {
+ cloudPlatformName: cloudPlatform,
+ }),
+};
- return new Promise(resolve => {
- return Navigation.handleAction(Routes.BACKUP_SHEET, {
- step: WalletBackupStepTypes.backup_cloud,
- password,
- onSuccess: async (password: string) => {
- resolve(password);
- },
- });
+export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => {
+ console.log({
+ backupType,
+ backedUp,
+ backupFile,
});
+ if (!backupType || !backupFile) {
+ return false;
+ }
+
+ if (IS_IOS || backupType === WalletBackupTypes.manual) {
+ 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;
+ }
+
+ const backupFiles = backupsStore.getState().backups;
+ return backupFiles.files.some(file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(backupFile));
};
diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx
index 026acd32b4e..03ddf4442c8 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 { BackendNetworks } from '@/components/BackendNetworks';
function WalletScreen() {
const { params } = useRoute>();
@@ -37,7 +38,6 @@ function WalletScreen() {
const [initialized, setInitialized] = useState(!!params?.initialized);
const initializeWallet = useInitializeWallet();
const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings();
-
const loadAccountLateData = useLoadAccountLateData();
const loadGlobalLateData = useLoadGlobalLateData();
const insets = useSafeAreaInsets();
@@ -71,11 +71,14 @@ function WalletScreen() {
}
}, [initializeWallet, initialized, params, setParams]);
+ useEffect(() => {
+ runWalletBackupStatusChecks();
+ }, []);
+
useEffect(() => {
if (walletReady) {
loadAccountLateData();
loadGlobalLateData();
- runWalletBackupStatusChecks();
}
}, [loadAccountLateData, loadGlobalLateData, walletReady]);
@@ -111,6 +114,7 @@ function WalletScreen() {
+
{/* NOTE: This component listens for Mobile Wallet Protocol requests and handles them */}
diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts
new file mode 100644
index 00000000000..a54a6e759fc
--- /dev/null
+++ b/src/state/backups/backups.ts
@@ -0,0 +1,163 @@
+import { BackupFile, 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: BackupFile | undefined;
+ setMostRecentBackup: (backup: BackupFile | 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, backups: { files: [] }, mostRecentBackup: undefined });
+ return {
+ success: false,
+ retry: false,
+ };
+ }
+ }
+
+ set({ status: CloudBackupState.Syncing });
+ logger.debug('[backupsStore]: Syncing with cloud');
+ await syncCloud();
+
+ set({ status: CloudBackupState.Fetching });
+ logger.debug('[backupsStore]: Fetching backups');
+ const backups = await fetchAllBackups();
+
+ set({ backups });
+
+ const { wallets } = store.getState().wallets;
+
+ // if the user has any cloud backups, set the provider to cloud
+ if (backups.files.length > 0) {
+ set({
+ backupProvider: walletBackupTypes.cloud,
+ mostRecentBackup: getMostRecentCloudBackup(backups.files),
+ });
+ } else if (hasManuallyBackedUpWallet(wallets)) {
+ set({ backupProvider: walletBackupTypes.manual });
+ } else {
+ set({ backupProvider: undefined });
+ }
+
+ logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`);
+
+ set({ status: CloudBackupState.Ready });
+ return {
+ success: true,
+ retry: false,
+ };
+ } catch (e) {
+ logger.error(new RainbowError('[backupsStore]: Failed to fetch all backups'), {
+ error: e,
+ });
+ set({ status: CloudBackupState.FailedToInitialize });
+ }
+
+ return {
+ success: false,
+ retry: retryOnFailure,
+ };
+ };
+
+ if (mutex.isLocked() || returnEarlyIfLockedStates.includes(status)) {
+ logger.debug('[backupsStore]: Mutex is locked or returnEarlyIfLockedStates includes status', {
+ status,
+ });
+ return {
+ success: false,
+ retry: false,
+ };
+ }
+
+ const releaser = await mutex.acquire();
+ logger.debug('[backupsStore]: Acquired mutex');
+ const { success, retry } = await 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/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 }),
+}));
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);