diff --git a/src/App.tsx b/src/App.tsx
index 4977067dc60..1a749b8ce6d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,6 +1,6 @@
import '@/languages';
import * as Sentry from '@sentry/react-native';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState, memo } from 'react';
import { AppRegistry, Dimensions, LogBox, StyleSheet, View } from 'react-native';
import { Toaster } from 'sonner-native';
import { MobileWalletProtocolProvider } from '@coinbase/mobile-wallet-protocol-host';
@@ -9,9 +9,8 @@ import { useApplicationSetup } from '@/hooks/useApplicationSetup';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { enableScreens } from 'react-native-screens';
-import { connect, Provider as ReduxProvider } from 'react-redux';
+import { connect, Provider as ReduxProvider, shallowEqual } from 'react-redux';
import { RecoilRoot } from 'recoil';
-import PortalConsumer from '@/components/PortalConsumer';
import ErrorBoundary from '@/components/error-boundary/ErrorBoundary';
import { OfflineToast } from '@/components/toasts';
import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetworkRequests, showNetworkResponses } from '@/config/debug';
@@ -24,7 +23,6 @@ import store, { AppDispatch, type AppState } from '@/redux/store';
import { MainThemeProvider } from '@/theme/ThemeContext';
import { SharedValuesProvider } from '@/helpers/SharedValuesContext';
import { InitialRouteContext } from '@/navigation/initialRoute';
-import { Portal } from '@/react-native-cool-modals/Portal';
import { NotificationsHandler } from '@/notifications/NotificationsHandler';
import { analyticsV2 } from '@/analytics';
import { getOrCreateDeviceId } from '@/analytics/utils';
@@ -39,6 +37,7 @@ import { RootStackParamList } from '@/navigation/types';
import { IS_ANDROID, IS_DEV } from '@/env';
import { prefetchDefaultFavorites } from '@/resources/favorites';
import Routes from '@/navigation/Routes';
+import { BackupsSync } from '@/state/sync/BackupsSync';
import { BackendNetworks } from '@/components/BackendNetworks';
import { AbsolutePortalRoot } from './components/AbsolutePortal';
@@ -68,13 +67,11 @@ function App({ walletReady }: AppProps) {
}, []);
return (
-
+ <>
{initialRoute && (
-
-
)}
@@ -82,14 +79,27 @@ function App({ walletReady }: AppProps) {
+
-
+
+ >
);
}
-const AppWithRedux = connect(state => ({
- walletReady: state.appState.walletReady,
-}))(App);
+const AppWithRedux = connect(
+ state => ({
+ walletReady: state.appState.walletReady,
+ }),
+ null,
+ null,
+ {
+ areStatesEqual: (next, prev) => {
+ // Only update if walletReady actually changed
+ return next.appState.walletReady === prev.appState.walletReady;
+ },
+ areOwnPropsEqual: shallowEqual,
+ }
+)(memo(App));
function Root() {
const [initializing, setInitializing] = useState(true);
diff --git a/src/components/AbsolutePortal.tsx b/src/components/AbsolutePortal.tsx
index c98b036cd70..a992e84c11c 100644
--- a/src/components/AbsolutePortal.tsx
+++ b/src/components/AbsolutePortal.tsx
@@ -1,5 +1,5 @@
import React, { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
-import { View } from 'react-native';
+import { StyleProp, ViewStyle, View } from 'react-native';
const absolutePortal = {
nodes: [] as ReactNode[],
@@ -24,7 +24,7 @@ const absolutePortal = {
},
};
-export const AbsolutePortalRoot = () => {
+export const AbsolutePortalRoot = ({ style }: { style?: StyleProp }) => {
const [nodes, setNodes] = useState(absolutePortal.nodes);
useEffect(() => {
@@ -32,7 +32,7 @@ export const AbsolutePortalRoot = () => {
return () => unsubscribe();
}, []);
- return {nodes};
+ return {nodes};
};
export const AbsolutePortal = ({ children }: PropsWithChildren) => {
diff --git a/src/components/ExchangeTokenRow.tsx b/src/components/ExchangeTokenRow.tsx
index 0e509debbe4..9fabd7219f5 100644
--- a/src/components/ExchangeTokenRow.tsx
+++ b/src/components/ExchangeTokenRow.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import isEqual from 'react-fast-compare';
import { Box, Column, Columns, Inline, Stack, Text } from '@/design-system';
import { isNativeAsset } from '@/handlers/assets';
-import { useAsset, useDimensions } from '@/hooks';
+import { useAsset } from '@/hooks';
import { ButtonPressAnimation } from '@/components/animations';
import { FloatingEmojis } from '@/components/floating-emojis';
import { IS_IOS } from '@/env';
@@ -34,7 +34,6 @@ export default React.memo(function ExchangeTokenRow({
disabled,
},
}: ExchangeTokenRowProps) {
- const { width: deviceWidth } = useDimensions();
const item = useAsset({
address,
chainId,
@@ -101,10 +100,8 @@ export default React.memo(function ExchangeTokenRow({
{isInfoButtonVisible && }
{showFavoriteButton &&
(IS_IOS ? (
- // @ts-ignore
{
- if (isWalletLoading) {
- setComponent(, true);
- }
- return hide;
- }, [hide, isWalletLoading, setComponent]);
-
- return null;
-}
diff --git a/src/components/WalletLoadingListener.tsx b/src/components/WalletLoadingListener.tsx
new file mode 100644
index 00000000000..6a9e605ab4f
--- /dev/null
+++ b/src/components/WalletLoadingListener.tsx
@@ -0,0 +1,17 @@
+import React, { useEffect } from 'react';
+import { LoadingOverlay } from './modal';
+import { sheetVerticalOffset } from '@/navigation/effects';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
+
+export default function WalletLoadingListener() {
+ const loadingState = walletLoadingStore(state => state.loadingState);
+
+ useEffect(() => {
+ if (loadingState) {
+ walletLoadingStore.getState().setComponent();
+ }
+ return walletLoadingStore.getState().hide;
+ }, [loadingState]);
+
+ return null;
+}
diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx
index d122b9a622e..49bf81fd43e 100644
--- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx
+++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx
@@ -174,10 +174,8 @@ export default React.memo(function FastCurrencySelectionRow({
{showFavoriteButton &&
chainId === ChainId.mainnet &&
(ios ? (
- // @ts-ignore
-
+
@@ -216,13 +216,17 @@ function SendButton() {
);
}
-export function MoreButton() {
- // ////////////////////////////////////////////////////
- // Handlers
-
+export function CopyButton() {
const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom);
const { accountAddress } = useAccountProfile();
+ const { isDamaged } = useWallets();
+
const handlePressCopy = React.useCallback(() => {
+ if (isDamaged) {
+ showWalletErrorAlert();
+ return;
+ }
+
if (!isToastActive) {
setToastActive(true);
setTimeout(() => {
@@ -230,7 +234,7 @@ export function MoreButton() {
}, 2000);
}
Clipboard.setString(accountAddress);
- }, [accountAddress, isToastActive, setToastActive]);
+ }, [accountAddress, isDamaged, isToastActive, setToastActive]);
return (
<>
diff --git a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx
index 616935fe6ff..9859a4ae105 100644
--- a/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx
+++ b/src/components/asset-list/RecyclerAssetList2/profile-header/ProfileNameRow.tsx
@@ -109,7 +109,6 @@ export function ProfileNameRow({
scaleTo={0}
size={50}
wiggleFactor={0}
- // @ts-expect-error – JS component
setOnNewEmoji={newOnNewEmoji => (onNewEmoji.current = newOnNewEmoji)}
/>
diff --git a/src/components/backup/AddWalletToCloudBackupStep.tsx b/src/components/backup/AddWalletToCloudBackupStep.tsx
deleted file mode 100644
index 62f92a99e2f..00000000000
--- a/src/components/backup/AddWalletToCloudBackupStep.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useCallback } from 'react';
-import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system';
-import * as lang from '@/languages';
-import { ImgixImage } from '../images';
-import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png';
-import { Source } from 'react-native-fast-image';
-import { cloudPlatform } from '@/utils/platform';
-import { ButtonPressAnimation } from '../animations';
-import Routes from '@/navigation/routesNames';
-import { useNavigation } from '@/navigation';
-import { useWallets } from '@/hooks';
-import { WalletCountPerType, useVisibleWallets } from '@/screens/SettingsSheet/useVisibleWallets';
-import { format } from 'date-fns';
-import { useCreateBackup } from './useCreateBackup';
-import { login } from '@/handlers/cloudBackup';
-
-const imageSize = 72;
-
-export default function AddWalletToCloudBackupStep() {
- const { goBack } = useNavigation();
- const { wallets, selectedWallet } = useWallets();
-
- const walletTypeCount: WalletCountPerType = {
- phrase: 0,
- privateKey: 0,
- };
-
- const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount });
-
- const { onSubmit } = useCreateBackup({
- walletId: selectedWallet.id,
- navigateToRoute: {
- route: Routes.SETTINGS_SHEET,
- params: {
- screen: Routes.SETTINGS_SECTION_BACKUP,
- },
- },
- });
-
- const potentiallyLoginAndSubmit = useCallback(async () => {
- await login();
- return onSubmit({});
- }, [onSubmit]);
-
- const onMaybeLater = useCallback(() => goBack(), [goBack]);
-
- return (
-
-
-
-
-
- {lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)}
-
-
-
-
-
-
-
-
- potentiallyLoginAndSubmit().then(success => success && goBack())}>
-
-
-
-
- {' '}
- {lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, {
- cloudPlatform,
- })}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {lang.t(lang.l.back_up.cloud.mayber_later)}
-
-
-
-
-
-
-
-
-
-
- {lastBackupDate && (
-
-
-
-
- {lang.t(lang.l.back_up.cloud.latest_backup, {
- date: format(lastBackupDate, "M/d/yy 'at' h:mm a"),
- })}
-
-
-
-
- )}
-
- );
-}
diff --git a/src/components/backup/BackupManuallyStep.tsx b/src/components/backup/BackupManuallyStep.tsx
deleted file mode 100644
index da18d73806a..00000000000
--- a/src/components/backup/BackupManuallyStep.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, { useCallback } from 'react';
-import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system';
-import * as lang from '@/languages';
-import { ImgixImage } from '../images';
-import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png';
-import { Source } from 'react-native-fast-image';
-import { ButtonPressAnimation } from '../animations';
-import { useNavigation } from '@/navigation';
-import Routes from '@/navigation/routesNames';
-import { useWallets } from '@/hooks';
-import walletTypes from '@/helpers/walletTypes';
-import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes';
-import walletBackupTypes from '@/helpers/walletBackupTypes';
-
-const imageSize = 72;
-
-export default function BackupManuallyStep() {
- const { navigate, goBack } = useNavigation();
- const { selectedWallet } = useWallets();
-
- const onManualBackup = async () => {
- const title =
- selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey
- ? (selectedWallet.addresses || [])[0].label
- : selectedWallet.name;
-
- goBack();
- navigate(Routes.SETTINGS_SHEET, {
- screen: SETTINGS_BACKUP_ROUTES.SECRET_WARNING,
- params: {
- isBackingUp: true,
- title,
- backupType: walletBackupTypes.manual,
- walletId: selectedWallet.id,
- },
- });
- };
-
- const onMaybeLater = useCallback(() => goBack(), [goBack]);
-
- return (
-
-
-
-
-
- {lang.t(lang.l.back_up.manual.backup_manually_now)}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {lang.t(lang.l.back_up.manual.back_up_now)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {lang.t(lang.l.back_up.manual.already_backed_up)}
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/backup/BackupSheet.tsx b/src/components/backup/BackupSheet.tsx
index 5b6a0a4300a..12e15c60190 100644
--- a/src/components/backup/BackupSheet.tsx
+++ b/src/components/backup/BackupSheet.tsx
@@ -2,13 +2,10 @@ import { RouteProp, useRoute } from '@react-navigation/native';
import React, { useCallback } from 'react';
import { BackupCloudStep, RestoreCloudStep } from '.';
import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes';
-import BackupChooseProviderStep from '@/components/backup/BackupChooseProviderStep';
+import BackupWalletPrompt from '@/components/backup/BackupWalletPrompt';
import { BackgroundProvider } from '@/design-system';
import { SimpleSheet } from '@/components/sheet/SimpleSheet';
-import AddWalletToCloudBackupStep from '@/components/backup/AddWalletToCloudBackupStep';
-import BackupManuallyStep from './BackupManuallyStep';
import { getHeightForStep } from '@/navigation/config';
-import { CloudBackupProvider } from './CloudBackupProvider';
type BackupSheetParams = {
BackupSheet: {
@@ -21,38 +18,32 @@ type BackupSheetParams = {
};
export default function BackupSheet() {
- const { params: { step = WalletBackupStepTypes.no_provider } = {} } = useRoute>();
+ const { params: { step = WalletBackupStepTypes.backup_prompt } = {} } = useRoute>();
const renderStep = useCallback(() => {
switch (step) {
- case WalletBackupStepTypes.backup_now_to_cloud:
- return ;
- case WalletBackupStepTypes.backup_now_manually:
- return ;
case WalletBackupStepTypes.backup_cloud:
return ;
case WalletBackupStepTypes.restore_from_backup:
return ;
- case WalletBackupStepTypes.no_provider:
+ case WalletBackupStepTypes.backup_prompt:
default:
- return ;
+ return ;
}
}, [step]);
return (
-
-
- {({ backgroundColor }) => (
-
- {renderStep()}
-
- )}
-
-
+
+ {({ backgroundColor }) => (
+
+ {renderStep()}
+
+ )}
+
);
}
diff --git a/src/components/backup/BackupChooseProviderStep.tsx b/src/components/backup/BackupWalletPrompt.tsx
similarity index 66%
rename from src/components/backup/BackupChooseProviderStep.tsx
rename to src/components/backup/BackupWalletPrompt.tsx
index 38325639704..77071534f48 100644
--- a/src/components/backup/BackupChooseProviderStep.tsx
+++ b/src/components/backup/BackupWalletPrompt.tsx
@@ -1,7 +1,6 @@
-import React from 'react';
-import { useCreateBackup } from '@/components/backup/useCreateBackup';
+import React, { useCallback, useMemo } from 'react';
import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system';
-import * as lang from '@/languages';
+import * as i18n from '@/languages';
import { ImgixImage } from '../images';
import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png';
import ManuallyBackedUpIcon from '@/assets/ManuallyBackedUp.png';
@@ -14,13 +13,13 @@ import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { SETTINGS_BACKUP_ROUTES } from '@/screens/SettingsSheet/components/Backups/routes';
import { useWallets } from '@/hooks';
-import walletTypes from '@/helpers/walletTypes';
+import WalletTypes from '@/helpers/walletTypes';
import walletBackupTypes from '@/helpers/walletBackupTypes';
-import { IS_ANDROID } from '@/env';
-import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
-import { WrappedAlert as Alert } from '@/helpers/alert';
-import { RainbowError, logger } from '@/logger';
-import { Linking } from 'react-native';
+import { useCreateBackup } from '@/components/backup/useCreateBackup';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
+import { TextColor } from '@/design-system/color/palettes';
+import { CustomColor } from '@/design-system/color/useForegroundColor';
const imageSize = 72;
@@ -28,67 +27,31 @@ export default function BackupSheetSectionNoProvider() {
const { colors } = useTheme();
const { navigate, goBack } = useNavigation();
const { selectedWallet } = useWallets();
+ const createBackup = useCreateBackup();
+ const { status } = backupsStore(state => ({
+ status: state.status,
+ }));
- const { onSubmit, loading } = useCreateBackup({
- walletId: selectedWallet.id,
- navigateToRoute: {
- route: Routes.SETTINGS_SHEET,
- params: {
- screen: Routes.SETTINGS_SECTION_BACKUP,
- },
- },
- });
-
- const onCloudBackup = async () => {
- if (loading !== 'none') {
- return;
- }
- // NOTE: On Android we need to make sure the user is signed into a Google account before trying to backup
- // otherwise we'll fake backup and it's confusing...
- if (IS_ANDROID) {
- try {
- await login();
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (!accountDetails) {
- Alert.alert(lang.t(lang.l.back_up.errors.no_account_found));
- return;
- }
- });
- } catch (e) {
- logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), {
- error: e,
- });
- Alert.alert(lang.t(lang.l.back_up.errors.no_account_found));
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.label),
- lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: lang.t(lang.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- }
+ const onCloudBackup = useCallback(() => {
+ // pop the bottom sheet, and navigate to the backup section inside settings sheet
+ goBack();
+ navigate(Routes.SETTINGS_SHEET, {
+ screen: Routes.SETTINGS_SECTION_BACKUP,
+ initial: false,
+ });
- onSubmit({});
- };
+ executeFnIfCloudBackupAvailable({
+ fn: () =>
+ createBackup({
+ walletId: selectedWallet.id,
+ }),
+ logout: true,
+ });
+ }, [createBackup, goBack, navigate, selectedWallet.id]);
- const onManualBackup = async () => {
+ const onManualBackup = useCallback(async () => {
const title =
- selectedWallet?.imported && selectedWallet.type === walletTypes.privateKey
+ selectedWallet?.imported && selectedWallet.type === WalletTypes.privateKey
? (selectedWallet.addresses || [])[0].label
: selectedWallet.name;
@@ -102,13 +65,38 @@ export default function BackupSheetSectionNoProvider() {
walletId: selectedWallet.id,
},
});
- };
+ }, [goBack, navigate, selectedWallet.addresses, selectedWallet.id, selectedWallet?.imported, selectedWallet.name, selectedWallet.type]);
+
+ const isCloudBackupDisabled = useMemo(() => {
+ return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable;
+ }, [status]);
+
+ const { color, text } = useMemo<{ text: string; color: TextColor | CustomColor }>(() => {
+ if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) {
+ return {
+ text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled),
+ color: 'primary (Deprecated)',
+ };
+ }
+
+ if (status === CloudBackupState.Ready) {
+ return {
+ text: i18n.t(i18n.l.back_up.cloud.cloud_backup),
+ color: 'primary (Deprecated)',
+ };
+ }
+
+ return {
+ text: i18n.t(i18n.l.back_up.cloud.statuses.syncing),
+ color: 'yellow',
+ };
+ }, [status]);
return (
- {lang.t(lang.l.back_up.cloud.how_would_you_like_to_backup)}
+ {i18n.t(i18n.l.back_up.cloud.how_would_you_like_to_backup)}
@@ -116,8 +104,7 @@ export default function BackupSheetSectionNoProvider() {
- {/* replace this with BackUpMenuButton */}
-
+
@@ -133,18 +120,18 @@ export default function BackupSheetSectionNoProvider() {
marginRight={{ custom: -12 }}
marginTop={{ custom: 0 }}
marginBottom={{ custom: -8 }}
- source={WalletsAndBackupIcon as Source}
+ source={WalletsAndBackupIcon}
width={{ custom: imageSize }}
size={imageSize}
/>
-
- {lang.t(lang.l.back_up.cloud.cloud_backup)}
+
+ {text}
- {lang.t(lang.l.back_up.cloud.recommended_for_beginners)}
+ {i18n.t(i18n.l.back_up.cloud.recommended_for_beginners)}
{' '}
- {lang.t(lang.l.back_up.cloud.choose_backup_cloud_description, {
+ {i18n.t(i18n.l.back_up.cloud.choose_backup_cloud_description, {
cloudPlatform,
})}
@@ -192,10 +179,10 @@ export default function BackupSheetSectionNoProvider() {
size={imageSize}
/>
- {lang.t(lang.l.back_up.cloud.manual_backup)}
+ {i18n.t(i18n.l.back_up.cloud.manual_backup)}
- {lang.t(lang.l.back_up.cloud.choose_backup_manual_description)}
+ {i18n.t(i18n.l.back_up.cloud.choose_backup_manual_description)}
diff --git a/src/components/backup/ChooseBackupStep.tsx b/src/components/backup/ChooseBackupStep.tsx
index 2f7b68cedf6..d08d4cdb0e2 100644
--- a/src/components/backup/ChooseBackupStep.tsx
+++ b/src/components/backup/ChooseBackupStep.tsx
@@ -6,26 +6,24 @@ import { useDimensions } from '@/hooks';
import { useNavigation } from '@/navigation';
import styled from '@/styled-thing';
import { margin, padding } from '@/styles';
-import { Box, Stack, Text } from '@/design-system';
-import { RouteProp, useRoute } from '@react-navigation/native';
+import { Box, Stack } from '@/design-system';
import { sharedCoolModalTopOffset } from '@/navigation/config';
-import { ImgixImage } from '../images';
+import { ImgixImage } from '@/components/images';
import MenuContainer from '@/screens/SettingsSheet/components/MenuContainer';
import Menu from '@/screens/SettingsSheet/components/Menu';
import { format } from 'date-fns';
import MenuItem from '@/screens/SettingsSheet/components/MenuItem';
import Routes from '@/navigation/routesNames';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { RestoreSheetParams } from '@/screens/RestoreSheet';
+import { BackupFile, parseTimestampFromFilename } from '@/model/backup';
import { Source } from 'react-native-fast-image';
import { IS_ANDROID } from '@/env';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import useCloudBackups, { CloudBackupStep } from '@/hooks/useCloudBackups';
-import { Centered } from '../layout';
-import { cloudPlatform } from '@/utils/platform';
-import Spinner from '../Spinner';
-import ActivityIndicator from '../ActivityIndicator';
+import { Page } from '@/components/layout';
+import Spinner from '@/components/Spinner';
+import ActivityIndicator from '@/components/ActivityIndicator';
import { useTheme } from '@/theme';
+import { backupsStore, CloudBackupState, LoadingStates } from '@/state/backups/backups';
+import { titleForBackupState } from '@/screens/SettingsSheet/utils';
const Title = styled(RNText).attrs({
align: 'left',
@@ -53,60 +51,27 @@ const Masthead = styled(Box).attrs({
});
export function ChooseBackupStep() {
- const {
- params: { fromSettings },
- } = useRoute>();
const { colors } = useTheme();
- const { isFetching, backups, userData, step, fetchBackups } = useCloudBackups();
+ const { status, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
+
+ const isLoading = LoadingStates.includes(status);
const { top } = useSafeAreaInsets();
const { height: deviceHeight } = useDimensions();
const { navigate } = useNavigation();
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
-
const onSelectCloudBackup = useCallback(
- (selectedBackup: Backup) => {
+ (selectedBackup: BackupFile) => {
navigate(Routes.RESTORE_CLOUD_SHEET, {
- backups,
- userData,
selectedBackup,
- fromSettings,
});
},
- [navigate, userData, backups, fromSettings]
+ [navigate]
);
const height = IS_ANDROID ? deviceHeight - top : deviceHeight - sharedCoolModalTopOffset - 48;
@@ -132,7 +97,7 @@ export function ChooseBackupStep() {
- {!isFetching && step === CloudBackupStep.FAILED && (
+ {status === CloudBackupState.FailedToInitialize && (
)}
- {!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..ce0774f2ec3 100644
--- a/src/components/backup/RestoreCloudStep.tsx
+++ b/src/components/backup/RestoreCloudStep.tsx
@@ -6,8 +6,7 @@ import WalletAndBackup from '@/assets/WalletsAndBackup.png';
import { KeyboardArea } from 'react-native-keyboard-area';
import {
- Backup,
- fetchBackupPassword,
+ BackupFile,
getLocalBackupPassword,
restoreCloudBackup,
RestoreCloudBackupResultStates,
@@ -17,10 +16,10 @@ import { cloudPlatform } from '@/utils/platform';
import { PasswordField } from '../fields';
import { Text } from '../text';
import { WrappedAlert as Alert } from '@/helpers/alert';
-import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
+import { isCloudBackupPasswordValid, normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { useDimensions, useInitializeWallet } from '@/hooks';
-import { useNavigation } from '@/navigation';
+import { Navigation, useNavigation } from '@/navigation';
import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets';
import Routes from '@/navigation/routesNames';
import styled from '@/styled-thing';
@@ -35,8 +34,16 @@ import RainbowButtonTypes from '../buttons/rainbow-button/RainbowButtonTypes';
import { RouteProp, useRoute } from '@react-navigation/native';
import { RestoreSheetParams } from '@/screens/RestoreSheet';
import { Source } from 'react-native-fast-image';
-import { useTheme } from '@/theme';
-import useCloudBackups from '@/hooks/useCloudBackups';
+import { ThemeContextProps, useTheme } from '@/theme';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { isEmpty } from 'lodash';
+import { backupsStore } from '@/state/backups/backups';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
+
+type ComponentProps = {
+ theme: ThemeContextProps;
+ color: ThemeContextProps['colors'][keyof ThemeContextProps['colors']];
+};
const Title = styled(Text).attrs({
size: 'big',
@@ -45,7 +52,7 @@ const Title = styled(Text).attrs({
...padding.object(12, 0, 0),
});
-const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({
+const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({
align: 'left',
color: color || colors.alpha(colors.blueGreyDark, 0.5),
lineHeight: 'looser',
@@ -53,7 +60,7 @@ const DescriptionText = styled(Text).attrs(({ theme: { colors }, color }: any) =
weight: 'medium',
}))({});
-const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: any) => ({
+const ButtonText = styled(Text).attrs(({ theme: { colors }, color }: ComponentProps) => ({
align: 'center',
letterSpacing: 'rounded',
color: color || colors.alpha(colors.blueGreyDark, 0.5),
@@ -71,38 +78,46 @@ const Masthead = styled(Box).attrs({
});
const KeyboardSizeView = styled(KeyboardArea)({
- backgroundColor: ({ theme: { colors } }: any) => colors.transparent,
+ backgroundColor: ({ theme: { colors } }: ComponentProps) => colors.transparent,
});
type RestoreCloudStepParams = {
RestoreSheet: {
- selectedBackup: Backup;
+ selectedBackup: BackupFile;
};
};
export default function RestoreCloudStep() {
const { params } = useRoute>();
+ const { password } = backupsStore(state => ({
+ password: state.password,
+ }));
- const { userData } = useCloudBackups();
+ const loadingState = walletLoadingStore(state => state.loadingState);
const { selectedBackup } = params;
const { isDarkMode } = useTheme();
- const [loading, setLoading] = useState(false);
+ const { canGoBack, goBack } = useNavigation();
+
+ const onRestoreSuccess = useCallback(() => {
+ while (canGoBack()) {
+ goBack();
+ }
+ }, [canGoBack, goBack]);
const dispatch = useDispatch();
const { width: deviceWidth, height: deviceHeight } = useDimensions();
- const { replace, navigate, getState: dangerouslyGetState, goBack } = useNavigation();
const [validPassword, setValidPassword] = useState(false);
const [incorrectPassword, setIncorrectPassword] = useState(false);
- const [password, setPassword] = useState('');
const passwordRef = useRef(null);
const initializeWallet = useInitializeWallet();
useEffect(() => {
const fetchPasswordIfPossible = async () => {
- const pwd = await fetchBackupPassword();
+ const pwd = await getLocalBackupPassword();
if (pwd) {
- setPassword(pwd);
+ backupsStore.getState().setStoredPassword(pwd);
+ backupsStore.getState().setPassword(pwd);
}
};
fetchPasswordIfPossible();
@@ -118,35 +133,42 @@ export default function RestoreCloudStep() {
}, [incorrectPassword, password]);
const onPasswordChange = useCallback(({ nativeEvent: { text: inputText } }: { nativeEvent: { text: string } }) => {
- setPassword(inputText);
+ backupsStore.getState().setPassword(inputText);
setIncorrectPassword(false);
}, []);
const onSubmit = useCallback(async () => {
- setLoading(true);
+ // NOTE: Localizing password to prevent an empty string from being saved if we re-render
+ const pwd = password.trim();
+ let filename = selectedBackup.name;
+
+ const prevWalletsState = await dispatch(walletsLoadState());
+
try {
if (!selectedBackup.name) {
throw new Error('No backup file selected');
}
- const prevWalletsState = await dispatch(walletsLoadState());
-
+ walletLoadingStore.setState({
+ loadingState: WalletLoadingStates.RESTORING_WALLET,
+ });
const status = await restoreCloudBackup({
- password,
- userData,
- nameOfSelectedBackupFile: selectedBackup.name,
+ password: pwd,
+ backupFilename: filename,
});
-
if (status === RestoreCloudBackupResultStates.success) {
// Store it in the keychain in case it was missing
- const hasSavedPassword = await getLocalBackupPassword();
- if (!hasSavedPassword) {
- await saveLocalBackupPassword(password);
+ if (backupsStore.getState().storedPassword !== pwd) {
+ await saveLocalBackupPassword(pwd);
+ }
+
+ // Reset the storedPassword state for next restoration process
+ if (backupsStore.getState().storedPassword) {
+ backupsStore.getState().setStoredPassword('');
}
InteractionManager.runAfterInteractions(async () => {
const newWalletsState = await dispatch(walletsLoadState());
- let filename = selectedBackup.name;
if (IS_ANDROID && filename) {
filename = normalizeAndroidBackupFilename(filename);
}
@@ -188,14 +210,21 @@ export default function RestoreCloudStep() {
const p2 = dispatch(addressSetSelected(firstAddress));
await Promise.all([p1, p2]);
await initializeWallet(null, null, null, false, false, null, true, null);
-
- const operation = dangerouslyGetState()?.index === 1 ? navigate : replace;
- operation(Routes.SWIPE_LAYOUT, {
- screen: Routes.WALLET_SCREEN,
- });
-
- setLoading(false);
});
+
+ onRestoreSuccess();
+ backupsStore.getState().setPassword('');
+ if (isEmpty(prevWalletsState)) {
+ Navigation.handleAction(
+ Routes.SWIPE_LAYOUT,
+ {
+ screen: Routes.WALLET_SCREEN,
+ },
+ true
+ );
+ } else {
+ Navigation.handleAction(Routes.WALLET_SCREEN, {});
+ }
} else {
switch (status) {
case RestoreCloudBackupResultStates.incorrectPassword:
@@ -211,18 +240,17 @@ export default function RestoreCloudStep() {
}
} catch (e) {
Alert.alert(lang.t('back_up.restore_cloud.error_while_restoring'));
+ } finally {
+ walletLoadingStore.setState({
+ loadingState: null,
+ });
}
-
- setLoading(false);
- }, [selectedBackup.name, password, userData, dispatch, initializeWallet, dangerouslyGetState, navigate, replace]);
+ }, [password, selectedBackup.name, dispatch, onRestoreSuccess, initializeWallet]);
const onPasswordSubmit = useCallback(() => {
validPassword && onSubmit();
}, [onSubmit, validPassword]);
- const isPasswordValid =
- (password !== '' && password.length < cloudBackupPasswordMinLength && !passwordRef?.current?.isFocused()) || incorrectPassword;
-
return (
@@ -248,8 +276,8 @@ export default function RestoreCloudStep() {
;
};
};
-export type useCreateBackupStateType = 'none' | 'loading' | 'success' | 'error';
+type ConfirmBackupProps = {
+ password: string;
+} & UseCreateBackupProps;
-export enum BackupTypes {
- Single = 'single',
- All = 'all',
-}
-
-export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupProps) => {
+export const useCreateBackup = () => {
const dispatch = useDispatch();
const { navigate } = useNavigation();
- const { fetchBackups } = useCloudBackups();
const walletCloudBackup = useWalletCloudBackup();
const { wallets } = useWallets();
- const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]);
- const [loading, setLoading] = useState('none');
-
- const [password, setPassword] = useState('');
const setLoadingStateWithTimeout = useCallback(
- (state: useCreateBackupStateType, resetInMS = 2500) => {
- setLoading(state);
+ ({ state, outOfSync = false, failInMs = 10_000 }: { state: CloudBackupState; outOfSync?: boolean; failInMs?: number }) => {
+ backupsStore.getState().setStatus(state);
+ if (outOfSync) {
+ setTimeout(() => {
+ backupsStore.getState().setStatus(CloudBackupState.Syncing);
+ }, 1_000);
+ }
setTimeout(() => {
- setLoading('none');
- }, resetInMS);
+ const currentState = backupsStore.getState().status;
+ if (currentState === state) {
+ backupsStore.getState().setStatus(CloudBackupState.Ready);
+ }
+ }, failInMs);
},
- [setLoading]
+ []
+ );
+
+ const onSuccess = useCallback(
+ async (password: string) => {
+ if (backupsStore.getState().storedPassword !== password) {
+ await saveLocalBackupPassword(password);
+ }
+ // Reset the storedPassword state for next backup
+ backupsStore.getState().setStoredPassword('');
+ analytics.track('Backup Complete', {
+ category: 'backup',
+ label: cloudPlatform,
+ });
+ setLoadingStateWithTimeout({
+ state: CloudBackupState.Success,
+ outOfSync: true,
+ });
+ backupsStore.getState().syncAndFetchBackups();
+ },
+ [setLoadingStateWithTimeout]
);
- const onSuccess = useCallback(async () => {
- const hasSavedPassword = await getLocalBackupPassword();
- if (!hasSavedPassword) {
- await saveLocalBackupPassword(password);
- }
- analytics.track('Backup Complete', {
- category: 'backup',
- label: cloudPlatform,
- });
- setLoadingStateWithTimeout('success');
- fetchBackups();
- }, [setLoadingStateWithTimeout, fetchBackups, password]);
const onError = useCallback(
- (msg: string) => {
+ (msg: string, isDamaged?: boolean) => {
InteractionManager.runAfterInteractions(async () => {
- DelayedAlert({ title: msg }, 500);
- setLoadingStateWithTimeout('error', 5000);
+ if (isDamaged) {
+ showWalletErrorAlert();
+ } else {
+ DelayedAlert({ title: msg }, 500);
+ }
+ setLoadingStateWithTimeout({ state: CloudBackupState.Error });
});
},
[setLoadingStateWithTimeout]
);
const onConfirmBackup = useCallback(
- async ({ password, type }: { password: string; type: BackupTypes }) => {
+ async ({ password, walletId, navigateToRoute }: ConfirmBackupProps) => {
analytics.track('Tapped "Confirm Backup"');
- setLoading('loading');
+ backupsStore.getState().setStatus(CloudBackupState.InProgress);
- if (type === BackupTypes.All) {
+ if (typeof walletId === 'undefined') {
if (!wallets) {
- onError('Error loading wallets. Please try again.');
- setLoading('error');
+ onError(i18n.t(i18n.l.back_up.errors.no_keys_found));
+ backupsStore.getState().setStatus(CloudBackupState.Error);
+ return;
+ }
+
+ const validWallets = Object.fromEntries(Object.entries(wallets).filter(([_, wallet]) => !wallet.damaged));
+ if (Object.keys(validWallets).length === 0) {
+ onError(i18n.t(i18n.l.back_up.errors.no_keys_found), true);
+ backupsStore.getState().setStatus(CloudBackupState.Error);
return;
}
+
backupAllWalletsToCloud({
- wallets: wallets as AllRainbowWallets,
+ wallets: validWallets,
password,
- latestBackup,
onError,
onSuccess,
dispatch,
@@ -94,12 +114,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
return;
}
- if (!walletId) {
- onError('Wallet not found. Please try again.');
- setLoading('error');
- return;
- }
-
await walletCloudBackup({
onError,
onSuccess,
@@ -111,13 +125,13 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
navigate(navigateToRoute.route, navigateToRoute.params || {});
}
},
- [walletId, walletCloudBackup, onError, onSuccess, navigateToRoute, wallets, latestBackup, dispatch, navigate]
+ [walletCloudBackup, onError, wallets, onSuccess, dispatch, navigate]
);
- const getPassword = useCallback(async (): Promise => {
+ const getPassword = useCallback(async (props: UseCreateBackupProps): Promise => {
const password = await getLocalBackupPassword();
if (password) {
- setPassword(password);
+ backupsStore.getState().setStoredPassword(password);
return password;
}
@@ -126,32 +140,36 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
nativeScreen: true,
step: walletBackupStepTypes.backup_cloud,
onSuccess: async (password: string) => {
- setPassword(password);
- resolve(password);
+ return resolve(password);
},
onCancel: async () => {
- resolve(null);
+ return resolve(null);
},
- walletId,
+ ...props,
});
});
- }, [walletId]);
+ }, []);
- const onSubmit = useCallback(
- async ({ type = BackupTypes.Single }: { type?: BackupTypes }) => {
- const password = await getPassword();
- if (password) {
- onConfirmBackup({
- password,
- type,
+ const createBackup = useCallback(
+ async (props: UseCreateBackupProps) => {
+ if (backupsStore.getState().status !== CloudBackupState.Ready) {
+ return false;
+ }
+ const password = await getPassword(props);
+ if (!password) {
+ setLoadingStateWithTimeout({
+ state: CloudBackupState.Ready,
});
- return true;
+ return false;
}
- setLoadingStateWithTimeout('error');
- return false;
+ onConfirmBackup({
+ password,
+ ...props,
+ });
+ return true;
},
[getPassword, onConfirmBackup, setLoadingStateWithTimeout]
);
- return { onSuccess, onError, onSubmit, loading };
+ return createBackup;
};
diff --git a/src/components/fields/PasswordField.tsx b/src/components/fields/PasswordField.tsx
index 0925b29862c..6d28e81e802 100644
--- a/src/components/fields/PasswordField.tsx
+++ b/src/components/fields/PasswordField.tsx
@@ -1,14 +1,37 @@
import React, { forwardRef, useCallback, Ref } from 'react';
-import { useTheme } from '../../theme/ThemeContext';
+import { ThemeContextProps, useTheme } from '../../theme/ThemeContext';
import { Input } from '../inputs';
import { cloudBackupPasswordMinLength } from '@/handlers/cloudBackup';
import { useDimensions } from '@/hooks';
import styled from '@/styled-thing';
-import { padding } from '@/styles';
+import { padding, position } from '@/styles';
import ShadowStack from '@/react-native-shadow-stack';
import { Box } from '@/design-system';
import { TextInput, TextInputProps, View } from 'react-native';
import { IS_IOS, IS_ANDROID } from '@/env';
+import { Icon } from '../icons';
+
+const FieldAccessoryBadgeSize = 22;
+const FieldAccessoryBadgeWrapper = styled(ShadowStack).attrs(
+ ({ theme: { colors, isDarkMode }, color }: { theme: ThemeContextProps; color: string }) => ({
+ ...position.sizeAsObject(FieldAccessoryBadgeSize),
+ borderRadius: FieldAccessoryBadgeSize,
+ shadows: [[0, 4, 12, isDarkMode ? colors.shadow : color, isDarkMode ? 0.1 : 0.4]],
+ })
+)({
+ marginBottom: 12,
+ position: 'absolute',
+ right: 12,
+ top: 12,
+});
+
+function FieldAccessoryBadge({ color, name }: { color: string; name: string }) {
+ return (
+
+
+
+ );
+}
const Container = styled(Box)({
width: '100%',
@@ -53,9 +76,9 @@ interface PasswordFieldProps extends TextInputProps {
}
const PasswordField = forwardRef(
- ({ password, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => {
+ ({ password, isInvalid, returnKeyType = 'done', style, textContentType, ...props }, ref: Ref) => {
const { width: deviceWidth } = useDimensions();
- const { isDarkMode } = useTheme();
+ const { isDarkMode, colors } = useTheme();
const handleFocus = useCallback(() => {
if (ref && 'current' in ref && ref.current) {
@@ -67,6 +90,7 @@ const PasswordField = forwardRef(
+ {isInvalid && }
);
diff --git a/src/components/floating-emojis/FloatingEmojis.js b/src/components/floating-emojis/FloatingEmojis.js
deleted file mode 100644
index f4dc8342f50..00000000000
--- a/src/components/floating-emojis/FloatingEmojis.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Animated, View } from 'react-native';
-import FloatingEmoji from './FloatingEmoji';
-import GravityEmoji from './GravityEmoji';
-import { useTimeout } from '@/hooks';
-import { position } from '@/styles';
-
-const EMPTY_ARRAY = [];
-const getEmoji = emojis => Math.floor(Math.random() * emojis.length);
-const getRandomNumber = (min, max) => Math.random() * (max - min) + min;
-
-const FloatingEmojis = ({
- centerVertically,
- children,
- disableHorizontalMovement,
- disableRainbow,
- disableVerticalMovement,
- distance,
- duration,
- emojis,
- fadeOut,
- gravityEnabled,
- marginTop,
- opacity,
- opacityThreshold,
- range,
- scaleTo,
- setOnNewEmoji,
- size,
- wiggleFactor,
- ...props
-}) => {
- const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]);
- const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY);
- const [startTimeout, stopTimeout] = useTimeout();
- const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []);
-
- // 🚧️ TODO: 🚧️
- // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something)
- // otherwise, the FloatingEmojis look weird during stack transitions
-
- const onNewEmoji = useCallback(
- (x, y) => {
- // Set timeout to automatically clearEmojis after the latest one has finished animating
- stopTimeout();
- startTimeout(clearEmojis, duration * 1.1);
-
- setEmojis(existingEmojis => {
- const newEmoji = {
- // if a user has smashed the button 7 times, they deserve a 🌈 rainbow
- emojiToRender:
- (existingEmojis.length + 1) % 7 === 0 && !disableRainbow
- ? 'rainbow'
- : emojisArray.length === 1
- ? emojisArray[0]
- : emojisArray[getEmoji(emojisArray)],
- x: x ? x - getRandomNumber(-20, 20) : getRandomNumber(...range),
- y: y || 0,
- };
- return [...existingEmojis, newEmoji];
- });
- },
- [clearEmojis, disableRainbow, duration, emojisArray, range, startTimeout, stopTimeout]
- );
-
- useEffect(() => {
- setOnNewEmoji?.(onNewEmoji);
- return () => setOnNewEmoji?.(undefined);
- }, [setOnNewEmoji, onNewEmoji]);
-
- return (
-
- {typeof children === 'function' ? children({ onNewEmoji }) : children}
-
- {gravityEnabled
- ? floatingEmojis.map(({ emojiToRender, x, y }, index) => (
-
- ))
- : floatingEmojis.map(({ emojiToRender, x, y }, index) => (
-
- ))}
-
-
- );
-};
-
-FloatingEmojis.propTypes = {
- centerVertically: PropTypes.bool,
- children: PropTypes.node,
- disableHorizontalMovement: PropTypes.bool,
- disableRainbow: PropTypes.bool,
- disableVerticalMovement: PropTypes.bool,
- distance: PropTypes.number,
- duration: PropTypes.number,
- emojis: PropTypes.arrayOf(PropTypes.string).isRequired,
- fadeOut: PropTypes.bool,
- gravityEnabled: PropTypes.bool,
- marginTop: PropTypes.number,
- opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
- opacityThreshold: PropTypes.number,
- range: PropTypes.arrayOf(PropTypes.number),
- scaleTo: PropTypes.number,
- setOnNewEmoji: PropTypes.func,
- size: PropTypes.string.isRequired,
- wiggleFactor: PropTypes.number,
-};
-
-FloatingEmojis.defaultProps = {
- distance: 130,
- duration: 2000,
- // Defaults the emoji to 👍️ (thumbs up).
- // To view complete list of emojis compatible with this component,
- // head to https://github.com/muan/unicode-emoji-json/blob/master/data-by-emoji.json
- emojis: ['thumbs_up'],
- fadeOut: true,
- opacity: 1,
- range: [0, 80],
- scaleTo: 1,
- size: 30,
- wiggleFactor: 0.5,
-};
-
-export default FloatingEmojis;
diff --git a/src/components/floating-emojis/FloatingEmojis.tsx b/src/components/floating-emojis/FloatingEmojis.tsx
new file mode 100644
index 00000000000..7eccc8b69de
--- /dev/null
+++ b/src/components/floating-emojis/FloatingEmojis.tsx
@@ -0,0 +1,151 @@
+import React, { useCallback, useEffect, useMemo, useState, ReactNode } from 'react';
+import { Animated, View, ViewProps } from 'react-native';
+import FloatingEmoji from './FloatingEmoji';
+import GravityEmoji from './GravityEmoji';
+import { useTimeout } from '@/hooks';
+import { position } from '@/styles';
+import { DebugLayout } from '@/design-system';
+import { DEVICE_HEIGHT, DEVICE_WIDTH } from '@/utils/deviceUtils';
+import { AbsolutePortal } from '../AbsolutePortal';
+
+interface Emoji {
+ emojiToRender: string;
+ x: number;
+ y: number;
+}
+
+interface FloatingEmojisProps extends Omit {
+ centerVertically?: boolean;
+ children?: ReactNode | ((props: { onNewEmoji: (x?: number, y?: number) => void }) => ReactNode);
+ disableHorizontalMovement?: boolean;
+ disableRainbow?: boolean;
+ disableVerticalMovement?: boolean;
+ distance?: number;
+ duration?: number;
+ emojis: string[];
+ fadeOut?: boolean;
+ gravityEnabled?: boolean;
+ marginTop?: number;
+ opacity?: number | Animated.AnimatedInterpolation;
+ opacityThreshold?: number;
+ range?: [number, number];
+ scaleTo?: number;
+ setOnNewEmoji?: (fn: ((x?: number, y?: number) => void) | undefined) => void;
+ size: number;
+ wiggleFactor?: number;
+}
+
+const EMPTY_ARRAY: Emoji[] = [];
+const getEmoji = (emojis: string[]) => Math.floor(Math.random() * emojis.length);
+const getRandomNumber = (min: number, max: number) => Math.random() * (max - min) + min;
+
+const FloatingEmojis: React.FC = ({
+ centerVertically,
+ children,
+ disableHorizontalMovement,
+ disableRainbow,
+ disableVerticalMovement,
+ distance = 130,
+ duration = 2000,
+ emojis,
+ fadeOut = true,
+ gravityEnabled,
+ marginTop,
+ opacity = 1,
+ opacityThreshold,
+ range: [rangeMin, rangeMax] = [0, 80],
+ scaleTo = 1,
+ setOnNewEmoji,
+ size = 30,
+ wiggleFactor = 0.5,
+ style,
+ ...props
+}) => {
+ const emojisArray = useMemo(() => (Array.isArray(emojis) ? emojis : [emojis]), [emojis]);
+ const [floatingEmojis, setEmojis] = useState(EMPTY_ARRAY);
+ const [startTimeout, stopTimeout] = useTimeout();
+ const clearEmojis = useCallback(() => setEmojis(EMPTY_ARRAY), []);
+
+ // 🚧️ TODO: 🚧️
+ // Clear emojis if page navigatorPosition falls below 0.93 (which we should call like `pageTransitionThreshold` or something)
+ // otherwise, the FloatingEmojis look weird during stack transitions
+
+ const onNewEmoji = useCallback(
+ (x?: number, y?: number) => {
+ // Set timeout to automatically clearEmojis after the latest one has finished animating
+ stopTimeout();
+ startTimeout(clearEmojis, duration * 1.1);
+
+ setEmojis(existingEmojis => {
+ const newEmoji = {
+ emojiToRender:
+ (existingEmojis.length + 1) % 7 === 0 && !disableRainbow
+ ? 'rainbow'
+ : emojisArray.length === 1
+ ? emojisArray[0]
+ : emojisArray[getEmoji(emojisArray)],
+ x: x !== undefined ? x - getRandomNumber(-20, 20) : getRandomNumber(rangeMin, rangeMax),
+ y: y || 0,
+ };
+ return [...existingEmojis, newEmoji];
+ });
+ },
+ [clearEmojis, disableRainbow, duration, emojisArray, rangeMin, rangeMax, startTimeout, stopTimeout]
+ );
+
+ useEffect(() => {
+ setOnNewEmoji?.(onNewEmoji);
+ return () => setOnNewEmoji?.(undefined);
+ }, [setOnNewEmoji, onNewEmoji]);
+
+ return (
+
+ {typeof children === 'function' ? children({ onNewEmoji }) : children}
+
+
+ {gravityEnabled
+ ? floatingEmojis.map(({ emojiToRender, x, y }, index) => (
+
+ ))
+ : floatingEmojis.map(({ emojiToRender, x, y }, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default FloatingEmojis;
diff --git a/src/components/floating-emojis/GravityEmoji.tsx b/src/components/floating-emojis/GravityEmoji.tsx
index 2bf06a3901f..0b1de95b47c 100644
--- a/src/components/floating-emojis/GravityEmoji.tsx
+++ b/src/components/floating-emojis/GravityEmoji.tsx
@@ -4,7 +4,9 @@ import { Emoji } from '../text';
interface GravityEmojiProps {
distance: number;
+ duration: number;
emoji: string;
+ index: number;
left: number;
size: number;
top: number;
diff --git a/src/components/remote-promo-sheet/runChecks.ts b/src/components/remote-promo-sheet/runChecks.ts
index f83170eecce..9167de41182 100644
--- a/src/components/remote-promo-sheet/runChecks.ts
+++ b/src/components/remote-promo-sheet/runChecks.ts
@@ -1,7 +1,6 @@
import { IS_TEST } from '@/env';
-import { runFeatureUnlockChecks } from '@/handlers/walletReadyEvents';
+import { runFeaturesLocalCampaignAndBackupChecks } from '@/handlers/walletReadyEvents';
import { logger } from '@/logger';
-import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks';
import { checkForRemotePromoSheet } from '@/components/remote-promo-sheet/checkForRemotePromoSheet';
import { useCallback, useEffect } from 'react';
import { InteractionManager } from 'react-native';
@@ -19,11 +18,7 @@ export const useRunChecks = ({ runChecksOnMount = true, walletReady }: { runChec
return;
}
- const showedFeatureUnlock = await runFeatureUnlockChecks();
- if (showedFeatureUnlock) return;
-
- const showedLocalPromo = await runLocalCampaignChecks();
- if (showedLocalPromo) return;
+ if (await runFeaturesLocalCampaignAndBackupChecks()) return;
if (!remotePromoSheets) {
logger.debug('[useRunChecks]: remote promo sheets is disabled');
diff --git a/src/components/secret-display/SecretDisplaySection.tsx b/src/components/secret-display/SecretDisplaySection.tsx
index 0ef93ba05e6..3cd37f05611 100644
--- a/src/components/secret-display/SecretDisplaySection.tsx
+++ b/src/components/secret-display/SecretDisplaySection.tsx
@@ -1,5 +1,4 @@
import { RouteProp, useRoute } from '@react-navigation/native';
-import { captureException } from '@sentry/react-native';
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import { createdWithBiometricError, identifyWalletType, loadPrivateKey, loadSeedPhraseAndMigrateIfNeeded } from '@/model/wallet';
import ActivityIndicator from '../ActivityIndicator';
@@ -25,6 +24,7 @@ import { useNavigation } from '@/navigation';
import { ImgixImage } from '../images';
import RoutesWithPlatformDifferences from '@/navigation/routesNames';
import { Source } from 'react-native-fast-image';
+import { backupsStore } from '@/state/backups/backups';
const MIN_HEIGHT = 740;
@@ -63,6 +63,9 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }:
const { colors } = useTheme();
const { params } = useRoute>();
const { selectedWallet, wallets } = useWallets();
+ const { backupProvider } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ }));
const { onManuallyBackupWalletId } = useWalletManualBackup();
const { navigate } = useNavigation();
@@ -124,9 +127,12 @@ export function SecretDisplaySection({ onSecretLoaded, onWalletTypeIdentified }:
const handleConfirmSaved = useCallback(() => {
if (backupType === WalletBackupTypes.manual) {
onManuallyBackupWalletId(walletId);
+ if (!backupProvider) {
+ backupsStore.getState().setBackupProvider(WalletBackupTypes.manual);
+ }
navigate(RoutesWithPlatformDifferences.SETTINGS_SECTION_BACKUP);
}
- }, [backupType, walletId, onManuallyBackupWalletId, navigate]);
+ }, [backupType, onManuallyBackupWalletId, walletId, backupProvider, navigate]);
const getIconForBackupType = useCallback(() => {
if (isBackingUp) {
diff --git a/src/design-system/components/Inline/Inline.tsx b/src/design-system/components/Inline/Inline.tsx
index 5754bae6a93..3f93791cbc8 100644
--- a/src/design-system/components/Inline/Inline.tsx
+++ b/src/design-system/components/Inline/Inline.tsx
@@ -51,7 +51,7 @@ export function Inline({
>
{wrap || !separator
? children
- : Children.map(children, (child, index) => {
+ : Children.toArray(children).map((child, index) => {
if (!child) return null;
return (
<>
diff --git a/src/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..b62749da519 100644
--- a/src/handlers/walletReadyEvents.ts
+++ b/src/handlers/walletReadyEvents.ts
@@ -1,4 +1,3 @@
-import { IS_TESTING } from 'react-native-dotenv';
import { triggerOnSwipeLayout } from '../navigation/onNavigationStateChange';
import { getKeychainIntegrityState } from './localstorage/globalSettings';
import { runLocalCampaignChecks } from '@/components/remote-promo-sheet/localCampaignChecks';
@@ -6,18 +5,15 @@ import { EthereumAddress } from '@/entities';
import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes';
import WalletTypes from '@/helpers/walletTypes';
import { featureUnlockChecks } from '@/featuresToUnlock';
-import { AllRainbowWallets, RainbowAccount, RainbowWallet } from '@/model/wallet';
+import { AllRainbowWallets, RainbowAccount } from '@/model/wallet';
import { Navigation } from '@/navigation';
import store from '@/redux/store';
import { checkKeychainIntegrity } from '@/redux/wallets';
import Routes from '@/navigation/routesNames';
import { logger } from '@/logger';
-import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils';
-import walletBackupTypes from '@/helpers/walletBackupTypes';
-import { InteractionManager } from 'react-native';
-
-const BACKUP_SHEET_DELAY_MS = 3000;
+import { IS_TEST } from '@/env';
+import { backupsStore, LoadingStates } from '@/state/backups/backups';
export const runKeychainIntegrityChecks = async () => {
const keychainIntegrityState = await getKeychainIntegrityState();
@@ -26,60 +22,38 @@ export const runKeychainIntegrityChecks = async () => {
}
};
-export const runWalletBackupStatusChecks = () => {
- const {
- selected,
- wallets,
- }: {
- wallets: AllRainbowWallets | null;
- selected: RainbowWallet | undefined;
- } = store.getState().wallets;
-
- // count how many visible, non-imported and non-readonly wallets are not backed up
- if (!wallets) return;
-
- const { backupProvider } = checkWalletsForBackupStatus(wallets);
-
- const rainbowWalletsNotBackedUp = Object.values(wallets).filter(wallet => {
- const hasVisibleAccount = wallet.addresses?.find((account: RainbowAccount) => account.visible);
- return (
- !wallet.imported &&
- !!hasVisibleAccount &&
- wallet.type !== WalletTypes.readOnly &&
- wallet.type !== WalletTypes.bluetooth &&
- !wallet.backedUp
- );
+const delay = (ms: number) =>
+ new Promise(resolve => {
+ setTimeout(resolve, ms);
});
- if (!rainbowWalletsNotBackedUp.length) return;
-
- logger.debug('[walletReadyEvents]: there is a rainbow wallet not backed up');
+const promptForBackupOnceReadyOrNotAvailable = async (): Promise => {
+ const { status } = backupsStore.getState();
+ if (LoadingStates.includes(status)) {
+ await delay(1000);
+ return promptForBackupOnceReadyOrNotAvailable();
+ }
- const hasSelectedWallet = rainbowWalletsNotBackedUp.find(notBackedUpWallet => notBackedUpWallet.id === selected!.id);
- logger.debug('[walletReadyEvents]: rainbow wallet not backed up that is selected?', {
- hasSelectedWallet,
- });
+ logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`);
+ triggerOnSwipeLayout(() =>
+ Navigation.handleAction(Routes.BACKUP_SHEET, {
+ step: WalletBackupStepTypes.backup_prompt,
+ })
+ );
+ return true;
+};
- // if one of them is selected, show the default BackupSheet
- if (selected && hasSelectedWallet && IS_TESTING !== 'true') {
- let stepType: string = WalletBackupStepTypes.no_provider;
- if (backupProvider === walletBackupTypes.cloud) {
- stepType = WalletBackupStepTypes.backup_now_to_cloud;
- } else if (backupProvider === walletBackupTypes.manual) {
- stepType = WalletBackupStepTypes.backup_now_manually;
- }
+export const runWalletBackupStatusChecks = async (): Promise => {
+ const { selected } = store.getState().wallets;
+ if (!selected || IS_TEST) return false;
- setTimeout(() => {
- logger.debug(`[walletReadyEvents]: showing ${stepType} backup sheet for selected wallet`);
- triggerOnSwipeLayout(() =>
- Navigation.handleAction(Routes.BACKUP_SHEET, {
- step: stepType,
- })
- );
- }, BACKUP_SHEET_DELAY_MS);
- return;
+ const selectedWalletNeedsBackedUp =
+ !selected.backedUp && !selected.damaged && selected.type !== WalletTypes.readOnly && selected.type !== WalletTypes.bluetooth;
+ if (selectedWalletNeedsBackedUp) {
+ logger.debug('[walletReadyEvents]: Selected wallet is not backed up, prompting backup sheet');
+ return promptForBackupOnceReadyOrNotAvailable();
}
- return;
+ return false;
};
export const runFeatureUnlockChecks = async (): Promise => {
@@ -107,19 +81,24 @@ export const runFeatureUnlockChecks = async (): Promise => {
// short circuits once the first feature is unlocked
for (const featureUnlockCheck of featureUnlockChecks) {
- InteractionManager.runAfterInteractions(async () => {
- const unlockNow = await featureUnlockCheck(walletsToCheck);
- if (unlockNow) {
- return true;
- }
- });
+ const unlockNow = await featureUnlockCheck(walletsToCheck);
+ if (unlockNow) {
+ return true;
+ }
}
return false;
};
-export const runFeatureAndLocalCampaignChecks = async () => {
- const showingFeatureUnlock: boolean = await runFeatureUnlockChecks();
- if (!showingFeatureUnlock) {
- await runLocalCampaignChecks();
+export const runFeaturesLocalCampaignAndBackupChecks = async () => {
+ if (await runFeatureUnlockChecks()) {
+ return true;
}
+ if (await runLocalCampaignChecks()) {
+ return true;
+ }
+ if (await runWalletBackupStatusChecks()) {
+ return true;
+ }
+
+ return false;
};
diff --git a/src/helpers/walletBackupStepTypes.ts b/src/helpers/walletBackupStepTypes.ts
index d3afc9598a2..2fbf0cb8f9e 100644
--- a/src/helpers/walletBackupStepTypes.ts
+++ b/src/helpers/walletBackupStepTypes.ts
@@ -1,5 +1,5 @@
export default {
- no_provider: 'no_provider',
+ backup_prompt: 'backup_prompt',
backup_manual: 'backup_manual',
backup_cloud: 'backup_cloud',
restore_from_backup: 'restore_from_backup',
diff --git a/src/helpers/walletLoadingStates.ts b/src/helpers/walletLoadingStates.ts
new file mode 100644
index 00000000000..a9cdd674d2e
--- /dev/null
+++ b/src/helpers/walletLoadingStates.ts
@@ -0,0 +1,10 @@
+import * as i18n from '@/languages';
+
+export const WalletLoadingStates = {
+ BACKING_UP_WALLET: i18n.t('loading.backing_up'),
+ CREATING_WALLET: i18n.t('loading.creating_wallet'),
+ IMPORTING_WALLET: i18n.t('loading.importing_wallet'),
+ RESTORING_WALLET: i18n.t('loading.restoring'),
+} as const;
+
+export type WalletLoadingStates = (typeof WalletLoadingStates)[keyof typeof WalletLoadingStates];
diff --git a/src/hooks/useActiveRoute.ts b/src/hooks/useActiveRoute.ts
new file mode 100644
index 00000000000..523eb741004
--- /dev/null
+++ b/src/hooks/useActiveRoute.ts
@@ -0,0 +1,16 @@
+import { Navigation, useNavigation } from '@/navigation';
+import { useEffect, useState } from 'react';
+
+export const useActiveRoute = () => {
+ const { addListener } = useNavigation();
+ const [activeRoute, setActiveRoute] = useState(Navigation.getActiveRoute());
+
+ useEffect(() => {
+ const unsubscribe = addListener('state', () => {
+ setActiveRoute(Navigation.getActiveRoute());
+ });
+ return unsubscribe;
+ }, [addListener]);
+
+ return activeRoute?.name;
+};
diff --git a/src/hooks/useCloudBackups.ts b/src/hooks/useCloudBackups.ts
deleted file mode 100644
index 506e669c682..00000000000
--- a/src/hooks/useCloudBackups.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useEffect, useState } from 'react';
-import type { BackupUserData, CloudBackups } from '../model/backup';
-import { fetchAllBackups, fetchUserDataFromCloud, isCloudBackupAvailable, syncCloud } from '@/handlers/cloudBackup';
-import { RainbowError, logger } from '@/logger';
-
-export const enum CloudBackupStep {
- IDLE,
- SYNCING,
- FETCHING_USER_DATA,
- FETCHING_ALL_BACKUPS,
- FAILED,
-}
-
-export default function useCloudBackups() {
- const [isFetching, setIsFetching] = useState(false);
- const [backups, setBackups] = useState({
- files: [],
- });
-
- const [step, setStep] = useState(CloudBackupStep.SYNCING);
-
- const [userData, setUserData] = useState();
-
- const fetchBackups = async () => {
- try {
- setIsFetching(true);
- const isAvailable = isCloudBackupAvailable();
- if (!isAvailable) {
- logger.debug('[useCloudBackups]: Cloud backup is not available');
- setIsFetching(false);
- setStep(CloudBackupStep.IDLE);
- return;
- }
-
- setStep(CloudBackupStep.SYNCING);
- logger.debug('[useCloudBackups]: Syncing with cloud');
- await syncCloud();
-
- setStep(CloudBackupStep.FETCHING_USER_DATA);
- logger.debug('[useCloudBackups]: Fetching user data');
- const userData = await fetchUserDataFromCloud();
- setUserData(userData);
-
- setStep(CloudBackupStep.FETCHING_ALL_BACKUPS);
- logger.debug('[useCloudBackups]: Fetching all backups');
- const backups = await fetchAllBackups();
-
- logger.debug(`[useCloudBackups]: Retrieved ${backups.files.length} backup files`);
- setBackups(backups);
- setStep(CloudBackupStep.IDLE);
- } catch (e) {
- setStep(CloudBackupStep.FAILED);
- logger.error(new RainbowError('[useCloudBackups]: Failed to fetch all backups'), {
- error: e,
- });
- }
- setIsFetching(false);
- };
-
- useEffect(() => {
- fetchBackups();
- }, []);
-
- return {
- isFetching,
- backups,
- fetchBackups,
- userData,
- step,
- };
-}
diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts
index d0f92e12a3a..f1cde6313da 100644
--- a/src/hooks/useImportingWallet.ts
+++ b/src/hooks/useImportingWallet.ts
@@ -3,7 +3,6 @@ import lang from 'i18n-js';
import { keys } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { InteractionManager, Keyboard, TextInput } from 'react-native';
-import { IS_TESTING } from 'react-native-dotenv';
import { useDispatch } from 'react-redux';
import useAccountSettings from './useAccountSettings';
import { fetchENSAvatar } from './useENSAvatar';
@@ -29,9 +28,9 @@ import { deriveAccountFromWalletInput } from '@/utils/wallet';
import { logger, RainbowError } from '@/logger';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';
-import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils';
-import walletBackupTypes from '@/helpers/walletBackupTypes';
import { ChainId } from '@/state/backendNetworks/types';
+import { backupsStore } from '@/state/backups/backups';
+import { IS_TEST } from '@/env';
export default function useImportingWallet({ showImportModal = true } = {}) {
const { accountAddress } = useAccountSettings();
@@ -52,6 +51,10 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
const { updateWalletENSAvatars } = useWalletENSAvatar();
const profilesEnabled = useExperimentalFlag(PROFILES);
+ const { backupProvider } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ }));
+
const inputRef = useRef(null);
const { handleFocus } = useMagicAutofocus(inputRef);
@@ -291,7 +294,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
image,
true
);
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(walletsLoadState());
handleSetImporting(false);
} else {
const previousWalletCount = keys(wallets).length;
@@ -346,19 +349,11 @@ 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;
- } else if (backupProvider === walletBackupTypes.manual) {
- stepType = WalletBackupStepTypes.backup_now_manually;
- }
-
- IS_TESTING !== 'true' &&
+ if (!IS_TEST) {
Navigation.handleAction(Routes.BACKUP_SHEET, {
- step: stepType,
+ step: WalletBackupStepTypes.backup_prompt,
});
+ }
}
}, 1000);
@@ -414,6 +409,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) {
showImportModal,
profilesEnabled,
dangerouslyGetParent,
+ backupProvider,
]);
return {
diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts
index 5f934050e9d..80aa4e903ea 100644
--- a/src/hooks/useInitializeWallet.ts
+++ b/src/hooks/useInitializeWallet.ts
@@ -68,7 +68,7 @@ export default function useInitializeWallet() {
if (shouldRunMigrations && !seedPhrase) {
logger.debug('[useInitializeWallet]: shouldRunMigrations && !seedPhrase? => true');
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(walletsLoadState());
logger.debug('[useInitializeWallet]: walletsLoadState call #1');
await runMigrations();
logger.debug('[useInitializeWallet]: done with migrations');
@@ -110,7 +110,7 @@ export default function useInitializeWallet() {
if (seedPhrase || isNew) {
logger.debug('[useInitializeWallet]: walletsLoadState call #2');
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(walletsLoadState());
}
if (isNil(walletAddress)) {
diff --git a/src/hooks/useManageCloudBackups.ts b/src/hooks/useManageCloudBackups.ts
index 141f26b7f4e..323fd1d62db 100644
--- a/src/hooks/useManageCloudBackups.ts
+++ b/src/hooks/useManageCloudBackups.ts
@@ -3,12 +3,21 @@ import lang from 'i18n-js';
import { useDispatch } from 'react-redux';
import { cloudPlatform } from '../utils/platform';
import { WrappedAlert as Alert } from '@/helpers/alert';
-import { GoogleDriveUserData, getGoogleAccountUserData, deleteAllBackups, logoutFromGoogleDrive } from '@/handlers/cloudBackup';
-import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets';
+import {
+ GoogleDriveUserData,
+ getGoogleAccountUserData,
+ deleteAllBackups,
+ logoutFromGoogleDrive as logout,
+ login,
+} from '@/handlers/cloudBackup';
+import { clearAllWalletsBackupStatus } from '@/redux/wallets';
import { showActionSheetWithOptions } from '@/utils';
import { IS_ANDROID } from '@/env';
import { RainbowError, logger } from '@/logger';
import * as i18n from '@/languages';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
+import * as keychain from '@/keychain';
+import { authenticateWithPIN } from '@/handlers/authentication';
export default function useManageCloudBackups() {
const dispatch = useDispatch();
@@ -48,10 +57,21 @@ export default function useManageCloudBackups() {
await dispatch(clearAllWalletsBackupStatus());
};
+ const logoutFromGoogleDrive = async () => {
+ await logout();
+ backupsStore.setState({
+ backupProvider: undefined,
+ backups: { files: [] },
+ mostRecentBackup: undefined,
+ status: CloudBackupState.NotAvailable,
+ });
+ };
+
const loginToGoogleDrive = async () => {
- await dispatch(updateWalletBackupStatusesBasedOnCloudUserData());
try {
+ await login();
const accountDetails = await getGoogleAccountUserData();
+ backupsStore.getState().syncAndFetchBackups();
setAccountDetails(accountDetails ?? undefined);
} catch (error) {
logger.error(new RainbowError(`[useManageCloudBackups]: Logging into Google Drive failed.`), {
@@ -78,14 +98,36 @@ export default function useManageCloudBackups() {
},
async (buttonIndex: any) => {
if (buttonIndex === 0) {
- if (IS_ANDROID) {
- logoutFromGoogleDrive();
- setAccountDetails(undefined);
- }
- removeBackupStateFromAllWallets();
+ try {
+ let userPIN: string | undefined;
+ const hasBiometricsEnabled = await keychain.getSupportedBiometryType();
+ if (IS_ANDROID && !hasBiometricsEnabled) {
+ try {
+ userPIN = (await authenticateWithPIN()) ?? undefined;
+ } catch (e) {
+ Alert.alert(i18n.t(i18n.l.back_up.wrong_pin));
+ return;
+ }
+ }
- await deleteAllBackups();
- Alert.alert(lang.t('back_up.backup_deleted_successfully'));
+ // Prompt for authentication before allowing them to delete backups
+ await keychain.getAllKeys();
+
+ if (IS_ANDROID) {
+ logoutFromGoogleDrive();
+ setAccountDetails(undefined);
+ }
+ removeBackupStateFromAllWallets();
+
+ await deleteAllBackups();
+ Alert.alert(lang.t('back_up.backup_deleted_successfully'));
+ } catch (e) {
+ logger.error(new RainbowError(`[useManageCloudBackups]: Error deleting all backups`), {
+ error: (e as Error).message,
+ });
+
+ Alert.alert(lang.t('back_up.errors.keychain_access'));
+ }
}
}
);
@@ -94,7 +136,7 @@ export default function useManageCloudBackups() {
if (_buttonIndex === 1 && IS_ANDROID) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets().then(() => loginToGoogleDrive());
+ loginToGoogleDrive();
}
}
);
diff --git a/src/hooks/useUpdateEmoji.ts b/src/hooks/useUpdateEmoji.ts
index d38f229ae20..7a6781788b0 100644
--- a/src/hooks/useUpdateEmoji.ts
+++ b/src/hooks/useUpdateEmoji.ts
@@ -17,11 +17,11 @@ export default function useUpdateEmoji() {
const saveInfo = useCallback(
async (name: string, color: number) => {
const walletId = selectedWallet.id;
- const newWallets: typeof wallets = {
+ const newWallets = {
...wallets,
[walletId]: {
...wallets![walletId],
- addresses: wallets![walletId].addresses.map((singleAddress: { address: string }) =>
+ addresses: wallets![walletId].addresses.map(singleAddress =>
singleAddress.address.toLowerCase() === accountAddress.toLowerCase()
? {
...singleAddress,
diff --git a/src/hooks/useWalletCloudBackup.ts b/src/hooks/useWalletCloudBackup.ts
index 57b9caac681..cb5d6350a5e 100644
--- a/src/hooks/useWalletCloudBackup.ts
+++ b/src/hooks/useWalletCloudBackup.ts
@@ -1,16 +1,14 @@
-import { captureException } from '@sentry/react-native';
-import lang from 'i18n-js';
import { values } from 'lodash';
-import { useCallback, useMemo } from 'react';
+import { useCallback } from 'react';
import { Linking } from 'react-native';
import { useDispatch } from 'react-redux';
-import { addWalletToCloudBackup, backupWalletToCloud, findLatestBackUp } from '../model/backup';
+import { backupWalletToCloud } from '../model/backup';
import { setWalletBackedUp } from '../redux/wallets';
import { cloudPlatform } from '../utils/platform';
import useWallets from './useWallets';
import { WrappedAlert as Alert } from '@/helpers/alert';
import { analytics } from '@/analytics';
-import { CLOUD_BACKUP_ERRORS, isCloudBackupAvailable } from '@/handlers/cloudBackup';
+import { CLOUD_BACKUP_ERRORS, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
import WalletBackupTypes from '@/helpers/walletBackupTypes';
import { logger, RainbowError } from '@/logger';
import { getSupportedBiometryType } from '@/keychain';
@@ -41,7 +39,6 @@ export function getUserError(e: Error) {
export default function useWalletCloudBackup() {
const dispatch = useDispatch();
const { wallets } = useWallets();
- const latestBackup = useMemo(() => findLatestBackUp(wallets), [wallets]);
const walletCloudBackup = useCallback(
async ({
@@ -52,36 +49,63 @@ export default function useWalletCloudBackup() {
}: {
handleNoLatestBackup?: () => void;
handlePasswordNotFound?: () => void;
- onError?: (error: string) => void;
- onSuccess?: () => void;
+ onError?: (error: string, isDamaged?: boolean) => void;
+ onSuccess?: (password: string) => void;
password: string;
walletId: string;
}): Promise => {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- analytics.track('iCloud not enabled', {
- category: 'backup',
- });
- Alert.alert(lang.t('modal.back_up.alerts.cloud_not_enabled.label'), lang.t('modal.back_up.alerts.cloud_not_enabled.description'), [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- analytics.track('View how to Enable iCloud', {
- category: 'backup',
- });
- },
- text: lang.t('modal.back_up.alerts.cloud_not_enabled.show_me'),
- },
- {
- onPress: () => {
- analytics.track('Ignore how to enable iCloud', {
- category: 'backup',
- });
- },
- style: 'cancel',
- text: lang.t('modal.back_up.alerts.cloud_not_enabled.no_thanks'),
- },
- ]);
+ if (IS_ANDROID) {
+ try {
+ await login();
+ const userData = await getGoogleAccountUserData();
+ if (!userData) {
+ Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
+ return false;
+ }
+ } catch (e) {
+ logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), {
+ error: e,
+ });
+ Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
+ return false;
+ }
+ } else {
+ const isAvailable = await isCloudBackupAvailable();
+ if (!isAvailable) {
+ analytics.track('iCloud not enabled', {
+ category: 'backup',
+ });
+ Alert.alert(
+ i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
+ i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
+ [
+ {
+ onPress: () => {
+ Linking.openURL('https://support.apple.com/en-us/HT204025');
+ analytics.track('View how to Enable iCloud', {
+ category: 'backup',
+ });
+ },
+ text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
+ },
+ {
+ onPress: () => {
+ analytics.track('Ignore how to enable iCloud', {
+ category: 'backup',
+ });
+ },
+ style: 'cancel',
+ text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
+ },
+ ]
+ );
+ return false;
+ }
+ }
+
+ const wallet = wallets?.[walletId];
+ if (wallet?.damaged) {
+ onError?.(i18n.t(i18n.l.back_up.errors.damaged_wallet), true);
return false;
}
@@ -101,23 +125,14 @@ export default function useWalletCloudBackup() {
logger.debug('[useWalletCloudBackup]: password fetched correctly');
let updatedBackupFile = null;
+
try {
- if (!latestBackup) {
- logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${wallets![walletId]}`);
- updatedBackupFile = await backupWalletToCloud({
- password,
- wallet: wallets![walletId],
- userPIN,
- });
- } else {
- logger.debug(`[useWalletCloudBackup]: adding wallet to ${cloudPlatform} backup: ${wallets![walletId]}`);
- updatedBackupFile = await addWalletToCloudBackup({
- password,
- wallet: wallets![walletId],
- filename: latestBackup,
- userPIN,
- });
- }
+ logger.debug(`[useWalletCloudBackup]: backing up to ${cloudPlatform}: ${(wallets || {})[walletId]}`);
+ updatedBackupFile = await backupWalletToCloud({
+ password,
+ wallet: (wallets || {})[walletId],
+ userPIN,
+ });
} catch (e: any) {
const userError = getUserError(e);
!!onError && onError(userError);
@@ -134,7 +149,7 @@ export default function useWalletCloudBackup() {
logger.debug('[useWalletCloudBackup]: backup completed!');
await dispatch(setWalletBackedUp(walletId, WalletBackupTypes.cloud, updatedBackupFile));
logger.debug('[useWalletCloudBackup]: backup saved everywhere!');
- !!onSuccess && onSuccess();
+ !!onSuccess && onSuccess(password);
return true;
} catch (e) {
logger.error(new RainbowError(`[useWalletCloudBackup]: error while trying to save wallet backup state: ${e}`));
@@ -148,7 +163,7 @@ export default function useWalletCloudBackup() {
return false;
},
- [dispatch, latestBackup, wallets]
+ [dispatch, wallets]
);
return walletCloudBackup;
diff --git a/src/hooks/useWallets.ts b/src/hooks/useWallets.ts
index 38363886917..20de06f22a1 100644
--- a/src/hooks/useWallets.ts
+++ b/src/hooks/useWallets.ts
@@ -5,14 +5,12 @@ import { RainbowWallet } from '@/model/wallet';
import { AppState } from '@/redux/store';
const walletSelector = createSelector(
- ({ wallets: { isWalletLoading, selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({
- isWalletLoading,
- selectedWallet: selected as any,
+ ({ wallets: { selected = {} as RainbowWallet, walletNames, wallets } }: AppState) => ({
+ selectedWallet: selected,
walletNames,
wallets,
}),
- ({ isWalletLoading, selectedWallet, walletNames, wallets }) => ({
- isWalletLoading,
+ ({ selectedWallet, walletNames, wallets }) => ({
selectedWallet,
walletNames,
wallets,
@@ -20,13 +18,12 @@ const walletSelector = createSelector(
);
export default function useWallets() {
- const { isWalletLoading, selectedWallet, walletNames, wallets } = useSelector(walletSelector);
+ const { selectedWallet, walletNames, wallets } = useSelector(walletSelector);
return {
isDamaged: selectedWallet?.damaged,
isReadOnlyWallet: selectedWallet.type === WalletTypes.readOnly,
isHardwareWallet: !!selectedWallet.deviceId,
- isWalletLoading,
selectedWallet,
walletNames,
wallets,
diff --git a/src/languages/en_US.json b/src/languages/en_US.json
index 4bfe7a65a0d..13044d44f18 100644
--- a/src/languages/en_US.json
+++ b/src/languages/en_US.json
@@ -96,7 +96,8 @@
"generic": "Error while trying to backup. Error code: %{errorCodes}",
"no_keys_found": "No keys found. Please try again.",
"backup_not_found": "Backup not found. Please try again.",
- "no_account_found": "Unable to retrieve backup files. Make sure you're logged in."
+ "no_account_found": "Unable to retrieve backup files. Make sure you're logged in.",
+ "damaged_wallet": "Unable to backup wallet. Missing keychain data."
},
"wrong_pin": "The PIN code you entered was incorrect and we can't make a backup. Please try again with the correct code.",
"already_backed_up": {
@@ -115,6 +116,8 @@
"no_backups": "No backups found",
"failed_to_fetch_backups": "Failed to fetch backups",
"retry": "Retry",
+ "refresh": "Refresh",
+ "syncing_cloud_store": "Syncing to %{cloudPlatformName}",
"fetching_backups": "Retrieving backups from %{cloudPlatformName}",
"back_up_to_platform": "Back up to %{cloudPlatformName}",
"restore_from_platform": "Restore from %{cloudPlatformName}",
@@ -137,7 +140,7 @@
"choose_backup_cloud_description": "Securely back up your wallet to %{cloudPlatform} so you can restore it if you lose your device or get a new one.",
"choose_backup_manual_description": "Back up your wallet manually by saving your secret phrase in a secure location.",
"enable_cloud_backups_description": "If you prefer to back up your wallets manually, you can do so below.",
- "latest_backup": "Last Backup: %{date}",
+ "latest_backup": "Latest Backup: %{date}",
"back_up_all_wallets_to_cloud": "Back Up All Wallets to %{cloudPlatformName}",
"most_recent_backup": "Most Recent Backup",
"out_of_date": "Out of Date",
@@ -145,6 +148,12 @@
"older_backups": "Older Backups",
"no_older_backups": "No Older Backups",
"older_backups_title": "%{date} at %{time}",
+ "statuses": {
+ "not_enabled": "Not Enabled",
+ "syncing": "Syncing",
+ "out_of_date": "Out of Date",
+ "up_to_date": "Up to Date"
+ },
"password": {
"a_password_youll_remember_part_one": "This password is",
"not": "not",
@@ -1220,6 +1229,12 @@
"check_out_this_wallet": "Check out this wallet's collectibles on 🌈 Rainbow at %{showcaseUrl}"
}
},
+ "loading": {
+ "backing_up": "Backing up...",
+ "creating_wallet": "Creating wallet...",
+ "importing_wallet": "Importing...",
+ "restoring": "Restoring..."
+ },
"message": {
"click_to_copy_to_clipboard": "Click to copy to clipboard",
"coming_soon": "Coming soon...",
diff --git a/src/model/backup.ts b/src/model/backup.ts
index 2eb50a7c297..c838796664d 100644
--- a/src/model/backup.ts
+++ b/src/model/backup.ts
@@ -1,15 +1,23 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
-import { NativeModules } from 'react-native';
+import { NativeModules, Linking } from 'react-native';
import { captureException } from '@sentry/react-native';
import { endsWith } from 'lodash';
-import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup';
+import {
+ CLOUD_BACKUP_ERRORS,
+ encryptAndSaveDataToCloud,
+ getDataFromCloud,
+ isCloudBackupAvailable,
+ getGoogleAccountUserData,
+ login,
+ logoutFromGoogleDrive,
+ normalizeAndroidBackupFilename,
+} from '@/handlers/cloudBackup';
+import { Alert as NativeAlert } from '@/components/alerts';
import WalletBackupTypes from '../helpers/walletBackupTypes';
-import WalletTypes from '../helpers/walletTypes';
-import { Alert } from '@/components/alerts';
import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants';
import * as keychain from '@/model/keychain';
import * as kc from '@/keychain';
-import { AllRainbowWallets, allWalletsVersion, createWallet, RainbowWallet } from './wallet';
+import { AllRainbowWallets, createWallet, RainbowWallet } from './wallet';
import { analytics } from '@/analytics';
import { logger, RainbowError } from '@/logger';
import { IS_ANDROID, IS_DEV } from '@/env';
@@ -24,16 +32,19 @@ import Routes from '@/navigation/routesNames';
import { clearAllStorages } from './mmkv';
import walletBackupStepTypes from '@/helpers/walletBackupStepTypes';
import { getRemoteConfig } from './remoteConfig';
+import { WrappedAlert as Alert } from '@/helpers/alert';
+import { AppDispatch } from '@/redux/store';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
const { DeviceUUID } = NativeModules;
const encryptor = new AesEncryptor();
const PIN_REGEX = /^\d{4}$/;
export interface CloudBackups {
- files: Backup[];
+ files: BackupFile[];
}
-export interface Backup {
+export interface BackupFile {
isDirectory: boolean;
isFile: boolean;
lastModified: string;
@@ -44,8 +55,9 @@ export interface Backup {
}
export const parseTimestampFromFilename = (filename: string) => {
+ const name = normalizeAndroidBackupFilename(filename);
return Number(
- filename
+ name
.replace('.backup_', '')
.replace('backup_', '')
.replace('.json', '')
@@ -54,6 +66,27 @@ export const parseTimestampFromFilename = (filename: string) => {
);
};
+/**
+ * Parse the timestamp from a backup file name
+ * @param filename - The name of the backup file backup_${now}.json
+ * @returns The timestamp as a number
+ */
+export const parseTimestampFromBackupFile = (filename: string | null): number | undefined => {
+ if (!filename) {
+ return;
+ }
+ const match = filename.match(/backup_(\d+)\.json/);
+ if (!match) {
+ return;
+ }
+
+ if (Number.isNaN(Number(match[1]))) {
+ return;
+ }
+
+ return Number(match[1]);
+};
+
type BackupPassword = string;
interface BackedUpData {
@@ -63,9 +96,72 @@ interface BackedUpData {
export interface BackupUserData {
wallets: AllRainbowWallets;
}
+type MaybePromise = T | Promise;
+
+export const executeFnIfCloudBackupAvailable = async ({ fn, logout = false }: { fn: () => MaybePromise; logout?: boolean }) => {
+ backupsStore.getState().setStatus(CloudBackupState.InProgress);
+
+ if (IS_ANDROID) {
+ try {
+ if (logout) {
+ await logoutFromGoogleDrive();
+ }
+
+ const currentUser = await getGoogleAccountUserData();
+ if (!currentUser) {
+ await login();
+ await backupsStore.getState().syncAndFetchBackups();
+ }
+
+ const userData = await getGoogleAccountUserData();
+ if (!userData) {
+ Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
+ backupsStore.getState().setStatus(CloudBackupState.NotAvailable);
+ return;
+ }
+ // execute the function
+
+ // NOTE: Set this back to ready in order to process the backup
+ backupsStore.getState().setStatus(CloudBackupState.Ready);
+ return await fn();
+ } catch (e) {
+ logger.error(new RainbowError('[BackupSheetSectionNoProvider]: No account found'), {
+ error: e,
+ });
+ Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
+ backupsStore.getState().setStatus(CloudBackupState.NotAvailable);
+ }
+ } else {
+ const isAvailable = await isCloudBackupAvailable();
+ if (!isAvailable) {
+ Alert.alert(
+ i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
+ i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
+ [
+ {
+ onPress: () => {
+ Linking.openURL('https://support.apple.com/en-us/HT204025');
+ },
+ text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
+ },
+ {
+ style: 'cancel',
+ text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
+ },
+ ]
+ );
+ backupsStore.getState().setStatus(CloudBackupState.NotAvailable);
+ return;
+ }
+
+ // NOTE: Set this back to ready in order to process the backup
+ backupsStore.getState().setStatus(CloudBackupState.Ready);
+ return await fn();
+ }
+};
async function extractSecretsForWallet(wallet: RainbowWallet) {
- const allKeys = await keychain.loadAllKeys();
+ const allKeys = await kc.getAllKeys();
if (!allKeys) throw new Error(CLOUD_BACKUP_ERRORS.KEYCHAIN_ACCESS_ERROR);
const secrets = {} as { [key: string]: string };
@@ -100,17 +196,15 @@ async function extractSecretsForWallet(wallet: RainbowWallet) {
export async function backupAllWalletsToCloud({
wallets,
password,
- latestBackup,
onError,
onSuccess,
dispatch,
}: {
wallets: AllRainbowWallets;
password: BackupPassword;
- latestBackup: string | null;
onError?: (message: string) => void;
- onSuccess?: () => void;
- dispatch: any;
+ onSuccess?: (password: BackupPassword) => void;
+ dispatch: AppDispatch;
}) {
let userPIN: string | undefined;
const hasBiometricsEnabled = await kc.getSupportedBiometryType();
@@ -126,11 +220,9 @@ export async function backupAllWalletsToCloud({
try {
/**
* Loop over all keys and decrypt if necessary for android
- * if no latest backup, create first backup with all secrets
- * if latest backup, update updatedAt and add new secrets to the backup
*/
- const allKeys = await keychain.loadAllKeys();
+ const allKeys = await kc.getAllKeys();
if (!allKeys) {
onError?.(i18n.t(i18n.l.back_up.errors.no_keys_found));
return;
@@ -157,49 +249,21 @@ export async function backupAllWalletsToCloud({
label: cloudPlatform,
});
- let updatedBackupFile: any = null;
- if (!latestBackup) {
- const data = {
- createdAt: now,
- secrets: {},
- };
- const promises = Object.entries(allSecrets).map(async ([username, password]) => {
- const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN);
-
- data.secrets = {
- ...data.secrets,
- ...processedNewSecrets,
- };
- });
-
- await Promise.all(promises);
- updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`);
- } else {
- // if we have a latest backup file, we need to update the updatedAt and add new secrets to the backup file..
- const backup = await getDataFromCloud(password, latestBackup);
- if (!backup) {
- onError?.(i18n.t(i18n.l.back_up.errors.backup_not_found));
- return;
- }
+ const data = {
+ createdAt: now,
+ secrets: {},
+ };
+ const promises = Object.entries(allSecrets).map(async ([username, password]) => {
+ const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN);
- const data = {
- createdAt: backup.createdAt,
- secrets: backup.secrets,
+ data.secrets = {
+ ...data.secrets,
+ ...processedNewSecrets,
};
+ });
- const promises = Object.entries(allSecrets).map(async ([username, password]) => {
- const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded({ [username]: password }, userPIN);
-
- data.secrets = {
- ...data.secrets,
- ...processedNewSecrets,
- };
- });
-
- await Promise.all(promises);
- updatedBackupFile = await encryptAndSaveDataToCloud(data, password, latestBackup);
- }
-
+ await Promise.all(promises);
+ const updatedBackupFile = await encryptAndSaveDataToCloud(data, password, `backup_${now}.json`);
const walletIdsToUpdate = Object.keys(wallets);
await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, WalletBackupTypes.cloud, updatedBackupFile));
@@ -209,16 +273,18 @@ export async function backupAllWalletsToCloud({
label: cloudPlatform,
});
- onSuccess?.();
- } catch (error: any) {
- const userError = getUserError(error);
- onError?.(userError);
- captureException(error);
- analytics.track(`Error backing up all wallets to ${cloudPlatform}`, {
- category: 'backup',
- error: userError,
- label: cloudPlatform,
- });
+ onSuccess?.(password);
+ } catch (error) {
+ if (error instanceof Error) {
+ const userError = getUserError(error);
+ onError?.(userError);
+ captureException(error);
+ analytics.track(`Error backing up all wallets to ${cloudPlatform}`, {
+ category: 'backup',
+ error: userError,
+ label: cloudPlatform,
+ });
+ }
}
}
@@ -251,9 +317,15 @@ export async function addWalletToCloudBackup({
wallet: RainbowWallet;
filename: string;
userPIN?: string;
-}): Promise {
- // @ts-ignore
+}): Promise {
const backup = await getDataFromCloud(password, filename);
+ if (!backup) {
+ logger.error(new RainbowError('[backup]: Unable to get backup data for filename'), {
+ filename,
+ });
+ return null;
+ }
+
const now = Date.now();
const newSecretsToBeAddedToBackup = await extractSecretsForWallet(wallet);
const processedNewSecrets = await decryptAllPinEncryptedSecretsIfNeeded(newSecretsToBeAddedToBackup, userPIN);
@@ -321,25 +393,6 @@ export async function decryptAllPinEncryptedSecretsIfNeeded(secrets: Record {
- // Check if there's a wallet backed up
- if (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) {
- // If there is one, let's grab the latest backup
- if (!latestBackup || Number(wallet.backupDate) > latestBackup) {
- filename = wallet.backupFile;
- latestBackup = Number(wallet.backupDate);
- }
- }
- });
- }
- return filename;
-}
-
export const RestoreCloudBackupResultStates = {
success: 'success',
failedWhenRestoring: 'failedWhenRestoring',
@@ -368,16 +421,14 @@ const sanitizeFilename = (filename: string) => {
*/
export async function restoreCloudBackup({
password,
- userData,
- nameOfSelectedBackupFile,
+ backupFilename,
}: {
password: BackupPassword;
- userData: BackupUserData | undefined;
- nameOfSelectedBackupFile: string;
+ backupFilename: string;
}): Promise {
try {
// 1 - sanitize filename to remove extra things we don't care about
- const filename = sanitizeFilename(nameOfSelectedBackupFile);
+ const filename = sanitizeFilename(backupFilename);
if (!filename) {
return RestoreCloudBackupResultStates.failedWhenRestoring;
}
@@ -402,26 +453,6 @@ export async function restoreCloudBackup({
}
}
- if (userData) {
- // Restore only wallets that were backed up in cloud
- // or wallets that are read-only
- const walletsToRestore: AllRainbowWallets = {};
- Object.values(userData?.wallets ?? {}).forEach(wallet => {
- if (
- (wallet.backedUp && wallet.backupDate && wallet.backupFile && wallet.backupType === WalletBackupTypes.cloud) ||
- wallet.type === WalletTypes.readOnly
- ) {
- walletsToRestore[wallet.id] = wallet;
- }
- });
-
- // All wallets
- dataToRestore[allWalletsKey] = {
- version: allWalletsVersion,
- wallets: walletsToRestore,
- };
- }
-
const restoredSuccessfully = await restoreSpecificBackupIntoKeychain(dataToRestore, userPIN);
return restoredSuccessfully ? RestoreCloudBackupResultStates.success : RestoreCloudBackupResultStates.failedWhenRestoring;
} catch (error) {
@@ -525,74 +556,6 @@ async function restoreSpecificBackupIntoKeychain(backedUpData: BackedUpData, use
}
}
-async function restoreCurrentBackupIntoKeychain(backedUpData: BackedUpData, newPIN?: string): Promise {
- try {
- // Access control config per each type of key
- const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions();
- const encryptedBackupPinData = backedUpData[pinKey];
- const backupPIN = await decryptPIN(encryptedBackupPinData);
-
- await Promise.all(
- Object.keys(backedUpData).map(async key => {
- let value = backedUpData[key];
- const theKeyIsASeedPhrase = endsWith(key, seedPhraseKey);
- const theKeyIsAPrivateKey = endsWith(key, privateKeyKey);
- const accessControl: typeof kc.publicAccessControlOptions =
- theKeyIsASeedPhrase || theKeyIsAPrivateKey ? privateAccessControlOptions : kc.publicAccessControlOptions;
-
- /*
- * Backups that were saved encrypted with PIN to the cloud need to be
- * decrypted with the backup PIN first, and then if we still need
- * to store them as encrypted,
- * we need to re-encrypt them with a new PIN
- */
- if (theKeyIsASeedPhrase) {
- const parsedValue = JSON.parse(value);
- parsedValue.seedphrase = await decryptSecretFromBackupPin({
- secret: parsedValue.seedphrase,
- backupPIN,
- });
- value = JSON.stringify(parsedValue);
- } else if (theKeyIsAPrivateKey) {
- const parsedValue = JSON.parse(value);
- parsedValue.privateKey = await decryptSecretFromBackupPin({
- secret: parsedValue.privateKey,
- backupPIN,
- });
- value = JSON.stringify(parsedValue);
- }
-
- /*
- * Since we're decrypting the data that was saved as PIN code encrypted,
- * we will allow the user to create a new PIN code.
- * We store the old PIN code in the backup, but we don't want to restore it,
- * since it will override the new PIN code that we just saved to keychain.
- */
- if (key === pinKey) {
- return;
- }
-
- if (typeof value === 'string') {
- return kc.set(key, value, {
- ...accessControl,
- androidEncryptionPin: newPIN,
- });
- } else {
- return kc.setObject(key, value, {
- ...accessControl,
- androidEncryptionPin: newPIN,
- });
- }
- })
- );
-
- return true;
- } catch (e) {
- logger.error(new RainbowError(`[backup]: Error restoring current backup into keychain: ${e}`));
- return false;
- }
-}
-
async function decryptSecretFromBackupPin({ secret, backupPIN }: { secret?: string; backupPIN?: string }) {
let processedSecret = secret;
@@ -638,13 +601,9 @@ export async function saveBackupPassword(password: BackupPassword): Promise {
- const rainbowBackupPassword = await keychain.loadString('RainbowBackupPassword');
- if (typeof rainbowBackupPassword === 'number') {
- return null;
- }
-
- if (rainbowBackupPassword) {
- return rainbowBackupPassword;
+ const { value } = await kc.get('RainbowBackupPassword');
+ if (value) {
+ return value;
}
return await fetchBackupPassword();
@@ -653,7 +612,7 @@ export async function getLocalBackupPassword(): Promise {
export async function saveLocalBackupPassword(password: string) {
const privateAccessControlOptions = await keychain.getPrivateAccessControlOptions();
- await keychain.saveString('RainbowBackupPassword', password, privateAccessControlOptions);
+ await kc.set('RainbowBackupPassword', password, privateAccessControlOptions);
saveBackupPassword(password);
}
@@ -666,7 +625,7 @@ export async function fetchBackupPassword(): Promise {
try {
const { value: results } = await kc.getSharedWebCredentials();
if (results) {
- return results.password as BackupPassword;
+ return results.password;
}
return null;
} catch (e) {
@@ -695,7 +654,7 @@ export async function getDeviceUUID(): Promise {
}
const FailureAlert = () =>
- Alert({
+ NativeAlert({
buttons: [
{
style: 'cancel',
diff --git a/src/navigation/HardwareWalletTxNavigator.tsx b/src/navigation/HardwareWalletTxNavigator.tsx
index 28e290065dc..4b209dabe30 100644
--- a/src/navigation/HardwareWalletTxNavigator.tsx
+++ b/src/navigation/HardwareWalletTxNavigator.tsx
@@ -63,7 +63,7 @@ export const HardwareWalletTxNavigator = () => {
const { navigate } = useNavigation();
- const deviceId = selectedWallet?.deviceId;
+ const deviceId = selectedWallet.deviceId ?? '';
const [isReady, setIsReady] = useRecoilState(LedgerIsReadyAtom);
const [readyForPolling, setReadyForPolling] = useRecoilState(readyForPollingAtom);
const [triggerPollerCleanup, setTriggerPollerCleanup] = useRecoilState(triggerPollerCleanupAtom);
diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx
index 05b09059948..1e03c2d96a3 100644
--- a/src/navigation/Routes.android.tsx
+++ b/src/navigation/Routes.android.tsx
@@ -90,6 +90,8 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane
import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel';
import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel';
import { RootStackParamList } from './types';
+import WalletLoadingListener from '@/components/WalletLoadingListener';
+import { Portal as CMPortal } from '@/react-native-cool-modals/Portal';
import { NetworkSelector } from '@/components/NetworkSwitcher';
const Stack = createStackNavigator();
@@ -213,7 +215,7 @@ function BSNavigator() {
step === walletBackupStepTypes.restore_from_backup
) {
heightForStep = backupSheetSizes.long;
- } else if (step === walletBackupStepTypes.no_provider) {
+ } else if (step === walletBackupStepTypes.backup_prompt) {
heightForStep = backupSheetSizes.medium;
}
@@ -274,6 +276,10 @@ const AppContainerWithAnalytics = React.forwardRef
+
+ {/* NOTE: Internally, these use some navigational checks */}
+
+
));
diff --git a/src/navigation/Routes.ios.tsx b/src/navigation/Routes.ios.tsx
index c07ef6294b5..201eb3aa374 100644
--- a/src/navigation/Routes.ios.tsx
+++ b/src/navigation/Routes.ios.tsx
@@ -103,6 +103,8 @@ import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPane
import { ClaimRewardsPanel } from '@/screens/points/claim-flow/ClaimRewardsPanel';
import { ClaimClaimablePanel } from '@/screens/claimables/ClaimPanel';
import { RootStackParamList } from './types';
+import WalletLoadingListener from '@/components/WalletLoadingListener';
+import { Portal as CMPortal } from '@/react-native-cool-modals/Portal';
import { NetworkSelector } from '@/components/NetworkSwitcher';
const Stack = createStackNavigator();
@@ -289,6 +291,10 @@ const AppContainerWithAnalytics = React.forwardRef
+
+ {/* NOTE: Internally, these use some navigational checks */}
+
+
));
diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx
index 38b7a0032fe..9097c84e1d6 100644
--- a/src/navigation/config.tsx
+++ b/src/navigation/config.tsx
@@ -103,12 +103,10 @@ export const getHeightForStep = (step: string) => {
case WalletBackupStepTypes.backup_manual:
case WalletBackupStepTypes.restore_from_backup:
return backupSheetSizes.long;
- case WalletBackupStepTypes.no_provider:
+ case WalletBackupStepTypes.backup_prompt:
return backupSheetSizes.medium;
case WalletBackupStepTypes.check_identifier:
return backupSheetSizes.check_identifier;
- case WalletBackupStepTypes.backup_now_manually:
- return backupSheetSizes.shorter;
default:
return backupSheetSizes.short;
}
diff --git a/src/react-native-cool-modals/Portal.js b/src/react-native-cool-modals/Portal.js
deleted file mode 100644
index 5d03cdadeb8..00000000000
--- a/src/react-native-cool-modals/Portal.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
-import { Platform, requireNativeComponent, StyleSheet, View } from 'react-native';
-
-const NativePortalContext = createContext();
-
-export function usePortal() {
- return useContext(NativePortalContext);
-}
-
-const NativePortal = Platform.OS === 'ios' ? requireNativeComponent('WindowPortal') : View;
-
-const Wrapper = Platform.OS === 'ios' ? ({ children }) => children : View;
-
-export function Portal({ children }) {
- const [Component, setComponentState] = useState(null);
- const [blockTouches, setBlockTouches] = useState(false);
-
- const hide = useCallback(() => {
- setComponentState();
- setBlockTouches(false);
- }, []);
-
- const setComponent = useCallback((value, blockTouches) => {
- setComponentState(value);
- setBlockTouches(blockTouches);
- }, []);
-
- const contextValue = useMemo(
- () => ({
- hide,
- setComponent,
- }),
- [hide, setComponent]
- );
-
- return (
-
-
- {children}
-
- {Component}
-
-
-
- );
-}
diff --git a/src/react-native-cool-modals/Portal.tsx b/src/react-native-cool-modals/Portal.tsx
new file mode 100644
index 00000000000..dd2830ee0b4
--- /dev/null
+++ b/src/react-native-cool-modals/Portal.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { IS_IOS } from '@/env';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
+import { requireNativeComponent, StyleSheet, View } from 'react-native';
+import Routes from '@/navigation/routesNames';
+import { useActiveRoute } from '@/hooks/useActiveRoute';
+
+const NativePortal = IS_IOS ? requireNativeComponent('WindowPortal') : View;
+const Wrapper = IS_IOS ? ({ children }: { children: React.ReactNode }) => children : View;
+
+export function Portal() {
+ const activeRoute = useActiveRoute();
+
+ const { blockTouches, Component } = walletLoadingStore(state => ({
+ blockTouches: state.blockTouches,
+ Component: state.Component,
+ }));
+
+ if (!Component || (activeRoute === Routes.PIN_AUTHENTICATION_SCREEN && !IS_IOS)) {
+ return null;
+ }
+
+ console.log('blockTouches', blockTouches);
+
+ return (
+
+
+ {Component}
+
+
+ );
+}
diff --git a/src/redux/wallets.ts b/src/redux/wallets.ts
index deb49a5ea9b..d17f8b4c0d8 100644
--- a/src/redux/wallets.ts
+++ b/src/redux/wallets.ts
@@ -3,10 +3,8 @@ import { toChecksumAddress } from 'ethereumjs-util';
import { isEmpty, keys } from 'lodash';
import { Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
-import { backupUserDataIntoCloud, fetchUserDataFromCloud } from '../handlers/cloudBackup';
import { saveKeychainIntegrityState } from '../handlers/localstorage/globalSettings';
import { getWalletNames, saveWalletNames } from '../handlers/localstorage/walletNames';
-import WalletBackupTypes from '../helpers/walletBackupTypes';
import WalletTypes from '../helpers/walletTypes';
import { fetchENSAvatar } from '../hooks/useENSAvatar';
import { hasKey } from '../model/keychain';
@@ -30,6 +28,7 @@ import { AppGetState, AppState } from './store';
import { fetchReverseRecord } from '@/handlers/ens';
import { lightModeThemeColors } from '@/styles';
import { RainbowError, logger } from '@/logger';
+import { parseTimestampFromBackupFile } from '@/model/backup';
// -- Types ---------------------------------------- //
@@ -37,11 +36,6 @@ import { RainbowError, logger } from '@/logger';
* The current state of the `wallets` reducer.
*/
interface WalletsState {
- /**
- * The current loading state of the wallet.
- */
- isWalletLoading: any;
-
/**
* The currently selected wallet.
*/
@@ -62,21 +56,12 @@ interface WalletsState {
* An action for the `wallets` reducer.
*/
type WalletsAction =
- | WalletsSetIsLoadingAction
| WalletsSetSelectedAction
| WalletsUpdateAction
| WalletsUpdateNamesAction
| WalletsLoadAction
| WalletsAddedAccountAction;
-/**
- * An action that sets the wallet loading state.
- */
-interface WalletsSetIsLoadingAction {
- type: typeof WALLETS_SET_IS_LOADING;
- payload: WalletsState['isWalletLoading'];
-}
-
/**
* An action that sets the selected wallet.
*/
@@ -130,90 +115,88 @@ const WALLETS_SET_SELECTED = 'wallets/SET_SELECTED';
/**
* Loads wallet information from storage and updates state accordingly.
*/
-export const walletsLoadState =
- (profilesEnabled = false) =>
- async (dispatch: ThunkDispatch, getState: AppGetState) => {
- try {
- const { accountAddress } = getState().settings;
- let addressFromKeychain: string | null = accountAddress;
- const allWalletsResult = await getAllWallets();
- const wallets = allWalletsResult?.wallets || {};
- if (isEmpty(wallets)) return;
- const selected = await getSelectedWallet();
- // Prevent irrecoverable state (no selected wallet)
- let selectedWallet = selected?.wallet;
- // Check if the selected wallet is among all the wallets
- if (selectedWallet && !wallets[selectedWallet.id]) {
- // If not then we should clear it and default to the first one
- const firstWalletKey = Object.keys(wallets)[0];
- selectedWallet = wallets[firstWalletKey];
- await setSelectedWallet(selectedWallet);
- }
+export const walletsLoadState = () => async (dispatch: ThunkDispatch, getState: AppGetState) => {
+ try {
+ const { accountAddress } = getState().settings;
+ let addressFromKeychain: string | null = accountAddress;
+ const allWalletsResult = await getAllWallets();
+ const wallets = allWalletsResult?.wallets || {};
+ if (isEmpty(wallets)) return;
+ const selected = await getSelectedWallet();
+ // Prevent irrecoverable state (no selected wallet)
+ let selectedWallet = selected?.wallet;
+ // Check if the selected wallet is among all the wallets
+ if (selectedWallet && !wallets[selectedWallet.id]) {
+ // If not then we should clear it and default to the first one
+ const firstWalletKey = Object.keys(wallets)[0];
+ selectedWallet = wallets[firstWalletKey];
+ await setSelectedWallet(selectedWallet);
+ }
- if (!selectedWallet) {
- const address = await loadAddress();
- if (!address) {
- selectedWallet = wallets[Object.keys(wallets)[0]];
- } else {
- keys(wallets).some(key => {
- const someWallet = wallets[key];
- const found = (someWallet.addresses || []).some(account => {
- return toChecksumAddress(account.address) === toChecksumAddress(address!);
- });
- if (found) {
- selectedWallet = someWallet;
- logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result');
- }
- return found;
+ if (!selectedWallet) {
+ const address = await loadAddress();
+ if (!address) {
+ selectedWallet = wallets[Object.keys(wallets)[0]];
+ } else {
+ keys(wallets).some(key => {
+ const someWallet = wallets[key];
+ const found = (someWallet.addresses || []).some(account => {
+ return toChecksumAddress(account.address) === toChecksumAddress(address!);
});
- }
+ if (found) {
+ selectedWallet = someWallet;
+ logger.debug('[redux/wallets]: Found selected wallet based on loadAddress result');
+ }
+ return found;
+ });
}
+ }
- // Recover from broken state (account address not in selected wallet)
- if (!addressFromKeychain) {
- addressFromKeychain = await loadAddress();
- logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress");
- }
+ // Recover from broken state (account address not in selected wallet)
+ if (!addressFromKeychain) {
+ addressFromKeychain = await loadAddress();
+ logger.debug("[redux/wallets]: addressFromKeychain wasn't set on settings so it is being loaded from loadAddress");
+ }
- const selectedAddress = selectedWallet?.addresses.find(a => {
- return a.visible && a.address === addressFromKeychain;
- });
+ const selectedAddress = selectedWallet?.addresses.find(a => {
+ return a.visible && a.address === addressFromKeychain;
+ });
- // Let's select the first visible account if we don't have a selected address
- if (!selectedAddress) {
- const allWallets = Object.values(allWalletsResult?.wallets || {});
- let account = null;
- for (const wallet of allWallets) {
- for (const rainbowAccount of wallet.addresses || []) {
- if (rainbowAccount.visible) {
- account = rainbowAccount;
- break;
- }
+ // Let's select the first visible account if we don't have a selected address
+ if (!selectedAddress) {
+ const allWallets = Object.values(allWalletsResult?.wallets || {});
+ let account = null;
+ for (const wallet of allWallets) {
+ for (const rainbowAccount of wallet.addresses || []) {
+ if (rainbowAccount.visible) {
+ account = rainbowAccount;
+ break;
}
}
- if (!account) return;
- await dispatch(settingsUpdateAccountAddress(account.address));
- await saveAddress(account.address);
- logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one');
}
+ if (!account) return;
+ await dispatch(settingsUpdateAccountAddress(account.address));
+ await saveAddress(account.address);
+ logger.debug('[redux/wallets]: Selected the first visible address because there was not selected one');
+ }
- const walletNames = await getWalletNames();
- dispatch({
- payload: {
- selected: selectedWallet,
- walletNames,
- wallets,
- },
- type: WALLETS_LOAD,
- });
+ const walletNames = await getWalletNames();
+ dispatch({
+ payload: {
+ selected: selectedWallet,
+ walletNames,
+ wallets,
+ },
+ type: WALLETS_LOAD,
+ });
- return wallets;
- } catch (error) {
- logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), {
- message: (error as Error)?.message,
- });
- }
- };
+ return wallets;
+ } catch (error) {
+ logger.error(new RainbowError('[redux/wallets]: Exception during walletsLoadState'), {
+ message: (error as Error)?.message,
+ });
+ }
+};
/**
* Saves new wallets to storage and updates state accordingly.
@@ -252,21 +235,21 @@ export const walletsSetSelected = (wallet: RainbowWallet) => async (dispatch: Di
* @param updateUserMetadata Whether to update user metadata.
*/
export const setAllWalletsWithIdsAsBackedUp =
- (
- walletIds: RainbowWallet['id'][],
- method: RainbowWallet['backupType'],
- backupFile: RainbowWallet['backupFile'] = null,
- updateUserMetadata = true
- ) =>
+ (walletIds: RainbowWallet['id'][], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) =>
async (dispatch: ThunkDispatch, getState: AppGetState) => {
const { wallets, selected } = getState().wallets;
const newWallets = { ...wallets };
+ let backupDate = Date.now();
+ if (backupFile) {
+ backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now();
+ }
+
walletIds.forEach(walletId => {
newWallets[walletId] = {
...newWallets[walletId],
backedUp: true,
- backupDate: Date.now(),
+ backupDate,
backupFile,
backupType: method,
};
@@ -276,17 +259,6 @@ export const setAllWalletsWithIdsAsBackedUp =
if (selected?.id && walletIds.includes(selected?.id)) {
await dispatch(walletsSetSelected(newWallets[selected.id]));
}
-
- if (method === WalletBackupTypes.cloud && updateUserMetadata) {
- try {
- await backupUserDataIntoCloud({ wallets: newWallets });
- } catch (e) {
- logger.error(new RainbowError('[redux/wallets]: Saving multiple wallets UserData to cloud failed.'), {
- message: (e as Error)?.message,
- });
- throw e;
- }
- }
};
/**
@@ -296,122 +268,28 @@ export const setAllWalletsWithIdsAsBackedUp =
* @param walletId The ID of the wallet to modify.
* @param method The backup type used.
* @param backupFile The backup file, if present.
- * @param updateUserMetadata Whether to update user metadata.
*/
export const setWalletBackedUp =
- (
- walletId: RainbowWallet['id'],
- method: RainbowWallet['backupType'],
- backupFile: RainbowWallet['backupFile'] = null,
- updateUserMetadata = true
- ) =>
+ (walletId: RainbowWallet['id'], method: RainbowWallet['backupType'], backupFile: RainbowWallet['backupFile'] = null) =>
async (dispatch: ThunkDispatch, getState: AppGetState) => {
const { wallets, selected } = getState().wallets;
const newWallets = { ...wallets };
+ let backupDate = Date.now();
+ if (backupFile) {
+ backupDate = parseTimestampFromBackupFile(backupFile) ?? Date.now();
+ }
newWallets[walletId] = {
...newWallets[walletId],
backedUp: true,
- backupDate: Date.now(),
+ backupDate,
backupFile,
backupType: method,
};
await dispatch(walletsUpdate(newWallets));
- if (selected!.id === walletId) {
+ if (selected?.id === walletId) {
await dispatch(walletsSetSelected(newWallets[walletId]));
}
-
- if (method === WalletBackupTypes.cloud && updateUserMetadata) {
- try {
- await backupUserDataIntoCloud({ wallets: newWallets });
- } catch (e) {
- logger.error(new RainbowError('[redux/wallets]: Saving wallet UserData to cloud failed.'), {
- message: (e as Error)?.message,
- });
- throw e;
- }
- }
- };
-
-/**
- * Grabs user data stored in the cloud and based on this data marks wallets
- * as backed up or not
- */
-export const updateWalletBackupStatusesBasedOnCloudUserData =
- () => async (dispatch: ThunkDispatch, getState: AppGetState) => {
- const { wallets, selected } = getState().wallets;
- const newWallets = { ...wallets };
-
- let currentUserData: { wallets: { [p: string]: RainbowWallet } } | undefined;
- try {
- currentUserData = await fetchUserDataFromCloud();
- } catch (error) {
- logger.error(new RainbowError('[redux/wallets]: There was an error when trying to update wallet backup statuses'), {
- error: (error as Error).message,
- });
- return;
- }
- if (currentUserData === undefined) {
- return;
- }
-
- // build hashmap of address to wallet based on backup metadata
- const addressToWalletLookup = new Map();
- Object.values(currentUserData.wallets).forEach(wallet => {
- wallet.addresses?.forEach(account => {
- addressToWalletLookup.set(account.address, wallet);
- });
- });
-
- /*
- marking wallet as already backed up if all addresses are backed up properly
- and linked to the same wallet
-
- we assume it's not backed up if:
- * we don't have an address in the backup metadata
- * we have an address in the backup metadata, but it's linked to multiple
- wallet ids (should never happen, but that's a sanity check)
- */
- Object.values(newWallets).forEach(wallet => {
- const localWalletId = wallet.id;
-
- let relatedCloudWalletId: string | null = null;
- for (const account of wallet.addresses || []) {
- const walletDataForCurrentAddress = addressToWalletLookup.get(account.address);
- if (!walletDataForCurrentAddress) {
- return;
- }
- if (relatedCloudWalletId === null) {
- relatedCloudWalletId = walletDataForCurrentAddress.id;
- } else if (relatedCloudWalletId !== walletDataForCurrentAddress.id) {
- logger.warn(
- '[redux/wallets]: Wallet address is linked to multiple or different accounts in the cloud backup metadata. It could mean that there is an issue with the cloud backup metadata.'
- );
- return;
- }
- }
-
- if (relatedCloudWalletId === null) {
- return;
- }
-
- // update only if we checked the wallet is actually backed up
- const cloudBackupData = currentUserData?.wallets[relatedCloudWalletId];
- if (cloudBackupData) {
- newWallets[localWalletId] = {
- ...newWallets[localWalletId],
- backedUp: cloudBackupData.backedUp,
- backupDate: cloudBackupData.backupDate,
- backupFile: cloudBackupData.backupFile,
- backupType: cloudBackupData.backupType,
- };
- }
- });
-
- await dispatch(walletsUpdate(newWallets));
- if (selected?.id) {
- await dispatch(walletsSetSelected(newWallets[selected.id]));
- }
};
/**
@@ -706,7 +584,6 @@ export const checkKeychainIntegrity = () => async (dispatch: ThunkDispatch {
switch (action.type) {
- case WALLETS_SET_IS_LOADING:
- return { ...state, isWalletLoading: action.payload };
case WALLETS_SET_SELECTED:
return { ...state, selected: action.payload };
case WALLETS_UPDATE:
diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts
index a749e2f72d0..26db739a248 100644
--- a/src/resources/transactions/consolidatedTransactions.ts
+++ b/src/resources/transactions/consolidatedTransactions.ts
@@ -135,6 +135,7 @@ export function useConsolidatedTransactions(
keepPreviousData: true,
getNextPageParam: lastPage => lastPage?.nextPage,
refetchInterval: CONSOLIDATED_TRANSACTIONS_INTERVAL,
+ enabled: !!address,
retry: 3,
}
);
diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx
index 4b010ff3f16..92712d4241d 100644
--- a/src/screens/AddWalletSheet.tsx
+++ b/src/screens/AddWalletSheet.tsx
@@ -5,11 +5,10 @@ import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import React, { useRef } from 'react';
import * as i18n from '@/languages';
-import { HARDWARE_WALLETS, PROFILES, useExperimentalFlag } from '@/config';
+import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config';
import { analytics, analyticsV2 } from '@/analytics';
-import { InteractionManager, Linking } from 'react-native';
+import { InteractionManager } from 'react-native';
import { createAccountForWallet, walletsLoadState } from '@/redux/wallets';
-import WalletBackupTypes from '@/helpers/walletBackupTypes';
import { createWallet } from '@/model/wallet';
import WalletTypes from '@/helpers/walletTypes';
import { logger, RainbowError } from '@/logger';
@@ -19,20 +18,13 @@ import PairHairwareWallet from '@/assets/PairHardwareWallet.png';
import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png';
import WatchWalletIcon from '@/assets/watchWallet.png';
import { useDispatch } from 'react-redux';
-import {
- backupUserDataIntoCloud,
- getGoogleAccountUserData,
- GoogleDriveUserData,
- isCloudBackupAvailable,
- login,
- logoutFromGoogleDrive,
-} from '@/handlers/cloudBackup';
import showWalletErrorAlert from '@/helpers/support';
import { cloudPlatform } from '@/utils/platform';
-import { IS_ANDROID } from '@/env';
import { RouteProp, useRoute } from '@react-navigation/native';
-import { WrappedAlert as Alert } from '@/helpers/alert';
import { useInitializeWallet, useWallets } from '@/hooks';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet;
@@ -52,7 +44,6 @@ export const AddWalletSheet = () => {
const { goBack, navigate } = useNavigation();
const hardwareWalletsEnabled = useExperimentalFlag(HARDWARE_WALLETS);
- const profilesEnabled = useExperimentalFlag(PROFILES);
const dispatch = useDispatch();
const initializeWallet = useInitializeWallet();
const creatingWallet = useRef();
@@ -83,6 +74,10 @@ export const AddWalletSheet = () => {
},
onCloseModal: async (args: any) => {
if (args) {
+ walletLoadingStore.setState({
+ loadingState: WalletLoadingStates.CREATING_WALLET,
+ });
+
const name = args?.name ?? '';
const color = args?.color ?? null;
// Check if the selected wallet is the primary
@@ -113,31 +108,18 @@ export const AddWalletSheet = () => {
try {
// If we found it and it's not damaged use it to create the new account
if (primaryWalletKey && !wallets?.[primaryWalletKey].damaged) {
- const newWallets = await dispatch(createAccountForWallet(primaryWalletKey, color, name));
+ await dispatch(createAccountForWallet(primaryWalletKey, color, name));
// @ts-ignore
await initializeWallet();
- // If this wallet was previously backed up to the cloud
- // We need to update userData backup so it can be restored too
- if (wallets?.[primaryWalletKey].backedUp && wallets[primaryWalletKey].backupType === WalletBackupTypes.cloud) {
- try {
- await backupUserDataIntoCloud({ wallets: newWallets });
- } catch (e) {
- logger.error(new RainbowError('[AddWalletSheet]: Updating wallet userdata failed after new account creation'), {
- error: e,
- });
- throw e;
- }
- }
-
- // If doesn't exist, we need to create a new wallet
} else {
+ // If doesn't exist, we need to create a new wallet
await createWallet({
color,
name,
clearCallbackOnStartCreation: true,
});
- await dispatch(walletsLoadState(profilesEnabled));
- // @ts-ignore
+ await dispatch(walletsLoadState());
+ // @ts-expect-error - needs refactor to object params
await initializeWallet();
}
} catch (e) {
@@ -149,6 +131,10 @@ export const AddWalletSheet = () => {
showWalletErrorAlert();
}, 1000);
}
+ } finally {
+ walletLoadingStore.setState({
+ loadingState: null,
+ });
}
}
creatingWallet.current = false;
@@ -197,47 +183,11 @@ export const AddWalletSheet = () => {
isFirstWallet,
type: 'seed',
});
- if (IS_ANDROID) {
- try {
- await logoutFromGoogleDrive();
- await login();
-
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (accountDetails) {
- return navigate(Routes.RESTORE_SHEET);
- }
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- });
- } catch (e) {
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- logger.error(new RainbowError('[AddWalletSheet]: Error while trying to restore from cloud'), {
- error: e,
- });
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- navigate(Routes.RESTORE_SHEET);
- }
+ executeFnIfCloudBackupAvailable({
+ fn: () => navigate(Routes.RESTORE_SHEET),
+ logout: true,
+ });
};
const restoreFromCloudDescription = i18n.t(TRANSLATIONS.options.cloud.description_restore_sheet, {
diff --git a/src/screens/RestoreSheet.tsx b/src/screens/RestoreSheet.tsx
index 4a3e324bb65..f8186c86341 100644
--- a/src/screens/RestoreSheet.tsx
+++ b/src/screens/RestoreSheet.tsx
@@ -1,5 +1,5 @@
import { RouteProp, useRoute } from '@react-navigation/native';
-import React, { useMemo } from 'react';
+import React, { useCallback, useMemo } from 'react';
import RestoreCloudStep from '../components/backup/RestoreCloudStep';
import ChooseBackupStep from '@/components/backup/ChooseBackupStep';
import Routes from '@/navigation/routesNames';
diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx
index f7f71b7173f..c634befe7b0 100644
--- a/src/screens/SendSheet.tsx
+++ b/src/screens/SendSheet.tsx
@@ -50,7 +50,7 @@ import Routes from '@/navigation/routesNames';
import styled from '@/styled-thing';
import { borders } from '@/styles';
import { convertAmountAndPriceToNativeDisplay, convertAmountFromNativeValue, formatInputDecimals, lessThan } from '@/helpers/utilities';
-import { deviceUtils, ethereumUtils, getUniqueTokenType, safeAreaInsetValues } from '@/utils';
+import { deviceUtils, ethereumUtils, getUniqueTokenType, isLowerCaseMatch, safeAreaInsetValues } from '@/utils';
import { logger, RainbowError } from '@/logger';
import { IS_ANDROID, IS_IOS } from '@/env';
import { NoResults } from '@/components/list';
@@ -62,13 +62,14 @@ import { getNextNonce } from '@/state/nonces';
import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage';
import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance';
import { REGISTRATION_STEPS } from '@/helpers/ens';
-import { useUserAssetsStore } from '@/state/assets/userAssets';
import { ChainId } from '@/state/backendNetworks/types';
import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks';
import { RootStackParamList } from '@/navigation/types';
import { ThemeContextProps, useTheme } from '@/theme';
import { StaticJsonRpcProvider } from '@ethersproject/providers';
import { Contact } from '@/redux/contacts';
+import { useUserAssetsStore } from '@/state/assets/userAssets';
+import store from '@/redux/store';
const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10);
const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight;
@@ -95,6 +96,17 @@ const SheetContainer = styled(Column).attrs({
});
const validateRecipient = (toAddress?: string, tokenAddress?: string) => {
+ const { wallets } = store.getState().wallets;
+ // check for if the recipient is in a damaged wallet state and prevent
+ if (wallets) {
+ const internalWallet = Object.values(wallets).find(wallet =>
+ wallet.addresses.some(address => isLowerCaseMatch(address.address, toAddress))
+ );
+ if (internalWallet?.damaged) {
+ return false;
+ }
+ }
+
if (!toAddress || toAddress?.toLowerCase() === tokenAddress?.toLowerCase()) {
return false;
}
diff --git a/src/screens/SettingsSheet/SettingsSheet.tsx b/src/screens/SettingsSheet/SettingsSheet.tsx
index 7a68ad83d86..094cdc17456 100644
--- a/src/screens/SettingsSheet/SettingsSheet.tsx
+++ b/src/screens/SettingsSheet/SettingsSheet.tsx
@@ -21,7 +21,6 @@ import { useDimensions } from '@/hooks';
import { SETTINGS_BACKUP_ROUTES } from './components/Backups/routes';
import { IS_ANDROID } from '@/env';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { CloudBackupProvider } from '@/components/backup/CloudBackupProvider';
const Stack = createStackNavigator();
@@ -52,102 +51,100 @@ export function SettingsSheet() {
const memoSettingsOptions = useMemo(() => settingsOptions(colors), [colors]);
return (
-
-
- {({ backgroundColor }) => (
-
+ {({ backgroundColor }) => (
+
+
-
-
- {() => (
-
- )}
-
- {Object.values(SettingsPages).map(
- ({ component, getTitle, key }) =>
- component && (
-
- )
+ {() => (
+
)}
- ({
- cardStyleInterpolator: settingsCardStyleInterpolator,
- title: route.params?.title,
- })}
- />
- ({
- cardStyleInterpolator: settingsCardStyleInterpolator,
- title: route.params?.title,
- })}
- />
- ({
- cardStyleInterpolator: settingsCardStyleInterpolator,
- title: route.params?.title,
- })}
- />
- ({
- cardStyleInterpolator: settingsCardStyleInterpolator,
- title: route.params?.title,
- })}
- />
- ({
- cardStyleInterpolator: settingsCardStyleInterpolator,
- title: route.params?.title,
- })}
- />
-
-
- )}
-
-
+
+ {Object.values(SettingsPages).map(
+ ({ component, getTitle, key }) =>
+ component && (
+
+ )
+ )}
+ ({
+ cardStyleInterpolator: settingsCardStyleInterpolator,
+ title: route.params?.title,
+ })}
+ />
+ ({
+ cardStyleInterpolator: settingsCardStyleInterpolator,
+ title: route.params?.title,
+ })}
+ />
+ ({
+ cardStyleInterpolator: settingsCardStyleInterpolator,
+ title: route.params?.title,
+ })}
+ />
+ ({
+ cardStyleInterpolator: settingsCardStyleInterpolator,
+ title: route.params?.title,
+ })}
+ />
+ ({
+ cardStyleInterpolator: settingsCardStyleInterpolator,
+ title: route.params?.title,
+ })}
+ />
+
+
+ )}
+
);
}
diff --git a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx
index 1b2f4334e8e..ba33ae5da99 100644
--- a/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx
+++ b/src/screens/SettingsSheet/components/Backups/BackUpMenuButton.tsx
@@ -1,4 +1,3 @@
-import { useCreateBackupStateType } from '@/components/backup/useCreateBackup';
import { useTheme } from '@/theme';
import React, { useState, useMemo, useEffect } from 'react';
import * as i18n from '@/languages';
@@ -6,102 +5,103 @@ import MenuItem from '../MenuItem';
import Spinner from '@/components/Spinner';
import { FloatingEmojis } from '@/components/floating-emojis';
import { useDimensions } from '@/hooks';
+import { CloudBackupState } from '@/state/backups/backups';
export const BackUpMenuItem = ({
icon = '',
- loading,
+ backupState,
onPress,
title,
+ disabled,
}: {
icon?: string;
- loading: useCreateBackupStateType;
+ backupState: CloudBackupState;
title: string;
onPress: () => void;
+ disabled?: boolean;
}) => {
const { colors } = useTheme();
const { width: deviceWidth } = useDimensions();
const [emojiTrigger, setEmojiTrigger] = useState void)>(null);
useEffect(() => {
- if (loading === 'success') {
+ if (backupState === CloudBackupState.Success) {
for (let i = 0; i < 20; i++) {
setTimeout(() => {
emojiTrigger?.();
}, 100 * i);
}
}
- }, [emojiTrigger, loading]);
+ }, [emojiTrigger, backupState]);
const accentColor = useMemo(() => {
- switch (loading) {
- case 'success':
+ switch (backupState) {
+ case CloudBackupState.Success:
return colors.green;
- case 'error':
+ case CloudBackupState.Error:
return colors.red;
default:
return undefined;
}
- }, [colors, loading]);
+ }, [colors, backupState]);
const titleText = useMemo(() => {
- switch (loading) {
- case 'loading':
+ switch (backupState) {
+ case CloudBackupState.InProgress:
return i18n.t(i18n.l.back_up.cloud.backing_up);
- case 'success':
+ case CloudBackupState.Success:
return i18n.t(i18n.l.back_up.cloud.backup_success);
- case 'error':
+ case CloudBackupState.Error:
return i18n.t(i18n.l.back_up.cloud.backup_failed);
default:
return title;
}
- }, [loading, title]);
+ }, [backupState, title]);
const localIcon = useMemo(() => {
- switch (loading) {
- case 'success':
+ switch (backupState) {
+ case CloudBackupState.Success:
return '';
- case 'error':
+ case CloudBackupState.Error:
return '';
default:
return icon;
}
- }, [icon, loading]);
+ }, [icon, backupState]);
return (
- <>
- {/* @ts-ignore js */}
-
- {({ onNewEmoji }: { onNewEmoji: () => void }) => (
-
- ) : (
-
- )
- }
- onPress={() => {
- setEmojiTrigger(() => onNewEmoji);
- onPress();
- }}
- size={52}
- titleComponent={}
- />
- )}
-
- >
+
+ {({ onNewEmoji }) => (
+
+ ) : (
+
+ )
+ }
+ onPress={() => {
+ setEmojiTrigger(() => onNewEmoji);
+ onPress();
+ }}
+ size={52}
+ titleComponent={}
+ />
+ )}
+
);
};
diff --git a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
index 1842d3fae2a..90cbdddeff3 100644
--- a/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
+++ b/src/screens/SettingsSheet/components/Backups/ViewCloudBackups.tsx
@@ -5,19 +5,18 @@ import { Text as RNText } from '@/components/text';
import Menu from '../Menu';
import MenuContainer from '../MenuContainer';
import MenuItem from '../MenuItem';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
+import { BackupFile, parseTimestampFromFilename } from '@/model/backup';
import { format } from 'date-fns';
-import { Stack } from '@/design-system';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
-import { IS_ANDROID } from '@/env';
import walletBackupStepTypes from '@/helpers/walletBackupStepTypes';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { Centered } from '@/components/layout';
+import { Page } from '@/components/layout';
import Spinner from '@/components/Spinner';
import ActivityIndicator from '@/components/ActivityIndicator';
-import { cloudPlatform } from '@/utils/platform';
import { useTheme } from '@/theme';
+import { CloudBackupState, LoadingStates, backupsStore } from '@/state/backups/backups';
+import { titleForBackupState } from '../../utils';
+import { Box } from '@/design-system';
const LoadingText = styled(RNText).attrs(({ theme: { colors } }: any) => ({
color: colors.blueGreyDark,
@@ -32,43 +31,14 @@ const ViewCloudBackups = () => {
const { navigate } = useNavigation();
const { colors } = useTheme();
- const { isFetching, backups } = useCloudBackups();
-
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
+ const { status, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
const onSelectCloudBackup = useCallback(
- async (selectedBackup: Backup) => {
+ async (selectedBackup: BackupFile) => {
navigate(Routes.BACKUP_SHEET, {
step: walletBackupStepTypes.restore_from_backup,
selectedBackup,
@@ -77,80 +47,110 @@ const ViewCloudBackups = () => {
[navigate]
);
- return (
-
-
- {!isFetching && !cloudBackups.length && (
-
- } />
-
- )}
+ const renderNoBackupsState = () => (
+ <>
+
+ } />
+
+ >
+ );
+
+ const renderMostRecentBackup = () => {
+ if (!mostRecentBackup) {
+ return null;
+ }
+
+ return (
+
+
+ }
+ onPress={() => onSelectCloudBackup(mostRecentBackup)}
+ size={52}
+ width="full"
+ titleComponent={}
+ />
+
+
+ );
+ };
+
+ const renderOlderBackups = () => (
+ <>
+
+
+ {backups.files
+ .filter(backup => backup.name !== mostRecentBackup?.name)
+ .sort((a, b) => {
+ const timestampA = new Date(parseTimestampFromFilename(a.name)).getTime();
+ const timestampB = new Date(parseTimestampFromFilename(b.name)).getTime();
+ return timestampB - timestampA;
+ })
+ .map(backup => (
+
+
+
+
+ backupsStore.getState().syncAndFetchBackups()}
+ titleComponent={}
+ />
+
+ >
+ );
- {!isFetching && cloudBackups.length && (
- <>
- {mostRecentBackup && (
-
- }
- onPress={() => onSelectCloudBackup(mostRecentBackup)}
- size={52}
- width="full"
- titleComponent={}
- />
-
- )}
+ const renderBackupsList = () => (
+ <>
+ {renderMostRecentBackup()}
+ {renderOlderBackups()}
+ >
+ );
-
- {cloudBackups.map(
- backup =>
- backup.name !== mostRecentBackup?.name && (
- onSelectCloudBackup(backup)}
- size={52}
- width="full"
- titleComponent={
-
- }
- />
- )
- )}
+ const isLoading = LoadingStates.includes(status);
- {cloudBackups.length === 1 && (
- }
- />
- )}
-
- >
- )}
+ if (isLoading) {
+ return (
+
+ {android ? : }
+ {titleForBackupState[status]}
+
+ );
+ }
- {isFetching && (
-
- {android ? : }
- {
-
- {i18n.t(i18n.l.back_up.cloud.fetching_backups, {
- cloudPlatformName: cloudPlatform,
- })}
-
- }
-
- )}
-
+ return (
+
+ {status === CloudBackupState.Ready && !backups.files.length && renderNoBackupsState()}
+ {status === CloudBackupState.Ready && backups.files.length > 0 && renderBackupsList()}
);
};
diff --git a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
index d085c3f62fd..9fddd15964d 100644
--- a/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
+++ b/src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
@@ -29,31 +29,23 @@ import Routes from '@/navigation/routesNames';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { SETTINGS_BACKUP_ROUTES } from './routes';
import { analyticsV2 } from '@/analytics';
-import { InteractionManager, Linking } from 'react-native';
+import { InteractionManager } from 'react-native';
import { useDispatch } from 'react-redux';
-import { createAccountForWallet, walletsLoadState } from '@/redux/wallets';
-import {
- GoogleDriveUserData,
- backupUserDataIntoCloud,
- getGoogleAccountUserData,
- isCloudBackupAvailable,
- login,
-} from '@/handlers/cloudBackup';
+import { createAccountForWallet } from '@/redux/wallets';
import { logger, RainbowError } from '@/logger';
-import { RainbowAccount, createWallet } from '@/model/wallet';
-import { PROFILES, useExperimentalFlag } from '@/config';
+import { RainbowAccount } from '@/model/wallet';
import showWalletErrorAlert from '@/helpers/support';
-import { IS_ANDROID, IS_IOS } from '@/env';
+import { IS_IOS } from '@/env';
import ImageAvatar from '@/components/contacts/ImageAvatar';
-import { useCreateBackup } from '@/components/backup/useCreateBackup';
import { BackUpMenuItem } from './BackUpMenuButton';
-import { checkWalletsForBackupStatus } from '../../utils';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets';
import { format } from 'date-fns';
import { removeFirstEmojiFromString } from '@/helpers/emojiHandler';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { WrappedAlert as Alert } from '@/helpers/alert';
+import { useCreateBackup } from '@/components/backup/useCreateBackup';
+import { backupsStore } from '@/state/backups/backups';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
+import { isWalletBackedUpForCurrentAccount } from '../../utils';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
type ViewWalletBackupParams = {
ViewWalletBackup: { walletId: string; title: string; imported?: boolean };
@@ -126,107 +118,38 @@ const ContextMenuWrapper = ({ children, account, menuConfig, onPressMenuItem }:
const ViewWalletBackup = () => {
const { params } = useRoute>();
- const { backups } = useCloudBackups();
+ const createBackup = useCreateBackup();
+ const { status, backupProvider, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backupProvider: state.backupProvider,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
const { walletId, title: incomingTitle } = params;
const creatingWallet = useRef();
const { isDamaged, wallets } = useWallets();
const wallet = wallets?.[walletId];
const dispatch = useDispatch();
const initializeWallet = useInitializeWallet();
- const profilesEnabled = useExperimentalFlag(PROFILES);
-
- const walletTypeCount: WalletCountPerType = {
- phrase: 0,
- privateKey: 0,
- };
-
- const { lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount });
-
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
-
- const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
const isSecretPhrase = WalletTypes.mnemonic === wallet?.type;
-
const title = wallet?.type === WalletTypes.privateKey ? wallet?.addresses[0].label : incomingTitle;
+ const isBackedUp = isWalletBackedUpForCurrentAccount({
+ backupType: wallet?.backupType,
+ backedUp: wallet?.backedUp,
+ backupFile: wallet?.backupFile,
+ });
const { navigate } = useNavigation();
const [isToastActive, setToastActive] = useRecoilState(addressCopiedToastAtom);
- const { onSubmit, loading } = useCreateBackup({
- walletId,
- });
const backupWalletsToCloud = useCallback(async () => {
- if (IS_ANDROID) {
- try {
- await login();
-
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (accountDetails) {
- return onSubmit({});
- }
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- });
- } catch (e) {
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- logger.error(new RainbowError(`[ViewWalletBackup]: Logging into Google Drive failed`), { error: e });
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- }
-
- onSubmit({});
- }, [onSubmit]);
+ executeFnIfCloudBackupAvailable({
+ fn: () =>
+ createBackup({
+ walletId,
+ }),
+ });
+ }, [createBackup, walletId]);
const onNavigateToSecretWarning = useCallback(() => {
navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, {
@@ -265,36 +188,17 @@ const ViewWalletBackup = () => {
},
onCloseModal: async (args: any) => {
if (args) {
+ walletLoadingStore.setState({
+ loadingState: WalletLoadingStates.CREATING_WALLET,
+ });
+
const name = args?.name ?? '';
const color = args?.color ?? null;
// Check if the selected wallet is the primary
try {
// If we found it and it's not damaged use it to create the new account
if (wallet && !wallet.damaged) {
- const newWallets = await dispatch(createAccountForWallet(wallet.id, color, name));
- // @ts-expect-error - no params
- await initializeWallet();
- // If this wallet was previously backed up to the cloud
- // We need to update userData backup so it can be restored too
- if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) {
- try {
- await backupUserDataIntoCloud({ wallets: newWallets });
- } catch (e) {
- logger.error(new RainbowError(`[ViewWalletBackup]: Updating wallet userdata failed after new account creation`), {
- error: e,
- });
- throw e;
- }
- }
-
- // If doesn't exist, we need to create a new wallet
- } else {
- await createWallet({
- color,
- name,
- clearCallbackOnStartCreation: true,
- });
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(createAccountForWallet(wallet.id, color, name));
// @ts-expect-error - no params
await initializeWallet();
}
@@ -307,6 +211,10 @@ const ViewWalletBackup = () => {
showWalletErrorAlert();
}, 1000);
}
+ } finally {
+ walletLoadingStore.setState({
+ loadingState: null,
+ });
}
}
creatingWallet.current = false;
@@ -324,7 +232,7 @@ const ViewWalletBackup = () => {
error: e,
});
}
- }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, profilesEnabled, wallet]);
+ }, [creatingWallet, dispatch, isDamaged, navigate, initializeWallet, wallet]);
const handleCopyAddress = React.useCallback(
(address: string) => {
@@ -386,7 +294,7 @@ const ViewWalletBackup = () => {
return (
- {!wallet?.backedUp && (
+ {!isBackedUp && (
<>
{
/>
- {backupProvider === walletBackupTypes.cloud && (
+
{
title={i18n.t(i18n.l.back_up.cloud.back_up_all_wallets_to_cloud, {
cloudPlatformName: cloudPlatform,
})}
- loading={loading}
+ backupState={status}
onPress={backupWalletsToCloud}
/>
-
- )}
-
- {backupProvider !== walletBackupTypes.cloud && (
-
}
@@ -456,20 +355,12 @@ const ViewWalletBackup = () => {
titleComponent={}
testID={'back-up-manually'}
/>
-
- )}
+
>
)}
- {wallet?.backedUp && (
+ {isBackedUp && (
<>
{
paddingBottom={{ custom: 24 }}
iconComponent={
}
titleComponent={
{
{
>
)}
-
- }
- onPress={onNavigateToSecretWarning}
- size={52}
- titleComponent={
-
+
+
- }
- />
-
+
+
+ )}
+
+
+
+ }
+ onPress={onNavigateToSecretWarning}
+ size={52}
+ titleComponent={
+
+ }
+ />
+
+
{wallet?.addresses
.filter(a => a.visible)
- .map((account: RainbowAccount) => (
-
- }
- labelComponent={
- account.label.endsWith('.eth') || account.label !== '' ? (
-
- ) : null
- }
- titleComponent={
-
- }
- rightComponent={}
- />
-
- ))}
+ .map((account: RainbowAccount) => {
+ const isNamedOrEns = account.label.endsWith('.eth') || removeFirstEmojiFromString(account.label) !== '';
+ const label = isNamedOrEns ? abbreviations.address(account.address, 3, 5) : undefined;
+ const title = isNamedOrEns
+ ? abbreviations.abbreviateEnsForDisplay(removeFirstEmojiFromString(account.label), 20) ?? ''
+ : abbreviations.address(account.address, 3, 5) ?? '';
+
+ return (
+
+ }
+ labelComponent={label ? : null}
+ titleComponent={}
+ rightComponent={}
+ />
+
+ );
+ })}
{wallet?.type !== WalletTypes.privateKey && (
-
- }
- onPress={onCreateNewWallet}
- size={52}
- titleComponent={}
- />
-
+
+
+ }
+ onPress={onCreateNewWallet}
+ size={52}
+ titleComponent={}
+ />
+
+
)}
diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
index 9823fd2555f..74ec4a4e969 100644
--- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
+++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
@@ -1,5 +1,4 @@
-/* eslint-disable no-nested-ternary */
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useMemo, useRef } from 'react';
import { cloudPlatform } from '@/utils/platform';
import Menu from '../Menu';
import MenuContainer from '../MenuContainer';
@@ -12,11 +11,11 @@ import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes';
import ImageAvatar from '@/components/contacts/ImageAvatar';
import { useENSAvatar, useInitializeWallet, useManageCloudBackups, useWallets } from '@/hooks';
import { useNavigation } from '@/navigation';
-import { abbreviations } from '@/utils';
+import { abbreviations, deviceUtils } from '@/utils';
import { addressHashedEmoji } from '@/utils/profileUtils';
import * as i18n from '@/languages';
-import MenuHeader from '../MenuHeader';
-import { checkWalletsForBackupStatus } from '../../utils';
+import MenuHeader, { StatusType } from '../MenuHeader';
+import { checkLocalWalletsForBackupStatus, isWalletBackedUpForCurrentAccount } from '../../utils';
import { Inline, Text, Box, Stack } from '@/design-system';
import { ContactAvatar } from '@/components/contacts';
import { useTheme } from '@/theme';
@@ -25,26 +24,40 @@ import { backupsCard } from '@/components/cards/utils/constants';
import { WalletCountPerType, useVisibleWallets } from '../../useVisibleWallets';
import { SETTINGS_BACKUP_ROUTES } from './routes';
import { RainbowAccount, createWallet } from '@/model/wallet';
-import { PROFILES, useExperimentalFlag } from '@/config';
import { useDispatch } from 'react-redux';
import { walletsLoadState } from '@/redux/wallets';
import { RainbowError, logger } from '@/logger';
import { IS_ANDROID, IS_IOS } from '@/env';
-import { BackupTypes, useCreateBackup } from '@/components/backup/useCreateBackup';
+import { useCreateBackup } from '@/components/backup/useCreateBackup';
import { BackUpMenuItem } from './BackUpMenuButton';
import { format } from 'date-fns';
import { removeFirstEmojiFromString } from '@/helpers/emojiHandler';
-import { Backup, parseTimestampFromFilename } from '@/model/backup';
-import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
-import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
-import { WrappedAlert as Alert } from '@/helpers/alert';
-import { Linking } from 'react-native';
-import { noop } from 'lodash';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+import { executeFnIfCloudBackupAvailable } from '@/model/backup';
+import { walletLoadingStore } from '@/state/walletLoading/walletLoading';
+import { AbsolutePortalRoot } from '@/components/AbsolutePortal';
+import { FlatList, ScrollView } from 'react-native';
type WalletPillProps = {
account: RainbowAccount;
};
+// constants for the account section
+const menuContainerPadding = 19.5; // 19px is the padding on the left and right of the container but we need 1px more to account for the shadows on each container
+const accountsContainerWidth = deviceUtils.dimensions.width - menuContainerPadding * 4;
+const spaceBetweenAccounts = 4;
+const accountsItemWidth = accountsContainerWidth / 3;
+const basePadding = 16;
+const rowHeight = 36;
+
+const getAccountSectionHeight = (numAccounts: number) => {
+ const rows = Math.ceil(Math.max(1, numAccounts) / 3);
+ const paddingBetween = (rows - 1) * 4;
+
+ return basePadding + rows * rowHeight - paddingBetween;
+};
+
const WalletPill = ({ account }: WalletPillProps) => {
const label = useMemo(() => removeFirstEmojiFromString(account.label), [account.label]);
@@ -58,7 +71,7 @@ const WalletPill = ({ account }: WalletPillProps) => {
key={account.address}
flexDirection="row"
alignItems="center"
- backgroundColor={colors.alpha(colors.grey, 0.4)}
+ backgroundColor={colors.alpha(colors.grey, 0.24)}
borderRadius={23}
shadowColor={isDarkMode ? colors.shadow : colors.alpha(colors.blueGreyDark, 0.1)}
elevation={12}
@@ -67,6 +80,7 @@ const WalletPill = ({ account }: WalletPillProps) => {
paddingLeft={{ custom: 4 }}
paddingRight={{ custom: 8 }}
padding={{ custom: 4 }}
+ width={{ custom: accountsItemWidth }}
>
{ENSAvatar?.imageUrl ? (
@@ -82,27 +96,22 @@ const WalletPill = ({ account }: WalletPillProps) => {
);
};
-const getAccountSectionHeight = (numAccounts: number) => {
- const basePadding = 16;
- const rowHeight = 36;
- const rows = Math.ceil(Math.max(1, numAccounts) / 3);
- const paddingBetween = (rows - 1) * 4;
-
- return basePadding + rows * rowHeight - paddingBetween;
-};
-
export const WalletsAndBackup = () => {
const { navigate } = useNavigation();
const { wallets } = useWallets();
- const profilesEnabled = useExperimentalFlag(PROFILES);
- const { backups } = useCloudBackups();
const dispatch = useDispatch();
- const initializeWallet = useInitializeWallet();
+ const scrollviewRef = useRef(null);
- const { onSubmit, loading } = useCreateBackup({
- walletId: undefined, // NOTE: This is not used when backing up All wallets
- });
+ const createBackup = useCreateBackup();
+ const { status, backupProvider, backups, mostRecentBackup } = backupsStore(state => ({
+ status: state.status,
+ backupProvider: state.backupProvider,
+ backups: state.backups,
+ mostRecentBackup: state.mostRecentBackup,
+ }));
+
+ const initializeWallet = useInitializeWallet();
const { manageCloudBackups } = useManageCloudBackups();
@@ -111,52 +120,15 @@ export const WalletsAndBackup = () => {
privateKey: 0,
};
- const { allBackedUp, backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
+ const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]);
- const { visibleWallets, lastBackupDate } = useVisibleWallets({ wallets, walletTypeCount });
-
- const cloudBackups = backups.files
- .filter(backup => {
- if (IS_ANDROID) {
- return !backup.name.match(/UserData/i);
- }
-
- return backup.isFile && backup.size > 0 && !backup.name.match(/UserData/i);
- })
- .sort((a, b) => {
- return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
- });
-
- const mostRecentBackup = cloudBackups.reduce(
- (prev, current) => {
- if (!current) {
- return prev;
- }
-
- if (!prev) {
- return current;
- }
-
- const prevTimestamp = new Date(prev.lastModified).getTime();
- const currentTimestamp = new Date(current.lastModified).getTime();
- if (currentTimestamp > prevTimestamp) {
- return current;
- }
-
- return prev;
- },
- undefined as Backup | undefined
- );
+ const visibleWallets = useVisibleWallets({ wallets, walletTypeCount });
const sortedWallets = useMemo(() => {
- const notBackedUpSecretPhraseWallets = visibleWallets.filter(
- wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic
- );
- const notBackedUpPrivateKeyWallets = visibleWallets.filter(
- wallet => !wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey
- );
- const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.mnemonic);
- const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.isBackedUp && wallet.type === EthereumWalletType.privateKey);
+ const notBackedUpSecretPhraseWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.mnemonic);
+ const notBackedUpPrivateKeyWallets = visibleWallets.filter(wallet => !wallet.backedUp && wallet.type === EthereumWalletType.privateKey);
+ const backedUpSecretPhraseWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.mnemonic);
+ const backedUpPrivateKeyWallets = visibleWallets.filter(wallet => wallet.backedUp && wallet.type === EthereumWalletType.privateKey);
return [
...notBackedUpSecretPhraseWallets,
@@ -166,48 +138,28 @@ export const WalletsAndBackup = () => {
];
}, [visibleWallets]);
- const backupAllNonBackedUpWalletsTocloud = useCallback(async () => {
- if (IS_ANDROID) {
- try {
- await login();
-
- getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
- if (accountDetails) {
- return onSubmit({ type: BackupTypes.All });
+ const backupAllNonBackedUpWalletsTocloud = useCallback(() => {
+ executeFnIfCloudBackupAvailable({
+ fn: () => createBackup({}),
+ });
+ }, [createBackup]);
+
+ const enableCloudBackups = useCallback(() => {
+ executeFnIfCloudBackupAvailable({
+ fn: async () => {
+ // NOTE: For Android we could be coming from a not-logged-in state, so we
+ // need to check if we have any wallets to back up first.
+ if (IS_ANDROID) {
+ const currentBackups = backupsStore.getState().backups;
+ if (checkLocalWalletsForBackupStatus(wallets, currentBackups).allBackedUp) {
+ return;
}
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- });
- } catch (e) {
- Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
- logger.error(new RainbowError(`[WalletsAndBackup]: Logging into Google Drive failed`), {
- error: e,
- });
- }
- } else {
- const isAvailable = await isCloudBackupAvailable();
- if (!isAvailable) {
- Alert.alert(
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.label),
- i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.description),
- [
- {
- onPress: () => {
- Linking.openURL('https://support.apple.com/en-us/HT204025');
- },
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.show_me),
- },
- {
- style: 'cancel',
- text: i18n.t(i18n.l.modal.back_up.alerts.cloud_not_enabled.no_thanks),
- },
- ]
- );
- return;
- }
- }
-
- onSubmit({ type: BackupTypes.All });
- }, [onSubmit]);
+ }
+ return createBackup({});
+ },
+ logout: true,
+ });
+ }, [createBackup, wallets]);
const onViewCloudBackups = useCallback(async () => {
navigate(SETTINGS_BACKUP_ROUTES.VIEW_CLOUD_BACKUPS, {
@@ -223,13 +175,17 @@ export const WalletsAndBackup = () => {
onCloseModal: async ({ name }: { name: string }) => {
const nameValue = name.trim() !== '' ? name.trim() : '';
try {
+ walletLoadingStore.setState({
+ loadingState: WalletLoadingStates.CREATING_WALLET,
+ });
+
await createWallet({
color: null,
name: nameValue,
clearCallbackOnStartCreation: true,
});
- await dispatch(walletsLoadState(profilesEnabled));
+ await dispatch(walletsLoadState());
// @ts-expect-error - no params
await initializeWallet();
@@ -237,10 +193,15 @@ export const WalletsAndBackup = () => {
logger.error(new RainbowError(`[WalletsAndBackup]: Failed to create new secret phrase`), {
error: err,
});
+ } finally {
+ walletLoadingStore.setState({
+ loadingState: null,
+ });
+ scrollviewRef.current?.scrollTo({ y: 0, animated: true });
}
},
});
- }, [dispatch, initializeWallet, navigate, profilesEnabled, walletTypeCount.phrase]);
+ }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]);
const onPressLearnMoreAboutCloudBackups = useCallback(() => {
navigate(Routes.LEARN_WEB_VIEW_SCREEN, {
@@ -263,6 +224,66 @@ export const WalletsAndBackup = () => {
[navigate, wallets]
);
+ const { status: iconStatusType, text } = useMemo<{ status: StatusType; text: string }>(() => {
+ if (!backupProvider) {
+ if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) {
+ return {
+ status: 'not-enabled',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled),
+ };
+ }
+
+ if (status !== CloudBackupState.Ready) {
+ return {
+ status: 'out-of-sync',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.syncing),
+ };
+ }
+
+ if (!allBackedUp) {
+ return {
+ status: 'out-of-date',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date),
+ };
+ }
+
+ return {
+ status: 'up-to-date',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date),
+ };
+ }
+
+ if (status === CloudBackupState.FailedToInitialize || status === CloudBackupState.NotAvailable) {
+ return {
+ status: 'not-enabled',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.not_enabled),
+ };
+ }
+
+ if (status !== CloudBackupState.Ready) {
+ return {
+ status: 'out-of-sync',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.syncing),
+ };
+ }
+
+ if (!allBackedUp) {
+ return {
+ status: 'out-of-date',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.out_of_date),
+ };
+ }
+
+ return {
+ status: 'up-to-date',
+ text: i18n.t(i18n.l.back_up.cloud.statuses.up_to_date),
+ };
+ }, [backupProvider, status, allBackedUp]);
+
+ const isCloudBackupDisabled = useMemo(() => {
+ return status !== CloudBackupState.Ready && status !== CloudBackupState.NotAvailable;
+ }, [status]);
+
const renderView = useCallback(() => {
switch (backupProvider) {
default:
@@ -275,7 +296,7 @@ export const WalletsAndBackup = () => {
paddingTop={{ custom: 8 }}
iconComponent={}
titleComponent={}
- statusComponent={}
+ statusComponent={}
labelComponent={
{
/>
-
-
-
+
+
+
+
+
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType });
+
return (
-
+
{
}
>
- {!backedUp && (
+ {!isBackedUp && (
@@ -330,37 +356,43 @@ export const WalletsAndBackup = () => {
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
- ))}
-
+ }
+ keyExtractor={item => item.address}
+ numColumns={3}
+ scrollEnabled={false}
+ />
}
/>
);
})}
+
{
titleComponent={}
/>
-
-
- }
- onPress={onViewCloudBackups}
- size={52}
- titleComponent={
-
- }
- />
- }
- onPress={manageCloudBackups}
- size={52}
- titleComponent={
-
- }
- />
-
);
@@ -416,12 +417,7 @@ export const WalletsAndBackup = () => {
paddingTop={{ custom: 8 }}
iconComponent={}
titleComponent={}
- statusComponent={
-
- }
+ statusComponent={}
labelComponent={
allBackedUp ? (
{
/>
-
+
-
-
+ }
+ >
+
+
+
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupFile, backupType, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupFile, backupType });
+
return (
-
+
{
}
>
- {!backedUp && }
+ {!isBackedUp && (
+
+ )}
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
- ))}
-
+ }
+ keyExtractor={item => item.address}
+ numColumns={3}
+ scrollEnabled={false}
+ />
}
/>
@@ -581,12 +588,13 @@ export const WalletsAndBackup = () => {
case WalletBackupTypes.manual: {
return (
- {sortedWallets.map(({ name, isBackedUp, accounts, key, numAccounts, backedUp, imported }) => {
+ {sortedWallets.map(({ id, name, backedUp, backupType, backupFile, imported, addresses }) => {
+ const isBackedUp = isWalletBackedUpForCurrentAccount({ backedUp, backupType, backupFile });
return (
-
+
{
}
>
- {!backedUp && }
+ {!isBackedUp && (
+
+ )}
{imported && }
1
+ addresses.length > 1
? i18n.t(i18n.l.wallet.back_ups.wallet_count_gt_one, {
- numAccounts,
+ numAccounts: addresses.length,
})
: i18n.t(i18n.l.wallet.back_ups.wallet_count, {
- numAccounts,
+ numAccounts: addresses.length,
})
}
/>
}
leftComponent={}
- onPress={() => onNavigateToWalletView(key, name)}
+ onPress={() => onNavigateToWalletView(id, name)}
size={60}
titleComponent={}
/>
- {accounts.map(account => (
-
- ))}
-
+ }
+ keyExtractor={item => item.address}
+ numColumns={3}
+ scrollEnabled={false}
+ />
}
/>
@@ -645,26 +664,29 @@ export const WalletsAndBackup = () => {
/>
-
- {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, {
- cloudPlatform,
- })}
+
+
+ {i18n.t(i18n.l.wallet.back_ups.cloud_backup_description, {
+ cloudPlatform,
+ })}
-
- {' '}
- {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)}
+
+ {' '}
+ {i18n.t(i18n.l.wallet.back_ups.cloud_backup_link)}
+
-
- }
- >
-
-
+ }
+ >
+
+
+
);
@@ -672,21 +694,29 @@ export const WalletsAndBackup = () => {
}
}, [
backupProvider,
- loading,
- backupAllNonBackedUpWalletsTocloud,
+ iconStatusType,
+ text,
+ status,
+ isCloudBackupDisabled,
+ enableCloudBackups,
sortedWallets,
onCreateNewSecretPhrase,
- onViewCloudBackups,
- manageCloudBackups,
navigate,
onNavigateToWalletView,
allBackedUp,
mostRecentBackup,
- lastBackupDate,
+ backupAllNonBackedUpWalletsTocloud,
+ onViewCloudBackups,
+ manageCloudBackups,
onPressLearnMoreAboutCloudBackups,
]);
- return {renderView()};
+ return (
+
+
+ {renderView()}
+
+ );
};
export default WalletsAndBackup;
diff --git a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
index b415e1d4d30..10e28e6ebc6 100644
--- a/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
+++ b/src/screens/SettingsSheet/components/GoogleAccountSection.tsx
@@ -3,14 +3,12 @@ import { getGoogleAccountUserData, GoogleDriveUserData, logoutFromGoogleDrive }
import ImageAvatar from '@/components/contacts/ImageAvatar';
import { showActionSheetWithOptions } from '@/utils';
import * as i18n from '@/languages';
-import { clearAllWalletsBackupStatus, updateWalletBackupStatusesBasedOnCloudUserData } from '@/redux/wallets';
-import { useDispatch } from 'react-redux';
import Menu from './Menu';
import MenuItem from './MenuItem';
import { logger, RainbowError } from '@/logger';
+import { backupsStore } from '@/state/backups/backups';
export const GoogleAccountSection: React.FC = () => {
- const dispatch = useDispatch();
const [accountDetails, setAccountDetails] = useState(undefined);
const [loading, setLoading] = useState(true);
@@ -29,12 +27,6 @@ export const GoogleAccountSection: React.FC = () => {
});
}, []);
- const removeBackupStateFromAllWallets = async () => {
- setLoading(true);
- await dispatch(clearAllWalletsBackupStatus());
- setLoading(false);
- };
-
const onGoogleAccountPress = () => {
showActionSheetWithOptions(
{
@@ -49,11 +41,10 @@ export const GoogleAccountSection: React.FC = () => {
if (buttonIndex === 0) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets().then(() => loginToGoogleDrive());
+ loginToGoogleDrive();
} else if (buttonIndex === 1) {
logoutFromGoogleDrive();
setAccountDetails(undefined);
- removeBackupStateFromAllWallets();
}
}
);
@@ -61,10 +52,10 @@ export const GoogleAccountSection: React.FC = () => {
const loginToGoogleDrive = async () => {
setLoading(true);
- await dispatch(updateWalletBackupStatusesBasedOnCloudUserData());
try {
const accountDetails = await getGoogleAccountUserData();
setAccountDetails(accountDetails ?? undefined);
+ backupsStore.getState().syncAndFetchBackups();
} catch (error) {
logger.error(new RainbowError(`[GoogleAccountSection]: Logging into Google Drive failed`), {
error: (error as Error).message,
diff --git a/src/screens/SettingsSheet/components/MenuContainer.tsx b/src/screens/SettingsSheet/components/MenuContainer.tsx
index 500960c55a5..cabb0157fb7 100644
--- a/src/screens/SettingsSheet/components/MenuContainer.tsx
+++ b/src/screens/SettingsSheet/components/MenuContainer.tsx
@@ -3,13 +3,14 @@ import { ScrollView } from 'react-native';
import { Box, Inset, Space, Stack } from '@/design-system';
interface MenuContainerProps {
+ scrollviewRef?: React.RefObject;
children: React.ReactNode;
Footer?: React.ReactNode;
testID?: string;
space?: Space;
}
-const MenuContainer = ({ children, testID, Footer, space = '36px' }: MenuContainerProps) => {
+const MenuContainer = ({ scrollviewRef, children, testID, Footer, space = '36px' }: MenuContainerProps) => {
return (
// ios scroll fix
(
);
-type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date';
+export type StatusType = 'not-enabled' | 'out-of-date' | 'up-to-date' | 'out-of-sync';
interface StatusIconProps {
status: StatusType;
@@ -87,6 +87,10 @@ const StatusIcon = ({ status, text }: StatusIconProps) => {
backgroundColor: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.1) : colors.alpha(colors.blueGreyDark, 0.1),
color: isDarkMode ? colors.alpha(colors.blueGreyDark, 0.6) : colors.alpha(colors.blueGreyDark, 0.8),
},
+ 'out-of-sync': {
+ backgroundColor: colors.alpha(colors.yellow, 0.2),
+ color: colors.yellow,
+ },
'out-of-date': {
backgroundColor: colors.alpha(colors.brightRed, 0.2),
color: colors.brightRed,
diff --git a/src/screens/SettingsSheet/components/SettingsSection.tsx b/src/screens/SettingsSheet/components/SettingsSection.tsx
index 9fae44a89eb..095b88cbb85 100644
--- a/src/screens/SettingsSheet/components/SettingsSection.tsx
+++ b/src/screens/SettingsSheet/components/SettingsSection.tsx
@@ -28,9 +28,11 @@ import { showActionSheetWithOptions } from '@/utils';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { ReviewPromptAction } from '@/storage/schema';
import { SettingsExternalURLs } from '../constants';
-import { capitalizeFirstLetter, checkWalletsForBackupStatus } from '../utils';
+import { checkLocalWalletsForBackupStatus } from '../utils';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { Box } from '@/design-system';
+import { capitalize } from 'lodash';
+import { backupsStore } from '@/state/backups/backups';
interface SettingsSectionProps {
onCloseModal: () => void;
@@ -59,10 +61,14 @@ const SettingsSection = ({
const isLanguageSelectionEnabled = useExperimentalFlag(LANGUAGE_SETTINGS);
const isNotificationsEnabled = useExperimentalFlag(NOTIFICATIONS);
+ const { backupProvider, backups } = backupsStore(state => ({
+ backupProvider: state.backupProvider,
+ backups: state.backups,
+ }));
+
const { isDarkMode, setTheme, colorScheme } = useTheme();
const onSendFeedback = useSendFeedback();
- const { backupProvider } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
const onPressReview = useCallback(async () => {
if (ios) {
@@ -85,7 +91,7 @@ const SettingsSection = ({
const onPressLearn = useCallback(() => Linking.openURL(SettingsExternalURLs.rainbowLearn), []);
- const { allBackedUp, canBeBackedUp } = useMemo(() => checkWalletsForBackupStatus(wallets), [wallets]);
+ const { allBackedUp } = useMemo(() => checkLocalWalletsForBackupStatus(wallets, backups), [wallets, backups]);
const themeMenuConfig = useMemo(() => {
return {
@@ -170,21 +176,19 @@ const SettingsSection = ({
return (
}>
- {canBeBackedUp && (
- }
- onPress={onPressBackup}
- rightComponent={
-
-
-
- }
- size={60}
- testID={'backup-section'}
- titleComponent={}
- />
- )}
+ }
+ onPress={onPressBackup}
+ rightComponent={
+
+
+
+ }
+ size={60}
+ testID={'backup-section'}
+ titleComponent={}
+ />
{isNotificationsEnabled && (
}
- rightComponent={{colorScheme ? capitalizeFirstLetter(colorScheme) : ''}}
+ rightComponent={{colorScheme ? capitalize(colorScheme) : ''}}
size={60}
testID={`theme-section-${isDarkMode ? 'dark' : 'light'}`}
titleComponent={}
diff --git a/src/screens/SettingsSheet/useVisibleWallets.ts b/src/screens/SettingsSheet/useVisibleWallets.ts
index 64e73aa0929..c677dd738db 100644
--- a/src/screens/SettingsSheet/useVisibleWallets.ts
+++ b/src/screens/SettingsSheet/useVisibleWallets.ts
@@ -1,9 +1,7 @@
-import { useState } from 'react';
import * as i18n from '@/languages';
import WalletTypes, { EthereumWalletType } from '@/helpers/walletTypes';
-import { DEFAULT_WALLET_NAME, RainbowAccount, RainbowWallet } from '@/model/wallet';
-import walletBackupTypes from '@/helpers/walletBackupTypes';
+import { RainbowWallet } from '@/model/wallet';
type WalletByKey = {
[key: string]: RainbowWallet;
@@ -19,20 +17,6 @@ export type WalletCountPerType = {
privateKey: number;
};
-export type AmendedRainbowWallet = RainbowWallet & {
- name: string;
- isBackedUp: boolean | undefined;
- accounts: RainbowAccount[];
- key: string;
- label: string;
- numAccounts: number;
-};
-
-type UseVisibleWalletReturnType = {
- visibleWallets: AmendedRainbowWallet[];
- lastBackupDate: number | undefined;
-};
-
export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount: WalletCountPerType) => {
switch (type) {
case EthereumWalletType.mnemonic:
@@ -48,51 +32,26 @@ export const getTitleForWalletType = (type: EthereumWalletType, walletTypeCount:
}
};
-const isWalletGroupNamed = (wallet: RainbowWallet) => wallet.name && wallet.name.trim() !== '' && wallet.name !== DEFAULT_WALLET_NAME;
-
-export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): UseVisibleWalletReturnType => {
- const [lastBackupDate, setLastBackupDate] = useState(undefined);
-
+export const useVisibleWallets = ({ wallets, walletTypeCount }: UseVisibleWalletProps): RainbowWallet[] => {
if (!wallets) {
- return {
- visibleWallets: [],
- lastBackupDate,
- };
+ return [];
}
- return {
- visibleWallets: Object.keys(wallets)
- .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth)
- .map(key => {
- const wallet = wallets[key];
- const visibleAccounts = (wallet.addresses || []).filter(a => a.visible);
- const totalAccounts = visibleAccounts.length;
-
- if (
- wallet.backedUp &&
- wallet.backupDate &&
- wallet.backupType === walletBackupTypes.cloud &&
- (!lastBackupDate || Number(wallet.backupDate) > lastBackupDate)
- ) {
- setLastBackupDate(Number(wallet.backupDate));
- }
-
- if (wallet.type === WalletTypes.mnemonic) {
- walletTypeCount.phrase += 1;
- } else if (wallet.type === WalletTypes.privateKey) {
- walletTypeCount.privateKey += 1;
- }
-
- return {
- ...wallet,
- name: isWalletGroupNamed(wallet) ? wallet.name : getTitleForWalletType(wallet.type, walletTypeCount),
- isBackedUp: wallet.backedUp,
- accounts: visibleAccounts,
- key,
- label: wallet.name,
- numAccounts: totalAccounts,
- };
- }),
- lastBackupDate,
- };
+ return Object.keys(wallets)
+ .filter(key => wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth)
+ .map(key => {
+ const wallet = wallets[key];
+
+ if (wallet.type === WalletTypes.mnemonic) {
+ walletTypeCount.phrase += 1;
+ } else if (wallet.type === WalletTypes.privateKey) {
+ walletTypeCount.privateKey += 1;
+ }
+
+ return {
+ ...wallet,
+ name: getTitleForWalletType(wallet.type, walletTypeCount),
+ addresses: Object.values(wallet.addresses).filter(address => address.visible),
+ };
+ });
};
diff --git a/src/screens/SettingsSheet/utils.ts b/src/screens/SettingsSheet/utils.ts
index 08fa3e03e22..0fb1d26faff 100644
--- a/src/screens/SettingsSheet/utils.ts
+++ b/src/screens/SettingsSheet/utils.ts
@@ -1,118 +1,121 @@
import WalletBackupTypes from '@/helpers/walletBackupTypes';
import WalletTypes from '@/helpers/walletTypes';
+import { useWallets } from '@/hooks';
+import { isEmpty } from 'lodash';
+import { BackupFile, CloudBackups, parseTimestampFromFilename } from '@/model/backup';
+import * as i18n from '@/languages';
+import { cloudPlatform } from '@/utils/platform';
+import { backupsStore, CloudBackupState } from '@/state/backups/backups';
import { RainbowWallet } from '@/model/wallet';
-import { Navigation } from '@/navigation';
-import { BackupUserData, getLocalBackupPassword } from '@/model/backup';
-import Routes from '@/navigation/routesNames';
-import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes';
-
-type WalletsByKey = {
- [key: string]: RainbowWallet;
-};
+import { IS_ANDROID, IS_IOS } from '@/env';
+import { normalizeAndroidBackupFilename } from '@/handlers/cloudBackup';
type WalletBackupStatus = {
allBackedUp: boolean;
areBackedUp: boolean;
canBeBackedUp: boolean;
- backupProvider: string | undefined;
};
-export const capitalizeFirstLetter = (str: string) => {
- return str.charAt(0).toUpperCase() + str.slice(1);
+export const hasManuallyBackedUpWallet = (wallets: ReturnType['wallets']) => {
+ if (!wallets) return false;
+ return Object.values(wallets).some(wallet => wallet.backupType === WalletBackupTypes.manual);
};
-export const checkUserDataForBackupProvider = (userData?: BackupUserData): { backupProvider: string | undefined } => {
- let backupProvider: string | undefined = undefined;
-
- if (!userData?.wallets) return { backupProvider };
-
- Object.values(userData.wallets).forEach(wallet => {
- if (wallet.backedUp && wallet.type !== WalletTypes.readOnly) {
- if (wallet.backupType === WalletBackupTypes.cloud) {
- backupProvider = WalletBackupTypes.cloud;
- } else if (backupProvider !== WalletBackupTypes.cloud && wallet.backupType === WalletBackupTypes.manual) {
- backupProvider = WalletBackupTypes.manual;
- }
- }
- });
-
- return { backupProvider };
-};
-
-export const checkWalletsForBackupStatus = (wallets: WalletsByKey | null): WalletBackupStatus => {
- if (!wallets)
+export const checkLocalWalletsForBackupStatus = (
+ wallets: ReturnType['wallets'],
+ backups: CloudBackups
+): WalletBackupStatus => {
+ if (!wallets || isEmpty(wallets)) {
return {
allBackedUp: false,
areBackedUp: false,
canBeBackedUp: false,
- backupProvider: undefined,
};
+ }
+
+ // FOR ANDROID, we need to check if the current google account also has the backup file
+ if (IS_ANDROID) {
+ return Object.values(wallets).reduce(
+ (acc, wallet) => {
+ const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth;
+ const hasBackupFile = backups.files.some(
+ file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(wallet.backupFile ?? '')
+ );
+
+ return {
+ allBackedUp: acc.allBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible),
+ areBackedUp: acc.areBackedUp && hasBackupFile && (wallet.backedUp || !isBackupEligible),
+ canBeBackedUp: acc.canBeBackedUp && isBackupEligible,
+ };
+ },
+ { allBackedUp: true, areBackedUp: true, canBeBackedUp: false }
+ );
+ }
+
+ return Object.values(wallets).reduce(
+ (acc, wallet) => {
+ const isBackupEligible = wallet.type !== WalletTypes.readOnly && wallet.type !== WalletTypes.bluetooth;
+
+ return {
+ allBackedUp: acc.allBackedUp && (wallet.backedUp || !isBackupEligible),
+ areBackedUp: acc.areBackedUp && (wallet.backedUp || !isBackupEligible || wallet.imported),
+ canBeBackedUp: acc.canBeBackedUp && isBackupEligible,
+ };
+ },
+ { allBackedUp: true, areBackedUp: true, canBeBackedUp: false }
+ );
+};
- let backupProvider: string | undefined = undefined;
- let areBackedUp = true;
- let canBeBackedUp = false;
- let allBackedUp = true;
-
- Object.keys(wallets).forEach(key => {
- if (wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) {
- if (wallets[key].backupType === WalletBackupTypes.cloud) {
- backupProvider = WalletBackupTypes.cloud;
- } else if (backupProvider !== WalletBackupTypes.cloud && wallets[key].backupType === WalletBackupTypes.manual) {
- backupProvider = WalletBackupTypes.manual;
- }
- }
+export const getMostRecentCloudBackup = (backups: BackupFile[]) => {
+ const cloudBackups = backups.sort((a, b) => {
+ return parseTimestampFromFilename(b.name) - parseTimestampFromFilename(a.name);
+ });
- if (!wallets[key].backedUp && wallets[key].type !== WalletTypes.readOnly && wallets[key].type !== WalletTypes.bluetooth) {
- allBackedUp = false;
+ return cloudBackups.reduce((prev, current) => {
+ if (!current) {
+ return prev;
}
- if (
- !wallets[key].backedUp &&
- wallets[key].type !== WalletTypes.readOnly &&
- wallets[key].type !== WalletTypes.bluetooth &&
- !wallets[key].imported
- ) {
- areBackedUp = false;
+ if (!prev) {
+ return current;
}
- if (wallets[key].type !== WalletTypes.bluetooth && wallets[key].type !== WalletTypes.readOnly) {
- canBeBackedUp = true;
+ const prevTimestamp = new Date(prev.lastModified).getTime();
+ const currentTimestamp = new Date(current.lastModified).getTime();
+ if (currentTimestamp > prevTimestamp) {
+ return current;
}
- });
- return {
- allBackedUp,
- areBackedUp,
- canBeBackedUp,
- backupProvider,
- };
+
+ return prev;
+ }, cloudBackups[0]);
};
-export const getWalletsThatNeedBackedUp = (wallets: { [key: string]: RainbowWallet } | null): RainbowWallet[] => {
- if (!wallets) return [];
- const walletsToBackup: RainbowWallet[] = [];
- Object.keys(wallets).forEach(key => {
- if (
- !wallets[key].backedUp &&
- wallets[key].type !== WalletTypes.readOnly &&
- wallets[key].type !== WalletTypes.bluetooth &&
- !wallets[key].imported
- ) {
- walletsToBackup.push(wallets[key]);
- }
- });
- return walletsToBackup;
+export const titleForBackupState: Partial> = {
+ [CloudBackupState.Initializing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, {
+ cloudPlatformName: cloudPlatform,
+ }),
+ [CloudBackupState.Syncing]: i18n.t(i18n.l.back_up.cloud.syncing_cloud_store, {
+ cloudPlatformName: cloudPlatform,
+ }),
+ [CloudBackupState.Fetching]: i18n.t(i18n.l.back_up.cloud.fetching_backups, {
+ cloudPlatformName: cloudPlatform,
+ }),
};
-export const fetchBackupPasswordAndNavigate = async () => {
- const password = await getLocalBackupPassword();
+export const isWalletBackedUpForCurrentAccount = ({ backupType, backedUp, backupFile }: Partial) => {
+ if (!backupType || !backupFile) {
+ return false;
+ }
- return new Promise(resolve => {
- return Navigation.handleAction(Routes.BACKUP_SHEET, {
- step: WalletBackupStepTypes.backup_cloud,
- password,
- onSuccess: async (password: string) => {
- resolve(password);
- },
- });
- });
+ if (IS_IOS || backupType === WalletBackupTypes.manual) {
+ return backedUp;
+ }
+
+ // NOTE: For Android, we also need to check if the current google account has the matching backup file
+ if (!backupFile) {
+ return false;
+ }
+
+ const backupFiles = backupsStore.getState().backups;
+ return backupFiles.files.some(file => normalizeAndroidBackupFilename(file.name) === normalizeAndroidBackupFilename(backupFile));
};
diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx
index acfcbcfb176..bf5fa201570 100644
--- a/src/screens/WalletScreen/index.tsx
+++ b/src/screens/WalletScreen/index.tsx
@@ -25,11 +25,11 @@ import { RemoteCardsSync } from '@/state/sync/RemoteCardsSync';
import { RemotePromoSheetSync } from '@/state/sync/RemotePromoSheetSync';
import { UserAssetsSync } from '@/state/sync/UserAssetsSync';
import { MobileWalletProtocolListener } from '@/components/MobileWalletProtocolListener';
-import { runWalletBackupStatusChecks } from '@/handlers/walletReadyEvents';
import { RouteProp, useRoute } from '@react-navigation/native';
import { RootStackParamList } from '@/navigation/types';
import { useNavigation } from '@/navigation';
import Routes from '@/navigation/Routes';
+import { BackendNetworks } from '@/components/BackendNetworks';
import walletTypes from '@/helpers/walletTypes';
enum WalletLoadingStates {
@@ -45,7 +45,6 @@ function WalletScreen() {
const walletState = useRef(WalletLoadingStates.IDLE);
const initializeWallet = useInitializeWallet();
const { network: currentNetwork, accountAddress, appIcon } = useAccountSettings();
-
const loadAccountLateData = useLoadAccountLateData();
const loadGlobalLateData = useLoadGlobalLateData();
const insets = useSafeAreaInsets();
@@ -149,7 +148,6 @@ function WalletScreen() {
if (walletReady) {
loadAccountLateData();
loadGlobalLateData();
- runWalletBackupStatusChecks();
}
}, [loadAccountLateData, loadGlobalLateData, walletReady]);
@@ -185,6 +183,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..ef1abf3ab23
--- /dev/null
+++ b/src/state/backups/backups.ts
@@ -0,0 +1,182 @@
+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 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',
+}
+
+const DEFAULT_TIMEOUT = 10_000;
+const MAX_RETRIES = 3;
+
+export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching];
+
+interface BackupsStore {
+ storedPassword: string;
+ setStoredPassword: (storedPassword: string) => void;
+
+ backupProvider: string | undefined;
+ setBackupProvider: (backupProvider: string | undefined) => void;
+
+ status: CloudBackupState;
+ setStatus: (status: CloudBackupState) => void;
+
+ backups: CloudBackups;
+ setBackups: (backups: CloudBackups) => void;
+
+ mostRecentBackup: BackupFile | undefined;
+ setMostRecentBackup: (backup: BackupFile | undefined) => void;
+
+ password: string;
+ setPassword: (password: string) => void;
+
+ syncAndFetchBackups: (
+ retryOnFailure?: boolean,
+ retryCount?: number
+ ) => Promise<{
+ success: boolean;
+ retry?: boolean;
+ }>;
+}
+
+const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching];
+
+export const backupsStore = createRainbowStore((set, get) => ({
+ storedPassword: '',
+ setStoredPassword: storedPassword => set({ storedPassword }),
+
+ backupProvider: undefined,
+ setBackupProvider: provider => set({ backupProvider: provider }),
+
+ status: CloudBackupState.Initializing,
+ setStatus: status => set({ status }),
+
+ backups: { files: [] },
+ setBackups: backups => set({ backups }),
+
+ mostRecentBackup: undefined,
+ setMostRecentBackup: backup => set({ mostRecentBackup: backup }),
+
+ password: '',
+ setPassword: password => set({ password }),
+
+ syncAndFetchBackups: async (retryOnFailure = true, retryCount = 0) => {
+ const { status } = get();
+
+ const timeoutPromise = new Promise<{ success: boolean; retry?: boolean }>(resolve => {
+ setTimeout(() => {
+ resolve({ success: false, retry: retryOnFailure });
+ }, DEFAULT_TIMEOUT);
+ });
+
+ const syncAndPullFiles = async (): Promise<{ success: boolean; retry?: boolean }> => {
+ try {
+ const isAvailable = await isCloudBackupAvailable();
+ if (!isAvailable) {
+ logger.debug('[backupsStore]: Cloud backup is not available');
+ set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined });
+ return {
+ success: false,
+ retry: false,
+ };
+ }
+
+ if (IS_ANDROID) {
+ const gdata = await getGoogleAccountUserData();
+ if (!gdata) {
+ logger.debug('[backupsStore]: Google account is not available');
+ set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined });
+ return {
+ success: false,
+ retry: false,
+ };
+ }
+ }
+
+ set({ status: CloudBackupState.Syncing });
+ logger.debug('[backupsStore]: Syncing with cloud');
+ await syncCloud();
+
+ set({ status: CloudBackupState.Fetching });
+ logger.debug('[backupsStore]: Fetching backups');
+ const backups = await fetchAllBackups();
+
+ set({ backups });
+
+ const { wallets } = store.getState().wallets;
+
+ // if the user has any cloud backups, set the provider to cloud
+ if (backups.files.length > 0) {
+ set({
+ backupProvider: walletBackupTypes.cloud,
+ mostRecentBackup: getMostRecentCloudBackup(backups.files),
+ });
+ } else if (hasManuallyBackedUpWallet(wallets)) {
+ set({ backupProvider: walletBackupTypes.manual });
+ } else {
+ set({ backupProvider: undefined });
+ }
+
+ logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`);
+
+ set({ status: CloudBackupState.Ready });
+ return {
+ success: true,
+ retry: false,
+ };
+ } catch (e) {
+ logger.error(new RainbowError('[backupsStore]: Failed to fetch all backups'), {
+ error: e,
+ });
+ set({ status: CloudBackupState.FailedToInitialize });
+ }
+
+ return {
+ success: false,
+ retry: retryOnFailure,
+ };
+ };
+
+ if (mutex.isLocked() || returnEarlyIfLockedStates.includes(status)) {
+ logger.debug('[backupsStore]: Mutex is locked or returnEarlyIfLockedStates includes status', {
+ status,
+ });
+ return {
+ success: false,
+ retry: false,
+ };
+ }
+
+ const releaser = await mutex.acquire();
+ logger.debug('[backupsStore]: Acquired mutex');
+ const { success, retry } = await Promise.race([syncAndPullFiles(), timeoutPromise]);
+ releaser();
+ logger.debug('[backupsStore]: Released mutex');
+ if (retry && retryCount < MAX_RETRIES) {
+ logger.debug(`[backupsStore]: Retrying sync and fetch backups attempt: ${retryCount + 1}`);
+ return get().syncAndFetchBackups(retryOnFailure, retryCount + 1);
+ }
+
+ if (retry && retryCount >= MAX_RETRIES) {
+ logger.error(new RainbowError('[backupsStore]: Max retry attempts reached. Sync failed.'));
+ }
+
+ return { success, retry };
+ },
+}));
diff --git a/src/state/sync/BackupsSync.tsx b/src/state/sync/BackupsSync.tsx
new file mode 100644
index 00000000000..a409490c205
--- /dev/null
+++ b/src/state/sync/BackupsSync.tsx
@@ -0,0 +1,12 @@
+import { useEffect, memo } from 'react';
+import { backupsStore } from '@/state/backups/backups';
+
+const BackupsSyncComponent = () => {
+ useEffect(() => {
+ backupsStore.getState().syncAndFetchBackups();
+ }, []);
+
+ return null;
+};
+
+export const BackupsSync = memo(BackupsSyncComponent);
diff --git a/src/state/walletLoading/walletLoading.ts b/src/state/walletLoading/walletLoading.ts
new file mode 100644
index 00000000000..7391b78e760
--- /dev/null
+++ b/src/state/walletLoading/walletLoading.ts
@@ -0,0 +1,18 @@
+import { createRainbowStore } from '../internal/createRainbowStore';
+import { WalletLoadingStates } from '@/helpers/walletLoadingStates';
+
+type WalletLoadingState = {
+ loadingState: WalletLoadingStates | null;
+ blockTouches: boolean;
+ Component: JSX.Element | null;
+ hide: () => void;
+ setComponent: (Component: JSX.Element, blockTouches?: boolean) => void;
+};
+
+export const walletLoadingStore = createRainbowStore(set => ({
+ loadingState: null,
+ blockTouches: false,
+ Component: null,
+ hide: () => set({ blockTouches: false, Component: null }),
+ setComponent: (Component: JSX.Element, blockTouches = true) => set({ blockTouches, Component }),
+}));