Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new logic for backup prompt #6388

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]);
walmat marked this conversation as resolved.
Show resolved Hide resolved

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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought this behavior would delay the next prompt by an additional week after every prompt (2 weeks, 3 weeks, etc). But it seems that timesPromptedForBackup is only ever set to 0 or 1, which is kind of confusing because I would assume timesPromptedForBackup gets incremented every time they are prompted.

Copy link
Contributor Author

@walmat walmat Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to spec we want 1 week after first prompt then every 2 weeks until backed up

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I get that was just saying the variable being named timesPromptedForBackup was confusing because it does not actually represent the number of times a user has been prompted for backup, it's only ever 0 or 1. Just a variable naming complaint, doesn't actually matter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, I could name it something like weeklyMultiplier or something more representative

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
Loading