From 9fb1a3a1efce4f4c23ebc5fd04b3d7b180c47c17 Mon Sep 17 00:00:00 2001 From: Finnian Jacobson-Schulte <140328381+finnian0826@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:40:29 +1200 Subject: [PATCH] feat(onboarding): Keyless Backup Setup Fail in Onboarding Flow (#5537) ### Description Added origin param to decide whether CAB flow was entered from Onboarding or Settings. The first screen that this param is passed to is SignInWithEmail, since that is the first CAB screen that is hit during onboarding. ### Test plan Unit tests added. **Manual:** Screen: ![Simulator Screenshot - iPhone 14 Pro - 2024-06-12 at 12 31 28](https://github.com/valora-inc/wallet/assets/140328381/c3b35555-cd21-4e0b-a28e-b529856bd428) Recovery phrase: https://github.com/valora-inc/wallet/assets/140328381/7e17e02b-620d-4045-9e23-1e9065e52d4a Skip: https://github.com/valora-inc/wallet/assets/140328381/313c348c-9b48-4ad8-9be1-f0adc7c09981 ### Related issues - Fixes #ACT-1222 ### Backwards compatibility Yes ### Network scalability If a new NetworkId and/or Network are added in the future, the changes in this PR will: - [X] Continue to work without code changes, OR trigger a compilation error (guaranteeing we find it when a new network is added) --- locales/base/translation.json | 3 +- src/analytics/Events.tsx | 1 + src/analytics/Properties.tsx | 4 +- src/analytics/docs.ts | 1 + src/keylessBackup/KeylessBackupIntro.test.tsx | 3 ++ src/keylessBackup/KeylessBackupIntro.tsx | 6 ++- .../KeylessBackupPhoneCodeInput.tsx | 1 + src/keylessBackup/KeylessBackupPhoneInput.tsx | 4 +- .../KeylessBackupProgress.test.tsx | 52 ++++++++++++++++-- src/keylessBackup/KeylessBackupProgress.tsx | 54 +++++++++++++++---- src/keylessBackup/SignInWithEmail.tsx | 4 +- src/navigator/types.tsx | 5 ++ .../registration/ImportSelect.test.tsx | 2 + src/onboarding/registration/ImportSelect.tsx | 6 ++- 14 files changed, 125 insertions(+), 21 deletions(-) diff --git a/locales/base/translation.json b/locales/base/translation.json index d53786f861c..7f8de91caab 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -129,7 +129,8 @@ "title": "Backup unavailable", "body": "You can set up wallet backup later or protect your account now by saving your recovery phrase.", "later": "I'll set up backup later", - "manual": "Save recovery phrase" + "manual": "Save recovery phrase", + "skip": "Skip (not recommended)" } }, "restore": { diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index 82e710a80c1..e1ce5e963b9 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -112,6 +112,7 @@ export enum KeylessBackupEvents { cab_progress_completed_continue = 'cab_progress_completed_continue', cab_progress_failed_later = 'cab_progress_failed_later', cab_progress_failed_manual = 'cab_progress_failed_manual', + cab_progress_failed_skip_onboarding = 'cab_progress_failed_skip_onboarding', cab_post_encrypted_mnemonic_failed = 'cab_post_encrypted_mnemonic_failed', cab_torus_keyshare_timeout = 'cab_torus_keyshare_timeout', cab_handle_keyless_backup_failed = 'cab_handle_keyless_backup_failed', diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index 7b22f7d7932..a125e92037a 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -64,6 +64,7 @@ import { SerializableRewardsInfo } from 'src/earn/types' import { ProviderSelectionAnalyticsData } from 'src/fiatExchanges/types' import { CICOFlow, FiatExchangeFlow, PaymentMethod } from 'src/fiatExchanges/utils' import { HomeActionName, NotificationBannerCTATypes, NotificationType } from 'src/home/types' +import { KeylessBackupOrigin } from 'src/keylessBackup/KeylessBackupProgress' import { KeylessBackupFlow, KeylessBackupStatus } from 'src/keylessBackup/types' import { LocalCurrencyCode } from 'src/localCurrency/consts' import { NftOrigin } from 'src/nfts/types' @@ -264,7 +265,8 @@ interface KeylessBackupEventsProperties { [KeylessBackupEvents.cab_issue_valora_keyshare_error]: CommonKeylessBackupProps [KeylessBackupEvents.cab_progress_completed_continue]: undefined [KeylessBackupEvents.cab_progress_failed_later]: undefined - [KeylessBackupEvents.cab_progress_failed_manual]: undefined + [KeylessBackupEvents.cab_progress_failed_manual]: { origin: KeylessBackupOrigin } + [KeylessBackupEvents.cab_progress_failed_skip_onboarding]: undefined [KeylessBackupEvents.cab_post_encrypted_mnemonic_failed]: { backupAlreadyExists: boolean } diff --git a/src/analytics/docs.ts b/src/analytics/docs.ts index 2e3039957c1..f66107d86ec 100644 --- a/src/analytics/docs.ts +++ b/src/analytics/docs.ts @@ -144,6 +144,7 @@ export const eventDocs: Record = { [KeylessBackupEvents.cab_progress_completed_continue]: ``, [KeylessBackupEvents.cab_progress_failed_later]: ``, [KeylessBackupEvents.cab_progress_failed_manual]: ``, + [KeylessBackupEvents.cab_progress_failed_skip_onboarding]: ``, [KeylessBackupEvents.cab_post_encrypted_mnemonic_failed]: ``, [KeylessBackupEvents.cab_torus_keyshare_timeout]: ``, [KeylessBackupEvents.cab_handle_keyless_backup_failed]: `When keyless backup fails to generate store encrypted mnemonic for setup or fails to retrieve and decrypt mnemonic for restore`, diff --git a/src/keylessBackup/KeylessBackupIntro.test.tsx b/src/keylessBackup/KeylessBackupIntro.test.tsx index 0246dc86ccd..4e0f168dfdd 100644 --- a/src/keylessBackup/KeylessBackupIntro.test.tsx +++ b/src/keylessBackup/KeylessBackupIntro.test.tsx @@ -3,6 +3,7 @@ import React from 'react' import { KeylessBackupEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import KeylessBackupIntro from 'src/keylessBackup/KeylessBackupIntro' +import { KeylessBackupOrigin } from 'src/keylessBackup/KeylessBackupProgress' import { KeylessBackupFlow } from 'src/keylessBackup/types' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -30,6 +31,7 @@ describe('KeylessBackupIntro', () => { }) expect(navigate).toHaveBeenCalledWith(Screens.SignInWithEmail, { keylessBackupFlow: KeylessBackupFlow.Setup, + origin: KeylessBackupOrigin.Settings, }) }) @@ -68,6 +70,7 @@ describe('KeylessBackupIntro', () => { }) expect(navigate).toHaveBeenCalledWith(Screens.SignInWithEmail, { keylessBackupFlow: KeylessBackupFlow.Restore, + origin: KeylessBackupOrigin.Settings, }) }) }) diff --git a/src/keylessBackup/KeylessBackupIntro.tsx b/src/keylessBackup/KeylessBackupIntro.tsx index 41c04c3cf8a..3bc80756b88 100644 --- a/src/keylessBackup/KeylessBackupIntro.tsx +++ b/src/keylessBackup/KeylessBackupIntro.tsx @@ -10,6 +10,7 @@ import CancelButton from 'src/components/CancelButton' import Card from 'src/components/Card' import TextButton from 'src/components/TextButton' import EnvelopeIcon from 'src/keylessBackup/EnvelopeIcon' +import { KeylessBackupOrigin } from 'src/keylessBackup/KeylessBackupProgress' import SmartphoneIcon from 'src/keylessBackup/SmartphoneIcon' import { KeylessBackupFlow } from 'src/keylessBackup/types' import { emptyHeader } from 'src/navigator/Headers' @@ -84,7 +85,10 @@ function KeylessBackupIntro({ route }: Props) { testID="keylessBackupIntro/Continue" onPress={() => { ValoraAnalytics.track(KeylessBackupEvents.cab_intro_continue, { keylessBackupFlow }) - navigate(Screens.SignInWithEmail, { keylessBackupFlow }) + navigate(Screens.SignInWithEmail, { + keylessBackupFlow, + origin: KeylessBackupOrigin.Settings, + }) }} text={isSetup ? t('continue') : t('next')} size={BtnSizes.FULL} diff --git a/src/keylessBackup/KeylessBackupPhoneCodeInput.tsx b/src/keylessBackup/KeylessBackupPhoneCodeInput.tsx index 941685c4ee1..51e43969d75 100644 --- a/src/keylessBackup/KeylessBackupPhoneCodeInput.tsx +++ b/src/keylessBackup/KeylessBackupPhoneCodeInput.tsx @@ -116,6 +116,7 @@ function KeylessBackupPhoneCodeInput({ onSuccess={() => { navigate(Screens.KeylessBackupProgress, { keylessBackupFlow: route.params.keylessBackupFlow, + origin: route.params.origin, }) }} title={{t('phoneVerificationInput.title')}} diff --git a/src/keylessBackup/KeylessBackupPhoneInput.tsx b/src/keylessBackup/KeylessBackupPhoneInput.tsx index f30f11ceec7..a721262545b 100644 --- a/src/keylessBackup/KeylessBackupPhoneInput.tsx +++ b/src/keylessBackup/KeylessBackupPhoneInput.tsx @@ -28,7 +28,7 @@ type Props = NativeStackScreenProps new Countries(i18n.language), [i18n.language]) @@ -78,6 +78,7 @@ function KeylessBackupPhoneInput({ route }: Props) { navigate(Screens.KeylessBackupPhoneInput, { keylessBackupFlow, selectedCountryCodeAlpha2: countryCodeAlpha2, + origin, }) }, }) @@ -90,6 +91,7 @@ function KeylessBackupPhoneInput({ route }: Props) { navigate(Screens.KeylessBackupPhoneCodeInput, { keylessBackupFlow, e164Number: phoneNumberInfo.e164Number, + origin, }) } diff --git a/src/keylessBackup/KeylessBackupProgress.test.tsx b/src/keylessBackup/KeylessBackupProgress.test.tsx index 652fe1ff841..5a62c90d314 100644 --- a/src/keylessBackup/KeylessBackupProgress.test.tsx +++ b/src/keylessBackup/KeylessBackupProgress.test.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Provider } from 'react-redux' import { KeylessBackupEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' -import KeylessBackupProgress from 'src/keylessBackup/KeylessBackupProgress' +import KeylessBackupProgress, { KeylessBackupOrigin } from 'src/keylessBackup/KeylessBackupProgress' import { keylessBackupAcceptZeroBalance, keylessBackupBail } from 'src/keylessBackup/slice' import { KeylessBackupFlow, KeylessBackupStatus } from 'src/keylessBackup/types' import { ensurePincode, navigate, navigateHome } from 'src/navigator/NavigationService' @@ -40,9 +40,13 @@ function createStore(keylessBackupStatus: KeylessBackupStatus, zeroBalance = fal }) } -function getProps(flow: KeylessBackupFlow = KeylessBackupFlow.Setup) { +function getProps( + flow: KeylessBackupFlow = KeylessBackupFlow.Setup, + origin: KeylessBackupOrigin = KeylessBackupOrigin.Settings +) { return getMockStackScreenProps(Screens.KeylessBackupProgress, { keylessBackupFlow: flow, + origin, }) } @@ -115,7 +119,49 @@ describe('KeylessBackupProgress', () => { expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) expect(ValoraAnalytics.track).toHaveBeenCalledWith( - KeylessBackupEvents.cab_progress_failed_manual + KeylessBackupEvents.cab_progress_failed_manual, + { origin: KeylessBackupOrigin.Settings } + ) + }) + it('navigates to recovery phrase on failure when coming from onboarding', async () => { + jest.mocked(ensurePincode).mockResolvedValueOnce(true) + const { getByTestId } = render( + + + + ) + expect(getByTestId('KeylessBackupProgress/ManualOnboarding')).toBeTruthy() + fireEvent.press(getByTestId('KeylessBackupProgress/ManualOnboarding')) + + await waitFor(() => expect(navigate).toHaveBeenCalledTimes(1)) + expect(navigate).toHaveBeenCalledWith(Screens.AccountKeyEducation) + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith( + KeylessBackupEvents.cab_progress_failed_manual, + { origin: KeylessBackupOrigin.Onboarding } + ) + }) + it('navigates to CYA on failure when coming from onboarding', async () => { + jest.mocked(ensurePincode).mockResolvedValueOnce(true) + const { getByTestId } = render( + + + + ) + expect(getByTestId('KeylessBackupProgress/Skip')).toBeTruthy() + fireEvent.press(getByTestId('KeylessBackupProgress/Skip')) + + await waitFor(() => expect(navigate).toHaveBeenCalledTimes(1)) + expect(navigate).toHaveBeenCalledWith(Screens.ChooseYourAdventure) + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith( + KeylessBackupEvents.cab_progress_failed_skip_onboarding ) }) }) diff --git a/src/keylessBackup/KeylessBackupProgress.tsx b/src/keylessBackup/KeylessBackupProgress.tsx index 3bb2083b045..32a0100da21 100644 --- a/src/keylessBackup/KeylessBackupProgress.tsx +++ b/src/keylessBackup/KeylessBackupProgress.tsx @@ -32,12 +32,18 @@ import Logger from 'src/utils/Logger' const TAG = 'keylessBackup/KeylessBackupProgress' +export enum KeylessBackupOrigin { + Onboarding = 'Onboarding', + Settings = 'Settings', +} + function KeylessBackupProgress({ route, navigation, }: NativeStackScreenProps) { const keylessBackupStatus = useSelector(keylessBackupStatusSelector) const { t } = useTranslation() + const { keylessBackupFlow, origin } = route.params const onPressHelp = () => { ValoraAnalytics.track(KeylessBackupEvents.cab_restore_failed_help) @@ -55,7 +61,7 @@ function KeylessBackupProgress({ navigation.setOptions({ headerRight: () => keylessBackupStatus === KeylessBackupStatus.Failed && - route.params.keylessBackupFlow === KeylessBackupFlow.Restore && ( + keylessBackupFlow === KeylessBackupFlow.Restore && ( } else { - return + return } } @@ -250,17 +256,19 @@ function Restore() { } } -function Setup() { +function Setup({ origin }: { origin: KeylessBackupOrigin }) { const keylessBackupStatus = useSelector(keylessBackupStatusSelector) const { t } = useTranslation() + const navigatedFromSettings = origin === KeylessBackupOrigin.Settings + const onPressContinue = () => { ValoraAnalytics.track(KeylessBackupEvents.cab_progress_completed_continue) navigateHome() } const onPressManual = async () => { - ValoraAnalytics.track(KeylessBackupEvents.cab_progress_failed_manual) + ValoraAnalytics.track(KeylessBackupEvents.cab_progress_failed_manual, { origin }) try { const pinIsCorrect = await ensurePincode() if (pinIsCorrect) { @@ -276,6 +284,16 @@ function Setup() { navigate(Screens.Settings) } + const onPressManualOnboarding = () => { + ValoraAnalytics.track(KeylessBackupEvents.cab_progress_failed_manual, { origin }) + navigate(Screens.AccountKeyEducation) + } + + const onPressSkip = () => { + ValoraAnalytics.track(KeylessBackupEvents.cab_progress_failed_skip_onboarding) + navigate(Screens.ChooseYourAdventure) + } + switch (keylessBackupStatus) { case KeylessBackupStatus.InProgress: { return renderInProgressState(t('keylessBackupStatus.setup.inProgress.title')) @@ -311,18 +329,32 @@ function Setup() { {t('keylessBackupStatus.setup.failed.body')}