Skip to content

Commit

Permalink
feat: add jumpstart confirmation screen (#5050)
Browse files Browse the repository at this point in the history
### Description

This PR adds:
- a new jumpstart confirmation screen. it is just displaying information
at this stage.
- navigation to this new screen from the enter amount screen
- some analytics 
- unit tests for the new screen

Not included: sending the transaction

### Test plan

![Simulator Screenshot - iPhone 14 Pro - 2024-03-07 at 10 38
04](https://github.com/valora-inc/wallet/assets/20150449/6600ffba-23f1-4a33-b00d-fe4c901e071b)


### Related issues

- Related to RET-994

### Backwards compatibility

Y

### Network scalability

Y
  • Loading branch information
kathaypacific committed Mar 7, 2024
1 parent 09e6ff9 commit 99f92a2
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 38 deletions.
7 changes: 7 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,13 @@
"description": "This link will be claimable by anyone who has access to it. We recommend sending no more than {{maxAmount}} {{localCurrencyCode}}."
}
},
"jumpstartSendConfirmationScreen": {
"title": "Create shareable link",
"sendAmountLabel": "CLAIM LINK AMOUNT",
"confirmButton": "Send funds and create link",
"detailsLabel": "HOW IT WORKS",
"details": "1. Tap generate link & your funds are sent into the claim link\n2. Send the claim link to a friend\n3. They’ll accept the funds\n4. If funds aren’t claimed, you can reclaim them anytime."
},
"transactionFeed": {
"approvalTransactionTitle": "Approval",
"estimatedNetworkFee": "Estimated network fee",
Expand Down
9 changes: 4 additions & 5 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,6 @@ export enum SendEvents {
send_select_recipient_recent_press = 'send_select_recipient_recent_press',
}

export enum JumpstartEvents {
send_select_recipient_jumpstart = 'send_select_recipient_jumpstart',
jumpstart_send_amount_exceeds_threshold = 'jumpstart_send_amount_exceeds_threshold',
}

export enum QrScreenEvents {
// Events for the QR screen redesign
qr_screen_copy_address = 'qr_screen_copy_address',
Expand Down Expand Up @@ -644,6 +639,10 @@ export enum TransactionDetailsEvents {
}

export enum JumpstartEvents {
send_select_recipient_jumpstart = 'send_select_recipient_jumpstart',
jumpstart_send_amount_exceeds_threshold = 'jumpstart_send_amount_exceeds_threshold',
jumpstart_send_amount_continue = 'jumpstart_send_amount_continue',
jumpstart_send_confirm = 'jumpstart_send_confirm',
jumpstart_claim_succeeded = 'jumpstart_claim_succeeded',
jumpstart_claim_failed = 'jumpstart_claim_failed',
jumpstart_claimed_token = 'jumpstart_claimed_token',
Expand Down
31 changes: 19 additions & 12 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -672,15 +672,6 @@ interface SendEventsProperties {
}
}

interface JumpstartEventsProperties {
[JumpstartEvents.send_select_recipient_jumpstart]: undefined
[JumpstartEvents.jumpstart_send_amount_exceeds_threshold]: {
tokenId: string
sendAmountUsd: string
thresholdUsd: number
}
}

interface FeeEventsProperties {
[FeeEvents.estimate_fee_failed]: {
feeType: string
Expand Down Expand Up @@ -1507,7 +1498,24 @@ interface TransactionDetailsProperties {
}
}

interface WalletJumpstartProperties {
interface JumpstartSendProperties {
localCurrency: LocalCurrencyCode
localCurrencyExchangeRate: string | null
tokenSymbol: string
tokenAmount: string | null
amountInUsd: string | null
tokenId: string | null
networkId: string | null
}
interface JumpstartEventsProperties {
[JumpstartEvents.send_select_recipient_jumpstart]: undefined
[JumpstartEvents.jumpstart_send_amount_exceeds_threshold]: {
tokenId: string
sendAmountUsd: string
thresholdUsd: number
}
[JumpstartEvents.jumpstart_send_amount_continue]: JumpstartSendProperties
[JumpstartEvents.jumpstart_send_confirm]: JumpstartSendProperties
[JumpstartEvents.jumpstart_claim_succeeded]: undefined
[JumpstartEvents.jumpstart_claim_failed]: undefined
[JumpstartEvents.jumpstart_claimed_token]: {
Expand Down Expand Up @@ -1556,7 +1564,6 @@ export type AnalyticsPropertiesList = AppEventsProperties &
NftsEventsProperties &
BuilderHooksProperties &
DappShortcutsProperties &
TransactionDetailsProperties &
WalletJumpstartProperties
TransactionDetailsProperties

export type AnalyticsEventType = keyof AnalyticsPropertiesList
11 changes: 6 additions & 5 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,12 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[SendEvents.send_select_recipient_recent_press]: `When a recent recipient is pressed`,
[JumpstartEvents.send_select_recipient_jumpstart]: `When the user taps the Jumpstart button on the select recipient screen to start sending crypto via escrow link`,
[JumpstartEvents.jumpstart_send_amount_exceeds_threshold]: `When the user enters a send value greater than allowed threshold, and is shown the max send amount warning`,

[JumpstartEvents.jumpstart_send_amount_continue]: `When the user taps the continue button on the jumpstart enter amount screen`,
[JumpstartEvents.jumpstart_send_confirm]: `When the user taps the confirm button on the jumpstart confirmation screen to start sending the transaction`,
[JumpstartEvents.jumpstart_claim_succeeded]: `When claiming from Wallet Jumpstart succeeded`,
[JumpstartEvents.jumpstart_claim_failed]: `When claiming from Wallet Jumpstart failed`,
[JumpstartEvents.jumpstart_claimed_token]: `When user successfully claimed an ERC20 token trough Wallet Jumpstart`,
[JumpstartEvents.jumpstart_claimed_nft]: `When user successfully claimed an NFT trough Wallet Jumpstart`,
// Events for the QR screen redesign
[QrScreenEvents.qr_screen_copy_address]: ``,
[QrScreenEvents.qr_scanner_open]: `When unique "QR scanner" button is pressed`,
Expand Down Expand Up @@ -546,10 +551,6 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[TransactionDetailsEvents.transaction_details_tap_check_status]: `When a user press 'Check status' on transaction details page`,
[TransactionDetailsEvents.transaction_details_tap_retry]: `When a user press 'Retry' on transaction details page`,
[TransactionDetailsEvents.transaction_details_tap_block_explorer]: `When a user press 'View on block explorer' on transaction details page`,
[JumpstartEvents.jumpstart_claim_succeeded]: `When claiming from Wallet Jumpstart succeeded`,
[JumpstartEvents.jumpstart_claim_failed]: `When claiming from Wallet Jumpstart failed`,
[JumpstartEvents.jumpstart_claimed_token]: `When user successfully claimed an ERC20 token trough Wallet Jumpstart`,
[JumpstartEvents.jumpstart_claimed_nft]: `When user successfully claimed an NFT trough Wallet Jumpstart`,

// Legacy event docs
// The below events had docs, but are no longer produced by the latest app version.
Expand Down
68 changes: 57 additions & 11 deletions src/jumpstart/JumpstartEnterAmount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'
import BigNumber from 'bignumber.js'
import React from 'react'
import { Provider } from 'react-redux'
import { JumpstartEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { createJumpstartLink } from 'src/firebase/dynamicLinks'
import JumpstartEnterAmount from 'src/jumpstart/JumpstartEnterAmount'
import { usePrepareJumpstartTransactions } from 'src/jumpstart/usePrepareJumpstartTransactions'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getDynamicConfigParams } from 'src/statsig'
import { StoredTokenBalance, TokenBalance } from 'src/tokens/slice'
import { TransactionRequest } from 'src/viem/prepareTransactions'
import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization'
import { createMockStore } from 'test/utils'
import {
mockAccount,
Expand All @@ -25,6 +32,7 @@ jest.mock('viem/accounts', () => ({
.fn()
.mockReturnValue('0x859c770be6bada3b0ae071d5368afaf9eb445584b35d914771dbf351db1e3df3'),
}))
jest.mock('src/firebase/dynamicLinks')

const mockStoreBalancesToTokenBalances = (storeBalances: StoredTokenBalance[]): TokenBalance[] => {
return storeBalances.map(
Expand Down Expand Up @@ -55,23 +63,24 @@ const store = createMockStore({
})

const executeSpy = jest.fn()
const mockTransactions: TransactionRequest[] = [
{
from: '0xfrom',
to: '0xto',
data: '0xdata',
gas: BigInt(5e15), // 0.005 CELO
maxFeePerGas: BigInt(1),
maxPriorityFeePerGas: undefined,
_baseFeePerGas: BigInt(1),
},
]
jest.mocked(usePrepareJumpstartTransactions).mockReturnValue({
execute: executeSpy,
reset: jest.fn(),
loading: false,
result: {
type: 'possible',
transactions: [
{
from: '0xfrom',
to: '0xto',
data: '0xdata',
gas: BigInt(5e15), // 0.005 CELO
maxFeePerGas: BigInt(1),
maxPriorityFeePerGas: undefined,
_baseFeePerGas: BigInt(1),
},
],
transactions: mockTransactions,
feeCurrency: tokenBalances[mockCeloTokenId],
},
error: undefined,
Expand Down Expand Up @@ -147,4 +156,41 @@ describe('JumpstartEnterAmount', () => {
await waitFor(() => expect(getByText('review')).not.toBeDisabled())
expect(queryByText('jumpstartEnterAmountScreen.maxAmountWarning.title')).toBeFalsy()
})

it('should navigate to the next screen on tap continue', async () => {
const mockLink = 'https://vlra.app/abc123'
jest.mocked(createJumpstartLink).mockResolvedValue(mockLink)

const { getByTestId, getByText } = render(
<Provider store={store}>
<JumpstartEnterAmount />
</Provider>
)

fireEvent.changeText(getByTestId('SendEnterAmount/Input'), '.25')

await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(1))
fireEvent.press(getByText('review'))

await waitFor(() =>
expect(navigate).toHaveBeenCalledWith(Screens.JumpstartSendConfirmation, {
link: mockLink,
sendAmount: '0.25',
tokenId: mockCeurTokenId,
preparedTransactions: getSerializablePreparedTransactions(mockTransactions),
})
)
expect(ValoraAnalytics.track).toHaveBeenCalledWith(
JumpstartEvents.jumpstart_send_amount_continue,
{
amountInUsd: '0.29',
localCurrency: 'PHP',
localCurrencyExchangeRate: '1.33',
networkId: 'celo-alfajores',
tokenAmount: '0.25',
tokenId: mockCeurTokenId,
tokenSymbol: 'cEUR',
}
)
})
})
35 changes: 30 additions & 5 deletions src/jumpstart/JumpstartEnterAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { createJumpstartLink } from 'src/firebase/dynamicLinks'
import { usePrepareJumpstartTransactions } from 'src/jumpstart/usePrepareJumpstartTransactions'
import { convertDollarsToLocalAmount } from 'src/localCurrency/convert'
import { getLocalCurrencyCode, usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { useSelector } from 'src/redux/hooks'
import EnterAmount from 'src/send/EnterAmount'
import { getDynamicConfigParams } from 'src/statsig'
Expand All @@ -17,6 +19,7 @@ import { StatsigDynamicConfigs } from 'src/statsig/types'
import { jumpstartSendTokensSelector } from 'src/tokens/selectors'
import { TokenBalance } from 'src/tokens/slice'
import Logger from 'src/utils/Logger'
import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization'
import { walletAddressSelector } from 'src/web3/selectors'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'

Expand Down Expand Up @@ -68,11 +71,33 @@ function JumpstartEnterAmount() {
parsedAmount: BigNumber
token: TokenBalance
}) => {
// TODO:
// 1. Pass the link in a navigation parameter to the
// next screen. (use navigateClearingStack so that the user cannot come
// back to this screen and reuse the private key)
// 2. add analytics
if (prepareJumpstartTransactions.result?.type !== 'possible') {
// should never happen
Logger.error(
TAG,
'No prepared transactions found when trying to proceed with jumpstart send'
)
return
}

navigate(Screens.JumpstartSendConfirmation, {
link,
sendAmount: parsedAmount.toString(),
tokenId: token.tokenId,
preparedTransactions: getSerializablePreparedTransactions(
prepareJumpstartTransactions.result.transactions
),
})

ValoraAnalytics.track(JumpstartEvents.jumpstart_send_amount_continue, {
localCurrency: localCurrencyCode,
localCurrencyExchangeRate: usdToLocalRate,
tokenSymbol: token.symbol,
tokenAmount: parsedAmount.toString(),
amountInUsd: parsedAmount.multipliedBy(token.priceUsd ?? 0).toFixed(2),
tokenId: token.tokenId,
networkId: token.networkId,
})
},
onError: (error) => {
Logger.error(TAG, 'Error while generating jumpstart dynamic link', error)
Expand Down
61 changes: 61 additions & 0 deletions src/jumpstart/JumpstartSendConfirmation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { fireEvent, render } from '@testing-library/react-native'
import React from 'react'
import { Provider } from 'react-redux'
import { JumpstartEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import JumpstartSendConfirmation from 'src/jumpstart/JumpstartSendConfirmation'
import MockedNavigator from 'test/MockedNavigator'
import { createMockStore } from 'test/utils'
import { mockCusdTokenBalance, mockCusdTokenId } from 'test/values'

describe('JumpstartSendConfirmation', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should render the correct information', () => {
const { getByText } = render(
<Provider store={createMockStore()}>
<MockedNavigator
component={JumpstartSendConfirmation}
params={{
tokenId: mockCusdTokenId,
sendAmount: '12.345',
}}
/>
</Provider>
)

expect(getByText('jumpstartSendConfirmationScreen.title')).toBeTruthy()
expect(getByText('12.35 cUSD')).toBeTruthy() // correct rounding
expect(getByText('₱16.42')).toBeTruthy() // local amount parsedAmount (12.345) *exchangeRate (1.33)
expect(getByText('jumpstartSendConfirmationScreen.details')).toBeTruthy()
expect(getByText('jumpstartSendConfirmationScreen.confirmButton')).toBeEnabled()
})

it('should execute the correct actions on press continue', () => {
const { getByText } = render(
<Provider store={createMockStore()}>
<MockedNavigator
component={JumpstartSendConfirmation}
params={{
tokenId: mockCusdTokenId,
sendAmount: '12.345',
}}
/>
</Provider>
)

fireEvent.press(getByText('jumpstartSendConfirmationScreen.confirmButton'))

expect(ValoraAnalytics.track).toHaveBeenCalledWith(JumpstartEvents.jumpstart_send_confirm, {
amountInUsd: '12.35',
localCurrency: 'PHP',
localCurrencyExchangeRate: '1.33',
networkId: mockCusdTokenBalance.networkId,
tokenAmount: '12.345',
tokenId: mockCusdTokenBalance.tokenId,
tokenSymbol: mockCusdTokenBalance.symbol,
})
})
})

0 comments on commit 99f92a2

Please sign in to comment.