Skip to content

Commit

Permalink
🛹 [FIX]: Handle Ledgersync onboarding errors (#7733)
Browse files Browse the repository at this point in the history
* onboarding unbacked errors for QR code and device sync

* Add the `onInitialResponse` callback to `getOrCreateTrustchain`

* Show the `NO_BACKUP_ONBOARDING_DEVICE` error

* Show the right UnbackedError from the normal QR code flow

* Show the actual unbacked errors UI

* Implement `DrawerProps.retry`

* Handle analytics

* Update change log

* Fix json formatting

* Remove onboarding device error missing description

---------

Co-authored-by: Theophile Sandoz <Theophile Sandoz>
  • Loading branch information
thesan committed Sep 4, 2024
1 parent 255f035 commit ef99222
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 55 deletions.
6 changes: 6 additions & 0 deletions .changeset/forty-rules-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"live-mobile": minor
"@ledgerhq/trustchain": patch
---

Handle Ledgersync onboarding errors
11 changes: 11 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -6877,6 +6877,12 @@
"description": "Please make sure you’ve created an encryption key on one of your Ledger Live apps before continuing your synchronization.",
"cta": "Create your encryption key"
},
"unbackedOnboarding": {
"title": "No backup detected",
"description": "Secure your other app with a Ledger then try again or continue your onboarding",
"cta": "Try again",
"cancel": "Continue without sync"
},
"backedWithDifferentSeeds": {
"title": "These apps have different backups",
"description": "Delete your encryption key from one of the apps and try again",
Expand All @@ -6887,6 +6893,11 @@
"cta": "I understand"
}
},
"unbackedOnboarding": {
"title": "This Ledger doesn’t secure a backup",
"cta": "Try another Ledger",
"cancel": "Continue without sync"
},
"alreadySecureError": {
"title": "This app is already secured by this Ledger",
"description": "If you are attempting to create a new backup, please connect the Ledger device you used to create your other encryption key.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { useSelector } from "react-redux";
import Activation from ".";
import { TrackScreen } from "~/analytics";
import {
RootNavigationComposite,
StackNavigatorNavigation,
} from "~/components/RootNavigator/types/helpers";
import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator";
import { NavigatorName, ScreenName } from "~/const";
import { hasCompletedOnboardingSelector } from "~/reducers/settings";
import ChooseSyncMethod from "../../screens/Synchronize/ChooseMethod";
import QrCodeMethod from "../../screens/Synchronize/QrCodeMethod";
import { Options, Steps } from "../../types/Activation";
Expand Down Expand Up @@ -52,6 +61,10 @@ const ActivationFlow = ({
if (input && inputCallback && nbDigits === input.length) handleSendDigits(inputCallback, input);
};

const hasCompletedOnboarding = useSelector(hasCompletedOnboardingSelector);
const { navigate } =
useNavigation<RootNavigationComposite<StackNavigatorNavigation<BaseNavigatorStackParamList>>>();

const getScene = () => {
switch (currentStep) {
case Steps.Activation:
Expand Down Expand Up @@ -90,6 +103,23 @@ const ActivationFlow = ({
return <SyncError tryAgain={navigateToQrCodeMethod} />;

case Steps.UnbackedError:
if (!hasCompletedOnboarding) {
return (
<SpecificError
primaryAction={navigateToQrCodeMethod}
secondaryAction={() => {
navigate(NavigatorName.BaseOnboarding, {
screen: NavigatorName.Onboarding,
params: {
screen: ScreenName.OnboardingPostWelcomeSelection,
params: { userHasDevice: true },
},
});
}}
error={ErrorReason.NO_BACKUP_ONBOARDING_QRCODE}
/>
);
}
return <SpecificError primaryAction={onCreateKey} error={ErrorReason.NO_BACKUP} />;

case Steps.AlreadyBacked:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,30 @@ import {
import { useDispatch, useSelector } from "react-redux";
import { useTrustchainSdk } from "./useTrustchainSdk";
import {
NoTrustchainInitialized,
TrustchainAlreadyInitialized,
TrustchainAlreadyInitializedWithOtherSeed,
TrustchainNotAllowed,
} from "@ledgerhq/trustchain/errors";
import { TrustchainResult, TrustchainResultType } from "@ledgerhq/trustchain/types";
import { useCallback, useEffect, useRef } from "react";
import { useCallback, useRef } from "react";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import { useNavigation } from "@react-navigation/native";
import { WalletSyncNavigatorStackParamList } from "~/components/RootNavigator/types/WalletSyncNavigator";
import { StackNavigatorNavigation } from "~/components/RootNavigator/types/helpers";
import { ScreenName } from "~/const";
import { hasCompletedOnboardingSelector } from "~/reducers/settings";
import { DrawerProps, SceneKind, useFollowInstructionDrawer } from "./useFollowInstructionDrawer";

export function useAddMember({ device }: { device: Device | null }): DrawerProps {
const [DrawerProps, setScene] = useFollowInstructionDrawer();
const trustchain = useSelector(trustchainSelector);
const dispatch = useDispatch();
const sdk = useTrustchainSdk();
const memberCredentials = useSelector(memberCredentialsSelector);
const memberCredentialsRef = useRef(memberCredentials);
const trustchainRef = useRef(trustchain);
const navigation = useNavigation<StackNavigatorNavigation<WalletSyncNavigatorStackParamList>>();
const hasCompletedOnboarding = useSelector(hasCompletedOnboardingSelector);

const transitionToNextScreen = useCallback(
(trustchainResult: TrustchainResult) => {
Expand All @@ -39,8 +41,8 @@ export function useAddMember({ device }: { device: Device | null }): DrawerProps
[dispatch, navigation],
);

useEffect(() => {
const addMember = async () => {
return useFollowInstructionDrawer(
async setScene => {
try {
if (!device) return;
if (!memberCredentialsRef.current) {
Expand All @@ -50,6 +52,10 @@ export function useAddMember({ device }: { device: Device | null }): DrawerProps
device.deviceId,
memberCredentialsRef.current,
{
onInitialResponse: trustchains => {
if (hasCompletedOnboarding || Object.keys(trustchains).length > 0) return;
else throw new NoTrustchainInitialized();
},
onStartRequestUserInteraction: () =>
setScene({ kind: SceneKind.DeviceInstructions, device }),
onEndRequestUserInteraction: () => setScene({ kind: SceneKind.Loader }),
Expand All @@ -67,15 +73,13 @@ export function useAddMember({ device }: { device: Device | null }): DrawerProps
setScene({ kind: SceneKind.AlreadySecuredSameSeed });
} else if (error instanceof TrustchainAlreadyInitializedWithOtherSeed) {
setScene({ kind: SceneKind.AlreadySecuredOtherSeed });
} else if (error instanceof NoTrustchainInitialized) {
setScene({ kind: SceneKind.UnbackedError });
} else if (error instanceof Error) {
setScene({ kind: SceneKind.GenericError, error });
}
}
};
if (device && device.deviceId) {
addMember();
}
}, [setScene, device, dispatch, sdk, transitionToNextScreen]);

return DrawerProps;
},
[device, sdk, transitionToNextScreen, hasCompletedOnboarding],
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigation } from "@react-navigation/native";
import { Dispatch, SetStateAction, useCallback, useState } from "react";
import { DependencyList, useCallback, useEffect, useState } from "react";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import { StackNavigatorNavigation } from "~/components/RootNavigator/types/helpers";
import { WalletSyncNavigatorStackParamList } from "~/components/RootNavigator/types/WalletSyncNavigator";
Expand All @@ -11,6 +11,7 @@ export enum SceneKind {
Loader,
WrongSeedError,
KeyError,
UnbackedError,
GenericError,
AlreadySecuredSameSeed,
AlreadySecuredOtherSeed,
Expand All @@ -22,24 +23,30 @@ type Scene =
| { kind: SceneKind.KeyError }
| { kind: SceneKind.AlreadySecuredSameSeed }
| { kind: SceneKind.AlreadySecuredOtherSeed }
| { kind: SceneKind.UnbackedError }
| { kind: SceneKind.GenericError; error: Error };

export type DrawerProps = {
scene: Scene;
onRetry: () => void;
retry: () => void;
goToDelete: () => void;
backToKeyError: () => void;
confirmDeleteKey: () => void;
};

export function useFollowInstructionDrawer(): [DrawerProps, Dispatch<SetStateAction<Scene>>] {
export function useFollowInstructionDrawer(
run: (setScene: (scene: Scene) => void) => Promise<void>,
deps: DependencyList = [],
): DrawerProps {
const navigation = useNavigation<StackNavigatorNavigation<WalletSyncNavigatorStackParamList>>();
const { deleteMutation } = useDestroyTrustchain();

const [scene, setScene] = useState<Scene>({ kind: SceneKind.Loader });

// eslint-disable-next-line no-console
const onRetry = useCallback(() => console.log("onRetry"), []);
const retry = useCallback(async () => {
setScene({ kind: SceneKind.Loader });
await run(setScene);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

const goToDelete = useCallback(() => {
setScene({ kind: SceneKind.WrongSeedError });
Expand All @@ -54,5 +61,9 @@ export function useFollowInstructionDrawer(): [DrawerProps, Dispatch<SetStateAct
navigation.navigate(ScreenName.WalletSyncManageKeyDeleteSuccess);
}, [deleteMutation, navigation]);

return [{ scene, onRetry, goToDelete, backToKeyError, confirmDeleteKey }, setScene];
useEffect(() => {
run(setScene);
}, [...deps]); // eslint-disable-line react-hooks/exhaustive-deps

return { scene, retry, goToDelete, backToKeyError, confirmDeleteKey };
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export enum AnalyticsPage {
SameSeed = "App already secured with this Ledger",
ScanAttemptWithSameBackup = "Scan attempt with same backup",
ScanAttemptWithDifferentBackups = "Scan attempt with different backups",
OnBoardingQRCodeNoBackup = "Onboarding no backup detected",
OnBoardingDeviceNoBackup = "Onboarding this Ledger does not secure a backup",
}

export enum AnalyticsFlow {
Expand Down Expand Up @@ -53,6 +55,8 @@ export enum AnalyticsButton {
LedgerSync = "Ledger Sync",
UseAnother = "Connect another ledger",
Understand = "I understand",
TryAnotherLedger = "Try another Ledger",
ContinueWihtoutSync = "Continue without sync",
}

type OnClickTrack = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useTrustchainSdk } from "./useTrustchainSdk";
import { TrustchainNotAllowed } from "@ledgerhq/trustchain/errors";
import { TrustchainMember, Trustchain } from "@ledgerhq/trustchain/types";
import { useCallback, useEffect } from "react";
import { useCallback } from "react";
import { Device } from "@ledgerhq/live-common/hw/actions/types";
import { useNavigation } from "@react-navigation/native";
import { ScreenName } from "~/const";
Expand All @@ -21,18 +21,12 @@ type Props = {
};

export function useRemoveMember({ device, member }: Props): DrawerProps {
const [DrawerProps, setScene] = useFollowInstructionDrawer();

const dispatch = useDispatch();
const sdk = useTrustchainSdk();
const trustchain = useSelector(trustchainSelector);
const memberCredentials = useSelector(memberCredentialsSelector);
const navigation = useNavigation<StackNavigatorNavigation<WalletSyncNavigatorStackParamList>>();

// eslint-disable-next-line no-console
const onResetFlow = useCallback(() => console.log("onResetFlow"), []);
// () => dispatch(setFlow({ flow: Flow.ManageInstances, step: Step.SynchronizedInstances })),

const transitionToNextScreen = useCallback(
(trustchainResult: Trustchain) => {
if (!member) return;
Expand All @@ -44,9 +38,10 @@ export function useRemoveMember({ device, member }: Props): DrawerProps {
[dispatch, member, navigation],
);

const removeMember = useCallback(
async (member: TrustchainMember) => {
return useFollowInstructionDrawer(
async setScene => {
try {
if (!member) return;
if (!device) return;
if (!trustchain || !memberCredentials) {
throw new Error("trustchain or memberCredentials is not set");
Expand All @@ -72,18 +67,6 @@ export function useRemoveMember({ device, member }: Props): DrawerProps {
}
}
},
[setScene, device, memberCredentials, sdk, transitionToNextScreen, trustchain],
[member, device, memberCredentials, sdk, transitionToNextScreen, trustchain],
);

useEffect(() => {
if (device && device.deviceId) {
if (!member) {
onResetFlow();
} else {
removeMember(member);
}
}
}, [device, member, onResetFlow, removeMember]);

return DrawerProps;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export enum ErrorReason {
ALREADY_BACKED_SCAN = "already-backed",
DIFFERENT_BACKUPS = "different-backups",
NO_BACKUP = "no-backup",
NO_BACKUP_ONBOARDING_DEVICE = "no-backup-onboarding-device",
NO_BACKUP_ONBOARDING_QRCODE = "no-backup-onboarding-qrcode",
}

export interface ErrorConfig {
Expand Down Expand Up @@ -43,6 +45,11 @@ export function useSpecificError({ primaryAction, secondaryAction }: SpecificPro
const onTryAgain = (page: AnalyticsPage) => {
onClickTrack({ button: AnalyticsButton.UseAnother, page });
};

const onTryAnotherLedger = (page: AnalyticsPage) => {
onClickTrack({ button: AnalyticsButton.TryAnotherLedger, page });
};

const onGoToDelete = (page: AnalyticsPage) => {
onClickTrack({ button: AnalyticsButton.DeleteKey, page });
};
Expand All @@ -59,6 +66,10 @@ export function useSpecificError({ primaryAction, secondaryAction }: SpecificPro
onClickTrack({ button: AnalyticsButton.CreateYourKey, page });
};

const ContinueWihtoutSync = (page: AnalyticsPage) => {
onClickTrack({ button: AnalyticsButton.ContinueWihtoutSync, page });
};

const errorConfig: Record<ErrorReason, ErrorConfig> = {
[ErrorReason.UNSECURED]: {
icon: <Icons.DeleteCircleFill size={"L"} color={colors.error.c60} />,
Expand Down Expand Up @@ -170,6 +181,39 @@ export function useSpecificError({ primaryAction, secondaryAction }: SpecificPro
onCreate(AnalyticsPage.SyncWithNoKey);
},
},
[ErrorReason.NO_BACKUP_ONBOARDING_QRCODE]: {
icon: <Icons.DeleteCircleFill size={"L"} color={colors.error.c60} />,
title: t("walletSync.synchronize.qrCode.unbackedOnboarding.title"),
description: t("walletSync.synchronize.qrCode.unbackedOnboarding.description"),
cta: t("walletSync.synchronize.qrCode.unbackedOnboarding.cta"),
ctaSecondary: t("walletSync.synchronize.qrCode.unbackedOnboarding.cancel"),
analyticsPage: AnalyticsPage.OnBoardingQRCodeNoBackup,
buttonType: "main" as ButtonProps["type"],
primaryAction: () => {
primaryAction();
onTryAgain(AnalyticsPage.OnBoardingQRCodeNoBackup);
},
secondaryAction: () => {
secondaryAction?.();
ContinueWihtoutSync(AnalyticsPage.OnBoardingQRCodeNoBackup);
},
},
[ErrorReason.NO_BACKUP_ONBOARDING_DEVICE]: {
icon: <Icons.DeleteCircleFill size={"L"} color={colors.error.c60} />,
title: t("walletSync.synchronize.unbackedOnboarding.title"),
cta: t("walletSync.synchronize.unbackedOnboarding.cta"),
ctaSecondary: t("walletSync.synchronize.unbackedOnboarding.cancel"),
analyticsPage: AnalyticsPage.OnBoardingDeviceNoBackup,
buttonType: "main" as ButtonProps["type"],
primaryAction: () => {
primaryAction();
onTryAnotherLedger(AnalyticsPage.OnBoardingDeviceNoBackup);
},
secondaryAction: () => {
secondaryAction?.();
ContinueWihtoutSync(AnalyticsPage.OnBoardingDeviceNoBackup);
},
},
};

const getErrorConfig = (error: ErrorReason) => errorConfig[error];
Expand Down
Loading

0 comments on commit ef99222

Please sign in to comment.