Skip to content

Commit

Permalink
feat(onboarding): add terms and conditions checkbox variant (#5298)
Browse files Browse the repository at this point in the history
### Description

Updates the welcome screen to include a terms & conditions checkbox
depending on the exp configuration
-
[Figma](https://www.figma.com/file/mthUlChSbWXXxLe2AV13Wd/Onboarding-2024?type=design&node-id=2062-2326&mode=design&t=rxhrpZ0gIOfdH6yL-0)
- [statsig
experiment](https://console.statsig.com/4plizaPmWwPL21ASV4QAO0/experiments/onboarding_terms_and_conditions/setup)

### Test plan

Unit tests, manual

| iOS | Android |
|--------|--------|
| <video
src="https://github.com/valora-inc/wallet/assets/5062591/25fd685f-640a-4a6d-89c3-0720a4ea33d9"
/> | <video
src="https://github.com/valora-inc/wallet/assets/5062591/c725b3dc-9e7b-4f01-a7b7-2a10f070bf3c"
/> |

### Related issues

- Fixes ACT-1162

### Backwards compatibility

Yes

### Network scalability

N/A
  • Loading branch information
satish-ravi committed Apr 23, 2024
1 parent d5999ab commit 9132662
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 59 deletions.
3 changes: 2 additions & 1 deletion locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,8 @@
"restoreWallet": "Restore wallet",
"hasWallet": "I already have a wallet",
"header": "Welcome to {{appName}}! Let's create your crypto wallet.",
"getStarted": "Get Started"
"getStarted": "Get Started",
"agreeToTerms": "By creating a wallet you agree to our <0>Terms and Conditions</0>"
},
"accessContacts": {
"disclosure": {
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[OnboardingEvents.backup_quiz_progress]: `whenever the backspace is pressed or word is chosen`,
[OnboardingEvents.backup_quiz_complete]: `(Count # of successful Recovery Phrase confirmations Backup_Quiz)`,
[OnboardingEvents.backup_quiz_incorrect]: `(Count # of failed Recovery Phrase confirmations Backup_Quiz)`,
[OnboardingEvents.terms_and_conditions_accepted]: ``,
[OnboardingEvents.terms_and_conditions_accepted]: `when the accept button on the terms and conditions screen is pressed or when the checkbox is checked and the create / import button is pressed on the welcome screen`,
[OnboardingEvents.celo_education_start]: ``,
[OnboardingEvents.celo_education_scroll]: ``,
[OnboardingEvents.celo_education_complete]: ``,
Expand Down
19 changes: 16 additions & 3 deletions src/icons/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import Colors from 'src/styles/colors'

const CheckBox = ({ checked, testID }: { checked: boolean; testID?: string }) => {
type Props = {
checked: boolean
testID?: string
checkedColor?: Colors
uncheckedColor?: Colors
}

const CheckBox = ({
checked,
testID,
checkedColor = Colors.primary,
uncheckedColor = Colors.gray3,
}: Props) => {
if (checked)
return (
<Svg testID={`${testID}/checked`} width={18} height={18} fill="none">
<Path
d="M16 0H2C.9 0 0 .9 0 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2Zm0 16H2V2h14v14ZM14.99 6l-1.41-1.42-6.59 6.59L4.41 8.6l-1.42 1.41 4 3.99 8-8Z"
fill="#1AB775"
fill={checkedColor}
/>
</Svg>
)
return (
<Svg testID={`${testID}/unchecked`} width={18} height={18} fill="none">
<Path
d="M16 2v14H2V2h14Zm0-2H2C.9 0 0 .9 0 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2Z"
fill="#B4B9BD"
fill={uncheckedColor}
/>
</Svg>
)
Expand Down
314 changes: 266 additions & 48 deletions src/onboarding/welcome/Welcome.test.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,292 @@
import { fireEvent, render } from '@testing-library/react-native'
import { fireEvent, render, waitFor } from '@testing-library/react-native'
import * as React from 'react'
import { Provider } from 'react-redux'
import { acceptTerms, chooseCreateAccount, chooseRestoreAccount } from 'src/account/actions'
import { OnboardingEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { firstOnboardingScreen } from 'src/onboarding/steps'
import Welcome from 'src/onboarding/welcome/Welcome'
import { patchUpdateStatsigUser } from 'src/statsig'
import { getExperimentParams, patchUpdateStatsigUser } from 'src/statsig'
import { createMockStore } from 'test/utils'

jest.mock('src/onboarding/steps')
jest.mock('src/statsig', () => ({
...(jest.requireActual('src/statsig') as any),
getExperimentParams: jest.fn(),
patchUpdateStatsigUser: jest.fn(),
}))

describe('Welcome', () => {
beforeAll(() => {
jest.spyOn(Date, 'now').mockImplementation(() => 123)
jest.mocked(firstOnboardingScreen).mockReturnValue(Screens.PincodeSet)
})
it('renders and behaves correctly', async () => {
const store = createMockStore()
const { getByTestId } = render(
<Provider store={store}>
<Welcome /*{...getMockStackScreenProps(Screens.)}*/ />
</Provider>
)
fireEvent.press(getByTestId('CreateAccountButton'))
jest.runOnlyPendingTimers()
await Promise.resolve() // waits for patchUpdateStatsigUser promise to resolve
expect(patchUpdateStatsigUser).toHaveBeenCalledWith({ custom: { startOnboardingTime: 123 } })
expect(store.getActions()).toMatchInlineSnapshot(`
[

beforeEach(() => {
jest.clearAllMocks()
})

describe.each([{ variant: 'control' }, { variant: 'colloquial_terms' }])(
'$variant',
({ variant }) => {
beforeAll(() => {
jest.mocked(getExperimentParams).mockReturnValue({ variant })
})
it('renders components', async () => {
const store = createMockStore()
const { getByTestId, queryByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
expect(getByTestId('CreateAccountButton')).toBeTruthy()
expect(getByTestId('RestoreAccountButton')).toBeTruthy()
expect(queryByTestId('TermsCheckbox/unchecked')).toBeFalsy()
expect(queryByTestId('TermsCheckbox/checked')).toBeFalsy()
})
it('create updates statsig, fires action and navigates to terms screen for first time onboarding', async () => {
const store = createMockStore()
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByTestId('CreateAccountButton'))
await waitFor(() =>
expect(patchUpdateStatsigUser).toHaveBeenCalledWith({
custom: { startOnboardingTime: 123 },
})
)
expect(store.getActions()).toEqual([chooseCreateAccount(123)])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.RegulatoryTerms)
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.create_account_start)
})
it('create skips statsig update if not onboarding the first time', () => {
const store = createMockStore({
account: {
startOnboardingTime: 111,
},
})
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByTestId('CreateAccountButton'))
expect(store.getActions()).toEqual([chooseCreateAccount(123)])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.RegulatoryTerms)
expect(patchUpdateStatsigUser).not.toHaveBeenCalled()
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.create_account_start)
})
it('restore fires action and navigates to terms screen', () => {
const store = createMockStore()
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByTestId('RestoreAccountButton'))
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.RegulatoryTerms)
expect(store.getActions()).toEqual([chooseRestoreAccount()])
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.restore_account_start)
})
it.each([
{
"now": 123,
"type": "ACCOUNT/CHOOSE_CREATE",
buttonId: 'CreateAccountButton',
action: chooseCreateAccount(123),
event: OnboardingEvents.create_account_start,
},
]
`)
expect(navigate).toHaveBeenCalledWith(Screens.RegulatoryTerms)
{
buttonId: 'RestoreAccountButton',
action: chooseRestoreAccount(),
event: OnboardingEvents.restore_account_start,
},
])(
'$buttonId goes to the onboarding screen if terms already accepted',
({ buttonId, action, event }) => {
const store = createMockStore({
account: {
acceptedTerms: true,
startOnboardingTime: 111,
},
})
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

store.clearActions()
fireEvent.press(getByTestId(buttonId))
expect(firstOnboardingScreen).toHaveBeenCalled()
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
expect(store.getActions()).toEqual([action])
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(event)
}
)
}
)

fireEvent.press(getByTestId('RestoreAccountButton'))
jest.runOnlyPendingTimers()
expect(navigate).toHaveBeenCalledWith(Screens.RegulatoryTerms)
expect(store.getActions()).toMatchInlineSnapshot(`
[
{
"type": "ACCOUNT/CHOOSE_RESTORE",
describe('checkbox', () => {
beforeAll(() => {
jest.mocked(getExperimentParams).mockReturnValue({ variant: 'checkbox' })
})

it('renders correctly', async () => {
const store = createMockStore()
const { getByTestId, queryByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
expect(getByTestId('CreateAccountButton')).toBeTruthy()
expect(getByTestId('RestoreAccountButton')).toBeTruthy()
expect(getByTestId('TermsCheckbox/unchecked')).toBeTruthy()
expect(queryByTestId('TermsCheckbox/checked')).toBeFalsy()
})

it('checkbox toggles buttons', async () => {
const store = createMockStore()
const { getByTestId, queryByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
expect(getByTestId('TermsCheckbox/unchecked')).toBeTruthy()
expect(queryByTestId('TermsCheckbox/checked')).toBeFalsy()
expect(getByTestId('CreateAccountButton')).toBeDisabled()
expect(getByTestId('RestoreAccountButton')).toBeDisabled()

fireEvent.press(getByTestId('TermsCheckbox/unchecked'))

expect(getByTestId('TermsCheckbox/checked')).toBeTruthy()
expect(queryByTestId('TermsCheckbox/unchecked')).toBeFalsy()
expect(getByTestId('CreateAccountButton')).toBeEnabled()
expect(getByTestId('RestoreAccountButton')).toBeEnabled()

fireEvent.press(getByTestId('TermsCheckbox/checked'))

expect(getByTestId('TermsCheckbox/unchecked')).toBeTruthy()
expect(queryByTestId('TermsCheckbox/checked')).toBeFalsy()
expect(getByTestId('CreateAccountButton')).toBeDisabled()
expect(getByTestId('RestoreAccountButton')).toBeDisabled()
})

it('create updates statsig, fires actions and navigates to onboarding screen for first time onboarding', async () => {
const store = createMockStore()
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
fireEvent.press(getByTestId('TermsCheckbox/unchecked'))
fireEvent.press(getByTestId('CreateAccountButton'))
await waitFor(() =>
expect(patchUpdateStatsigUser).toHaveBeenCalledWith({
custom: { startOnboardingTime: 123 },
})
)
expect(store.getActions()).toEqual([chooseCreateAccount(123), acceptTerms()])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.create_account_start)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(
OnboardingEvents.terms_and_conditions_accepted
)
})

it('create skips statsig if not first time onboarding', async () => {
const store = createMockStore({
account: {
startOnboardingTime: 111,
},
]
`)
})
it('goes to the onboarding screen', async () => {
const store = createMockStore({
account: {
acceptedTerms: true,
},
})
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
fireEvent.press(getByTestId('TermsCheckbox/unchecked'))
fireEvent.press(getByTestId('CreateAccountButton'))
expect(store.getActions()).toEqual([chooseCreateAccount(123), acceptTerms()])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
expect(patchUpdateStatsigUser).not.toHaveBeenCalled()
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.create_account_start)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(
OnboardingEvents.terms_and_conditions_accepted
)
})

it('restore fires actions and navigates to onboarding screen', () => {
const store = createMockStore()
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)
fireEvent.press(getByTestId('TermsCheckbox/unchecked'))
fireEvent.press(getByTestId('RestoreAccountButton'))
expect(store.getActions()).toEqual([chooseRestoreAccount(), acceptTerms()])
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(OnboardingEvents.restore_account_start)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(
OnboardingEvents.terms_and_conditions_accepted
)
})
jest.mocked(firstOnboardingScreen).mockReturnValue(Screens.PincodeSet)
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByTestId('CreateAccountButton'))
jest.runOnlyPendingTimers()
await Promise.resolve() // waits for patchUpdateStatsigUser promise to resolve
expect(firstOnboardingScreen).toHaveBeenCalled()
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
it.each([
{
buttonId: 'CreateAccountButton',
action: chooseCreateAccount(123),
event: OnboardingEvents.create_account_start,
},
{
buttonId: 'RestoreAccountButton',
action: chooseRestoreAccount(),
event: OnboardingEvents.restore_account_start,
},
])(
'$buttonId skips accept terms action and analytics event when terms already accepted',
({ buttonId, action, event }) => {
const store = createMockStore({
account: {
acceptedTerms: true,
startOnboardingTime: 111,
},
})
const { getByTestId } = render(
<Provider store={store}>
<Welcome />
</Provider>
)

fireEvent.press(getByTestId(buttonId))
expect(firstOnboardingScreen).toHaveBeenCalled()
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(event)
expect(ValoraAnalytics.track).not.toHaveBeenCalledWith(
OnboardingEvents.terms_and_conditions_accepted
)
expect(store.getActions()).toEqual([action])
}
)
})
})
Loading

0 comments on commit 9132662

Please sign in to comment.