-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add import select screen (#4800)
### Description Adds an import select screen when the Statsig feature gate `show_cloud_account_backup_restore` is set to true. By default, when the flag is false, the old restore flow should be served. <p align="left"> <img src="https://github.com/valora-inc/wallet/assets/26950305/ec44f6c2-ab49-4ec2-8ec3-ba460a3bf8d2" width="45%" height="auto" /> </p> ### Test plan - [x] Tested Locally on iOS and Android (feature flags were tested by manually hardcoding - no address to target) - [x] Unit tests added ### Related issues - Fixes #ACT-874 ### Backwards compatibility Yes
- Loading branch information
Showing
15 changed files
with
332 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import * as React from 'react' | ||
import Svg, { Path } from 'react-native-svg' | ||
import colors from 'src/styles/colors' | ||
|
||
interface Props { | ||
color?: string | ||
width?: number | ||
height?: number | ||
} | ||
|
||
const CloudCheck = ({ color = colors.primary, width = 20, height = 14 }: Props) => ( | ||
<Svg width={width} height={height} fill="none" viewBox="0 0 20 14"> | ||
<Path | ||
fill={color} | ||
d="m8.625 11.167 4.708-4.708-1.208-1.209-3.52 3.521-1.75-1.75-1.188 1.188 2.958 2.958Zm-3.208 2.5c-1.264 0-2.344-.438-3.24-1.313C1.282 11.48.834 10.41.834 9.146c0-1.083.327-2.049.98-2.896a4.331 4.331 0 0 1 2.562-1.625 5.656 5.656 0 0 1 2.083-3.104A5.702 5.702 0 0 1 10 .333c1.625 0 3.003.566 4.135 1.698 1.132 1.132 1.698 2.51 1.698 4.136a3.64 3.64 0 0 1 2.386 1.239c.632.716.948 1.553.948 2.51 0 1.043-.365 1.928-1.094 2.657-.73.73-1.615 1.094-2.656 1.094h-10Zm0-1.667h10c.583 0 1.076-.201 1.479-.604.403-.403.604-.896.604-1.48 0-.582-.201-1.076-.604-1.478a2.012 2.012 0 0 0-1.48-.605h-1.25V6.168c0-1.153-.405-2.136-1.218-2.948C12.136 2.405 11.153 2 10 2s-2.135.406-2.947 1.219c-.813.812-1.22 1.795-1.22 2.948h-.416a2.81 2.81 0 0 0-2.063.854A2.81 2.81 0 0 0 2.5 9.084c0 .805.285 1.493.854 2.062A2.81 2.81 0 0 0 5.417 12Z" | ||
/> | ||
</Svg> | ||
) | ||
export default CloudCheck |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import * as React from 'react' | ||
import Svg, { Path } from 'react-native-svg' | ||
import colors from 'src/styles/colors' | ||
|
||
interface Props { | ||
color?: string | ||
width?: number | ||
height?: number | ||
} | ||
|
||
const Lock = ({ color = colors.primary, width = 20, height = 20 }: Props) => ( | ||
<Svg width={width} height={height} fill="none" viewBox="0 0 20 20"> | ||
<Path | ||
fill={color} | ||
d="M5 18.334c-.458 0-.85-.164-1.177-.49a1.605 1.605 0 0 1-.49-1.177V8.333c0-.458.164-.85.49-1.177.326-.326.719-.49 1.177-.49h.833V5c0-1.153.407-2.135 1.22-2.948C7.865 1.24 8.847.833 10 .833s2.136.407 2.948 1.219c.813.813 1.219 1.795 1.219 2.948v1.667H15c.458 0 .85.163 1.177.489.327.327.49.72.49 1.178v8.333c0 .458-.163.85-.49 1.177-.326.326-.719.49-1.177.49H5Zm0-1.667h10V8.333H5v8.334Zm5-2.5c.458 0 .85-.163 1.178-.49.326-.326.489-.718.489-1.177 0-.458-.163-.85-.49-1.177a1.607 1.607 0 0 0-1.177-.49c-.458 0-.85.164-1.177.49-.326.326-.49.719-.49 1.177 0 .459.164.851.49 1.178.326.326.719.489 1.177.489Zm-2.5-7.5h5V5a2.41 2.41 0 0 0-.73-1.77A2.411 2.411 0 0 0 10 2.5a2.41 2.41 0 0 0-1.77.73A2.41 2.41 0 0 0 7.5 5v1.667Z" | ||
/> | ||
</Svg> | ||
) | ||
export default Lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { fireEvent, render } from '@testing-library/react-native' | ||
import * as React from 'react' | ||
import 'react-native' | ||
import { Provider } from 'react-redux' | ||
import ImportSelect from 'src/importSelect/ImportSelect' | ||
import { KeylessBackupFlow } from 'src/keylessBackup/types' | ||
import { navigate } from 'src/navigator/NavigationService' | ||
import { Screens } from 'src/navigator/Screens' | ||
import { createMockStore, getMockStackScreenProps } from 'test/utils' | ||
|
||
jest.mock('src/analytics/ValoraAnalytics') | ||
const mockScreenProps = getMockStackScreenProps(Screens.ImportSelect) | ||
|
||
describe('ImportSelect', () => { | ||
it('renders correctly', () => { | ||
const { getByText } = render( | ||
<Provider store={createMockStore()}> | ||
<ImportSelect {...mockScreenProps} /> | ||
</Provider> | ||
) | ||
|
||
expect(getByText('importSelect.title')).toBeTruthy() | ||
expect(getByText('importSelect.description')).toBeTruthy() | ||
expect(getByText('importSelect.emailAndPhone.title')).toBeTruthy() | ||
expect(getByText('importSelect.emailAndPhone.description')).toBeTruthy() | ||
expect(getByText('importSelect.recoveryPhrase.title')).toBeTruthy() | ||
expect(getByText('importSelect.recoveryPhrase.description')).toBeTruthy() | ||
}) | ||
|
||
it('should be able to navigate to cloud restore', () => { | ||
const { getByTestId } = render( | ||
<Provider store={createMockStore()}> | ||
<ImportSelect {...mockScreenProps} /> | ||
</Provider> | ||
) | ||
|
||
fireEvent.press(getByTestId('ImportSelect/CloudBackup')) | ||
expect(navigate).toHaveBeenCalledWith(Screens.SignInWithEmail, { | ||
keylessBackupFlow: KeylessBackupFlow.Restore, | ||
}) | ||
}) | ||
|
||
it('should be able to navigate to mnemonic restore', () => { | ||
const { getByTestId } = render( | ||
<Provider store={createMockStore()}> | ||
<ImportSelect {...mockScreenProps} /> | ||
</Provider> | ||
) | ||
|
||
fireEvent.press(getByTestId('ImportSelect/Mnemonic')) | ||
expect(navigate).toHaveBeenCalledWith(Screens.ImportWallet, { clean: true }) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { useHeaderHeight } from '@react-navigation/elements' | ||
import { NativeStackScreenProps } from '@react-navigation/native-stack' | ||
import React, { useLayoutEffect } from 'react' | ||
import { useTranslation } from 'react-i18next' | ||
import { ScrollView, StyleSheet, Text, View } from 'react-native' | ||
import { SafeAreaView } from 'react-native-safe-area-context' | ||
import { useDispatch } from 'react-redux' | ||
import { cancelCreateOrRestoreAccount } from 'src/account/actions' | ||
import { OnboardingEvents } from 'src/analytics/Events' | ||
import ValoraAnalytics from 'src/analytics/ValoraAnalytics' | ||
import Card from 'src/components/Card' | ||
import Touchable from 'src/components/Touchable' | ||
import CloudCheck from 'src/icons/CloudCheck' | ||
import Lock from 'src/icons/Lock' | ||
import { KeylessBackupFlow } from 'src/keylessBackup/types' | ||
import { nuxNavigationOptions } from 'src/navigator/Headers' | ||
import { navigate, navigateClearingStack } from 'src/navigator/NavigationService' | ||
import { Screens } from 'src/navigator/Screens' | ||
import { StackParamList } from 'src/navigator/types' | ||
import TopBarTextButtonOnboarding from 'src/onboarding/TopBarTextButtonOnboarding' | ||
import colors from 'src/styles/colors' | ||
import { typeScale } from 'src/styles/fonts' | ||
import { Shadow, Spacing } from 'src/styles/styles' | ||
|
||
type Props = NativeStackScreenProps<StackParamList, Screens.ImportSelect> | ||
|
||
function ActionCard({ | ||
title, | ||
description, | ||
icon, | ||
onPress, | ||
testID, | ||
}: { | ||
title: string | ||
description: string | ||
icon: React.ReactNode | ||
onPress?: () => void | ||
testID?: string | ||
}) { | ||
return ( | ||
<Card style={styles.card} rounded={true} shadow={Shadow.SoftLight} testID={testID}> | ||
<Touchable borderRadius={8} style={styles.touchable} onPress={onPress}> | ||
<View style={styles.cardContent}> | ||
<View style={styles.topLine}> | ||
{icon} | ||
<Text style={styles.cardTitle}>{title}</Text> | ||
</View> | ||
<Text style={styles.cardDescription}>{description}</Text> | ||
</View> | ||
</Touchable> | ||
</Card> | ||
) | ||
} | ||
|
||
export default function ImportSelect({ navigation }: Props) { | ||
const dispatch = useDispatch() | ||
const headerHeight = useHeaderHeight() | ||
const { t } = useTranslation() | ||
|
||
const handleNavigateBack = () => { | ||
dispatch(cancelCreateOrRestoreAccount()) | ||
ValoraAnalytics.track(OnboardingEvents.restore_account_cancel) | ||
navigateClearingStack(Screens.Welcome) | ||
} | ||
|
||
useLayoutEffect(() => { | ||
navigation.setOptions({ | ||
headerLeft: () => ( | ||
<TopBarTextButtonOnboarding | ||
title={t('cancel')} | ||
onPress={handleNavigateBack} | ||
titleStyle={{ color: colors.gray5 }} | ||
/> | ||
), | ||
headerStyle: { | ||
backgroundColor: 'transparent', | ||
}, | ||
}) | ||
}, [navigation]) | ||
|
||
return ( | ||
<SafeAreaView style={styles.safeArea} edges={['bottom']}> | ||
<ScrollView | ||
contentContainerStyle={styles.scrollContainer} | ||
style={[headerHeight ? { marginTop: headerHeight } : undefined]} | ||
> | ||
<View style={styles.viewContainer}> | ||
<View style={styles.screenTextContainer}> | ||
<Text style={styles.screenTitle}>{t('importSelect.title')}</Text> | ||
<Text style={styles.screenDescription}>{t('importSelect.description')}</Text> | ||
</View> | ||
<ActionCard | ||
title={t('importSelect.emailAndPhone.title')} | ||
description={t('importSelect.emailAndPhone.description')} | ||
icon={<CloudCheck />} | ||
onPress={() => | ||
navigate(Screens.SignInWithEmail, { keylessBackupFlow: KeylessBackupFlow.Restore }) | ||
} | ||
testID="ImportSelect/CloudBackup" | ||
/> | ||
<ActionCard | ||
title={t('importSelect.recoveryPhrase.title')} | ||
description={t('importSelect.recoveryPhrase.description')} | ||
icon={<Lock />} | ||
onPress={() => navigate(Screens.ImportWallet, { clean: true })} | ||
testID="ImportSelect/Mnemonic" | ||
/> | ||
</View> | ||
</ScrollView> | ||
</SafeAreaView> | ||
) | ||
} | ||
|
||
ImportSelect.navigationOptions = { | ||
...nuxNavigationOptions, | ||
// Prevent swipe back on iOS, users have to explicitly press cancel | ||
gestureEnabled: false, | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
card: { | ||
padding: 0, | ||
width: '100%', | ||
}, | ||
cardContent: { | ||
padding: Spacing.Regular16, | ||
}, | ||
cardDescription: { | ||
...typeScale.bodySmall, | ||
marginLeft: 28, | ||
}, | ||
cardTitle: { | ||
...typeScale.labelMedium, | ||
color: colors.primary, | ||
}, | ||
safeArea: { | ||
alignItems: 'center', | ||
backgroundColor: colors.gray1, | ||
flexGrow: 1, | ||
justifyContent: 'space-between', | ||
}, | ||
scrollContainer: { | ||
flexGrow: 1, | ||
}, | ||
screenDescription: { | ||
...typeScale.bodyMedium, | ||
textAlign: 'center', | ||
}, | ||
screenTitle: { | ||
...typeScale.titleSmall, | ||
marginTop: Spacing.Thick24, | ||
textAlign: 'center', | ||
}, | ||
screenTextContainer: { | ||
gap: Spacing.Regular16, | ||
}, | ||
topLine: { | ||
alignItems: 'center', | ||
flexDirection: 'row', | ||
gap: Spacing.Smallest8, | ||
}, | ||
touchable: { | ||
overflow: 'hidden', | ||
}, | ||
viewContainer: { | ||
alignItems: 'center', | ||
flex: 1, | ||
gap: Spacing.Thick24, | ||
paddingHorizontal: Spacing.Thick24, | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.