Skip to content

Commit

Permalink
feat(onboarding): add terms and conditions colloquial variant (valora…
Browse files Browse the repository at this point in the history
…-inc#5293)

### Description

Implements the colliquial terms variant for the terms and conditions
experiment.
-
[Figma](https://www.figma.com/file/mthUlChSbWXXxLe2AV13Wd/Onboarding-2024?type=design&node-id=2071-2370&mode=design&t=Z20oNRHCqe8eUHLs-0)
- [statsig
experiment](https://console.statsig.com/4plizaPmWwPL21ASV4QAO0/experiments/onboarding_terms_and_conditions/setup)

### Test plan

| | Control | Colloquial |
|--------|--------|--------|
| iOS | <video
src="https://github.com/valora-inc/wallet/assets/5062591/e53f3c26-caee-4a0c-b63b-1e8bf48dcf0f"
/> | <video
src="https://github.com/valora-inc/wallet/assets/5062591/b62b96e0-de82-4d3b-98f2-28fd73d509af"
/> |
| Android | <video
src="https://github.com/valora-inc/wallet/assets/5062591/bf56c776-4efa-4462-9408-4e28277afe74"
/> | <video
src="https://github.com/valora-inc/wallet/assets/5062591/f064397b-a830-42b0-950a-e6398a8e6b15"
/> |



### Related issues

- Part of ACT-1162

### Backwards compatibility

N/A

### Network scalability

N/A
  • Loading branch information
satish-ravi authored and shottah committed May 15, 2024
1 parent f68b592 commit 516000d
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 64 deletions.
11 changes: 11 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,17 @@
"privacy": "By joining this network, you give us permission to collect anonymous information about your use of the app. Additionally, if you connect your phone number, a hashed copy of it will be stored. If you grant Kolektivo access to your contact list, Kolektivo will import each contact's name, phone number and profile picture to allow users to connect through the Kolektivo app. To learn how we collect and use this information please review our <0>Privacy Policy</0>.",
"goldDisclaimer": "When you create an \"account\" with Kolektivo you are creating a digital wallet to which only you hold the keys. No other person or entity, including Kolektivo, can recover your key, change or undo transactions, or recover lost funds. Be aware that digital assets are part of a new asset class and present a risk of financial loss. Carefully consider your financial circumstances and tolerance for financial risk before purchasing any digital asset."
},
"termsColloquial": {
"title": "Let’s start by creating your wallet",
"privacyHeading": "Your Info & Privacy:",
"privacy1": "We gather usage data which helps us improve the app and security. The type of information we collect, how we use it and your rights related to that information can all be seen in our <0>Privacy Policy</0>.",
"privacy2": "If you decide to link your phone number, we will store an encrypted copy of it.",
"privacy3": "If you decide to connect your contacts, we use their names, numbers, and profile pictures to make it easier to find them.",
"walletHeading": "Your Digital Wallet with Valora:",
"wallet1": "You’re about to create a digital wallet. Only you have the key to your wallet. We cannot recover your key or your assets if you lose your key. We also cannot reverse actions taken through Valora on blockchains.",
"wallet2": "Digital assets, the assets with which you will interact with Valora, come with unique risks. By using Valora, you accept these risks and take responsibility for them. Please consider your finances and risk tolerance before making choices.",
"fullTerms": "Read our full <0>Terms & Conditions</0>"
},
"fullNameOrPsuedonym": "Full name or pseudonym",
"namePlaceholder": "ex. name",
"nameAndPicGuideCopyTitle": "What’s your name?",
Expand Down
113 changes: 77 additions & 36 deletions src/onboarding/registration/RegulatoryTerms.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,88 @@ import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { RegulatoryTerms as RegulatoryTermsClass } from 'src/onboarding/registration/RegulatoryTerms'
import { firstOnboardingScreen } from 'src/onboarding/steps'
import { getExperimentParams } from 'src/statsig'
import { createMockStore, getMockI18nProps } from 'test/utils'

jest.mock('src/navigator/NavigationService', () => {
return { navigate: jest.fn() }
})
jest.mock('src/onboarding/steps')
jest.mock('src/statsig')

describe('RegulatoryTermsScreen', () => {
describe('when accept button is pressed', () => {
it('stores that info', async () => {
const store = createMockStore({})
const acceptTerms = jest.fn()
const wrapper = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)
fireEvent.press(wrapper.getByTestId('AcceptTermsButton'))
expect(acceptTerms).toHaveBeenCalled()
})
it('navigates to PincodeSet', () => {
const store = createMockStore({})
const acceptTerms = jest.fn()
jest.mocked(firstOnboardingScreen).mockReturnValue(Screens.PincodeSet)
const acceptTerms = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
})
it('renders correct components for control', () => {
jest.mocked(getExperimentParams).mockReturnValue({ variant: 'control' })
const store = createMockStore({})
const { getByTestId, queryByTestId } = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)

const wrapper = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)
fireEvent.press(wrapper.getByTestId('AcceptTermsButton'))
expect(firstOnboardingScreen).toHaveBeenCalled()
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
})
expect(getByTestId('scrollView')).toBeTruthy()
expect(queryByTestId('colloquialTermsSectionList')).toBeFalsy()
})

it('renders correct components for colloquial_terms', () => {
jest.mocked(getExperimentParams).mockReturnValue({ variant: 'colloquial_terms' })
const store = createMockStore({})
const { getByTestId, queryByTestId } = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)

expect(getByTestId('colloquialTermsSectionList')).toBeTruthy()
expect(queryByTestId('scrollView')).toBeFalsy()
})

