diff --git a/src/components/backup/BackupSheet.tsx b/src/components/backup/BackupSheet.tsx index 12e15c60190..fc08b7da52d 100644 --- a/src/components/backup/BackupSheet.tsx +++ b/src/components/backup/BackupSheet.tsx @@ -1,11 +1,14 @@ import { RouteProp, useRoute } from '@react-navigation/native'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { BackupCloudStep, RestoreCloudStep } from '.'; import WalletBackupStepTypes from '@/helpers/walletBackupStepTypes'; import BackupWalletPrompt from '@/components/backup/BackupWalletPrompt'; +import ManualBackupPrompt from '@/components/backup/ManualBackupPrompt'; import { BackgroundProvider } from '@/design-system'; import { SimpleSheet } from '@/components/sheet/SimpleSheet'; import { getHeightForStep } from '@/navigation/config'; +import CloudBackupPrompt from './CloudBackupPrompt'; +import { backupsStore } from '@/state/backups/backups'; type BackupSheetParams = { BackupSheet: { @@ -22,16 +25,38 @@ export default function BackupSheet() { const renderStep = useCallback(() => { switch (step) { - case WalletBackupStepTypes.backup_cloud: + case WalletBackupStepTypes.create_cloud_backup: return ; case WalletBackupStepTypes.restore_from_backup: return ; case WalletBackupStepTypes.backup_prompt: + return ; + case WalletBackupStepTypes.backup_prompt_manual: + return ; + case WalletBackupStepTypes.backup_prompt_cloud: + return ; default: return ; } }, [step]); + useEffect(() => { + return () => { + if ( + [ + WalletBackupStepTypes.backup_prompt, + WalletBackupStepTypes.backup_prompt_manual, + WalletBackupStepTypes.backup_prompt_cloud, + ].includes(step) + ) { + if (backupsStore.getState().timesPromptedForBackup === 0) { + backupsStore.getState().setTimesPromptedForBackup(1); + } + backupsStore.getState().setLastBackupPromptAt(Date.now()); + } + }; + }, [step]); + return ( {({ backgroundColor }) => ( diff --git a/src/components/backup/CloudBackupPrompt.tsx b/src/components/backup/CloudBackupPrompt.tsx new file mode 100644 index 00000000000..7d015cae976 --- /dev/null +++ b/src/components/backup/CloudBackupPrompt.tsx @@ -0,0 +1,122 @@ +import React, { useCallback } from 'react'; +import { Bleed, Box, Inline, Inset, Separator, Stack, Text } from '@/design-system'; +import * as lang from '@/languages'; +import { ImgixImage } from '../images'; +import WalletsAndBackupIcon from '@/assets/WalletsAndBackup.png'; +import { Source } from 'react-native-fast-image'; +import { cloudPlatform } from '@/utils/platform'; +import { ButtonPressAnimation } from '../animations'; +import Routes from '@/navigation/routesNames'; +import { useNavigation } from '@/navigation'; +import { useWallets } from '@/hooks'; +import { format } from 'date-fns'; +import { useCreateBackup } from './useCreateBackup'; +import { executeFnIfCloudBackupAvailable } from '@/model/backup'; +import { backupsStore } from '@/state/backups/backups'; + +const imageSize = 72; + +export default function CloudBackupPrompt() { + const { navigate, goBack } = useNavigation(); + const { mostRecentBackup } = backupsStore(state => ({ + mostRecentBackup: state.mostRecentBackup, + })); + const { selectedWallet } = useWallets(); + const createBackup = useCreateBackup(); + + const onCloudBackup = useCallback(() => { + // pop the bottom sheet, and navigate to the backup section inside settings sheet + goBack(); + navigate(Routes.SETTINGS_SHEET, { + screen: Routes.SETTINGS_SECTION_BACKUP, + initial: false, + }); + + executeFnIfCloudBackupAvailable({ + fn: () => + createBackup({ + walletId: selectedWallet.id, + }), + logout: true, + }); + }, [createBackup, goBack, navigate, selectedWallet.id]); + + const onMaybeLater = useCallback(() => goBack(), [goBack]); + + return ( + + + + + + {lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)} + + + + + + + + + + + + + + 􀎽{' '} + {lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, { + cloudPlatform, + })} + + + + + + + + + + + + + + + + {lang.t(lang.l.back_up.cloud.mayber_later)} + + + + + + + + + + + {mostRecentBackup && ( + + + + + {lang.t(lang.l.back_up.cloud.latest_backup, { + date: format(new Date(mostRecentBackup.lastModified), "M/d/yy 'at' h:mm a"), + })} + + + + + )} + + ); +} diff --git a/src/components/backup/ManualBackupPrompt.tsx b/src/components/backup/ManualBackupPrompt.tsx new file mode 100644 index 00000000000..e882215bc99 --- /dev/null +++ b/src/components/backup/ManualBackupPrompt.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useEffect } 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'; +import { backupsStore } from '@/state/backups/backups'; + +const imageSize = 72; + +export default function ManualBackupPrompt() { + 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/useCreateBackup.ts b/src/components/backup/useCreateBackup.ts index e227b1173c4..40a4e4e3beb 100644 --- a/src/components/backup/useCreateBackup.ts +++ b/src/components/backup/useCreateBackup.ts @@ -138,7 +138,7 @@ export const useCreateBackup = () => { return new Promise(resolve => { return Navigation.handleAction(Routes.BACKUP_SHEET, { nativeScreen: true, - step: walletBackupStepTypes.backup_cloud, + step: walletBackupStepTypes.create_cloud_backup, onSuccess: async (password: string) => { return resolve(password); }, diff --git a/src/handlers/walletReadyEvents.ts b/src/handlers/walletReadyEvents.ts index c33363ff727..ce9b662139f 100644 --- a/src/handlers/walletReadyEvents.ts +++ b/src/handlers/walletReadyEvents.ts @@ -13,7 +13,8 @@ import { checkKeychainIntegrity } from '@/redux/wallets'; import Routes from '@/navigation/routesNames'; import { logger } from '@/logger'; import { IS_TEST } from '@/env'; -import { backupsStore, LoadingStates } from '@/state/backups/backups'; +import { backupsStore, CloudBackupState, LoadingStates, oneWeekInMs } from '@/state/backups/backups'; +import walletBackupTypes from '@/helpers/walletBackupTypes'; export const runKeychainIntegrityChecks = async () => { const keychainIntegrityState = await getKeychainIntegrityState(); @@ -34,10 +35,28 @@ const promptForBackupOnceReadyOrNotAvailable = async (): Promise => { status = backupsStore.getState().status; } - logger.debug(`[walletReadyEvents]: BackupSheet: showing backup now sheet for selected wallet`); + if (status !== CloudBackupState.Ready) { + return false; + } + + const { backupProvider, timesPromptedForBackup, lastBackupPromptAt } = backupsStore.getState(); + + // prompt for backup every week if first time prompting, otherwise prompt every 2 weeks + if (lastBackupPromptAt && Date.now() - lastBackupPromptAt < oneWeekInMs * (timesPromptedForBackup + 1)) { + return false; + } + + const step = + backupProvider === walletBackupTypes.cloud + ? WalletBackupStepTypes.backup_prompt_cloud + : backupProvider === walletBackupTypes.manual + ? WalletBackupStepTypes.backup_prompt_manual + : WalletBackupStepTypes.backup_prompt; + + logger.debug(`[walletReadyEvents]: BackupSheet: showing ${step} backup sheet`); triggerOnSwipeLayout(() => Navigation.handleAction(Routes.BACKUP_SHEET, { - step: WalletBackupStepTypes.backup_prompt, + step, }) ); return true; diff --git a/src/helpers/walletBackupStepTypes.ts b/src/helpers/walletBackupStepTypes.ts index 2fbf0cb8f9e..67b14680737 100644 --- a/src/helpers/walletBackupStepTypes.ts +++ b/src/helpers/walletBackupStepTypes.ts @@ -1,9 +1,8 @@ export default { backup_prompt: 'backup_prompt', - backup_manual: 'backup_manual', - backup_cloud: 'backup_cloud', + backup_prompt_manual: 'backup_prompt_manual', + backup_prompt_cloud: 'backup_prompt_cloud', restore_from_backup: 'restore_from_backup', - backup_now_to_cloud: 'cloud', - backup_now_manually: 'manual', + create_cloud_backup: 'create_cloud_backup', check_identifier: 'check_identifier', }; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index 096099c8f61..051aebd864c 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -31,6 +31,7 @@ import { ReviewPromptAction } from '@/storage/schema'; import { ChainId } from '@/state/backendNetworks/types'; import { backupsStore } from '@/state/backups/backups'; import { IS_TEST } from '@/env'; +import walletBackupTypes from '@/helpers/walletBackupTypes'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); @@ -315,9 +316,7 @@ export default function useImportingWallet({ showImportModal = true } = {}) { dangerouslyGetParent?.()?.goBack(); InteractionManager.runAfterInteractions(async () => { if (previousWalletCount === 0) { - // on Android replacing is not working well, so we navigate and then remove the screen below - const action = navigate; - action(Routes.SWIPE_LAYOUT, { + navigate(Routes.SWIPE_LAYOUT, { params: { initialized: true }, screen: Routes.WALLET_SCREEN, }); @@ -333,6 +332,21 @@ export default function useImportingWallet({ showImportModal = true } = {}) { handleSetImporting(false); } + if ( + backupProvider === walletBackupTypes.cloud && + !( + IS_TEST || + isENSAddressFormat(input) || + isUnstoppableAddressFormat(input) || + isValidAddress(input) || + isValidBluetoothDeviceId(input) + ) + ) { + Navigation.handleAction(Routes.BACKUP_SHEET, { + step: WalletBackupStepTypes.backup_prompt_cloud, + }); + } + setTimeout(() => { InteractionManager.runAfterInteractions(() => { handleReviewPromptAction(ReviewPromptAction.WatchWallet); diff --git a/src/hooks/useInitializeWallet.ts b/src/hooks/useInitializeWallet.ts index 80aa4e903ea..cbd813c8439 100644 --- a/src/hooks/useInitializeWallet.ts +++ b/src/hooks/useInitializeWallet.ts @@ -16,7 +16,6 @@ import useLoadAccountData from './useLoadAccountData'; import useLoadGlobalEarlyData from './useLoadGlobalEarlyData'; import useOpenSmallBalances from './useOpenSmallBalances'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { PROFILES, useExperimentalFlag } from '@/config'; import { runKeychainIntegrityChecks } from '@/handlers/walletReadyEvents'; import { RainbowError, logger } from '@/logger'; import { getOrCreateDeviceId, getWalletContext } from '@/analytics/utils'; @@ -32,7 +31,6 @@ export default function useInitializeWallet() { const { network } = useAccountSettings(); const hideSplashScreen = useHideSplashScreen(); const { setIsSmallBalancesOpen } = useOpenSmallBalances(); - const profilesEnabled = useExperimentalFlag(PROFILES); const getWalletStatusForPerformanceMetrics = (isNew: boolean, isImporting: boolean): string => { if (isNew) { @@ -180,7 +178,7 @@ export default function useInitializeWallet() { return null; } }, - [dispatch, hideSplashScreen, loadAccountData, loadGlobalEarlyData, network, profilesEnabled, setIsSmallBalancesOpen] + [dispatch, hideSplashScreen, loadAccountData, loadGlobalEarlyData, network, setIsSmallBalancesOpen] ); return initializeWallet; diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 1e03c2d96a3..21493ff777a 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -209,11 +209,7 @@ function BSNavigator() { const { params: { step } = {} as any } = route.route; let heightForStep = backupSheetSizes.short; - if ( - step === walletBackupStepTypes.backup_cloud || - step === walletBackupStepTypes.backup_manual || - step === walletBackupStepTypes.restore_from_backup - ) { + if (step === walletBackupStepTypes.create_cloud_backup || step === walletBackupStepTypes.restore_from_backup) { heightForStep = backupSheetSizes.long; } else if (step === walletBackupStepTypes.backup_prompt) { heightForStep = backupSheetSizes.medium; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 9097c84e1d6..05ad779c1bf 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -99,8 +99,7 @@ export const backupSheetSizes = { export const getHeightForStep = (step: string) => { switch (step) { - case WalletBackupStepTypes.backup_cloud: - case WalletBackupStepTypes.backup_manual: + case WalletBackupStepTypes.create_cloud_backup: case WalletBackupStepTypes.restore_from_backup: return backupSheetSizes.long; case WalletBackupStepTypes.backup_prompt: diff --git a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx index d488790c223..863ca437e8a 100644 --- a/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx +++ b/src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx @@ -199,13 +199,15 @@ export const WalletsAndBackup = () => { loadingState: null, }); scrollviewRef.current?.scrollTo({ y: 0, animated: true }); + const step = + backupProvider === WalletBackupTypes.cloud ? walletBackupStepTypes.backup_prompt_cloud : walletBackupStepTypes.backup_prompt; Navigation.handleAction(Routes.BACKUP_SHEET, { - step: walletBackupStepTypes.backup_prompt, + step, }); } }, }); - }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase]); + }, [dispatch, initializeWallet, navigate, walletTypeCount.phrase, backupProvider]); const onPressLearnMoreAboutCloudBackups = useCallback(() => { navigate(Routes.LEARN_WEB_VIEW_SCREEN, { diff --git a/src/state/backups/backups.ts b/src/state/backups/backups.ts index 9c2c2ae87e6..26bbae4e5db 100644 --- a/src/state/backups/backups.ts +++ b/src/state/backups/backups.ts @@ -25,9 +25,17 @@ export enum CloudBackupState { const DEFAULT_TIMEOUT = 10_000; const MAX_RETRIES = 3; +export const oneWeekInMs = 7 * 24 * 60 * 60 * 1000; + export const LoadingStates = [CloudBackupState.Initializing, CloudBackupState.Syncing, CloudBackupState.Fetching]; interface BackupsStore { + timesPromptedForBackup: number; + setTimesPromptedForBackup: (timesPromptedForBackup: number) => void; + + lastBackupPromptAt: number | undefined; + setLastBackupPromptAt: (lastBackupPromptAt: number | undefined) => void; + storedPassword: string; setStoredPassword: (storedPassword: string) => void; @@ -57,136 +65,156 @@ interface BackupsStore { const returnEarlyIfLockedStates = [CloudBackupState.Syncing, CloudBackupState.Fetching]; -export const backupsStore = createRainbowStore((set, get) => ({ - storedPassword: '', - setStoredPassword: storedPassword => set({ storedPassword }), +export const backupsStore = createRainbowStore( + (set, get) => ({ + timesPromptedForBackup: 0, + setTimesPromptedForBackup: timesPromptedForBackup => set({ timesPromptedForBackup }), - backupProvider: undefined, - setBackupProvider: provider => set({ backupProvider: provider }), + lastBackupPromptAt: undefined, + setLastBackupPromptAt: lastBackupPromptAt => set({ lastBackupPromptAt }), + storedPassword: '', + setStoredPassword: storedPassword => set({ storedPassword }), - status: CloudBackupState.Initializing, - setStatus: status => set({ status }), + backupProvider: undefined, + setBackupProvider: provider => set({ backupProvider: provider }), - backups: { files: [] }, - setBackups: backups => set({ backups }), + status: CloudBackupState.Initializing, + setStatus: status => set({ status }), - mostRecentBackup: undefined, - setMostRecentBackup: backup => set({ mostRecentBackup: backup }), + backups: { files: [] }, + setBackups: backups => set({ backups }), - password: '', - setPassword: password => set({ password }), + mostRecentBackup: undefined, + setMostRecentBackup: backup => set({ mostRecentBackup: backup }), - syncAndFetchBackups: async (retryOnFailure = true, retryCount = 0) => { - const { status } = get(); + password: '', + setPassword: password => set({ password }), - const timeoutPromise = new Promise<{ success: boolean; retry?: boolean }>(resolve => { - setTimeout(() => { - resolve({ success: false, retry: retryOnFailure }); - }, DEFAULT_TIMEOUT); - }); + syncAndFetchBackups: async (retryOnFailure = true, retryCount = 0) => { + const { status } = get(); - const syncAndPullFiles = async (): Promise<{ success: boolean; retry?: boolean }> => { - try { - const isAvailable = await isCloudBackupAvailable(); - if (!isAvailable) { - logger.debug('[backupsStore]: Cloud backup is not available'); - set({ backupProvider: undefined, status: CloudBackupState.NotAvailable, backups: { files: [] }, mostRecentBackup: undefined }); - return { - success: false, - retry: false, - }; - } + const timeoutPromise = new Promise<{ success: boolean; retry?: boolean }>(resolve => { + setTimeout(() => { + resolve({ success: false, retry: retryOnFailure }); + }, DEFAULT_TIMEOUT); + }); - if (IS_ANDROID) { - const gdata = await getGoogleAccountUserData(true); - if (!gdata) { - logger.debug('[backupsStore]: Google account is not available'); + 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, }; } - } - set({ status: CloudBackupState.Syncing }); - logger.debug('[backupsStore]: Syncing with cloud'); - await syncCloud(); + if (IS_ANDROID) { + const gdata = await getGoogleAccountUserData(true); + 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.Fetching }); - logger.debug('[backupsStore]: Fetching backups'); - const backups = await fetchAllBackups(); + set({ status: CloudBackupState.Syncing }); + logger.debug('[backupsStore]: Syncing with cloud'); + await syncCloud(); - set({ backups }); + set({ status: CloudBackupState.Fetching }); + logger.debug('[backupsStore]: Fetching backups'); + const backups = await fetchAllBackups(); - const { wallets } = store.getState().wallets; + set({ backups }); - // 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), + 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, }); - } else if (hasManuallyBackedUpWallet(wallets)) { - set({ backupProvider: walletBackupTypes.manual }); - } else { - set({ backupProvider: undefined }); + set({ status: CloudBackupState.FailedToInitialize }); + + // See https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInStatusCodes#public-static-final-int-sign_in_cancelled + const stringifiedError = JSON.stringify(e); + if (stringifiedError.includes('12501')) { + logger.warn('[backupsStore]: Google sign in / oauth cancelled'); + return { + success: false, + retry: false, + }; + } } - logger.debug(`[backupsStore]: Retrieved ${backups.files.length} backup files`); + return { + success: false, + retry: retryOnFailure, + }; + }; - set({ status: CloudBackupState.Ready }); + if (mutex.isLocked() || returnEarlyIfLockedStates.includes(status)) { + logger.debug('[backupsStore]: Mutex is locked or returnEarlyIfLockedStates includes status', { + status, + }); return { - success: true, + success: false, retry: false, }; - } catch (e) { - logger.error(new RainbowError('[backupsStore]: Failed to fetch all backups'), { - error: e, - }); - set({ status: CloudBackupState.FailedToInitialize }); + } - // See https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInStatusCodes#public-static-final-int-sign_in_cancelled - const stringifiedError = JSON.stringify(e); - if (stringifiedError.includes('12501')) { - logger.warn('[backupsStore]: Google sign in / oauth cancelled'); - 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); } - return { - success: false, - retry: retryOnFailure, - }; - }; + if (retry && retryCount >= MAX_RETRIES) { + logger.error(new RainbowError('[backupsStore]: Max retry attempts reached. Sync failed.')); + } - 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 }; - }, -})); + return { success, retry }; + }, + }), + { + storageKey: 'backups', + version: 0, + partialize: state => ({ + lastBackupPromptAt: state.lastBackupPromptAt, + timesPromptedForBackup: state.timesPromptedForBackup, + }), + } +);