diff --git a/locales/base/translation.json b/locales/base/translation.json index de0d46a33ab..745ff902312 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -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", diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index fe2070ca690..ca0d2c08de3 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -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 { diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index a8a24e94c9d..02db05fd3de 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -656,6 +656,7 @@ interface SendEventsProperties { [SendEvents.send_select_recipient_recent_press]: { recipientType: RecipientType } + [SendEvents.send_select_recipient_jumpstart]: undefined } interface FeeEventsProperties { diff --git a/src/analytics/docs.ts b/src/analytics/docs.ts index 82c96238de2..5af59649e40 100644 --- a/src/analytics/docs.ts +++ b/src/analytics/docs.ts @@ -308,6 +308,7 @@ export const eventDocs: Record = { [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]: ``, diff --git a/src/analytics/types.ts b/src/analytics/types.ts index 86b1104c430..f99710c2424 100644 --- a/src/analytics/types.ts +++ b/src/analytics/types.ts @@ -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 diff --git a/src/icons/MagicWand.tsx b/src/icons/MagicWand.tsx new file mode 100644 index 00000000000..f9802f27b5b --- /dev/null +++ b/src/icons/MagicWand.tsx @@ -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 }) => ( + + + +) + +export default MagicWand diff --git a/src/navigator/types.tsx b/src/navigator/types.tsx index 5a32f2f4812..77ee9a963c1 100644 --- a/src/navigator/types.tsx +++ b/src/navigator/types.tsx @@ -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 + } + | { + origin: SendOrigin.Jumpstart + } +) interface ValidateRecipientParams { requesterAddress?: string diff --git a/src/send/SelectJumpstartRecipientButton.tsx b/src/send/SelectJumpstartRecipientButton.tsx new file mode 100644 index 00000000000..d267b0136a6 --- /dev/null +++ b/src/send/SelectJumpstartRecipientButton.tsx @@ -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 ( + + + + + + + {t('sendSelectRecipient.jumpstart.title')} + {t('sendSelectRecipient.jumpstart.subtitle')} + + + + ) +} + +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 diff --git a/src/send/SelectRecipientButtons.test.tsx b/src/send/SelectRecipientButtons.test.tsx index 11311d48446..f5affa0550f 100644 --- a/src/send/SelectRecipientButtons.test.tsx +++ b/src/send/SelectRecipientButtons.test.tsx @@ -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() @@ -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 () => { diff --git a/src/send/SelectRecipientButtons.tsx b/src/send/SelectRecipientButtons.tsx index 0f23aefa018..33a43d4b1bc 100644 --- a/src/send/SelectRecipientButtons.tsx +++ b/src/send/SelectRecipientButtons.tsx @@ -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' @@ -147,6 +148,7 @@ export default function SelectRecipientButtons({ onContactsPermissionGranted }: return ( <> + tokensWithNonZeroBalanceAndShowZeroBalanceSelector(state, supportedNetworkIds) @@ -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, @@ -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 diff --git a/src/statsig/constants.ts b/src/statsig/constants.ts index 543099cc11d..a10c3412d30 100644 --- a/src/statsig/constants.ts +++ b/src/statsig/constants.ts @@ -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 = { diff --git a/src/statsig/types.ts b/src/statsig/types.ts index b526b1cd583..2b84804bb09 100644 --- a/src/statsig/types.ts +++ b/src/statsig/types.ts @@ -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 {