describe.each([{ variant: 'control' }, { variant: 'colloquial_terms' }])(
'when accept button is pressed ($variant)',
({ variant }) => {
beforeAll(() => {
jest.mocked(getExperimentParams).mockReturnValue({ variant })
})
it('stores that info', async () => {
const store = createMockStore({})
const wrapper = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)
fireEvent.press(wrapper.getByTestId('AcceptTermsButton'))
expect(acceptTerms).toHaveBeenCalled()
})
it('navigates to PincodeSet', () => {
const store = createMockStore({})
jest.mocked(firstOnboardingScreen).mockReturnValue(Screens.PincodeSet)

const wrapper = render(
<Provider store={store}>
<RegulatoryTermsClass
{...getMockI18nProps()}
acceptTerms={acceptTerms}
recoveringFromStoreWipe={false}
/>
</Provider>
)
fireEvent.press(wrapper.getByTestId('AcceptTermsButton'))
expect(firstOnboardingScreen).toHaveBeenCalled()
expect(navigate).toHaveBeenCalledWith(Screens.PincodeSet)
})
}
)
})
150 changes: 125 additions & 25 deletions src/onboarding/registration/RegulatoryTerms.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Trans, WithTranslation } from 'react-i18next'
import { Platform, ScrollView, StyleSheet, Text } from 'react-native'
import { Platform, ScrollView, SectionList, StyleSheet, Text, View } from 'react-native'
import { SafeAreaInsetsContext, SafeAreaView } from 'react-native-safe-area-context'
import { connect } from 'react-redux'
import { acceptTerms } from 'src/account/actions'
Expand All @@ -17,8 +17,12 @@ import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { firstOnboardingScreen } from 'src/onboarding/steps'
import { RootState } from 'src/redux/reducers'
import { getExperimentParams } from 'src/statsig'
import { ExperimentConfigs } from 'src/statsig/constants'
import { StatsigExperiments } from 'src/statsig/types'
import Colors from 'src/styles/colors'
import fontStyles from 'src/styles/fonts'
import fontStyles, { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { navigateToURI } from 'src/utils/linking'

const MARGIN = 24
Expand Down Expand Up @@ -71,33 +75,108 @@ export class RegulatoryTerms extends React.Component<Props> {
navigateToURI(PRIVACY_LINK)
}

render() {
renderTerms() {
const { t } = this.props

return (
<SafeAreaView style={styles.container}>
<DevSkipButton nextScreen={Screens.PincodeSet} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
testID="scrollView"
>
<Logo color={Colors.black} size={32} />
<Text style={styles.title}>{t('terms.title')}</Text>
<Text style={styles.disclaimer}>
<Trans i18nKey={'terms.info'}>
<Text onPress={this.onPressGoToTerms} style={styles.disclaimerLink} />
</Trans>
</Text>
<Text style={styles.header}>{t('terms.heading1')}</Text>
<Text style={styles.disclaimer}>
<Trans i18nKey={'terms.privacy'}>
<Text onPress={this.onPressGoToPrivacyPolicy} style={styles.disclaimerLink} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
testID="scrollView"
>
<Logo color={Colors.black} size={32} />
<Text style={styles.title}>{t('terms.title')}</Text>
<Text style={styles.disclaimer}>
<Trans i18nKey={'terms.info'}>
<Text onPress={this.onPressGoToTerms} style={styles.link} />
</Trans>
</Text>
<Text style={styles.header}>{t('terms.heading1')}</Text>
<Text style={styles.disclaimer}>
<Trans i18nKey={'terms.privacy'}>
<Text onPress={this.onPressGoToPrivacyPolicy} style={styles.link} />
</Trans>
</Text>
<Text style={styles.header}>{t('terms.heading2')}</Text>
<Text style={styles.disclaimer}>{t('terms.goldDisclaimer')}</Text>
</ScrollView>
)
}

renderColloquialTerms() {
const { t } = this.props

return (
<SectionList
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
testID="colloquialTermsSectionList"
sections={[
{
title: t('termsColloquial.privacyHeading'),
data: [
{ text: 'termsColloquial.privacy1', onPress: this.onPressGoToPrivacyPolicy },
{ text: 'termsColloquial.privacy2' },
{ text: 'termsColloquial.privacy3' },
],
},
{
title: t('termsColloquial.walletHeading'),
data: [{ text: 'termsColloquial.wallet1' }, { text: 'termsColloquial.wallet2' }],
},
]}
renderItem={({ item }) => {
return (
<View style={styles.itemContainer}>
<Text style={styles.item}>{'\u2022'}</Text>
{item.onPress ? (
<Text style={styles.item}>
<Trans i18nKey={item.text}>
<Text onPress={item.onPress} style={styles.link} />
</Trans>
</Text>
) : (
<Text style={styles.item}>
<Trans i18nKey={item.text} />
</Text>
)}
</View>
)
}}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.sectionHeader}>{title}</Text>
)}
ListHeaderComponent={
<Text style={styles.titleColloquial}>{t('termsColloquial.title')}</Text>
}
ListFooterComponent={
<Text style={styles.fullTerms}>
<Trans i18nKey="termsColloquial.fullTerms">
<Text onPress={this.onPressGoToTerms} style={styles.link} />
</Trans>
</Text>
<Text style={styles.header}>{t('terms.heading2')}</Text>
<Text style={styles.disclaimer}>{t('terms.goldDisclaimer')}</Text>
</ScrollView>
}
stickySectionHeadersEnabled={false}
/>
)
}

