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

Fix: Wallets being marked as backed up by walletLoadState() #5593

Merged
merged 4 commits into from
Apr 11, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 44 additions & 14 deletions src/components/backup/RestoreCloudStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { useDispatch } from 'react-redux';
import WalletAndBackup from '@/assets/WalletsAndBackup.png';
import { KeyboardArea } from 'react-native-keyboard-area';

import { Backup, fetchBackupPassword, restoreCloudBackup, RestoreCloudBackupResultStates, saveBackupPassword } from '@/model/backup';
import {
Backup,
fetchBackupPassword,
getLocalBackupPassword,
restoreCloudBackup,
RestoreCloudBackupResultStates,
saveLocalBackupPassword,
} from '@/model/backup';
import { cloudPlatform } from '@/utils/platform';
import { PasswordField } from '../fields';
import { Text } from '../text';
Expand All @@ -14,7 +21,13 @@ import { cloudBackupPasswordMinLength, isCloudBackupPasswordValid, normalizeAndr
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { useDimensions, useInitializeWallet } from '@/hooks';
import { useNavigation } from '@/navigation';
import { addressSetSelected, setAllWalletsWithIdsAsBackedUp, walletsLoadState, walletsSetSelected } from '@/redux/wallets';
import {
addressSetSelected,
setAllWalletsWithIdsAsBackedUp,
setWalletBackedUp,
walletsLoadState,
walletsSetSelected,
} from '@/redux/wallets';
import Routes from '@/navigation/routesNames';
import styled from '@/styled-thing';
import { padding } from '@/styles';
Expand Down Expand Up @@ -122,6 +135,8 @@ export default function RestoreCloudStep() {
throw new Error('No backup file selected');
}

const prevWalletsState = await dispatch(walletsLoadState());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

added this here because we need to diff the prev state with the new state once we receive a success response.

Copy link
Contributor

Choose a reason for hiding this comment

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

didnt think this would return


const status = await restoreCloudBackup({
password,
userData,
Expand All @@ -130,34 +145,49 @@ export default function RestoreCloudStep() {

if (status === RestoreCloudBackupResultStates.success) {
// Store it in the keychain in case it was missing
await saveBackupPassword(password);
const hasSavedPassword = await getLocalBackupPassword();
if (!hasSavedPassword) {
await saveLocalBackupPassword(password);
}
Comment on lines +148 to +151
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@skylarbarrera does this make sense to do here? This is the new local backup function we wrote not sure if you remember.

Copy link
Contributor

Choose a reason for hiding this comment

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

this seems fine, i think the issue was that password could be undefined/empty? that issue seems diff


InteractionManager.runAfterInteractions(async () => {
const wallets = await dispatch(walletsLoadState());
const newWalletsState = await dispatch(walletsLoadState());
let filename = selectedBackup.name;
if (IS_ANDROID && filename) {
/**
* We need to normalize the filename on Android, because sometimes
* the filename is returned with the path used for Google Drive storage.
* That is with REMOTE_BACKUP_WALLET_DIR included.
*/
filename = normalizeAndroidBackupFilename(filename);
}

logger.info('Done updating backup state');
const walletIdsToUpdate = Object.keys(wallets);
// NOTE: Marking the restored wallets as backed up
// @ts-expect-error TypeScript doesn't play nicely with Redux types here
const walletIdsToUpdate = Object.keys(newWalletsState || {}).filter(walletId => !(prevWalletsState || {})[walletId]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we want to mark all the wallets brought in from the restoration as backed up by cloud, so here is where we filter those down.

logger.log('updating backup state of wallets with ids', {
walletIds: JSON.stringify(walletIdsToUpdate),
});
logger.log('backupSelected.name', {
fileName: selectedBackup.name,
});

await dispatch(setAllWalletsWithIdsAsBackedUp(walletIdsToUpdate, walletBackupTypes.cloud, filename));

const walletKeys = Object.keys(wallets || {});
const firstWallet =
// @ts-expect-error TypeScript doesn't play nicely with Redux types here
walletKeys.length > 0 ? (wallets || {})[walletKeys[0]] : undefined;
const oldCloudIds: string[] = [];
const oldManualIds: string[] = [];
// NOTE: Looping over previous wallets and restoring backup state of that wallet
Object.values(prevWalletsState || {}).forEach(wallet => {
// NOTE: This handles cloud and manual backups
if (wallet.backedUp && wallet.backupType === walletBackupTypes.cloud) {
oldCloudIds.push(wallet.id);
} else if (wallet.backedUp && wallet.backupType === walletBackupTypes.manual) {
oldManualIds.push(wallet.id);
}
});

await dispatch(setAllWalletsWithIdsAsBackedUp(oldCloudIds, walletBackupTypes.cloud, filename));
await dispatch(setAllWalletsWithIdsAsBackedUp(oldManualIds, walletBackupTypes.manual, filename));
Comment on lines +173 to +186
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if there's a better way to do this, but we also need to handle previous wallets that were in a backed up state (whether manually or cloud),s o we loop over the prev state and grab those.

Copy link
Contributor

Choose a reason for hiding this comment

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

this hurts my head, may need to huddle on monday


const walletKeys = Object.keys(newWalletsState || {});
// @ts-expect-error TypeScript doesn't play nicely with Redux types here
const firstWallet = walletKeys.length > 0 ? (newWalletsState || {})[walletKeys[0]] : undefined;
const firstAddress = firstWallet ? firstWallet.addresses[0].address : undefined;
const p1 = dispatch(walletsSetSelected(firstWallet));
const p2 = dispatch(addressSetSelected(firstAddress));
Expand Down
7 changes: 3 additions & 4 deletions src/components/backup/useCreateBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
const walletCloudBackup = useWalletCloudBackup();
const { latestBackup, wallets } = useWallets();
const [loading, setLoading] = useState<useCreateBackupStateType>('none');
const [alreadyHasLocalPassword, setAlreadyHasLocalPassword] = useState(false);

const [password, setPassword] = useState('');

Expand All @@ -49,7 +48,8 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
[setLoading]
);
const onSuccess = useCallback(async () => {
if (!alreadyHasLocalPassword) {
const hasSavedPassword = await getLocalBackupPassword();
if (!hasSavedPassword) {
Comment on lines +51 to +52
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@skylarbarrera also think the main issue here was the state variable wasn't be persisted when we were fetching the password on the BackupCloudStep component.

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 think that makes sense to me

await saveLocalBackupPassword(password);
}
analytics.track('Backup Complete', {
Expand All @@ -58,7 +58,7 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
});
setLoadingStateWithTimeout('success');
fetchBackups();
}, [alreadyHasLocalPassword, setLoadingStateWithTimeout, fetchBackups, password]);
}, [setLoadingStateWithTimeout, fetchBackups, password]);

const onError = useCallback(
(msg: string) => {
Expand Down Expand Up @@ -115,7 +115,6 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
const getPassword = useCallback(async (): Promise<string | null> => {
const password = await getLocalBackupPassword();
if (password) {
setAlreadyHasLocalPassword(true);
setPassword(password);
return password;
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/backup/usePasswordValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const usePasswordValidation = (password: string, confirmPassword: string)
newLabel = lang.t('back_up.cloud.password.minimum_characters', {
minimumLength: cloudBackupPasswordMinLength,
});
passwordIsValid = false;
} else if (
isCloudBackupPasswordValid(password) &&
isCloudBackupPasswordValid(confirmPassword) &&
Expand All @@ -30,6 +31,7 @@ export const usePasswordValidation = (password: string, confirmPassword: string)
) {
newLabel = lang.t(lang.l.back_up.cloud.password.passwords_dont_match);
setLabelColor(colors.red);
passwordIsValid = false;
}

setValidPassword(passwordIsValid);
Expand Down
56 changes: 52 additions & 4 deletions src/screens/SettingsSheet/components/Backups/ViewWalletBackup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ import Routes from '@/navigation/routesNames';
import walletBackupTypes from '@/helpers/walletBackupTypes';
import { SETTINGS_BACKUP_ROUTES } from './routes';
import { analyticsV2 } from '@/analytics';
import { InteractionManager } from 'react-native';
import { InteractionManager, Linking } from 'react-native';
import { useDispatch } from 'react-redux';
import { createAccountForWallet, walletsLoadState } from '@/redux/wallets';
import { backupUserDataIntoCloud } from '@/handlers/cloudBackup';
import {
GoogleDriveUserData,
backupUserDataIntoCloud,
getGoogleAccountUserData,
isCloudBackupAvailable,
login,
} from '@/handlers/cloudBackup';
import { logger, RainbowError } from '@/logger';
import { captureException } from '@sentry/react-native';
import { RainbowAccount, createWallet } from '@/model/wallet';
Expand All @@ -47,6 +53,7 @@ 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';

type ViewWalletBackupParams = {
ViewWalletBackup: { walletId: string; title: string; imported?: boolean };
Expand Down Expand Up @@ -180,6 +187,47 @@ const ViewWalletBackup = () => {
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(e as RainbowError);
}
} 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]);

const onNavigateToSecretWarning = useCallback(() => {
navigate(SETTINGS_BACKUP_ROUTES.SECRET_WARNING, {
walletId,
Expand Down Expand Up @@ -395,7 +443,7 @@ const ViewWalletBackup = () => {
cloudPlatformName: cloudPlatform,
})}
loading={loading}
onPress={() => onSubmit({})}
onPress={backupWalletsToCloud}
/>
</Menu>
)}
Expand All @@ -415,7 +463,7 @@ const ViewWalletBackup = () => {
cloudPlatformName: cloudPlatform,
})}
loading={loading}
onPress={() => onSubmit({})}
onPress={backupWalletsToCloud}
/>
</Menu>
)}
Expand Down
70 changes: 68 additions & 2 deletions src/screens/SettingsSheet/components/Backups/WalletsAndBackup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import { format } from 'date-fns';
import { removeFirstEmojiFromString } from '@/helpers/emojiHandler';
import { Backup, parseTimestampFromFilename } from '@/model/backup';
import { useCloudBackups } from '@/components/backup/CloudBackupProvider';
import { login } from '@/handlers/cloudBackup';
import { GoogleDriveUserData, getGoogleAccountUserData, isCloudBackupAvailable, login } from '@/handlers/cloudBackup';
import { WrappedAlert as Alert } from '@/helpers/alert';
import { Linking } from 'react-native';

type WalletPillProps = {
account: RainbowAccount;
Expand Down Expand Up @@ -164,7 +166,40 @@ export const WalletsAndBackup = () => {

const backupAllNonBackedUpWalletsTocloud = useCallback(async () => {
if (IS_ANDROID) {
await login();
try {
await login();

getGoogleAccountUserData().then((accountDetails: GoogleDriveUserData | undefined) => {
if (accountDetails) {
return onSubmit({ type: BackupTypes.All });
}
Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
});
} catch (e) {
Alert.alert(i18n.t(i18n.l.back_up.errors.no_account_found));
logger.error(e as RainbowError);
}
} 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 });
Expand Down Expand Up @@ -320,6 +355,37 @@ export const WalletsAndBackup = () => {
titleComponent={<MenuItem.Title isLink text={i18n.t(i18n.l.back_up.manual.create_new_secret_phrase)} />}
/>
</Menu>

<Menu>
<MenuItem
hasSfSymbol
leftComponent={<MenuItem.TextIcon icon="􀣔" isLink />}
onPress={onViewCloudBackups}
size={52}
titleComponent={
<MenuItem.Title
isLink
text={i18n.t(i18n.l.back_up.cloud.manage_platform_backups, {
cloudPlatformName: cloudPlatform,
})}
/>
}
/>
<MenuItem
hasSfSymbol
leftComponent={<MenuItem.TextIcon icon="􀍡" isLink />}
onPress={manageCloudBackups}
size={52}
titleComponent={
<MenuItem.Title
isLink
text={i18n.t(i18n.l.back_up.cloud.cloud_platform_backup_settings, {
cloudPlatformName: cloudPlatform,
})}
/>
}
/>
</Menu>
</Stack>
</Stack>
);
Expand Down