Skip to content

Commit

Permalink
feat: add jumpstart button to select recipients screen (#4965)
Browse files Browse the repository at this point in the history
### Description

This PR adds the remote config and button for the start of the escrow
jumpstart flow. I think that the most straightforward way to implement
the escrow flow is to use the existing send flow rather than separate
the flows. I've made a POC that is working e2e in #4964, in case it
helps to understand this decision.

### Test plan

![Simulator Screenshot - iPhone 14 Pro - 2024-02-23 at 12 22
01](https://github.com/valora-inc/wallet/assets/20150449/8b524157-4c52-481b-af68-b7a3d47885ca)


### Related issues

- Related to RET-993

### Backwards compatibility

Y

### 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)
  • Loading branch information
kathaypacific committed Feb 23, 2024
1 parent 88b7ba9 commit eeb813c
Show file tree
Hide file tree
Showing 13 changed files with 152 additions and 5 deletions.
4 changes: 4 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2025,6 +2025,10 @@
"title": "Send crypto to your friends",
"subtitle": "Invite friends from your contacts list"
},
"jumpstart": {
"title": "Share crypto with a link",
"subtitle": "Copy the claim link & send crypto via your messaging app of choice"
},
"getStarted": {
"title": "Ways to send funds on Valora",
"subtitle": "GET STARTED",
Expand Down
1 change: 1 addition & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ export enum SendEvents {
send_select_recipient_invite_press = 'send_select_recipient_invite_press',
send_select_recipient_send_press = 'send_select_recipient_send_press',
send_select_recipient_recent_press = 'send_select_recipient_recent_press',
send_select_recipient_jumpstart = 'send_select_recipient_jumpstart',
}

export enum QrScreenEvents {
Expand Down
1 change: 1 addition & 0 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ interface SendEventsProperties {
[SendEvents.send_select_recipient_recent_press]: {
recipientType: RecipientType
}
[SendEvents.send_select_recipient_jumpstart]: undefined
}

interface FeeEventsProperties {
Expand Down
1 change: 1 addition & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[SendEvents.send_select_recipient_send_press]: `When the send button is pressed after selecting a recipient`,
[SendEvents.send_select_recipient_invite_press]: `When the invite button is pressed after selecting a recipient`,
[SendEvents.send_select_recipient_recent_press]: `When a recent recipient is pressed`,
[SendEvents.send_select_recipient_jumpstart]: `When the user taps the Jumpstart button on the select recipient screen to start sending crypto via escrow link`,

// Events for the QR screen redesign
[QrScreenEvents.qr_screen_copy_address]: ``,
Expand Down
1 change: 1 addition & 0 deletions src/analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum BackQuizProgress {
export enum SendOrigin {
AppSendFlow = 'app_send_flow', // Sending as part of the app send flow
Bidali = 'bidali', // Sending from Bidali
Jumpstart = 'jumpstart', // Sending as part of the jumpstart escrow flow
}

// Origin of WalletConnect pairing
Expand Down
14 changes: 14 additions & 0 deletions src/icons/MagicWand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
import Colors from 'src/styles/colors'

const MagicWand = ({ size = 24, color = Colors.successDark }) => (
<Svg width={size} height={size} viewBox="0 0 24 24">
<Path
fill={color}
d="m8.573 6.085-2.143 1.2 1.2-2.142L6.43 3l2.143 1.2L10.716 3l-1.2 2.143 1.2 2.142-2.143-1.2Zm10.284 8.4 2.143-1.2-1.2 2.142L21 17.57l-2.143-1.2-2.142 1.2 1.2-2.143-1.2-2.143 2.142 1.2ZM21 3l-1.2 2.143L21 7.285l-2.143-1.2-2.142 1.2 1.2-2.142L16.715 3l2.142 1.2L21 3Zm-7.422 9.239 2.091-2.091-1.817-1.817-2.09 2.091 1.816 1.817Zm.883-4.705 2.005 2.005c.334.317.334.874 0 1.209L6.465 20.749c-.335.335-.892.335-1.209 0l-2.005-2.005c-.335-.317-.335-.874 0-1.209L13.252 7.534c.335-.335.892-.335 1.209 0Z"
/>
</Svg>
)

export default MagicWand
14 changes: 10 additions & 4 deletions src/navigator/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ interface SendConfirmationParams {
feeTokenId?: string
}

interface SendEnterAmountParams {
recipient: Recipient & { address: string }
type SendEnterAmountParams = {
isFromScan: boolean
origin: SendOrigin
forceTokenId?: boolean
defaultTokenIdOverride?: string
}
} & (
| {
recipient: Recipient & { address: string }
origin: Exclude<SendOrigin, 'Jumpstart'>
}
| {
origin: SendOrigin.Jumpstart
}
)

interface ValidateRecipientParams {
requesterAddress?: string
Expand Down
77 changes: 77 additions & 0 deletions src/send/SelectJumpstartRecipientButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
import { SendEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { SendOrigin } from 'src/analytics/types'
import Touchable from 'src/components/Touchable'
import CircledIcon from 'src/icons/CircledIcon'
import MagicWand from 'src/icons/MagicWand'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import { useTokensWithTokenBalance } from 'src/tokens/hooks'

function SelectRecipientJumpstartButton() {
const { t } = useTranslation()
const showJumpstart = getFeatureGate(StatsigFeatureGates.SHOW_JUMPSTART_SEND)
const tokensWithBalance = useTokensWithTokenBalance()

const handlePress = () => {
ValoraAnalytics.track(SendEvents.send_select_recipient_jumpstart)
navigate(Screens.SendEnterAmount, {
isFromScan: false,
origin: SendOrigin.Jumpstart,
})
}

if (!showJumpstart || tokensWithBalance.length === 0) {
return null
}

return (
<Touchable onPress={handlePress} style={styles.container}>
<View style={styles.body}>
<CircledIcon radius={40} backgroundColor={colors.successLight}>
<MagicWand />
</CircledIcon>
<View style={styles.textSection}>
<Text style={styles.title}>{t('sendSelectRecipient.jumpstart.title')}</Text>
<Text style={styles.subtitle}>{t('sendSelectRecipient.jumpstart.subtitle')}</Text>
</View>
</View>
</Touchable>
)
}

const styles = StyleSheet.create({
container: {
padding: Spacing.Regular16,
borderWidth: 1,
borderColor: colors.primary,
borderRadius: 12,
marginHorizontal: Spacing.Thick24,
},
subtitle: {
...typeScale.bodyXSmall,
color: colors.gray3,
},
title: {
...typeScale.labelMedium,
},
body: {
flexDirection: 'row',
alignItems: 'center',
},
textSection: {
paddingLeft: Spacing.Small12,
flexDirection: 'column',
flex: 1,
},
})

export default SelectRecipientJumpstartButton
24 changes: 24 additions & 0 deletions src/send/SelectRecipientButtons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { RESULTS, check, request } from 'react-native-permissions'
import { Provider } from 'react-redux'
import { SendEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { SendOrigin } from 'src/analytics/types'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import SelectRecipientButtons from 'src/send/SelectRecipientButtons'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { navigateToPhoneSettings } from 'src/utils/linking'
import { createMockStore } from 'test/utils'

jest.mock('react-native-permissions', () => require('react-native-permissions/mock'))
jest.mock('src/statsig')

const renderComponent = (phoneNumberVerified = false) => {
const onPermissionsGranted = jest.fn()
Expand All @@ -27,6 +31,26 @@ describe('SelectRecipientButtons', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(check).mockResolvedValue(RESULTS.DENIED)
jest.mocked(getDynamicConfigParams).mockReturnValue({
showBalances: ['celo-alfajores'],
})
})

it('renders the jumpstart button if it is enabled', async () => {
jest
.mocked(getFeatureGate)
.mockImplementation((gate) => gate === StatsigFeatureGates.SHOW_JUMPSTART_SEND)
const { getByText } = renderComponent()

fireEvent.press(getByText('sendSelectRecipient.jumpstart.title'))

await waitFor(() =>
expect(ValoraAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_jumpstart)
)
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
origin: SendOrigin.Jumpstart,
})
})

it('renders QR and contacts button with no check mark on contacts if phone number is not verified', async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/send/SelectRecipientButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Social from 'src/icons/Social'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import useSelector from 'src/redux/useSelector'
import SelectRecipientJumpstartButton from 'src/send/SelectJumpstartRecipientButton'
import Logger from 'src/utils/Logger'
import { navigateToPhoneSettings } from 'src/utils/linking'

Expand Down Expand Up @@ -147,6 +148,7 @@ export default function SelectRecipientButtons({ onContactsPermissionGranted }:

return (
<>
<SelectRecipientJumpstartButton />
<SelectRecipientButton
testID={'SelectRecipient/QR'}
title={t('sendSelectRecipient.qr.title')}
Expand Down
16 changes: 15 additions & 1 deletion src/send/SendEnterAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getNumberFormatSettings } from 'react-native-localize'
import { SafeAreaView } from 'react-native-safe-area-context'
import { SendEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { SendOrigin } from 'src/analytics/types'
import BackButton from 'src/components/BackButton'
import { BottomSheetRefType } from 'src/components/BottomSheet'
import Button, { BtnSizes } from 'src/components/Button'
Expand Down Expand Up @@ -97,7 +98,7 @@ function FeeAmount({ feeTokenId, feeAmount }: { feeTokenId: string; feeAmount: B

function SendEnterAmount({ route }: Props) {
const { t } = useTranslation()
const { defaultTokenIdOverride, origin, recipient, isFromScan, forceTokenId } = route.params
const { defaultTokenIdOverride, origin, isFromScan, forceTokenId } = route.params
const supportedNetworkIds = getSupportedNetworkIdsForSend()
const tokens = useSelector((state) =>
tokensWithNonZeroBalanceAndShowZeroBalanceSelector(state, supportedNetworkIds)
Expand Down Expand Up @@ -158,6 +159,13 @@ function SendEnterAmount({ route }: Props) {
// should never happen because button is disabled if send is not possible
throw new Error('Send is not possible')
}

if (origin === SendOrigin.Jumpstart) {
// TODO handle send transaction and navigation
return
}

const recipient = route.params.recipient
navigate(Screens.SendConfirmation, {
origin,
isFromScan,
Expand Down Expand Up @@ -212,6 +220,12 @@ function SendEnterAmount({ route }: Props) {
const feeCurrencies = useSelector((state) => feeCurrenciesSelector(state, token.networkId))

useEffect(() => {
if (origin === SendOrigin.Jumpstart) {
// TODO: remove this block and handle preparing jumpstart transactions
return
}

const recipient = route.params.recipient
if (!walletAddress) {
Logger.error(TAG, 'Wallet address not set. Cannot refresh prepared transactions.')
return
Expand Down
1 change: 1 addition & 0 deletions src/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const FeatureGates = {
[StatsigFeatureGates.SHOW_SWAP_TOKEN_FILTERS]: false,
[StatsigFeatureGates.SHUFFLE_SWAP_TOKENS_ORDER]: false,
[StatsigFeatureGates.SHOW_NFT_CELEBRATION]: false,
[StatsigFeatureGates.SHOW_JUMPSTART_SEND]: false,
}

export const ExperimentConfigs = {
Expand Down
1 change: 1 addition & 0 deletions src/statsig/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum StatsigFeatureGates {
SHOW_SWAP_TOKEN_FILTERS = 'show_swap_token_filters',
SHUFFLE_SWAP_TOKENS_ORDER = 'shuffle_swap_tokens_order',
SHOW_NFT_CELEBRATION = 'show_nft_celebration',
SHOW_JUMPSTART_SEND = 'show_jumpstart_send',
}

export enum StatsigExperiments {
Expand Down

0 comments on commit eeb813c

Please sign in to comment.