render() {
const { t } = this.props

const { variant } = getExperimentParams(
ExperimentConfigs[StatsigExperiments.ONBOARDING_TERMS_AND_CONDITIONS]
)

return (
<SafeAreaView
style={styles.container}
// don't apply safe area padding to top on iOS since it is opens like a
// bottom sheet (modal animated screen)
edges={Platform.select({ ios: ['bottom', 'left', 'right'] })}
>
<DevSkipButton nextScreen={Screens.PincodeSet} />
{variant === 'colloquial_terms' ? this.renderColloquialTerms() : this.renderTerms()}
<SafeAreaInsetsContext.Consumer>
{(insets) => (
<Button
Expand Down Expand Up @@ -144,11 +223,32 @@ const styles = StyleSheet.create({
...fontStyles.small,
marginBottom: 15,
},
disclaimerLink: {
link: {
textDecorationLine: 'underline',
},
button: {
marginTop: MARGIN,
marginHorizontal: MARGIN,
},
titleColloquial: {
...typeScale.titleSmall,
marginBottom: Spacing.Small12,
},
sectionHeader: {
...typeScale.labelSemiBoldSmall,
marginVertical: Spacing.Small12,
},
itemContainer: {
flexDirection: 'row',
gap: Spacing.Smallest8,
},
item: {
...typeScale.bodySmall,
flexShrink: 1,
},
fullTerms: {
...typeScale.labelSemiBoldSmall,
marginVertical: Spacing.Small12,
color: Colors.infoDark,
},
})
25 changes: 23 additions & 2 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { StatsigDynamicConfigs, StatsigExperiments, StatsigFeatureGates } from 'src/statsig/types'
import {
StatsigDynamicConfigs,
StatsigExperiments,
StatsigFeatureGates,
StatsigParameter,
} from 'src/statsig/types'
import { NetworkId } from 'src/transactions/types'
import networkConfig from 'src/web3/networkConfig'

Expand All @@ -24,7 +29,7 @@ export const FeatureGates = {
[StatsigFeatureGates.SHOW_JUMPSTART_SEND]: false,
[StatsigFeatureGates.USE_TAB_NAVIGATOR]: false,
[StatsigFeatureGates.SHOW_POINTS]: false,
}
} satisfies { [key in StatsigFeatureGates]: boolean }

export const ExperimentConfigs = {
// NOTE: the keys of defaultValues MUST be parameter names
Expand All @@ -46,6 +51,17 @@ export const ExperimentConfigs = {
skipVerification: false,
},
},
[StatsigExperiments.ONBOARDING_TERMS_AND_CONDITIONS]: {
experimentName: StatsigExperiments.ONBOARDING_TERMS_AND_CONDITIONS,
defaultValues: {
variant: 'control' as 'control' | 'colloquial_terms' | 'checkbox',
},
},
} satisfies {
[key in StatsigExperiments]: {
experimentName: key
defaultValues: { [key: string]: StatsigParameter }
}
}

export const DynamicConfigs = {
Expand Down Expand Up @@ -119,4 +135,9 @@ export const DynamicConfigs = {
rewardReminderDate: new Date(0).toISOString(),
},
},
} satisfies {
[key in StatsigDynamicConfigs]: {
configName: key
defaultValues: { [key: string]: StatsigParameter }
}
}
2 changes: 1 addition & 1 deletion src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export enum StatsigFeatureGates {
}

export enum StatsigExperiments {
SWAPPING_NON_NATIVE_TOKENS = 'swapping_non_native_tokens',
DAPP_RANKINGS = 'dapp_rankings',
SWAP_BUY_AMOUNT = 'swap_buy_amount',
ONBOARDING_PHONE_VERIFICATION = 'onboarding_phone_verification',
ONBOARDING_TERMS_AND_CONDITIONS = 'onboarding_terms_and_conditions',
}

export type StatsigParameter =
Expand Down

0 comments on commit 516000d

Please sign in to comment.