Skip to content

Commit

Permalink
Implement new logic for backup prompt (#6388)
Browse files Browse the repository at this point in the history
  • Loading branch information
walmat authored Jan 13, 2025
1 parent d312658 commit d3e8172
Show file tree
Hide file tree
Showing 12 changed files with 430 additions and 127 deletions.
29 changes: 27 additions & 2 deletions src/components/backup/BackupSheet.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -22,16 +25,38 @@ export default function BackupSheet() {

const renderStep = useCallback(() => {
switch (step) {
case WalletBackupStepTypes.backup_cloud:
case WalletBackupStepTypes.create_cloud_backup:
return <BackupCloudStep />;
case WalletBackupStepTypes.restore_from_backup:
return <RestoreCloudStep />;
case WalletBackupStepTypes.backup_prompt:
return <BackupWalletPrompt />;
case WalletBackupStepTypes.backup_prompt_manual:
return <ManualBackupPrompt />;
case WalletBackupStepTypes.backup_prompt_cloud:
return <CloudBackupPrompt />;
default:
return <BackupWalletPrompt />;
}
}, [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 (
<BackgroundProvider color="surfaceSecondary">
{({ backgroundColor }) => (
Expand Down
122 changes: 122 additions & 0 deletions src/components/backup/CloudBackupPrompt.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Inset horizontal={'24px'} vertical={'44px'}>
<Inset bottom={'44px'} horizontal={'24px'}>
<Stack alignHorizontal="center">
<Box
as={ImgixImage}
borderRadius={imageSize / 2}
height={{ custom: imageSize }}
marginLeft={{ custom: -12 }}
marginRight={{ custom: -12 }}
marginTop={{ custom: 0 }}
marginBottom={{ custom: 8 }}
source={WalletsAndBackupIcon as Source}
width={{ custom: imageSize }}
size={imageSize}
/>
<Text align="center" size="26pt" weight="bold" color="label">
{lang.t(lang.l.back_up.cloud.add_wallet_to_cloud_backups)}
</Text>
</Stack>
</Inset>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>

<ButtonPressAnimation scaleTo={0.95} onPress={onCloudBackup}>
<Box alignItems="center" justifyContent="center" paddingTop={'24px'} paddingBottom={'24px'}>
<Box alignItems="center" justifyContent="center" width="full">
<Inline alignHorizontal="justify" alignVertical="center" wrap={false}>
<Text color={'action (Deprecated)'} size="20pt" weight="bold">
􀎽{' '}
{lang.t(lang.l.back_up.cloud.back_to_cloud_platform_now, {
cloudPlatform,
})}
</Text>
</Inline>
</Box>
</Box>
</ButtonPressAnimation>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>

<ButtonPressAnimation scaleTo={0.95} onPress={onMaybeLater}>
<Box alignItems="center" justifyContent="center" paddingTop={'24px'} paddingBottom={'24px'}>
<Box alignItems="center" justifyContent="center" width="full">
<Inline alignHorizontal="justify" alignVertical="center" wrap={false}>
<Text color={'labelSecondary'} size="20pt" weight="bold">
{lang.t(lang.l.back_up.cloud.mayber_later)}
</Text>
</Inline>
</Box>
</Box>
</ButtonPressAnimation>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>

{mostRecentBackup && (
<Box alignItems="center" justifyContent="center" paddingTop={'24px'} paddingBottom={'24px'}>
<Box alignItems="center" justifyContent="center" width="full">
<Inline alignHorizontal="justify" alignVertical="center" wrap={false}>
<Text color={'labelTertiary'} size="15pt" weight="medium">
{lang.t(lang.l.back_up.cloud.latest_backup, {
date: format(new Date(mostRecentBackup.lastModified), "M/d/yy 'at' h:mm a"),
})}
</Text>
</Inline>
</Box>
</Box>
)}
</Inset>
);
}
101 changes: 101 additions & 0 deletions src/components/backup/ManualBackupPrompt.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Inset horizontal={'24px'} vertical={'44px'}>
<Inset bottom={'44px'} horizontal={'24px'}>
<Stack alignHorizontal="center">
<Box
as={ImgixImage}
borderRadius={imageSize / 2}
height={{ custom: imageSize }}
marginLeft={{ custom: -12 }}
marginRight={{ custom: -12 }}
marginTop={{ custom: 0 }}
marginBottom={{ custom: 8 }}
source={ManuallyBackedUpIcon as Source}
width={{ custom: imageSize }}
size={imageSize}
/>
<Text align="center" size="26pt" weight="bold" color="label">
{lang.t(lang.l.back_up.manual.backup_manually_now)}
</Text>
</Stack>
</Inset>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>

<ButtonPressAnimation scaleTo={0.95} onPress={onManualBackup}>
<Box alignItems="center" justifyContent="center" paddingTop={'24px'} paddingBottom={'24px'}>
<Box alignItems="center" justifyContent="center" width="full">
<Inline alignHorizontal="justify" alignVertical="center" wrap={false}>
<Text color={'action (Deprecated)'} size="20pt" weight="bold">
{lang.t(lang.l.back_up.manual.back_up_now)}
</Text>
</Inline>
</Box>
</Box>
</ButtonPressAnimation>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>

<ButtonPressAnimation scaleTo={0.95} onPress={onMaybeLater}>
<Box alignItems="center" justifyContent="center" paddingTop={'24px'} paddingBottom={'24px'}>
<Box alignItems="center" justifyContent="center" width="full">
<Inline alignHorizontal="justify" alignVertical="center" wrap={false}>
<Text color={'labelSecondary'} size="20pt" weight="bold">
{lang.t(lang.l.back_up.manual.already_backed_up)}
</Text>
</Inline>
</Box>
</Box>
</ButtonPressAnimation>

<Bleed horizontal="24px">
<Separator color="separatorSecondary" thickness={1} />
</Bleed>
</Inset>
);
}
2 changes: 1 addition & 1 deletion src/components/backup/useCreateBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
25 changes: 22 additions & 3 deletions src/handlers/walletReadyEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -34,10 +35,28 @@ const promptForBackupOnceReadyOrNotAvailable = async (): Promise<boolean> => {
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;
Expand Down
7 changes: 3 additions & 4 deletions src/helpers/walletBackupStepTypes.ts
Original file line number Diff line number Diff line change
@@ -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',
};
20 changes: 17 additions & 3 deletions src/hooks/useImportingWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
});
Expand All @@ -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);
Expand Down
Loading

0 comments on commit d3e8172

Please sign in to comment.