wallet-stack 1.0.0-alpha.128 → 1.0.0-alpha.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wallet-stack",
3
- "version": "1.0.0-alpha.128",
3
+ "version": "1.0.0-alpha.129",
4
4
  "author": "Valora Inc",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -493,6 +493,7 @@ interface SendEventsProperties {
493
493
  amountEnteredIn: AmountEnteredIn
494
494
  tokenId: string | null
495
495
  networkId: string | null
496
+ isMiniPayRecipient: boolean
496
497
  }
497
498
  [SendEvents.send_confirm_back]: undefined
498
499
  [SendEvents.send_confirm_send]:
@@ -1,7 +1,8 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import React from 'react'
3
3
  import { useTranslation } from 'react-i18next'
4
- import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
5
+ import { SafeAreaView } from 'react-native-safe-area-context'
5
6
  import { FiatExchangeEvents } from 'src/analytics/Events'
6
7
  import AppAnalytics from 'src/analytics/AppAnalytics'
7
8
  import BackButton from 'src/components/BackButton'
@@ -55,7 +56,7 @@ function ExternalExchanges({ route }: Props) {
55
56
  }
56
57
 
57
58
  return (
58
- <SafeAreaView style={styles.container}>
59
+ <SafeAreaView style={styles.container} edges={['bottom']}>
59
60
  <Text style={styles.pleaseSelectExchange}>
60
61
  {t('youCanTransferOut', {
61
62
  digitalAsset: tokenInfo?.symbol,
@@ -1,7 +1,8 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import React from 'react'
3
3
  import { useTranslation } from 'react-i18next'
4
- import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
5
+ import { SafeAreaView } from 'react-native-safe-area-context'
5
6
  import AppAnalytics from 'src/analytics/AppAnalytics'
6
7
  import { FiatExchangeEvents } from 'src/analytics/Events'
7
8
  import BackButton from 'src/components/BackButton'
@@ -54,7 +55,7 @@ function Spend(props: Props) {
54
55
 
55
56
  return (
56
57
  <ScrollView style={styles.container}>
57
- <SafeAreaView>
58
+ <SafeAreaView edges={['bottom']}>
58
59
  <Text style={styles.pleaseSelectProvider}>{t('useBalanceWithMerchants')}</Text>
59
60
  <View>
60
61
  {merchants
@@ -3,7 +3,8 @@ import { RouteProp } from '@react-navigation/native'
3
3
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
4
4
  import React from 'react'
5
5
  import { Trans, useTranslation } from 'react-i18next'
6
- import { SafeAreaView, StyleSheet, Text } from 'react-native'
6
+ import { StyleSheet, Text } from 'react-native'
7
+ import { SafeAreaView } from 'react-native-safe-area-context'
7
8
  import AppAnalytics from 'src/analytics/AppAnalytics'
8
9
  import { FiatExchangeEvents } from 'src/analytics/Events'
9
10
  import BackButton from 'src/components/BackButton'
@@ -105,7 +106,7 @@ function LinkAccountSection(props: {
105
106
  }
106
107
 
107
108
  return (
108
- <SafeAreaView style={styles.content}>
109
+ <SafeAreaView style={styles.content} edges={['bottom']}>
109
110
  <Text style={styles.title}>{t(bodyTitle)}</Text>
110
111
  <Text testID="descriptionText" style={styles.description}>
111
112
  <Trans i18nKey={description} values={{ providerName: quote.getProviderName() }}>
@@ -5,7 +5,8 @@ import BigNumber from 'bignumber.js'
5
5
  import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
6
6
  import { useAsync } from 'react-async-hook'
7
7
  import { useTranslation } from 'react-i18next'
8
- import { ActivityIndicator, BackHandler, SafeAreaView, StyleSheet, Text, View } from 'react-native'
8
+ import { ActivityIndicator, BackHandler, StyleSheet, Text, View } from 'react-native'
9
+ import { SafeAreaView } from 'react-native-safe-area-context'
9
10
  import AppAnalytics from 'src/analytics/AppAnalytics'
10
11
  import { FiatExchangeEvents } from 'src/analytics/Events'
11
12
  import BackButton from 'src/components/BackButton'
@@ -251,7 +252,7 @@ export default function FiatConnectReviewScreen({ route, navigation }: Props) {
251
252
  }
252
253
 
253
254
  return (
254
- <SafeAreaView style={styles.content}>
255
+ <SafeAreaView style={styles.content} edges={['bottom']}>
255
256
  <Dialog
256
257
  testID="expiredQuoteDialog"
257
258
  isVisible={showingExpiredQuoteDialog}
@@ -1,7 +1,8 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import React, { useEffect, useState, type JSX } from 'react'
3
3
  import { useTranslation } from 'react-i18next'
4
- import { ActivityIndicator, SafeAreaView, StyleSheet, Text, View } from 'react-native'
4
+ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
5
+ import { SafeAreaView } from 'react-native-safe-area-context'
5
6
  import AppAnalytics from 'src/analytics/AppAnalytics'
6
7
  import { FiatExchangeEvents } from 'src/analytics/Events'
7
8
  import BackButton from 'src/components/BackButton'
@@ -269,7 +270,7 @@ export default function FiatConnectTransferStatusScreen({ route, navigation }: P
269
270
  ),
270
271
  })
271
272
  return (
272
- <SafeAreaView style={styles.content}>
273
+ <SafeAreaView style={styles.content} edges={['bottom']}>
273
274
  <FailureSection flow={flow} normalizedQuote={normalizedQuote} fiatAccount={fiatAccount} />
274
275
  </SafeAreaView>
275
276
  )
@@ -282,7 +283,7 @@ export default function FiatConnectTransferStatusScreen({ route, navigation }: P
282
283
  // intentionally falls thru since TxProcessing and Completed use the same component
283
284
  case SendingTransferStatus.TxProcessing:
284
285
  return (
285
- <SafeAreaView style={styles.content}>
286
+ <SafeAreaView style={styles.content} edges={['bottom']}>
286
287
  <SuccessOrProcessingSection
287
288
  status={fiatConnectTransfer.status}
288
289
  flow={flow}
@@ -1,7 +1,8 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import React from 'react'
3
3
  import { Trans, useTranslation } from 'react-i18next'
4
- import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
5
+ import { SafeAreaView } from 'react-native-safe-area-context'
5
6
  import AppAnalytics from 'src/analytics/AppAnalytics'
6
7
  import { KeylessBackupEvents } from 'src/analytics/Events'
7
8
  import BackButton from 'src/components/BackButton'
@@ -33,7 +34,7 @@ function KeylessBackupIntro({ route }: Props) {
33
34
  const { t } = useTranslation()
34
35
 
35
36
  return (
36
- <SafeAreaView style={styles.container}>
37
+ <SafeAreaView style={styles.container} edges={['bottom']}>
37
38
  <ScrollView style={styles.scrollContainer}>
38
39
  {isSetup && <Text style={styles.title}>{t('keylessBackupIntro.setup.title')}</Text>}
39
40
  <Text
@@ -42,6 +42,7 @@ type SendEnterAmountParams = {
42
42
  origin: SendOrigin
43
43
  forceTokenId?: boolean
44
44
  defaultTokenIdOverride?: string
45
+ isMiniPayRecipient?: boolean
45
46
  }
46
47
 
47
48
  interface ValidateRecipientParams {
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect } from 'react'
2
2
  import { Trans, useTranslation } from 'react-i18next'
3
- import { SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native'
3
+ import { ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import { SafeAreaView } from 'react-native-safe-area-context'
4
5
  import AppAnalytics from 'src/analytics/AppAnalytics'
5
6
  import { NftEvents } from 'src/analytics/Events'
6
7
  import Touchable from 'src/components/Touchable'
@@ -31,7 +32,7 @@ export default function NftsLoadError({ testID }: Props) {
31
32
  }, [])
32
33
 
33
34
  return (
34
- <SafeAreaView style={styles.safeArea} testID={testID}>
35
+ <SafeAreaView style={styles.safeArea} testID={testID} edges={['bottom']}>
35
36
  <ScrollView
36
37
  contentContainerStyle={styles.contentContainerStyle}
37
38
  style={styles.scrollContainer}
@@ -10,6 +10,7 @@ import BackButton from 'src/components/BackButton'
10
10
  import { BottomSheetModalRefType } from 'src/components/BottomSheet'
11
11
  import Button, { BtnSizes } from 'src/components/Button'
12
12
  import FeeInfoBottomSheet from 'src/components/FeeInfoBottomSheet'
13
+ import { FilterChip } from 'src/components/FilterChipsCarousel'
13
14
  import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
14
15
  import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
15
16
  import { ReviewDetailsItem } from 'src/components/ReviewTransaction'
@@ -66,6 +67,7 @@ interface Props {
66
67
  children?: React.ReactNode
67
68
  ProceedComponent: ComponentType<ProceedComponentProps>
68
69
  disableBalanceCheck?: boolean
70
+ filterChips?: FilterChip<TokenBalance>[]
69
71
  }
70
72
 
71
73
  export const SendProceed = ({
@@ -107,6 +109,7 @@ export default function EnterAmount({
107
109
  children,
108
110
  ProceedComponent,
109
111
  disableBalanceCheck = false,
112
+ filterChips,
110
113
  }: Props) {
111
114
  const { t } = useTranslation()
112
115
  const insets = useSafeAreaInsets()
@@ -325,6 +328,7 @@ export default function EnterAmount({
325
328
  tokens={tokens}
326
329
  title={t('sendEnterAmountScreen.selectToken')}
327
330
  titleStyle={styles.title}
331
+ filterChips={filterChips}
328
332
  />
329
333
  </SafeAreaView>
330
334
  )
@@ -1,4 +1,4 @@
1
- import { fireEvent, render, waitFor } from '@testing-library/react-native'
1
+ import { fireEvent, render, waitFor, within } from '@testing-library/react-native'
2
2
  import BigNumber from 'bignumber.js'
3
3
  import React from 'react'
4
4
  import { Provider } from 'react-redux'
@@ -10,6 +10,7 @@ import { Screens } from 'src/navigator/Screens'
10
10
  import { RecipientType } from 'src/recipients/recipient'
11
11
  import SendEnterAmount from 'src/send/SendEnterAmount'
12
12
  import { usePrepareSendTransactions } from 'src/send/usePrepareSendTransactions'
13
+ import { getDynamicConfigParams } from 'src/statsig'
13
14
  import { getSerializablePreparedTransactionsPossible } from 'src/viem/preparedTransactionSerialization'
14
15
  import { PreparedTransactionsPossible } from 'src/viem/prepareTransactions'
15
16
  import MockedNavigator from 'test/MockedNavigator'
@@ -29,6 +30,10 @@ import {
29
30
  jest.mock('src/statsig')
30
31
  jest.mock('src/send/usePrepareSendTransactions')
31
32
 
33
+ jest.mocked(getDynamicConfigParams).mockReturnValue({
34
+ miniPayTokenIds: [],
35
+ })
36
+
32
37
  const mockPrepareTransactionsResultPossible: PreparedTransactionsPossible = {
33
38
  type: 'possible',
34
39
  transactions: [
@@ -164,6 +169,7 @@ describe('SendEnterAmount', () => {
164
169
  underlyingTokenAddress: mockCeloAddress,
165
170
  underlyingTokenSymbol: 'CELO',
166
171
  amountEnteredIn: 'token',
172
+ isMiniPayRecipient: false,
167
173
  })
168
174
  expect(navigate).toHaveBeenCalledWith(Screens.SendConfirmation, {
169
175
  origin: params.origin,
@@ -232,4 +238,105 @@ describe('SendEnterAmount', () => {
232
238
  expect(getByTestId('SendEnterAmount/TokenSelect')).toHaveTextContent('cUSD', { exact: false })
233
239
  expect(getByTestId('SendEnterAmount/TokenSelect')).toBeDisabled()
234
240
  })
241
+
242
+ describe('MiniPay filter', () => {
243
+ const miniPayTokenIds = [mockCusdTokenId, mockUSDCTokenId]
244
+
245
+ beforeEach(() => {
246
+ jest.mocked(getDynamicConfigParams).mockReturnValue({
247
+ miniPayTokenIds,
248
+ })
249
+ })
250
+
251
+ it('should show MiniPay chip pre-selected and only MiniPay tokens when isMiniPayRecipient is true', () => {
252
+ const { getAllByTestId, getByText } = render(
253
+ <Provider store={store}>
254
+ <MockedNavigator
255
+ component={SendEnterAmount}
256
+ params={{ ...params, isMiniPayRecipient: true }}
257
+ />
258
+ </Provider>
259
+ )
260
+
261
+ expect(getByText('MiniPay')).toBeTruthy()
262
+
263
+ const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
264
+ const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
265
+ // only cUSD visible (USDC is on a different network and filtered by selector)
266
+ expect(tokens).toHaveLength(1)
267
+ expect(tokens[0]).toHaveTextContent('cUSD', { exact: false })
268
+ })
269
+
270
+ it('should select default token from MiniPay list', () => {
271
+ const { getByTestId } = render(
272
+ <Provider store={store}>
273
+ <MockedNavigator
274
+ component={SendEnterAmount}
275
+ params={{ ...params, isMiniPayRecipient: true }}
276
+ />
277
+ </Provider>
278
+ )
279
+
280
+ // cUSD is the only MiniPay token with balance, not CELO (which is excluded)
281
+ expect(getByTestId('SendEnterAmount/TokenSelect')).toHaveTextContent('cUSD', { exact: false })
282
+ })
283
+
284
+ it('should show all tokens when MiniPay chip is toggled off', () => {
285
+ const { getAllByTestId, getByText } = render(
286
+ <Provider store={store}>
287
+ <MockedNavigator
288
+ component={SendEnterAmount}
289
+ params={{ ...params, isMiniPayRecipient: true }}
290
+ />
291
+ </Provider>
292
+ )
293
+
294
+ fireEvent.press(getByText('MiniPay'))
295
+
296
+ const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
297
+ const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
298
+ expect(tokens).toHaveLength(3)
299
+ })
300
+
301
+ it('should not show MiniPay chip when isMiniPayRecipient is not set', () => {
302
+ const { queryByText } = render(
303
+ <Provider store={store}>
304
+ <MockedNavigator component={SendEnterAmount} params={params} />
305
+ </Provider>
306
+ )
307
+
308
+ expect(queryByText('MiniPay')).toBeFalsy()
309
+ })
310
+
311
+ it('should include isMiniPayRecipient in send_amount_continue analytics', async () => {
312
+ jest.mocked(usePrepareSendTransactions).mockReturnValue({
313
+ prepareTransactionsResult: mockPrepareTransactionsResultPossible,
314
+ prepareTransactionLoading: false,
315
+ refreshPreparedTransactions: jest.fn(),
316
+ clearPreparedTransactions: jest.fn(),
317
+ prepareTransactionError: undefined,
318
+ })
319
+ const { getByTestId, getByText } = render(
320
+ <Provider store={store}>
321
+ <MockedNavigator
322
+ component={SendEnterAmount}
323
+ params={{ ...params, isMiniPayRecipient: true }}
324
+ />
325
+ </Provider>
326
+ )
327
+
328
+ fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '8')
329
+
330
+ await waitFor(() => expect(getByText('review')).not.toBeDisabled())
331
+ fireEvent.press(getByText('review'))
332
+
333
+ await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(1))
334
+ expect(AppAnalytics.track).toHaveBeenCalledWith(
335
+ SendEvents.send_amount_continue,
336
+ expect.objectContaining({
337
+ isMiniPayRecipient: true,
338
+ })
339
+ )
340
+ })
341
+ })
235
342
  })
@@ -1,6 +1,6 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import BigNumber from 'bignumber.js'
3
- import React, { useMemo } from 'react'
3
+ import React from 'react'
4
4
  import AppAnalytics from 'src/analytics/AppAnalytics'
5
5
  import { SendEvents } from 'src/analytics/Events'
6
6
  import { getLocalCurrencyCode, usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors'
@@ -11,6 +11,7 @@ import { useSelector } from 'src/redux/hooks'
11
11
  import EnterAmount, { ProceedArgs, SendProceed } from 'src/send/EnterAmount'
12
12
  import { lastUsedTokenIdSelector } from 'src/send/selectors'
13
13
  import { usePrepareSendTransactions } from 'src/send/usePrepareSendTransactions'
14
+ import useSendFilterChips from 'src/send/useSendFilterChips'
14
15
  import { sortedTokensWithBalanceOrShowZeroBalanceSelector } from 'src/tokens/selectors'
15
16
  import { TokenBalance } from 'src/tokens/slice'
16
17
  import Logger from 'src/utils/Logger'
@@ -22,18 +23,24 @@ type Props = NativeStackScreenProps<StackParamList, Screens.SendEnterAmount>
22
23
  const TAG = 'SendEnterAmount'
23
24
 
24
25
  function SendEnterAmount({ route }: Props) {
25
- const { defaultTokenIdOverride, origin, recipient, isFromScan, forceTokenId } = route.params
26
+ const {
27
+ defaultTokenIdOverride,
28
+ origin,
29
+ recipient,
30
+ isFromScan,
31
+ forceTokenId,
32
+ isMiniPayRecipient,
33
+ } = route.params
26
34
  // explicitly allow zero state tokens to be shown for exploration purposes for
27
35
  // new users with no balance
28
36
  const tokens = useSelector(sortedTokensWithBalanceOrShowZeroBalanceSelector)
29
37
  const lastUsedTokenId = useSelector(lastUsedTokenIdSelector)
30
-
31
- const defaultToken = useMemo(() => {
32
- const defaultToken = tokens.find((token) => token.tokenId === defaultTokenIdOverride)
33
- const lastUsedToken = tokens.find((token) => token.tokenId === lastUsedTokenId)
34
-
35
- return defaultToken ?? lastUsedToken ?? tokens[0]
36
- }, [tokens, defaultTokenIdOverride, lastUsedTokenId])
38
+ const { filterChips, defaultToken } = useSendFilterChips({
39
+ isMiniPayRecipient,
40
+ tokens,
41
+ defaultTokenIdOverride,
42
+ lastUsedTokenId,
43
+ })
37
44
 
38
45
  const localCurrencyCode = useSelector(getLocalCurrencyCode)
39
46
  const localCurrencyExchangeRate = useSelector(usdToLocalCurrencyRateSelector)
@@ -72,6 +79,7 @@ function SendEnterAmount({ route }: Props) {
72
79
  amountEnteredIn,
73
80
  tokenId: token.tokenId,
74
81
  networkId: token.networkId,
82
+ isMiniPayRecipient: isMiniPayRecipient ?? false,
75
83
  })
76
84
  }
77
85
 
@@ -116,6 +124,7 @@ function SendEnterAmount({ route }: Props) {
116
124
  tokenSelectionDisabled={!!forceTokenId}
117
125
  onPressProceed={handleReviewSend}
118
126
  ProceedComponent={SendProceed}
127
+ filterChips={filterChips}
119
128
  />
120
129
  )
121
130
  }
@@ -0,0 +1,46 @@
1
+ import { BooleanFilterChip } from 'src/components/FilterChipsCarousel'
2
+ import { getDynamicConfigParams } from 'src/statsig'
3
+ import { DynamicConfigs } from 'src/statsig/constants'
4
+ import { StatsigDynamicConfigs } from 'src/statsig/types'
5
+ import { TokenBalance } from 'src/tokens/slice'
6
+
7
+ export default function useSendFilterChips({
8
+ isMiniPayRecipient,
9
+ tokens,
10
+ defaultTokenIdOverride,
11
+ lastUsedTokenId,
12
+ }: {
13
+ isMiniPayRecipient?: boolean
14
+ tokens: TokenBalance[]
15
+ defaultTokenIdOverride?: string
16
+ lastUsedTokenId?: string | null
17
+ }): {
18
+ filterChips: BooleanFilterChip<TokenBalance>[]
19
+ defaultToken: TokenBalance | undefined
20
+ } {
21
+ const { miniPayTokenIds: configTokenIds } = getDynamicConfigParams(
22
+ DynamicConfigs[StatsigDynamicConfigs.SEND_CONFIG]
23
+ )
24
+ const miniPayTokenIds = isMiniPayRecipient && configTokenIds.length > 0 ? configTokenIds : null
25
+
26
+ const filterChips: BooleanFilterChip<TokenBalance>[] = miniPayTokenIds
27
+ ? [
28
+ {
29
+ id: 'minipay',
30
+ name: 'MiniPay',
31
+ filterFn: (token: TokenBalance) => miniPayTokenIds.includes(token.tokenId),
32
+ isSelected: true,
33
+ },
34
+ ]
35
+ : []
36
+
37
+ const eligibleTokens = miniPayTokenIds
38
+ ? tokens.filter((token) => miniPayTokenIds.includes(token.tokenId))
39
+ : tokens
40
+ const defaultToken =
41
+ eligibleTokens.find((token) => token.tokenId === defaultTokenIdOverride) ??
42
+ eligibleTokens.find((token) => token.tokenId === lastUsedTokenId) ??
43
+ eligibleTokens[0]
44
+
45
+ return { filterChips, defaultToken }
46
+ }
@@ -164,6 +164,12 @@ export const DynamicConfigs = {
164
164
  inviteRewardsVersion: 'none',
165
165
  },
166
166
  },
167
+ [StatsigDynamicConfigs.SEND_CONFIG]: {
168
+ configName: StatsigDynamicConfigs.SEND_CONFIG,
169
+ defaultValues: {
170
+ miniPayTokenIds: [] as string[],
171
+ },
172
+ },
167
173
  } satisfies {
168
174
  [key in StatsigDynamicConfigs]: {
169
175
  configName: key
@@ -11,6 +11,7 @@ export enum StatsigDynamicConfigs {
11
11
  DEMO_MODE_CONFIG = 'demo_mode_config',
12
12
  FIAT_CONNECT_CONFIG = 'fiat_connect_config',
13
13
  INVITE_REWARDS_CONFIG = 'invite_rewards_config',
14
+ SEND_CONFIG = 'send_config',
14
15
  }
15
16
 
16
17
  export enum StatsigFeatureGates {