diff --git a/packages/mobile/src/account/selectors.ts b/packages/mobile/src/account/selectors.ts index c86772ef430..bed6772ce1d 100644 --- a/packages/mobile/src/account/selectors.ts +++ b/packages/mobile/src/account/selectors.ts @@ -3,6 +3,7 @@ import * as RNLocalize from 'react-native-localize' import { createSelector } from 'reselect' import i18n from 'src/i18n' import { RootState } from 'src/redux/reducers' +import { currentAccountSelector } from 'src/web3/selectors' const inferCountryCode = () => { const localizedCountry = new Countries(i18n.language).getCountryByCodeAlpha2( @@ -27,6 +28,17 @@ export const pincodeTypeSelector = (state: RootState) => state.account.pincodeTy export const promptFornoIfNeededSelector = (state: RootState) => state.account.promptFornoIfNeeded export const isProfileUploadedSelector = (state: RootState) => state.account.profileUploaded export const cUsdDailyLimitSelector = (state: RootState) => state.account.dailyLimitCusd + +export const currentUserRecipientSelector = createSelector( + [currentAccountSelector, nameSelector, pictureSelector, userContactDetailsSelector], + (account, name, picture, contactDetails) => { + return { + address: account!, + name: name ?? undefined, + thumbnailPath: picture ?? contactDetails.thumbnailPath ?? undefined, + } + } +) export const dailyLimitRequestStatusSelector = (state: RootState) => state.account.dailyLimitRequestStatus export const recoveringFromStoreWipeSelector = (state: RootState) => diff --git a/packages/mobile/src/analytics/Properties.tsx b/packages/mobile/src/analytics/Properties.tsx index abee6a22ed4..b9ecae7ceaf 100644 --- a/packages/mobile/src/analytics/Properties.tsx +++ b/packages/mobile/src/analytics/Properties.tsx @@ -32,7 +32,6 @@ import { import { PaymentMethod } from 'src/fiatExchanges/FiatExchangeOptions' import { NotificationBannerCTATypes, NotificationBannerTypes } from 'src/home/NotificationBox' import { LocalCurrencyCode } from 'src/localCurrency/consts' -import { RecipientKind } from 'src/recipients/recipient' interface AppEventsProperties { [AppEvents.app_launched]: { @@ -479,7 +478,7 @@ interface EscrowEventsProperties { interface SendEventsProperties { [SendEvents.send_scan]: undefined [SendEvents.send_select_recipient]: { - recipientKind: RecipientKind + // TODO: decide what recipient info to collect, now that RecipientKind doesn't exist usedSearchBar: boolean } [SendEvents.send_cancel]: undefined @@ -549,7 +548,7 @@ interface RequestEventsProperties { [RequestEvents.request_cancel]: undefined [RequestEvents.request_scan]: undefined [RequestEvents.request_select_recipient]: { - recipientKind: RecipientKind + // TODO: decide what recipient info to collect, now that RecipientKind doesn't exist usedSearchBar: boolean } [RequestEvents.request_amount_continue]: { diff --git a/packages/mobile/src/apollo/types.ts b/packages/mobile/src/apollo/types.ts index d8403f1759f..e52cd06b39e 100644 --- a/packages/mobile/src/apollo/types.ts +++ b/packages/mobile/src/apollo/types.ts @@ -272,7 +272,8 @@ export interface TransferItemFragment { type: TokenTransactionType hash: string timestamp: number - address: string + address: string // wallet address (EOA) + account: string // account address (MTW) comment: Maybe amount: { __typename?: 'MoneyAmount' diff --git a/packages/mobile/src/components/Avatar.test.tsx b/packages/mobile/src/components/Avatar.test.tsx index b602b18b8b8..7ca65af75a1 100644 --- a/packages/mobile/src/components/Avatar.test.tsx +++ b/packages/mobile/src/components/Avatar.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import Avatar from 'src/components/Avatar' -import { RecipientKind } from 'src/recipients/recipient' import { createMockStore } from 'test/utils' const mockName = 'mockName' @@ -16,33 +15,34 @@ const store = createMockStore({ }) describe(Avatar, () => { - it('renders correctly with contact but without number', () => { + it('renders correctly with number and name', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() }) - it('renders correctly with number but without contact', () => { + it('renders correctly with just number', () => { const tree = renderer.create( - + ) expect(tree).toMatchSnapshot() }) - it('renders correctly with address but without contact nor number', () => { + it('renders correctly with address and name but no number', () => { const tree = renderer.create( - + + + ) + expect(tree).toMatchSnapshot() + }) + it('renders correctly with address but no name nor number', () => { + const tree = renderer.create( + + ) expect(tree).toMatchSnapshot() diff --git a/packages/mobile/src/components/Avatar.tsx b/packages/mobile/src/components/Avatar.tsx index 6f0ab8051ca..d9884ae4700 100644 --- a/packages/mobile/src/components/Avatar.tsx +++ b/packages/mobile/src/components/Avatar.tsx @@ -8,50 +8,30 @@ import { defaultCountryCodeSelector } from 'src/account/selectors' import ContactCircle from 'src/components/ContactCircle' import { formatShortenedAddress } from 'src/components/ShortenedAddress' import { Namespaces, withTranslation } from 'src/i18n' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { getDisplayName, Recipient } from 'src/recipients/recipient' const DEFAULT_ICON_SIZE = 40 interface OwnProps { - recipient?: Recipient + recipient: Recipient e164Number?: string - address?: string iconSize?: number displayNameStyle?: TextStyle } type Props = OwnProps & WithTranslation -// When redesigning, consider using getDisplayName from recipient.ts -function getDisplayName({ recipient, e164Number, address, t }: Props) { - if (recipient && recipient.displayName) { - return recipient.displayName - } - if (getE164Number(e164Number, recipient)) { - return t('mobileNumber') - } - if (address) { - return t('walletAddress') - } - // Rare but possible, such as when a user skips onboarding flow (in dev mode) and then views their own avatar - return t('global:unknown') -} - -export function getE164Number(e164Number?: string, recipient?: Recipient) { - return e164Number || (recipient && recipient.e164PhoneNumber) -} - export function Avatar(props: Props) { const defaultCountryCode = useSelector(defaultCountryCodeSelector) ?? undefined - const { address, recipient, e164Number, iconSize = DEFAULT_ICON_SIZE, displayNameStyle } = props + const { recipient, e164Number, iconSize = DEFAULT_ICON_SIZE, displayNameStyle, t } = props - const name = getDisplayName(props) - const e164NumberToShow = getE164Number(e164Number, recipient) - const thumbnailPath = getRecipientThumbnail(recipient) + const name = getDisplayName(recipient, t) + const address = recipient.address + const e164NumberToShow = recipient.e164PhoneNumber || e164Number return ( - + - ) + return } diff --git a/packages/mobile/src/components/ContactCircle.test.tsx b/packages/mobile/src/components/ContactCircle.test.tsx index 8459e93f559..9ac6419e228 100644 --- a/packages/mobile/src/components/ContactCircle.test.tsx +++ b/packages/mobile/src/components/ContactCircle.test.tsx @@ -5,95 +5,59 @@ import { Provider } from 'react-redux' import ContactCircle from 'src/components/ContactCircle' import { createMockStore } from 'test/utils' -const testContact = { - recordID: '1', - displayName: 'Zahara Tests Jorge', - phoneNumbers: [], - thumbnailPath: '', -} - const mockAddress = '0x123456' const mockName = 'Mock name' -const mockStore = createMockStore({ - identity: { - addressToDisplayName: { - [mockAddress]: { - name: mockName, - }, - }, - }, -}) + +const mockStore = createMockStore() describe('ContactCircle', () => { - it('renders correctly', () => { - const tree = render( - - - - ) - expect(tree).toMatchSnapshot() - }) - describe('when given contact', () => { - it('uses contact name for initial', () => { - const { getByText } = render( - - - - ) - expect(getByText('Z')).toBeTruthy() - }) - }) - describe('when not given a contact', () => { - it('uses name for initial', () => { - const { getByText } = render( + describe('when given recipient with only address', () => { + it('uses DefaultAvatar svg', () => { + const wrapper = render( - + ) - expect(getByText('J')).toBeTruthy() + expect(wrapper).toMatchSnapshot() }) }) - describe('when has a thumbnail', () => { + describe('when has a thumbnail and name', () => { it('renders image', () => { const mockThumbnnailPath = './test.jpg' const { getByType } = render( - + ) expect(getByType(Image).props.source).toEqual({ uri: './test.jpg' }) }) }) - describe('when has a saved name but no picture', () => { + describe('when has a name but no picture', () => { it('renders initial', () => { const { getByText } = render( - + ) expect(getByText(mockName[0])).toBeTruthy() }) }) - describe('when has a saved name and picture', () => { - it('renders picture', () => { - const mockImageUrl = 'https://somehost.com/test.jpg' - const { getByType } = render( - - - - ) - expect(getByType(Image).props.source).toEqual({ uri: mockImageUrl }) - }) - }) }) diff --git a/packages/mobile/src/components/ContactCircle.tsx b/packages/mobile/src/components/ContactCircle.tsx index fc2d6f7e44f..dbe1f8e058c 100644 --- a/packages/mobile/src/components/ContactCircle.tsx +++ b/packages/mobile/src/components/ContactCircle.tsx @@ -1,18 +1,13 @@ import fontStyles from '@celo/react-components/styles/fonts' import * as React from 'react' import { Image, StyleSheet, Text, View, ViewStyle } from 'react-native' -import { MinimalContact } from 'react-native-contacts' -import { useSelector } from 'react-redux' import DefaultAvatar from 'src/icons/DefaultAvatar' -import { addressToDisplayNameSelector } from 'src/identity/reducer' +import { Recipient } from 'src/recipients/recipient' interface Props { style?: ViewStyle - contact?: MinimalContact - name: string | null // Requiring a value so we need to be explicit if we dont have it - address?: string size?: number - thumbnailPath?: string | null + recipient: Recipient } const DEFAULT_ICON_SIZE = 40 @@ -21,27 +16,18 @@ const getAddressBackgroundColor = (address: string) => `hsl(${parseInt(address.substring(0, 5), 16) % 360}, 53%, 93%)` const getAddressForegroundColor = (address: string) => `hsl(${parseInt(address.substring(0, 5), 16) % 360}, 67%, 24%)` -const getContactInitial = (contact: MinimalContact) => getNameInitial(contact.displayName) const getNameInitial = (name: string) => name.charAt(0).toLocaleUpperCase() -function ContactCircle({ contact, size, thumbnailPath, name, address, style }: Props) { - const addressToDisplayName = useSelector(addressToDisplayNameSelector) - const addressInfo = address ? addressToDisplayName[address] : undefined - const displayName = name || addressInfo?.name +function ContactCircle({ size, recipient, style }: Props) { + const address = recipient.address const iconSize = size || DEFAULT_ICON_SIZE const iconBackgroundColor = getAddressBackgroundColor(address || '0x0') - const getInitials = () => - (contact && getContactInitial(contact)) || (displayName && getNameInitial(displayName)) || '#' - const renderThumbnail = () => { - const resolvedThumbnail = - thumbnailPath || (contact && contact.thumbnailPath) || addressInfo?.imageUrl - - if (resolvedThumbnail) { + if (recipient.thumbnailPath) { return ( {initials.toLocaleUpperCase()} diff --git a/packages/mobile/src/components/ContactCircleSelf.tsx b/packages/mobile/src/components/ContactCircleSelf.tsx index 35ad1fa3aee..82a80ac51da 100644 --- a/packages/mobile/src/components/ContactCircleSelf.tsx +++ b/packages/mobile/src/components/ContactCircleSelf.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { ViewStyle } from 'react-native' import { useSelector } from 'react-redux' -import { nameSelector, pictureSelector, userContactDetailsSelector } from 'src/account/selectors' +import { currentUserRecipientSelector } from 'src/account/selectors' import ContactCircle from 'src/components/ContactCircle' -import { currentAccountSelector } from 'src/web3/selectors' +import { Recipient } from 'src/recipients/recipient' interface Props { style?: ViewStyle size?: number @@ -11,18 +11,7 @@ interface Props { // A contact circle for the wallet user themselves export default function ContactCircleSelf({ style, size }: Props) { - const displayName = useSelector(nameSelector) - const pictureUri = useSelector(pictureSelector) - const address = useSelector(currentAccountSelector) - const contactDetails = useSelector(userContactDetailsSelector) + const recipient: Recipient = useSelector(currentUserRecipientSelector) - return ( - - ) + return } diff --git a/packages/mobile/src/components/__snapshots__/Avatar.test.tsx.snap b/packages/mobile/src/components/__snapshots__/Avatar.test.tsx.snap index 8eaddc55cfd..5ace4dba58c 100644 --- a/packages/mobile/src/components/__snapshots__/Avatar.test.tsx.snap +++ b/packages/mobile/src/components/__snapshots__/Avatar.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` renders correctly with address but without contact nor number 1`] = ` +exports[` renders correctly with address and name but no number 1`] = ` - W + M @@ -77,7 +77,7 @@ exports[` renders correctly with address but without contact nor number 1`] = ` ] } > - walletAddress + mockName `; -exports[` renders correctly with contact but without number 1`] = ` +exports[` renders correctly with address but no name nor number 1`] = ` - - M - + - mockName + walletFlow5:feedItemAddress + + + 0x0000...7E57 `; -exports[` renders correctly with number but without contact 1`] = ` +exports[` renders correctly with just number 1`] = ` + + + + + + + + +14155556666 + + + + 🇺🇸 + + + +1 415-555-6666 + + + +`; + +exports[` renders correctly with number and name 1`] = ` - mobileNumber + mockName - - J - + `; diff --git a/packages/mobile/src/escrow/EscrowedPaymentLineItem.test.tsx b/packages/mobile/src/escrow/EscrowedPaymentLineItem.test.tsx index 4d75b5cc029..c59e82878d4 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentLineItem.test.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentLineItem.test.tsx @@ -3,7 +3,6 @@ import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import { escrowPaymentDouble } from 'src/escrow/__mocks__' import EscrowedPaymentLineItem from 'src/escrow/EscrowedPaymentLineItem' -import { RecipientKind } from 'src/recipients/recipient' import { createMockStore } from 'test/utils' import { mockE164Number, mockE164NumberHashWithPepper, mockE164NumberPepper } from 'test/values' @@ -28,6 +27,9 @@ describe(EscrowedPaymentLineItem, () => { [mockE164Number]: mockE164NumberPepper, }, }, + recipients: { + phoneRecipientCache: {}, + }, }) const tree = renderer.create( @@ -50,10 +52,9 @@ describe(EscrowedPaymentLineItem, () => { }, }, recipients: { - recipientCache: { + phoneRecipientCache: { [mockE164Number]: { - kind: RecipientKind.Contact, - displayName: mockName, + name: mockName, contactId: '123', }, }, diff --git a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx index 7d846335da4..756e496d08d 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentLineItem.tsx @@ -9,10 +9,10 @@ interface Props { export default function EscrowedPaymentLineItem({ payment }: Props) { const { t } = useTranslation() - const [displayName] = useEscrowPaymentRecipient(payment) + const recipient = useEscrowPaymentRecipient(payment) // Using a fragment to suppress a limitation with TypeScript and functional // components returning a string // See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544 - return <>{displayName || t('global:unknown').toLowerCase()} + return <>{recipient.name ?? t('global:unknown').toLowerCase()} } diff --git a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx index 375b011902e..ef59ad65b26 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListItem.tsx @@ -22,11 +22,9 @@ interface Props { const TAG = 'EscrowedPaymentListItem' -const testID = 'EscrowedPaymentListItem' - function EscrowedPaymentListItem({ payment }: Props) { const { t } = useTranslation(Namespaces.inviteFlow11) - const [displayName, recipientPhoneNumber] = useEscrowPaymentRecipient(payment) + const recipient = useEscrowPaymentRecipient(payment) const onRemind = async () => { ValoraAnalytics.track(HomeEvents.notification_select, { @@ -37,7 +35,7 @@ function EscrowedPaymentListItem({ payment }: Props) { try { await Share.share({ message: t('walletFlow5:escrowedPaymentReminderSmsNoData') }) } catch (error) { - Logger.error(TAG, `Error sending reminder to ${recipientPhoneNumber}`, error) + Logger.error(TAG, `Error sending reminder to ${recipient.e164PhoneNumber}`, error) } } @@ -52,7 +50,7 @@ function EscrowedPaymentListItem({ payment }: Props) { const getCTA = () => { const ctas = [] - if (displayName) { + if (recipient.e164PhoneNumber) { ctas.push({ text: t('global:remind'), onPress: onRemind, @@ -65,7 +63,7 @@ function EscrowedPaymentListItem({ payment }: Props) { return ctas } - const nameToShow = displayName ?? t('global:unknown') + const nameToShow = recipient.name ?? t('global:unknown') const amount = { value: divideByWei(payment.amount), currencyCode: CURRENCIES[CURRENCY_ENUM.DOLLAR].code, @@ -77,14 +75,9 @@ function EscrowedPaymentListItem({ payment }: Props) { title={t('escrowPaymentNotificationTitle', { mobile: nameToShow })} amount={} details={payment.message} - icon={ - - } + icon={} callToActions={getCTA()} - testID={testID} + testID={'EscrowedPaymentListItem'} /> ) diff --git a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx index 57899af8acc..7948dc0fe2e 100644 --- a/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx +++ b/packages/mobile/src/escrow/EscrowedPaymentListScreen.tsx @@ -10,20 +10,16 @@ import { NotificationList, titleWithBalanceNavigationOptions, } from 'src/notifications/NotificationList' -import { NumberToRecipient } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' interface StateProps { dollarBalance: string | null sentEscrowedPayments: EscrowedPayment[] - recipientCache: NumberToRecipient } const mapStateToProps = (state: RootState): StateProps => ({ dollarBalance: state.stableToken.balance, sentEscrowedPayments: sentEscrowedPaymentsSelector(state), - recipientCache: recipientCacheSelector(state), }) type Props = WithTranslation & StateProps diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx index 4bea37575ae..26c4dd90cae 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationCard.tsx @@ -12,11 +12,11 @@ import { FeeInfo } from 'src/fees/saga' import { getFeeInTokens } from 'src/fees/selectors' import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' import { Namespaces } from 'src/i18n' -import { RecipientWithContact } from 'src/recipients/recipient' +import { MobileRecipient } from 'src/recipients/recipient' interface Props { recipientPhone: string - recipientContact?: RecipientWithContact + recipientContact: MobileRecipient amount: BigNumber feeInfo?: FeeInfo isLoadingFee?: boolean diff --git a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx index 5429b3f3d9a..b494fcd7fba 100644 --- a/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx +++ b/packages/mobile/src/escrow/ReclaimPaymentConfirmationScreen.tsx @@ -151,7 +151,9 @@ class ReclaimPaymentConfirmationScreen extends React.Component { - + + J diff --git a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap index dd830574946..9bc3a2888d3 100644 --- a/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap +++ b/packages/mobile/src/escrow/__snapshots__/ReclaimPaymentConfirmationScreen.test.tsx.snap @@ -113,23 +113,13 @@ exports[`ReclaimPaymentConfirmationScreen renders correctly in with fee in CELO ] } > - - M - + - mobileNumber + +14155550000 - - M - + - mobileNumber + +14155550000 - - M - + - mobileNumber + +14155550000 - - M - + - mobileNumber + +14155550000 - - M - + - mobileNumber + +14155550000 - - M - + - mobileNumber + +14155550000 { +export const useEscrowPaymentRecipient = (payment: EscrowedPayment): ContactRecipient => { const { recipientPhone, recipientIdentifier } = payment const identifierToE164Number = useSelector(identifierToE164NumberSelector) - const recipientCache = useSelector(recipientCacheSelector) + const recipientCache = useSelector((state) => state.recipients.phoneRecipientCache) const phoneNumber = identifierToE164Number[recipientIdentifier] ?? recipientPhone - const recipient = recipientCache[phoneNumber] + const recipient = recipientCache[phoneNumber] ?? { + name: phoneNumber, + e164PhoneNumber: phoneNumber, + contactId: '', + } - return [recipient?.displayName || phoneNumber, phoneNumber] + return recipient } diff --git a/packages/mobile/src/fiatExchanges/reducer.ts b/packages/mobile/src/fiatExchanges/reducer.ts index 8617197fb41..56f4d916f76 100644 --- a/packages/mobile/src/fiatExchanges/reducer.ts +++ b/packages/mobile/src/fiatExchanges/reducer.ts @@ -45,7 +45,7 @@ export interface TxHashToProvider { [txHash: string]: CicoProviderNames | undefined } -interface ProviderFeedInfo { +export interface ProviderFeedInfo { name: string icon: string } diff --git a/packages/mobile/src/fiatExchanges/saga.test.ts b/packages/mobile/src/fiatExchanges/saga.test.ts index 04111c5b616..749a87d4eea 100644 --- a/packages/mobile/src/fiatExchanges/saga.test.ts +++ b/packages/mobile/src/fiatExchanges/saga.test.ts @@ -11,7 +11,7 @@ import { Actions as IdentityActions, updateKnownAddresses } from 'src/identity/a import { providerAddressesSelector } from 'src/identity/reducer' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import { RecipientKind, RecipientWithAddress } from 'src/recipients/recipient' +import { AddressRecipient } from 'src/recipients/recipient' import { sendPaymentOrInvite, sendPaymentOrInviteFailure, @@ -24,11 +24,9 @@ Date.now = jest.fn(() => now) describe(watchBidaliPaymentRequests, () => { const amount = new BigNumber(20) - const recipient: RecipientWithAddress = { - kind: RecipientKind.Address, + const recipient: AddressRecipient = { address: '0xTEST', - displayId: 'BIDALI', - displayName: 'Bidali', + name: 'Bidali', thumbnailPath: 'https://firebasestorage.googleapis.com/v0/b/celo-mobile-mainnet.appspot.com/o/images%2Fbidali.png?alt=media', } @@ -48,7 +46,7 @@ describe(watchBidaliPaymentRequests, () => { await expectSaga(watchBidaliPaymentRequests) .put( updateKnownAddresses({ - '0xTEST': { name: recipient.displayName, imageUrl: recipient.thumbnailPath || null }, + '0xTEST': { name: recipient.name!, imageUrl: recipient.thumbnailPath || null }, }) ) .dispatch( diff --git a/packages/mobile/src/fiatExchanges/saga.ts b/packages/mobile/src/fiatExchanges/saga.ts index 5f28b300212..dceb47b7aa7 100644 --- a/packages/mobile/src/fiatExchanges/saga.ts +++ b/packages/mobile/src/fiatExchanges/saga.ts @@ -23,11 +23,12 @@ import { } from 'src/fiatExchanges/actions' import { lastUsedProviderSelector } from 'src/fiatExchanges/reducer' import { providerTxHashesChannel } from 'src/firebase/firebase' +import i18n from 'src/i18n' import { updateKnownAddresses } from 'src/identity/actions' import { providerAddressesSelector } from 'src/identity/reducer' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import { RecipientKind, RecipientWithAddress } from 'src/recipients/recipient' +import { AddressRecipient, getDisplayName } from 'src/recipients/recipient' import { Actions as SendActions } from 'src/send/actions' import { TransactionDataInput } from 'src/send/SendAmount' import { @@ -59,11 +60,9 @@ function* bidaliPaymentRequest({ throw new Error(`Unsupported payment currency from Bidali: ${currency}`) } - const recipient: RecipientWithAddress = { - kind: RecipientKind.Address, + const recipient: AddressRecipient = { address, - displayId: 'BIDALI', - displayName: 'Bidali', + name: 'Bidali', thumbnailPath: 'https://firebasestorage.googleapis.com/v0/b/celo-mobile-mainnet.appspot.com/o/images%2Fbidali.png?alt=media', } @@ -106,7 +105,10 @@ function* bidaliPaymentRequest({ // Keep address mapping locally yield put( updateKnownAddresses({ - [address]: { name: recipient.displayName, imageUrl: recipient.thumbnailPath || null }, + [address]: { + name: getDisplayName(recipient, i18n.t), + imageUrl: recipient.thumbnailPath || null, + }, }) ) break diff --git a/packages/mobile/src/firebase/notifications.test.ts b/packages/mobile/src/firebase/notifications.test.ts index 52ac290b964..c1df88c2fdc 100644 --- a/packages/mobile/src/firebase/notifications.test.ts +++ b/packages/mobile/src/firebase/notifications.test.ts @@ -5,11 +5,11 @@ import { showMessage } from 'src/alert/actions' import { SendOrigin } from 'src/analytics/types' import { openUrl } from 'src/app/actions' import { handleNotification } from 'src/firebase/notifications' -import { addressToDisplayNameSelector, addressToE164NumberSelector } from 'src/identity/reducer' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { NotificationReceiveState, NotificationTypes } from 'src/notifications/types' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { recipientInfoSelector } from 'src/recipients/reducer' +import { mockRecipientInfo } from 'test/values' describe(handleNotification, () => { beforeEach(() => { @@ -110,11 +110,7 @@ describe(handleNotification, () => { it('navigates to the transaction review screen if the app is not already in the foreground', async () => { await expectSaga(handleNotification, message, NotificationReceiveState.APP_OPENED_FRESH) - .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], - [select(addressToDisplayNameSelector), {}], - ]) + .provide([[select(recipientInfoSelector), mockRecipientInfo]]) .run() expect(navigate).toHaveBeenCalledWith(Screens.TransactionReview, { @@ -122,7 +118,7 @@ describe(handleNotification, () => { address: '0xtest', amount: { currencyCode: 'cUSD', value: new BigNumber('1e-17') }, comment: undefined, - recipient: undefined, + recipient: { address: '0xtest' }, type: 'RECEIVED', }, reviewProps: { @@ -156,10 +152,7 @@ describe(handleNotification, () => { it('navigates to the send confirmation screen if the app is not already in the foreground', async () => { await expectSaga(handleNotification, message, NotificationReceiveState.APP_OPENED_FRESH) - .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], - ]) + .provide([[select(recipientInfoSelector), mockRecipientInfo]]) .run() expect(navigate).toHaveBeenCalledWith(Screens.SendConfirmation, { @@ -168,7 +161,7 @@ describe(handleNotification, () => { amount: new BigNumber('10'), firebasePendingRequestUid: 'abc', reason: 'Pizza', - recipient: { address: '0xTEST', displayName: '0xTEST', kind: 'Address' }, + recipient: { address: '0xTEST' }, type: 'PAY_REQUEST', }, }) diff --git a/packages/mobile/src/firebase/notifications.ts b/packages/mobile/src/firebase/notifications.ts index 0a41876845e..71d50c85cfc 100644 --- a/packages/mobile/src/firebase/notifications.ts +++ b/packages/mobile/src/firebase/notifications.ts @@ -9,16 +9,14 @@ import { trackRewardsScreenOpenEvent, } from 'src/consumerIncentives/analyticsEventsTracker' import { CURRENCIES, resolveCurrency } from 'src/geth/consts' -import { addressToE164NumberSelector } from 'src/identity/reducer' import { NotificationReceiveState, NotificationTypes, TransferNotificationData, } from 'src/notifications/types' import { PaymentRequest } from 'src/paymentRequest/types' -import { getRequesterFromPaymentRequest } from 'src/paymentRequest/utils' -import { getRecipientFromAddress } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { getRecipientFromAddress, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { navigateToPaymentTransferReview, navigateToRequestedPaymentReview, @@ -41,13 +39,8 @@ function* handlePaymentRequested( return } - const addressToE164Number = yield select(addressToE164NumberSelector) - const recipientCache = yield select(recipientCacheSelector) - const targetRecipient = getRequesterFromPaymentRequest( - paymentRequest, - addressToE164Number, - recipientCache - ) + const info: RecipientInfo = yield select(recipientInfoSelector) + const targetRecipient = getRecipientFromAddress(paymentRequest.requesterAddress, info) navigateToRequestedPaymentReview({ firebasePendingRequestUid: paymentRequest.uid, @@ -63,8 +56,7 @@ function* handlePaymentReceived( notificationState: NotificationReceiveState ) { if (notificationState !== NotificationReceiveState.APP_ALREADY_OPEN) { - const recipientCache = yield select(recipientCacheSelector) - const addressToE164Number = yield select(addressToE164NumberSelector) + const info: RecipientInfo = yield select(recipientInfoSelector) const address = transferNotification.sender.toLowerCase() const currency = resolveCurrency(transferNotification.currency) @@ -78,7 +70,7 @@ function* handlePaymentReceived( }, address: transferNotification.sender.toLowerCase(), comment: transferNotification.comment, - recipient: getRecipientFromAddress(address, addressToE164Number, recipientCache), + recipient: getRecipientFromAddress(address, info), type: TokenTransactionType.Received, } ) diff --git a/packages/mobile/src/home/WalletHome.tsx b/packages/mobile/src/home/WalletHome.tsx index f84470a7032..239e4069b17 100644 --- a/packages/mobile/src/home/WalletHome.tsx +++ b/packages/mobile/src/home/WalletHome.tsx @@ -27,7 +27,7 @@ import Logo from 'src/icons/Logo' import { importContacts } from 'src/identity/actions' import DrawerTopBar from 'src/navigator/DrawerTopBar' import { NumberToRecipient } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { phoneRecipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' import { isAppConnected } from 'src/redux/selectors' import { initializeSentryUserContext } from 'src/sentry/actions' @@ -69,7 +69,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ address: currentAccountSelector(state), activeNotificationCount: getActiveNotificationCount(state), callToActNotification: callToActNotificationSelector(state), - recipientCache: recipientCacheSelector(state), + recipientCache: phoneRecipientCacheSelector(state), appConnected: isAppConnected(state), numberVerified: state.app.numberVerified, }) diff --git a/packages/mobile/src/identity/contactMapping.test.ts b/packages/mobile/src/identity/contactMapping.test.ts index 8409934d8e4..217056ebd6c 100644 --- a/packages/mobile/src/identity/contactMapping.test.ts +++ b/packages/mobile/src/identity/contactMapping.test.ts @@ -20,7 +20,7 @@ import { e164NumberToAddressSelector, secureSendPhoneNumberMappingSelector, } from 'src/identity/reducer' -import { setRecipientCache } from 'src/recipients/actions' +import { setPhoneRecipientCache } from 'src/recipients/actions' import { contactsToRecipients } from 'src/recipients/recipient' import { getAllContacts } from 'src/utils/contacts' import { getContractKitAsync } from 'src/web3/contracts' @@ -38,9 +38,6 @@ import { } from 'test/values' const recipients = contactsToRecipients(mockContactList, '+1') -const e164NumberRecipients = recipients!.e164NumberToRecipients -const otherRecipients = recipients!.otherRecipients -const allRecipients = { ...e164NumberRecipients, ...otherRecipients } describe('Import Contacts Saga', () => { it('imports contacts and creates contact mappings correctly', async () => { @@ -57,7 +54,7 @@ describe('Import Contacts Saga', () => { mockContactWithPhone2.thumbnailPath || null ) ) - .put(setRecipientCache(allRecipients)) + .put(setPhoneRecipientCache(recipients)) .run() }) diff --git a/packages/mobile/src/identity/contactMapping.ts b/packages/mobile/src/identity/contactMapping.ts index 9319e498e2e..a1f5e0e3e3a 100644 --- a/packages/mobile/src/identity/contactMapping.ts +++ b/packages/mobile/src/identity/contactMapping.ts @@ -39,7 +39,7 @@ import { } from 'src/identity/reducer' import { checkIfValidationRequired } from 'src/identity/secureSend' import { ImportContactsStatus } from 'src/identity/types' -import { setRecipientCache } from 'src/recipients/actions' +import { setPhoneRecipientCache } from 'src/recipients/actions' import { contactsToRecipients, NumberToRecipient } from 'src/recipients/recipient' import { getAllContacts } from 'src/utils/contacts' import Logger from 'src/utils/Logger' @@ -105,15 +105,15 @@ function* doImportContacts(doMatchmaking: boolean) { yield put(updateImportContactsProgress(ImportContactsStatus.Processing, 0, contacts.length)) const defaultCountryCode: string = yield select(defaultCountryCodeSelector) - const recipients = contactsToRecipients(contacts, defaultCountryCode) - if (!recipients) { + const e164NumberToRecipients = contactsToRecipients(contacts, defaultCountryCode) + if (!e164NumberToRecipients) { Logger.warn(TAG, 'No recipients found') return true } - const { e164NumberToRecipients, otherRecipients } = recipients yield call(updateUserContact, e164NumberToRecipients) - yield call(updateRecipientsCache, e164NumberToRecipients, otherRecipients) + Logger.debug(TAG, 'Updating recipients cache') + yield put(setPhoneRecipientCache(e164NumberToRecipients)) ValoraAnalytics.track(IdentityEvents.contacts_processing_complete) @@ -146,14 +146,6 @@ function* updateUserContact(e164NumberToRecipients: NumberToRecipient) { yield put(setUserContactDetails(userRecipient.contactId, userRecipient.thumbnailPath || null)) } -function* updateRecipientsCache( - e164NumberToRecipients: NumberToRecipient, - otherRecipients: NumberToRecipient -) { - Logger.debug(TAG, 'Updating recipients cache') - yield put(setRecipientCache({ ...e164NumberToRecipients, ...otherRecipients })) -} - export function* fetchAddressesAndValidateSaga({ e164Number, requesterAddress, diff --git a/packages/mobile/src/identity/matchmaking.test.ts b/packages/mobile/src/identity/matchmaking.test.ts index 824d312ff95..e683760caf7 100644 --- a/packages/mobile/src/identity/matchmaking.test.ts +++ b/packages/mobile/src/identity/matchmaking.test.ts @@ -8,7 +8,7 @@ import { PincodeType } from 'src/account/reducer' import { addContactsMatches } from 'src/identity/actions' import { fetchContactMatches } from 'src/identity/matchmaking' import { getUserSelfPhoneHashDetails } from 'src/identity/privateHashing' -import { NumberToRecipient, RecipientKind } from 'src/recipients/recipient' +import { NumberToRecipient } from 'src/recipients/recipient' import { isAccountUpToDate } from 'src/web3/dataEncryptionKey' import { getConnectedUnlockedAccount } from 'src/web3/saga' import { createMockStore } from 'test/utils' @@ -46,21 +46,18 @@ describe('Fetch contact matches', () => { const e164NumberToRecipients: NumberToRecipient = { [mockE164Number]: { - kind: RecipientKind.Contact, contactId: 'contactId1', - displayName: 'contact1', + name: 'contact1', e164PhoneNumber: mockE164Number, }, [mockE164Number2]: { - kind: RecipientKind.Contact, contactId: 'contactId2', - displayName: 'contact2', + name: 'contact2', e164PhoneNumber: mockE164Number2, }, '+491515555555': { - kind: RecipientKind.Contact, contactId: 'contactId3', - displayName: 'contact3', + name: 'contact3', e164PhoneNumber: '+491515555555', }, } diff --git a/packages/mobile/src/identity/reducer.ts b/packages/mobile/src/identity/reducer.ts index 93dc217b82b..991d3913795 100644 --- a/packages/mobile/src/identity/reducer.ts +++ b/packages/mobile/src/identity/reducer.ts @@ -9,7 +9,7 @@ import { removeKeyFromMapping } from 'src/identity/utils' import { AttestationCode } from 'src/identity/verification' import { getRehydratePayload, REHYDRATE } from 'src/redux/persist-helper' import { RootState } from 'src/redux/reducers' -import { Actions as SendActions, StoreLatestInRecentsAction } from 'src/send/actions' +import { StoreLatestInRecentsAction } from 'src/send/actions' export const ATTESTATION_CODE_PLACEHOLDER = 'ATTESTATION_CODE_PLACEHOLDER' export const ATTESTATION_ISSUER_PLACEHOLDER = 'ATTESTATION_ISSUER_PLACEHOLDER' @@ -41,6 +41,8 @@ export interface AddressInfoToDisplay { isProviderAddress?: boolean } +// This mapping is just for storing provider info from firebase +// other known recipient should be stored in the valoraRecipientCache export interface AddressToDisplayNameType { [address: string]: AddressInfoToDisplay | undefined } @@ -212,19 +214,6 @@ export const reducer = ( ...state, e164NumberToSalt: { ...state.e164NumberToSalt, ...action.e164NumberToSalt }, } - case SendActions.STORE_LATEST_IN_RECENTS: - if (!action.recipient.address) { - return state - } - action = { - type: Actions.UPDATE_KNOWN_ADDRESSES, - knownAddresses: { - [action.recipient.address]: { - name: action.recipient.displayName, - imageUrl: null, - }, - }, - } case Actions.UPDATE_KNOWN_ADDRESSES: return { ...state, diff --git a/packages/mobile/src/identity/saga.ts b/packages/mobile/src/identity/saga.ts index c407c40373d..0f4dc952f80 100644 --- a/packages/mobile/src/identity/saga.ts +++ b/packages/mobile/src/identity/saga.ts @@ -30,6 +30,7 @@ import { import { revokeVerificationSaga } from 'src/identity/revoke' import { validateAndReturnMatch } from 'src/identity/secureSend' import { reportRevealStatusSaga, startVerificationSaga } from 'src/identity/verification' +import { recipientHasNumber } from 'src/recipients/recipient' import { Actions as TransactionActions } from 'src/transactions/actions' import Logger from 'src/utils/Logger' import { fetchDataEncryptionKeyWrapper } from 'src/web3/dataEncryptionKey' @@ -45,8 +46,8 @@ export function* validateRecipientAddressSaga({ }: ValidateRecipientAddressAction) { Logger.debug(TAG, 'Starting Recipient Address Validation') try { - if (!recipient.e164PhoneNumber) { - throw Error(`Invalid recipient type for Secure Send: ${recipient.kind}`) + if (!recipientHasNumber(recipient)) { + throw Error(`Invalid recipient type for Secure Send, does not have e164Number`) } const userAddress = yield select(currentAccountSelector) diff --git a/packages/mobile/src/identity/secureSend.ts b/packages/mobile/src/identity/secureSend.ts index 7af140498a5..390c5599e9a 100644 --- a/packages/mobile/src/identity/secureSend.ts +++ b/packages/mobile/src/identity/secureSend.ts @@ -1,6 +1,6 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import { AddressValidationType, SecureSendPhoneNumberMapping } from 'src/identity/reducer' -import { Recipient } from 'src/recipients/recipient' +import { Recipient, recipientHasNumber } from 'src/recipients/recipient' import Logger from 'src/utils/Logger' const TAG = 'identity/secureSend' @@ -145,7 +145,7 @@ export function getAddressValidationType( recipient: Recipient, secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping ) { - const { e164PhoneNumber } = recipient + const e164PhoneNumber = recipient.e164PhoneNumber if ( !e164PhoneNumber || @@ -162,7 +162,7 @@ export function getSecureSendAddress( recipient: Recipient, secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping ) { - const { e164PhoneNumber } = recipient + const e164PhoneNumber = recipientHasNumber(recipient) ? recipient.e164PhoneNumber : undefined if (!e164PhoneNumber || !secureSendPhoneNumberMapping[e164PhoneNumber]) { return undefined } diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx index f36b1dc28e8..f638adeaeaa 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListItem.tsx @@ -19,7 +19,7 @@ import { AddressValidationType, SecureSendDetails } from 'src/identity/reducer' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { declinePaymentRequest } from 'src/paymentRequest/actions' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { Recipient } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' import { TransactionDataInput } from 'src/send/SendAmount' import Logger from 'src/utils/Logger' @@ -38,7 +38,7 @@ export default function IncomingPaymentRequestListItem({ id, amount, comment, re const [addressesFetched, setAddressesFetched] = useState(false) const navigation = useNavigation() - const { e164PhoneNumber } = requester + const e164PhoneNumber = requester.e164PhoneNumber const requesterAddress = requester.address const secureSendDetails: SecureSendDetails | undefined = useSelector( @@ -129,7 +129,7 @@ export default function IncomingPaymentRequestListItem({ id, amount, comment, re } - icon={ - - } + icon={} callToActions={[ { text: payButtonPressed ? ( diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx index 6c6efe1d43b..3105f46d320 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestListScreen.tsx @@ -4,48 +4,38 @@ import { WithTranslation } from 'react-i18next' import { View } from 'react-native' import { connect } from 'react-redux' import i18n, { Namespaces, withTranslation } from 'src/i18n' -import { addressToE164NumberSelector, AddressToE164NumberType } from 'src/identity/reducer' import { HeaderTitleWithBalance } from 'src/navigator/Headers' import { NotificationList } from 'src/notifications/NotificationList' import IncomingPaymentRequestListItem from 'src/paymentRequest/IncomingPaymentRequestListItem' import { getIncomingPaymentRequests } from 'src/paymentRequest/selectors' import { PaymentRequest } from 'src/paymentRequest/types' -import { getRequesterFromPaymentRequest } from 'src/paymentRequest/utils' -import { NumberToRecipient } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { getRecipientFromAddress, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' interface StateProps { dollarBalance: string | null paymentRequests: PaymentRequest[] - addressToE164Number: AddressToE164NumberType - recipientCache: NumberToRecipient + recipientInfo: RecipientInfo } -const mapStateToProps = (state: RootState): StateProps => { - return { - dollarBalance: state.stableToken.balance, - paymentRequests: getIncomingPaymentRequests(state), - addressToE164Number: addressToE164NumberSelector(state), - recipientCache: recipientCacheSelector(state), - } -} +const mapStateToProps = (state: RootState): StateProps => ({ + dollarBalance: state.stableToken.balance, + paymentRequests: getIncomingPaymentRequests(state), + recipientInfo: recipientInfoSelector(state), +}) type Props = WithTranslation & StateProps -export const listItemRenderer = (props: { - addressToE164Number: AddressToE164NumberType - recipientCache: NumberToRecipient -}) => (request: PaymentRequest, key: number | undefined = undefined) => ( +export const listItemRenderer = (props: { recipientInfo: RecipientInfo }) => ( + request: PaymentRequest, + key: number | undefined = undefined +) => ( diff --git a/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx index b022dfeba29..cb2126df870 100644 --- a/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx +++ b/packages/mobile/src/paymentRequest/IncomingPaymentRequestSummaryNotification.tsx @@ -6,7 +6,6 @@ import { HomeEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { NotificationBannerCTATypes, NotificationBannerTypes } from 'src/home/NotificationBox' import { Namespaces, withTranslation } from 'src/i18n' -import { addressToE164NumberSelector, AddressToE164NumberType } from 'src/identity/reducer' import { notificationIncomingRequest } from 'src/images/Images' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -14,9 +13,8 @@ import SummaryNotification from 'src/notifications/SummaryNotification' import { listItemRenderer } from 'src/paymentRequest/IncomingPaymentRequestListScreen' import PaymentRequestNotificationInner from 'src/paymentRequest/PaymentRequestNotificationInner' import { PaymentRequest } from 'src/paymentRequest/types' -import { getRequesterFromPaymentRequest } from 'src/paymentRequest/utils' -import { NumberToRecipient } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { getRecipientFromAddress, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' interface OwnProps { @@ -26,13 +24,11 @@ interface OwnProps { type Props = OwnProps & WithTranslation & StateProps interface StateProps { - addressToE164Number: AddressToE164NumberType - recipientCache: NumberToRecipient + recipientInfo: RecipientInfo } const mapStateToProps = (state: RootState): StateProps => ({ - addressToE164Number: addressToE164NumberSelector(state), - recipientCache: recipientCacheSelector(state), + recipientInfo: recipientInfoSelector(state), }) // Payment Request notification for the notification center on home screen @@ -50,22 +46,18 @@ export class IncomingPaymentRequestSummaryNotification extends React.Component

) } render() { - const { addressToE164Number, recipientCache, requests, t } = this.props + const { recipientInfo, requests, t } = this.props return requests.length === 1 ? ( listItemRenderer({ - addressToE164Number, - recipientCache, + recipientInfo, })(requests[0]) ) : ( diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.test.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.test.tsx index 616cfd7c60d..78e42580dd0 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.test.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.test.tsx @@ -3,7 +3,6 @@ import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import OutgoingPaymentRequestListItem from 'src/paymentRequest/OutgoingPaymentRequestListItem' -import { RecipientKind } from 'src/recipients/recipient' import { createMockStore } from 'test/utils' const store = createMockStore() @@ -12,11 +11,10 @@ const commonProps = { amount: '24', comment: 'Hey thanks for the loan, Ill pay you back ASAP. LOVE YOU', requestee: { - kind: RecipientKind.MobileNumber, e164PhoneNumber: '5126608970', displayId: '5126608970', address: '0x91623f625e23ac1400', - displayName: '5126608970', + name: '5126608970', contact: undefined, }, } diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx index 8d7fbe24475..9a31db80834 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListItem.tsx @@ -10,7 +10,7 @@ import { CURRENCIES, CURRENCY_ENUM } from 'src/geth/consts' import { NotificationBannerCTATypes, NotificationBannerTypes } from 'src/home/NotificationBox' import { Namespaces, withTranslation } from 'src/i18n' import { cancelPaymentRequest, updatePaymentRequestNotified } from 'src/paymentRequest/actions' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { getDisplayName, Recipient } from 'src/recipients/recipient' import Logger from 'src/utils/Logger' interface OwnProps { @@ -59,7 +59,6 @@ export class OutgoingPaymentRequestListItem extends React.Component { render() { const { requestee, id, comment, t } = this.props - const name = requestee.displayName const amount = { value: this.props.amount, currencyCode: CURRENCIES[CURRENCY_ENUM.DOLLAR].code, @@ -69,16 +68,12 @@ export class OutgoingPaymentRequestListItem extends React.Component { } details={comment} - icon={ - - } + icon={} callToActions={this.getCTA()} /> diff --git a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx index 3dacd7015e8..efcf261aa2b 100644 --- a/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx +++ b/packages/mobile/src/paymentRequest/OutgoingPaymentRequestListScreen.tsx @@ -3,11 +3,7 @@ import { WithTranslation } from 'react-i18next' import { View } from 'react-native' import { connect } from 'react-redux' import i18n, { Namespaces, withTranslation } from 'src/i18n' -import { - AddressToE164NumberType, - e164NumberToAddressSelector, - E164NumberToAddressType, -} from 'src/identity/reducer' +import { e164NumberToAddressSelector, E164NumberToAddressType } from 'src/identity/reducer' import { NotificationList, titleWithBalanceNavigationOptions, @@ -16,17 +12,15 @@ import { cancelPaymentRequest, updatePaymentRequestNotified } from 'src/paymentR import OutgoingPaymentRequestListItem from 'src/paymentRequest/OutgoingPaymentRequestListItem' import { getOutgoingPaymentRequests } from 'src/paymentRequest/selectors' import { PaymentRequest } from 'src/paymentRequest/types' -import { getRequesteeFromPaymentRequest } from 'src/paymentRequest/utils' -import { NumberToRecipient } from 'src/recipients/recipient' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { getRecipientFromAddress, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' interface StateProps { dollarBalance: string | null paymentRequests: PaymentRequest[] e164PhoneNumberAddressMapping: E164NumberToAddressType - recipientCache: NumberToRecipient - addressToE164Number: AddressToE164NumberType + recipientInfo: RecipientInfo } interface DispatchProps { @@ -38,23 +32,17 @@ const mapStateToProps = (state: RootState): StateProps => ({ dollarBalance: state.stableToken.balance, paymentRequests: getOutgoingPaymentRequests(state), e164PhoneNumberAddressMapping: e164NumberToAddressSelector(state), - recipientCache: recipientCacheSelector(state), - addressToE164Number: state.identity.addressToE164Number, + recipientInfo: recipientInfoSelector(state), }) type Props = WithTranslation & StateProps & DispatchProps export const listItemRenderer = (params: { - recipientCache: NumberToRecipient - addressToE164Number: AddressToE164NumberType + recipientInfo: RecipientInfo cancelPaymentRequest: typeof cancelPaymentRequest updatePaymentRequestNotified: typeof updatePaymentRequestNotified }) => (request: PaymentRequest, key: number | undefined = undefined) => { - const requestee = getRequesteeFromPaymentRequest( - request, - params.addressToE164Number, - params.recipientCache - ) + const requestee = getRecipientFromAddress(request.requesteeAddress, params.recipientInfo) return ( ({ e164PhoneNumberAddressMapping: e164NumberToAddressSelector(state), - addressToE164Number: addressToE164NumberSelector(state), - recipientCache: recipientCacheSelector(state), + recipientInfo: recipientInfoSelector(state), }) // Payment Request notification for the notification center on home screen @@ -63,21 +55,17 @@ export class OutgoingPaymentRequestSummaryNotification extends React.Component

) } render() { - const { recipientCache, requests, t } = this.props + const { requests, t } = this.props return requests.length === 1 ? ( listItemRenderer({ - addressToE164Number: this.props.addressToE164Number, - recipientCache, + recipientInfo: this.props.recipientInfo, // accessing via this.props.<...> to avoid shadowing cancelPaymentRequest: this.props.cancelPaymentRequest, updatePaymentRequestNotified: this.props.updatePaymentRequestNotified, diff --git a/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.tsx b/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.tsx index 300bb852fd5..f9c8e34420f 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestConfirmation.tsx @@ -23,7 +23,7 @@ import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' import { writePaymentRequest } from 'src/paymentRequest/actions' import { PaymentRequestStatus } from 'src/paymentRequest/types' -import { getDisplayName, getRecipientThumbnail } from 'src/recipients/recipient' +import { getDisplayName } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' import { ConfirmationInput, getConfirmationInput } from 'src/send/utils' import DisconnectBanner from 'src/shared/DisconnectBanner' @@ -98,8 +98,8 @@ class PaymentRequestConfirmation extends React.Component { onConfirm = async () => { const { amount, recipient, recipientAddress: requesteeAddress } = this.props.confirmationInput const { t } = this.props - if (!recipient || (!recipient.e164PhoneNumber && !recipient.address)) { - throw new Error("Can't request from recipient without valid e164 number or a wallet address") + if (!recipient) { + throw new Error("Can't request without valid recipient") } const address = this.props.account @@ -143,7 +143,7 @@ class PaymentRequestConfirmation extends React.Component { render() { const { t, confirmationInput } = this.props - const { recipient, recipientAddress: requesteeAddress } = confirmationInput + const { recipient } = confirmationInput const amount = { value: this.props.confirmationInput.amount, currencyCode: CURRENCIES[CURRENCY_ENUM.DOLLAR].code, // Only cUSD for now @@ -162,16 +162,10 @@ class PaymentRequestConfirmation extends React.Component { > - + {t('requesting')} - - {getDisplayName({ recipient, recipientAddress: requesteeAddress, t })} - + {getDisplayName(recipient, t)} diff --git a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx index 0d6cd36c774..fb8353d8d3c 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.test.tsx @@ -3,21 +3,24 @@ import 'react-native' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import PaymentRequestNotificationInner from 'src/paymentRequest/PaymentRequestNotificationInner' -import { RecipientKind, RecipientWithAddress } from 'src/recipients/recipient' -import { createMockStore } from 'test/utils' +import { AddressRecipient } from 'src/recipients/recipient' +import { createMockStore, getMockI18nProps } from 'test/utils' import { mockAccount, mockE164Number } from 'test/values' it('renders correctly', () => { const store = createMockStore() - const requesterRecipient: RecipientWithAddress = { - kind: RecipientKind.Address, - displayName: 'mockDisplayName', + const requesterRecipient: AddressRecipient = { + name: 'mockDisplayName', address: mockAccount, e164PhoneNumber: mockE164Number, } const tree = renderer.create( - + ) diff --git a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx index 55727388120..8aa4b20e890 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestNotificationInner.tsx @@ -1,14 +1,16 @@ +import { TFunction } from 'i18next' import * as React from 'react' -import { Recipient } from 'src/recipients/recipient' +import { getDisplayName, Recipient } from 'src/recipients/recipient' interface Props { amount: string recipient: Recipient + t: TFunction } export default function PaymentRequestNotificationInner(props: Props) { - const { recipient } = props - const displayName = recipient.displayName + const { recipient, t } = props + const displayName = getDisplayName(recipient, t) // Using a fragment to suppress a limitation with TypeScript and functional // components returning a string diff --git a/packages/mobile/src/paymentRequest/PaymentRequestUnavailable.tsx b/packages/mobile/src/paymentRequest/PaymentRequestUnavailable.tsx index 6898fcd735a..89e62f94d1e 100644 --- a/packages/mobile/src/paymentRequest/PaymentRequestUnavailable.tsx +++ b/packages/mobile/src/paymentRequest/PaymentRequestUnavailable.tsx @@ -10,6 +10,7 @@ import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { TopBarIconButton } from 'src/navigator/TopBarButton' import { StackParamList } from 'src/navigator/types' +import { getDisplayName, recipientHasNumber } from 'src/recipients/recipient' type RouteProps = StackScreenProps type Props = RouteProps @@ -25,17 +26,18 @@ export const paymentRequestUnavailableScreenNavOptions = () => ({ const PaymentRequestUnavailable = (props: Props) => { const { t } = useTranslation(Namespaces.paymentRequestFlow) const { recipient } = props.route.params.transactionData + const displayName = getDisplayName(recipient, t) return ( - {recipient.displayName === 'Mobile #' + {!recipient.name ? t('requestUnavailableNoDisplayNameHeader') - : t('requestUnavailableHeader', { displayName: recipient.displayName })} + : t('requestUnavailableHeader', { displayName })} - {recipient.e164PhoneNumber + {recipientHasNumber(recipient) ? t('requestUnavailableBody', { e164PhoneNumber: recipient.e164PhoneNumber }) : t('requestUnavailableNoNumberBody')} diff --git a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap index caa73daede5..c9b14a64e73 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestListScreen.test.tsx.snap @@ -204,7 +204,7 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - + + J @@ -462,7 +462,7 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - + + J @@ -720,7 +720,7 @@ exports[`IncomingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - + + J diff --git a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap index 62a5f363164..6e508c8f1a6 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/IncomingPaymentRequestSummaryNotification.test.tsx.snap @@ -144,23 +144,13 @@ exports[`IncomingPaymentRequestSummaryNotification renders a number when the add ] } > - - + - + @@ -405,23 +395,13 @@ exports[`IncomingPaymentRequestSummaryNotification renders correctly for just on ] } > - - + - + diff --git a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap index fe3f44ae035..0d1215c436e 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestListScreen.test.tsx.snap @@ -189,23 +189,13 @@ exports[`OutgoingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - - 0 - + @@ -447,23 +437,13 @@ exports[`OutgoingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - - 0 - + @@ -705,23 +685,13 @@ exports[`OutgoingPaymentRequestListScreen renders correctly with requests 1`] = ] } > - - 0 - + diff --git a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap index 70013436ed3..8397bf711f2 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/OutgoingPaymentRequestSummaryNotification.test.tsx.snap @@ -159,7 +159,7 @@ exports[`OutgoingPaymentRequestSummaryNotification renders a number when the add ] } > - 0 + J @@ -420,7 +420,7 @@ exports[`OutgoingPaymentRequestSummaryNotification renders correctly for just on ] } > - + + J diff --git a/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestConfirmation.test.tsx.snap b/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestConfirmation.test.tsx.snap index 9147990c767..955aee0cd85 100644 --- a/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestConfirmation.test.tsx.snap +++ b/packages/mobile/src/paymentRequest/__snapshots__/PaymentRequestConfirmation.test.tsx.snap @@ -77,7 +77,7 @@ exports[`PaymentRequestConfirmation renders correctly for request payment confir "justifyContent": "center", }, Object { - "backgroundColor": "hsl(195, 53%, 93%)", + "backgroundColor": "hsl(0, 53%, 93%)", "borderRadius": 20, "height": 40, "width": 40, @@ -94,7 +94,7 @@ exports[`PaymentRequestConfirmation renders correctly for request payment confir "fontSize": 16, }, Object { - "color": "hsl(195, 67%, 24%)", + "color": "hsl(0, 67%, 24%)", "fontSize": 20, }, ] diff --git a/packages/mobile/src/paymentRequest/utils.test.ts b/packages/mobile/src/paymentRequest/utils.test.ts index 218716cb6d7..efcbef55ded 100644 --- a/packages/mobile/src/paymentRequest/utils.test.ts +++ b/packages/mobile/src/paymentRequest/utils.test.ts @@ -2,13 +2,8 @@ import { hexToBuffer } from '@celo/utils/lib/address' import { expectSaga } from 'redux-saga-test-plan' import { call } from 'redux-saga/effects' import { PaymentRequest } from 'src/paymentRequest/types' -import { - decryptPaymentRequest, - encryptPaymentRequest, - getRequesteeFromPaymentRequest, - getRequesterFromPaymentRequest, -} from 'src/paymentRequest/utils' -import { RecipientKind } from 'src/recipients/recipient' +import { decryptPaymentRequest, encryptPaymentRequest } from 'src/paymentRequest/utils' +import { getRecipientFromAddress } from 'src/recipients/recipient' import { doFetchDataEncryptionKey } from 'src/web3/dataEncryptionKey' import { mockAccount, @@ -32,34 +27,44 @@ const req = mockPaymentRequests[0] describe('getRequesterFromPaymentRequest', () => { const address = req.requesterAddress const addressToE164Number = { [address]: mockE164Number } - const recipientCache = { [mockE164Number]: mockRecipient } + const phoneRecipientCache = { [mockE164Number]: mockRecipient } it('gets requester when only address is known', () => { - const recipient = getRequesterFromPaymentRequest(req, {}, {}) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number: {}, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.MobileNumber, address, - displayName: mockE164Number, }) }) it('gets requester when address is cached but not recipient', () => { - const recipient = getRequesterFromPaymentRequest(req, addressToE164Number, {}) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.MobileNumber, address, e164PhoneNumber: mockE164Number, - displayName: mockE164Number, }) }) it('gets requester when address and recip are cached', () => { - const recipient = getRequesterFromPaymentRequest(req, addressToE164Number, recipientCache) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache, + valoraRecipientCache: {}, + addressToE164Number, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.Address, address, e164PhoneNumber: mockE164Number, - displayName: mockName, + name: mockName, }) }) }) @@ -67,34 +72,44 @@ describe('getRequesterFromPaymentRequest', () => { describe('getRequesteeFromPaymentRequest', () => { const address = req.requesteeAddress const addressToE164Number = { [address]: mockE164Number } - const recipientCache = { [mockE164Number]: mockRecipient } + const phoneRecipientCache = { [mockE164Number]: mockRecipient } it('gets requestee when only address is known', () => { - const recipient = getRequesteeFromPaymentRequest(req, {}, {}) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number: {}, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.Address, address, - displayName: address, }) }) it('gets requestee when address is cached but not recipient', () => { - const recipient = getRequesteeFromPaymentRequest(req, addressToE164Number, {}) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.MobileNumber, address, e164PhoneNumber: mockE164Number, - displayName: mockE164Number, }) }) it('gets requestee when address and recip are cached', () => { - const recipient = getRequesteeFromPaymentRequest(req, addressToE164Number, recipientCache) + const recipient = getRecipientFromAddress(address, { + phoneRecipientCache, + valoraRecipientCache: {}, + addressToE164Number, + addressToDisplayName: {}, + }) expect(recipient).toMatchObject({ - kind: RecipientKind.Address, address, e164PhoneNumber: mockE164Number, - displayName: mockName, + name: mockName, }) }) }) diff --git a/packages/mobile/src/paymentRequest/utils.ts b/packages/mobile/src/paymentRequest/utils.ts index 9c134d4cdb5..211b4ad0cfb 100644 --- a/packages/mobile/src/paymentRequest/utils.ts +++ b/packages/mobile/src/paymentRequest/utils.ts @@ -5,88 +5,12 @@ import { call } from 'redux-saga/effects' import { MAX_COMMENT_LENGTH } from 'src/config' import { features } from 'src/flags' import i18n from 'src/i18n' -import { AddressToE164NumberType } from 'src/identity/reducer' import { PaymentRequest } from 'src/paymentRequest/types' -import { NumberToRecipient, Recipient, RecipientKind } from 'src/recipients/recipient' import Logger from 'src/utils/Logger' import { doFetchDataEncryptionKey } from 'src/web3/dataEncryptionKey' const TAG = 'paymentRequest/utils' -// Returns a recipient for the SENDER of a payment request -// i.e. this account when outgoing, another when incoming -export function getRequesterFromPaymentRequest( - paymentRequest: PaymentRequest, - addressToE164Number: AddressToE164NumberType, - recipientCache: NumberToRecipient -): Recipient { - return getRecipientObjectFromPaymentRequest( - paymentRequest, - addressToE164Number, - recipientCache, - false - ) -} - -// Returns a recipient for the TARGET of a payment request -// i.e. this account when incoming, another when outgoing -export function getRequesteeFromPaymentRequest( - paymentRequest: PaymentRequest, - addressToE164Number: AddressToE164NumberType, - recipientCache: NumberToRecipient -): Recipient { - return getRecipientObjectFromPaymentRequest( - paymentRequest, - addressToE164Number, - recipientCache, - true - ) -} - -function getRecipientObjectFromPaymentRequest( - paymentRequest: PaymentRequest, - addressToE164Number: AddressToE164NumberType, - recipientCache: NumberToRecipient, - isRequestee: boolean -): Recipient { - let address: string - let e164PhoneNumber: string | undefined - if (isRequestee) { - address = paymentRequest.requesteeAddress - e164PhoneNumber = addressToE164Number[address] ?? undefined - } else { - address = paymentRequest.requesterAddress - // For now, priority is given the # in the request over the cached # - // from the on-chain mapping. This may be revisted later. - e164PhoneNumber = - paymentRequest.requesterE164Number ?? addressToE164Number[address] ?? undefined - } - - if (!e164PhoneNumber) { - return { - kind: RecipientKind.Address, - address, - displayName: address, - } - } - - const cachedRecipient = recipientCache[e164PhoneNumber] - if (cachedRecipient) { - return { - ...cachedRecipient, - kind: RecipientKind.Address, - address, - } - } else { - return { - kind: RecipientKind.MobileNumber, - address, - e164PhoneNumber, - displayName: e164PhoneNumber, - } - } -} - // Encrypt sensitive data in the payment request using the recipient and sender DEK export function* encryptPaymentRequest(paymentRequest: PaymentRequest) { Logger.debug(`${TAG}@encryptPaymentRequest`, 'Encrypting payment request') diff --git a/packages/mobile/src/qrcode/utils.ts b/packages/mobile/src/qrcode/utils.ts index 7b0ff8ae318..09acac2bea1 100644 --- a/packages/mobile/src/qrcode/utils.ts +++ b/packages/mobile/src/qrcode/utils.ts @@ -7,11 +7,15 @@ import { SendOrigin } from 'src/analytics/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { ErrorMessages } from 'src/app/ErrorMessages' import { validateRecipientAddressSuccess } from 'src/identity/actions' -import { AddressToE164NumberType, E164NumberToAddressType } from 'src/identity/reducer' +import { E164NumberToAddressType } from 'src/identity/reducer' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { UriData, uriDataFromUrl } from 'src/qrcode/schema' -import { getRecipientFromAddress, NumberToRecipient } from 'src/recipients/recipient' +import { + getRecipientFromAddress, + recipientHasNumber, + RecipientInfo, +} from 'src/recipients/recipient' import { QrCode, SVG } from 'src/send/actions' import { TransactionDataInput } from 'src/send/SendAmount' import { handleSendPaymentData } from 'src/send/utils' @@ -55,8 +59,8 @@ function* handleSecureSend( secureSendTxData: TransactionDataInput, requesterAddress?: string ) { - if (!secureSendTxData.recipient.e164PhoneNumber) { - throw Error(`Invalid recipient type for Secure Send: ${secureSendTxData.recipient.kind}`) + if (!recipientHasNumber(secureSendTxData.recipient)) { + throw Error('Invalid recipient type for Secure Send, has no mobile number') } const userScannedAddress = address.toLowerCase() @@ -93,9 +97,8 @@ function* handleSecureSend( export function* handleBarcode( barcode: QrCode, - addressToE164Number: AddressToE164NumberType, - recipientCache: NumberToRecipient, e164NumberToAddress: E164NumberToAddressType, + recipientInfo: RecipientInfo, secureSendTxData?: TransactionDataInput, isOutgoingPaymentRequest?: true, requesterAddress?: string @@ -137,11 +140,7 @@ export function* handleBarcode( return } - const cachedRecipient = getRecipientFromAddress( - qrData.address, - addressToE164Number, - recipientCache - ) + const cachedRecipient = getRecipientFromAddress(qrData.address, recipientInfo) yield call(handleSendPaymentData, qrData, cachedRecipient, isOutgoingPaymentRequest, true) } diff --git a/packages/mobile/src/recipients/RecipientItem.tsx b/packages/mobile/src/recipients/RecipientItem.tsx index d8bceaaf51b..4072a77ddd5 100644 --- a/packages/mobile/src/recipients/RecipientItem.tsx +++ b/packages/mobile/src/recipients/RecipientItem.tsx @@ -3,42 +3,48 @@ import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' import variables from '@celo/react-components/styles/variables' import * as React from 'react' +import { WithTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import ContactCircle from 'src/components/ContactCircle' +import { Namespaces, withTranslation } from 'src/i18n' import Logo from 'src/icons/Logo' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { + getDisplayDetail, + getDisplayName, + Recipient, + recipientHasAddress, +} from 'src/recipients/recipient' import GetRewardPill from 'src/send/GetRewardPill' -interface Props { +interface OwnProps { recipient: Recipient onSelectRecipient(recipient: Recipient): void } +type Props = OwnProps & WithTranslation + class RecipientItem extends React.PureComponent { onPress = () => { this.props.onSelectRecipient(this.props.recipient) } render() { - const { recipient } = this.props + const { recipient, t } = this.props return ( - + - {recipient.displayName} + {getDisplayName(recipient, t)} - {recipient.displayId} + {!recipient.name ? ( + {getDisplayDetail(recipient)} + ) : null} - {recipient.address ? : } + {recipientHasAddress(recipient) ? : } @@ -75,4 +81,4 @@ const styles = StyleSheet.create({ }, }) -export default RecipientItem +export default withTranslation(Namespaces.paymentRequestFlow)(RecipientItem) diff --git a/packages/mobile/src/recipients/RecipientPicker.tsx b/packages/mobile/src/recipients/RecipientPicker.tsx index 87a9e211d50..ce4dc3dad87 100644 --- a/packages/mobile/src/recipients/RecipientPicker.tsx +++ b/packages/mobile/src/recipients/RecipientPicker.tsx @@ -17,20 +17,18 @@ import { import { SafeAreaInsetsContext } from 'react-native-safe-area-context' import { connect } from 'react-redux' import { Namespaces, withTranslation } from 'src/i18n' -import { AddressToE164NumberType } from 'src/identity/reducer' import { + AddressRecipient, getRecipientFromAddress, - NumberToRecipient, + MobileRecipient, Recipient, - RecipientKind, - RecipientWithAddress, - RecipientWithMobileNumber, + recipientHasContact, + recipientHasNumber, + RecipientInfo, } from 'src/recipients/recipient' import RecipientItem from 'src/recipients/RecipientItem' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' -import Logger from 'src/utils/Logger' -import { assertUnreachable } from 'src/utils/typescript' interface Section { key: string @@ -47,15 +45,13 @@ interface Props { } interface StateProps { - addressToE164Number: AddressToE164NumberType - recipientCache: NumberToRecipient + recipientInfo: RecipientInfo } type RecipientProps = Props & WithTranslation & StateProps const mapStateToProps = (state: RootState): StateProps => ({ - addressToE164Number: state.identity.addressToE164Number, - recipientCache: recipientCacheSelector(state), + recipientInfo: recipientInfoSelector(state), }) export class RecipientPicker extends React.Component { @@ -76,18 +72,12 @@ export class RecipientPicker extends React.Component { ) keyExtractor = (item: Recipient, index: number) => { - switch (item.kind) { - case RecipientKind.Contact: - return item.contactId + item.phoneNumberLabel + index - case RecipientKind.MobileNumber: - return item.e164PhoneNumber + index - case RecipientKind.QrCode: - return item.address + index - case RecipientKind.Address: - return item.address + index - default: - Logger.error('RecipientPicker', 'Unsupported recipient kind', item) - throw assertUnreachable(item) + if (recipientHasContact(item)) { + return item.contactId + item.e164PhoneNumber + index + } else if (recipientHasNumber(item)) { + return item.e164PhoneNumber + index + } else { + return item.address + index } } @@ -127,12 +117,11 @@ export class RecipientPicker extends React.Component { ) - renderSendToPhoneNumber = (displayId: string, e164PhoneNumber: string) => { - const { t, onSelectRecipient } = this.props - const recipient: RecipientWithMobileNumber = { - kind: RecipientKind.MobileNumber, - displayName: t('sendToMobileNumber'), - displayId, + renderSendToPhoneNumber = (displayNumber: string, e164PhoneNumber: string) => { + const { onSelectRecipient, t } = this.props + const recipient: MobileRecipient = { + displayNumber, + name: t('sendToMobileNumber'), e164PhoneNumber, } return ( @@ -144,13 +133,9 @@ export class RecipientPicker extends React.Component { } renderSendToAddress = () => { - const { t, searchQuery, addressToE164Number, recipientCache, onSelectRecipient } = this.props + const { searchQuery, recipientInfo, onSelectRecipient, t } = this.props const searchedAddress = searchQuery.toLowerCase() - const existingContact = getRecipientFromAddress( - searchedAddress, - addressToE164Number, - recipientCache - ) + const existingContact = getRecipientFromAddress(searchedAddress, recipientInfo) if (existingContact) { return ( <> @@ -159,10 +144,8 @@ export class RecipientPicker extends React.Component { ) } else { - const recipient: RecipientWithAddress = { - kind: RecipientKind.Address, - displayName: t('sendToAddress'), - displayId: searchedAddress.substring(2, 17) + '...', + const recipient: AddressRecipient = { + name: t('sendToAddress'), address: searchedAddress, } diff --git a/packages/mobile/src/recipients/__snapshots__/RecipientItem.test.tsx.snap b/packages/mobile/src/recipients/__snapshots__/RecipientItem.test.tsx.snap index edc7b3d2deb..2d755834f99 100644 --- a/packages/mobile/src/recipients/__snapshots__/RecipientItem.test.tsx.snap +++ b/packages/mobile/src/recipients/__snapshots__/RecipientItem.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RecipientItem renders correctly 1`] = ` +exports[` renders correctly 1`] = ` John Doe - - 14155550000 - ({ + type: Actions.SET_PHONE_RECIPIENT_CACHE, + recipients, +}) -export const setRecipientCache = (recipients: NumberToRecipient): SetRecipientCacheAction => ({ - type: Actions.SET_RECIPIENT_CACHE, +export const updateValoraRecipientCache = ( + recipients: AddressToRecipient +): UpdateValoraRecipientCacheAction => ({ + type: Actions.UPDATE_VALORA_RECIPIENT_CACHE, recipients, }) diff --git a/packages/mobile/src/recipients/recipient.test.ts b/packages/mobile/src/recipients/recipient.test.ts index 53a5a60176e..e7d821819e0 100644 --- a/packages/mobile/src/recipients/recipient.test.ts +++ b/packages/mobile/src/recipients/recipient.test.ts @@ -1,13 +1,5 @@ -import { contactsToRecipients, RecipientKind, sortRecipients } from 'src/recipients/recipient' -import { - mockContactList, - mockDisplayNumber, - mockE164Number, - mockRecipient, - mockRecipient2, - mockRecipient3, - mockRecipient4, -} from 'test/values' +import { contactsToRecipients, sortRecipients } from 'src/recipients/recipient' +import { mockContactList, mockRecipient, mockRecipient2, mockRecipient3 } from 'test/values' describe('contactsToRecipients', () => { it('returns a recipient per phone number', () => { @@ -18,45 +10,32 @@ describe('contactsToRecipients', () => { return expect(false).toBeTruthy() } - const recipientsWithE164Numbers = Object.values(recipients.e164NumberToRecipients) - const recipientsWithoutE164Numbers = Object.values(recipients.otherRecipients) - - expect(recipientsWithE164Numbers).toHaveLength(2) - expect(recipientsWithE164Numbers[1]).toMatchObject({ - kind: RecipientKind.Contact, - displayName: 'Alice The Person', - displayId: '(209) 555-9790', - e164PhoneNumber: '+12095559790', - phoneNumberLabel: 'mobile', - contactId: '1', - }) - expect(recipientsWithE164Numbers[0]).toMatchObject({ - kind: RecipientKind.Contact, - displayName: 'Bob Bobson', - displayId: mockDisplayNumber, - e164PhoneNumber: mockE164Number, - phoneNumberLabel: 'home', - contactId: '2', - }) - expect(recipientsWithoutE164Numbers).toHaveLength(1) - expect(recipientsWithoutE164Numbers[0]).toMatchObject({ - kind: RecipientKind.Contact, - displayName: 'Bob Bobson', - displayId: '100200', - phoneNumberLabel: 'mobile', - contactId: '2', + expect(recipients).toMatchObject({ + '+14155550000': { + name: 'Bob Bobson', + displayNumber: '(415) 555-0000', + e164PhoneNumber: '+14155550000', + contactId: '2', + thumbnailPath: '', + }, + '+12095559790': { + name: 'Alice The Person', + displayNumber: '(209) 555-9790', + e164PhoneNumber: '+12095559790', + contactId: '1', + thumbnailPath: '//path/', + }, }) }) }) describe('Recipient sorting', () => { - const recipients = [mockRecipient2, mockRecipient, mockRecipient4, mockRecipient3] + const recipients = [mockRecipient2, mockRecipient, mockRecipient3] it('Sorts recipients without any prioritized', () => { expect(sortRecipients(recipients)).toStrictEqual([ mockRecipient3, mockRecipient2, mockRecipient, - mockRecipient4, ]) }) it('Sorts recipients with some prioritized', () => { @@ -65,7 +44,6 @@ describe('Recipient sorting', () => { mockRecipient, mockRecipient3, mockRecipient2, - mockRecipient4, ]) }) }) diff --git a/packages/mobile/src/recipients/recipient.ts b/packages/mobile/src/recipients/recipient.ts index 07ef8ca6c3e..996fb2187d6 100644 --- a/packages/mobile/src/recipients/recipient.ts +++ b/packages/mobile/src/recipients/recipient.ts @@ -2,68 +2,80 @@ import { parsePhoneNumber } from '@celo/utils/lib/phoneNumbers' import * as fuzzysort from 'fuzzysort' import { TFunction } from 'i18next' import { MinimalContact } from 'react-native-contacts' -import i18n from 'src/i18n' -import { AddressToE164NumberType, E164NumberToAddressType } from 'src/identity/reducer' +import { formatShortenedAddress } from 'src/components/ShortenedAddress' +import { + AddressToDisplayNameType, + AddressToE164NumberType, + E164NumberToAddressType, +} from 'src/identity/reducer' import { ContactMatches, RecipientVerificationStatus } from 'src/identity/types' import Logger from 'src/utils/Logger' const TAG = 'recipients/recipient' -export enum RecipientKind { - MobileNumber = 'MobileNumber', - Contact = 'Contact', - QrCode = 'QrCode', - Address = 'Address', -} - -export type Recipient = - | RecipientWithMobileNumber - | RecipientWithContact - | RecipientWithQrCode - | RecipientWithAddress - -interface IRecipient { - kind: RecipientKind - displayName: string - displayId?: string +export type Recipient = { + name?: string + contactId?: string // unique ID given by phone OS + thumbnailPath?: string + displayNumber?: string e164PhoneNumber?: string address?: string -} +} & ({ e164PhoneNumber: string } | { address: string }) -export interface RecipientWithMobileNumber extends IRecipient { - kind: RecipientKind.MobileNumber - e164PhoneNumber: string // MobileNumber recipients always have a parsable number +export type MobileRecipient = Recipient & { + e164PhoneNumber: string } -export interface RecipientWithContact extends IRecipient { - kind: RecipientKind.Contact +// contacts pulled from the phone +export type ContactRecipient = MobileRecipient & { + name: string contactId: string - phoneNumberLabel?: string - thumbnailPath?: string } -export interface RecipientWithQrCode extends IRecipient { - kind: RecipientKind.QrCode +export type AddressRecipient = Recipient & { address: string - phoneNumberLabel?: string - contactId?: string - thumbnailPath?: string } -export interface RecipientWithAddress extends IRecipient { - kind: RecipientKind.Address - address: string - thumbnailPath?: string +export function recipientHasNumber(recipient: Recipient): recipient is MobileRecipient { + return recipient && 'e164PhoneNumber' in recipient && !!recipient.e164PhoneNumber +} + +export function recipientHasAddress(recipient: Recipient): recipient is AddressRecipient { + return recipient && 'address' in recipient && !!recipient.address +} + +export function recipientHasContact(recipient: Recipient): recipient is ContactRecipient { + return recipient && 'contactId' in recipient && 'name' in recipient && !!recipient.contactId +} + +export function getDisplayName(recipient: Recipient, t: TFunction) { + if (recipient.name) { + return recipient.name + } else if (recipient.displayNumber) { + return recipient.displayNumber + } else if (recipient.e164PhoneNumber) { + return recipient.e164PhoneNumber + } else if (recipient.address) { + return t('walletFlow5:feedItemAddress', { address: formatShortenedAddress(recipient.address) }) + } else { + return t('global:unknown') + } +} + +export function getDisplayDetail(recipient: Recipient) { + if (recipientHasNumber(recipient)) { + return recipient.displayNumber || recipient.e164PhoneNumber + } else { + return recipient.address.substring(2, 17) + '...' + } } export interface NumberToRecipient { - [number: string]: RecipientWithContact + [number: string]: ContactRecipient } -interface DisplayNameProps { - recipient: Recipient - recipientAddress?: string | null - t: TFunction +export interface AddressToRecipient { + [address: string]: AddressRecipient } /** @@ -75,8 +87,6 @@ export function contactsToRecipients(contacts: MinimalContact[], defaultCountryC // We need a map of e164Number to recipients so we can efficiently // update them later as the latest contact mappings arrive from the contact calls. const e164NumberToRecipients: NumberToRecipient = {} - // Recipients without e164Numbers go here instead - const otherRecipients: NumberToRecipient = {} for (const contact of contacts) { if (!contact.phoneNumbers || !contact.phoneNumbers.length) { @@ -93,69 +103,61 @@ export function contactsToRecipients(contacts: MinimalContact[], defaultCountryC continue } e164NumberToRecipients[parsedNumber.e164Number] = { - kind: RecipientKind.Contact, - displayName: contact.displayName || i18n.t('sendFlow7:mobileNumber'), - displayId: parsedNumber.displayNumber, + name: contact.displayName, + displayNumber: parsedNumber.displayNumber, e164PhoneNumber: parsedNumber.e164Number, - phoneNumberLabel: phoneNumber.label, // @ts-ignore TODO Minimal contact type is incorrect, on android it returns id contactId: contact.recordID || contact.id, thumbnailPath: contact.thumbnailPath, } } else { - otherRecipients[phoneNumber.number] = { - kind: RecipientKind.Contact, - displayName: contact.displayName || i18n.t('sendFlow7:mobileNumber'), - displayId: phoneNumber.number, - phoneNumberLabel: phoneNumber.label, - // @ts-ignore TODO Minimal contact type is incorrect, on android it returns id - contactId: contact.recordID || contact.id, - thumbnailPath: contact.thumbnailPath, - } + // don't do anything for contacts without e164PhoneNumber, as we can't interact with them anyways } } } - - return { e164NumberToRecipients, otherRecipients } + return e164NumberToRecipients } catch (error) { Logger.error(TAG, 'Failed to build recipients cache', error) throw error } } -export function getRecipientFromAddress( - address: string, - addressToE164Number: AddressToE164NumberType, - recipientCache: NumberToRecipient -) { - const e164PhoneNumber = addressToE164Number[address] - return e164PhoneNumber ? recipientCache[e164PhoneNumber] : undefined +export interface RecipientInfo { + addressToE164Number: AddressToE164NumberType + phoneRecipientCache: NumberToRecipient + valoraRecipientCache: AddressToRecipient + // this info comes from Firebase for known addresses (ex. Simplex, cUSD incentive programs) + // differentiated from valoraRecipients because they are not displayed in the RecipientPicker + addressToDisplayName: AddressToDisplayNameType } -export function getDisplayName({ recipient, recipientAddress, t }: DisplayNameProps) { - const { displayName, e164PhoneNumber } = recipient - if (displayName && displayName !== t('mobileNumber')) { - return displayName +export function getRecipientFromAddress(address: string, info: RecipientInfo) { + const e164PhoneNumber = info.addressToE164Number[address] + const numberRecipient = e164PhoneNumber ? info.phoneRecipientCache[e164PhoneNumber] : undefined + const valoraRecipient = info.valoraRecipientCache[address] + const displayInfo = info.addressToDisplayName[address] + + const recipient: Recipient = { + address, + name: valoraRecipient?.name || numberRecipient?.name || displayInfo?.name, + thumbnailPath: valoraRecipient?.thumbnailPath || displayInfo?.imageUrl || undefined, + contactId: valoraRecipient?.contactId || numberRecipient?.contactId, + e164PhoneNumber: e164PhoneNumber || undefined, + displayNumber: numberRecipient?.displayNumber, } - if (e164PhoneNumber) { - return e164PhoneNumber - } - if (recipientAddress) { - return recipientAddress - } - // Rare but possible, such as when a user skips onboarding flow (in dev mode) and then views their own avatar - return t('global:unknown') + + return recipient } export function getRecipientVerificationStatus( recipient: Recipient, e164NumberToAddress: E164NumberToAddressType ): RecipientVerificationStatus { - if (recipient.kind === RecipientKind.QrCode || recipient.kind === RecipientKind.Address) { + if (recipientHasAddress(recipient)) { return RecipientVerificationStatus.VERIFIED } - if (!recipient.e164PhoneNumber) { + if (!recipientHasNumber(recipient)) { return RecipientVerificationStatus.UNKNOWN } @@ -171,15 +173,6 @@ export function getRecipientVerificationStatus( return RecipientVerificationStatus.VERIFIED } -export function getRecipientThumbnail(recipient?: Recipient) { - switch (recipient?.kind) { - case RecipientKind.Contact: - case RecipientKind.Address: - return recipient.thumbnailPath - } - return undefined -} - type PreparedRecipient = Recipient & { displayPrepared: Fuzzysort.Prepared | undefined phonePrepared: Fuzzysort.Prepared | undefined @@ -214,8 +207,8 @@ function fuzzysortToRecipients( } function nameCompare(a: FuzzyRecipient, b: FuzzyRecipient) { - const nameA = a.displayName?.toUpperCase() ?? '' - const nameB = b.displayName?.toUpperCase() ?? '' + const nameA = a.name?.toUpperCase() ?? '' + const nameB = b.name?.toUpperCase() ?? '' if (nameA > nameB) { return 1 @@ -281,9 +274,9 @@ export function filterRecipientFactory( ) { const preparedRecipients = recipients.map((r) => ({ ...r, - displayPrepared: fuzzysort.prepare(r.displayName), - phonePrepared: r.e164PhoneNumber ? fuzzysort.prepare(r.e164PhoneNumber) : undefined, - addressPrepared: r.address ? fuzzysort.prepare(r.address) : undefined, + displayPrepared: fuzzysort.prepare(r.name!), + phonePrepared: recipientHasNumber(r) ? fuzzysort.prepare(r.e164PhoneNumber) : undefined, + addressPrepared: recipientHasAddress(r) ? fuzzysort.prepare(r.address) : undefined, })) return (query: string) => { @@ -306,14 +299,18 @@ export function areRecipientsEquivalent(recipient1: Recipient, recipient2: Recip } if ( - recipient1.e164PhoneNumber && - recipient2.e164PhoneNumber && + recipientHasNumber(recipient1) && + recipientHasNumber(recipient2) && recipient1.e164PhoneNumber === recipient2.e164PhoneNumber ) { return true } - if (recipient1.address && recipient2.address && recipient1.address === recipient2.address) { + if ( + recipientHasAddress(recipient1) && + recipientHasAddress(recipient2) && + recipient1.address === recipient2.address + ) { return true } diff --git a/packages/mobile/src/recipients/reducer.ts b/packages/mobile/src/recipients/reducer.ts index 45852d754f7..53b6787f911 100644 --- a/packages/mobile/src/recipients/reducer.ts +++ b/packages/mobile/src/recipients/reducer.ts @@ -1,18 +1,22 @@ import { Actions, ActionTypes } from 'src/recipients/actions' -import { NumberToRecipient } from 'src/recipients/recipient' -import { RehydrateAction } from 'src/redux/persist-helper' +import { AddressToRecipient, NumberToRecipient } from 'src/recipients/recipient' +import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' import { RootState } from 'src/redux/reducers' export interface State { - // RecipientCache contains the processed contact data imported from the + // phoneRecipientCache contains the processed contact data imported from the // phone for a single app session. // Think of contacts as raw data and recipients as filtered data // No relation to recent recipients, which is in /send/reducer.ts - recipientCache: NumberToRecipient + phoneRecipientCache: NumberToRecipient + // valoraRecipientCache contains accounts that the user has sent/recieved transactions from, + // and includes CIP8 profile data if available + valoraRecipientCache: AddressToRecipient } const initialState = { - recipientCache: {}, + phoneRecipientCache: {}, + valoraRecipientCache: {}, } export const recipientsReducer = ( @@ -20,14 +24,38 @@ export const recipientsReducer = ( action: ActionTypes | RehydrateAction ) => { switch (action.type) { - case Actions.SET_RECIPIENT_CACHE: + case REHYDRATE: { return { ...state, - recipientCache: action.recipients, + ...getRehydratePayload(action, 'recipients'), + phoneRecipientCache: initialState.phoneRecipientCache, + } + } + case Actions.SET_PHONE_RECIPIENT_CACHE: + return { + ...state, + phoneRecipientCache: action.recipients, + } + case Actions.UPDATE_VALORA_RECIPIENT_CACHE: + return { + ...state, + valoraRecipientCache: { ...state.valoraRecipientCache, ...action.recipients }, } default: return state } } -export const recipientCacheSelector = (state: RootState) => state.recipients.recipientCache +export const phoneRecipientCacheSelector = (state: RootState) => + state.recipients.phoneRecipientCache +export const valoraRecipientCacheSelector = (state: RootState) => + state.recipients.valoraRecipientCache + +export const recipientInfoSelector = (state: RootState) => { + return { + addressToE164Number: state.identity.addressToE164Number, + phoneRecipientCache: state.recipients.phoneRecipientCache, + valoraRecipientCache: state.recipients.valoraRecipientCache, + addressToDisplayName: state.identity.addressToDisplayName, + } +} diff --git a/packages/mobile/src/redux/reducers.ts b/packages/mobile/src/redux/reducers.ts index 5c597af1419..e08541b7ca6 100644 --- a/packages/mobile/src/redux/reducers.ts +++ b/packages/mobile/src/redux/reducers.ts @@ -108,5 +108,6 @@ export interface PersistedRootState { invite: InviteState escrow: EscrowState localCurrency: LocalCurrencyState + recipients: RecipientsState fiatExchanges: FiatExchangesState } diff --git a/packages/mobile/src/redux/store.ts b/packages/mobile/src/redux/store.ts index 775bd13dafe..fa82a0cbf8a 100644 --- a/packages/mobile/src/redux/store.ts +++ b/packages/mobile/src/redux/store.ts @@ -22,7 +22,7 @@ const persistConfig: any = { version: 12, // default is -1, increment as we make migrations keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems. storage: FSStorage(), - blacklist: ['geth', 'networkInfo', 'alert', 'fees', 'recipients', 'imports'], + blacklist: ['geth', 'networkInfo', 'alert', 'fees', 'imports'], stateReconciler: autoMergeLevel2, migrate: createMigrate(migrations), serialize: (data: any) => { diff --git a/packages/mobile/src/send/Send.test.tsx b/packages/mobile/src/send/Send.test.tsx index d2550d4e944..e4ec8d3b8a4 100644 --- a/packages/mobile/src/send/Send.test.tsx +++ b/packages/mobile/src/send/Send.test.tsx @@ -23,7 +23,7 @@ const defaultStore = { numberVerified: true, }, recipients: { - recipientCache: { + phoneRecipientCache: { [mockE164Number]: mockRecipient2, [mockE164NumberInvite]: mockRecipient4, }, diff --git a/packages/mobile/src/send/Send.tsx b/packages/mobile/src/send/Send.tsx index c4cd589642f..ba42b7e3417 100644 --- a/packages/mobile/src/send/Send.tsx +++ b/packages/mobile/src/send/Send.tsx @@ -13,7 +13,6 @@ import { hideAlert, showError } from 'src/alert/actions' import { RequestEvents, SendEvents } from 'src/analytics/Events' import { SendOrigin } from 'src/analytics/types' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' -import { ErrorMessages } from 'src/app/ErrorMessages' import { verificationPossibleSelector } from 'src/app/selectors' import { estimateFee, FeeType } from 'src/fees/actions' import i18n, { Namespaces, withTranslation } from 'src/i18n' @@ -32,7 +31,7 @@ import { sortRecipients, } from 'src/recipients/recipient' import RecipientPicker from 'src/recipients/RecipientPicker' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { phoneRecipientCacheSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' import { storeLatestInRecents } from 'src/send/actions' import { InviteRewardsBanner } from 'src/send/InviteRewardsBanner' @@ -91,7 +90,7 @@ const mapStateToProps = (state: RootState): StateProps => ({ verificationPossible: verificationPossibleSelector(state), devModeActive: state.account.devModeActive, recentRecipients: state.send.recentRecipients, - allRecipients: recipientCacheSelector(state), + allRecipients: phoneRecipientCacheSelector(state), matchedContacts: state.identity.matchedContacts, inviteRewardsEnabled: state.send.inviteRewardsEnabled, inviteRewardCusd: state.send.inviteRewardCusd, @@ -243,11 +242,7 @@ class Send extends React.Component { this.props.hideAlert() const isOutgoingPaymentRequest = this.props.route.params?.isOutgoingPaymentRequest - if (!recipient.e164PhoneNumber && !recipient.address) { - this.props.showError(ErrorMessages.CANT_SELECT_INVALID_PHONE) - return - } - + // TODO: move this to after a payment has been sent, or else a misclicked recipient will show up in recents this.props.storeLatestInRecents(recipient) ValoraAnalytics.track( @@ -255,7 +250,6 @@ class Send extends React.Component { ? RequestEvents.request_select_recipient : SendEvents.send_select_recipient, { - recipientKind: recipient.kind, usedSearchBar: this.state.searchQuery.length > 0, } ) diff --git a/packages/mobile/src/send/SendAmount.tsx b/packages/mobile/src/send/SendAmount.tsx index af773da4167..29724abc61f 100644 --- a/packages/mobile/src/send/SendAmount.tsx +++ b/packages/mobile/src/send/SendAmount.tsx @@ -49,7 +49,12 @@ import { emptyHeader, HeaderTitleWithBalance } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' -import { getRecipientVerificationStatus, Recipient, RecipientKind } from 'src/recipients/recipient' +import { + getRecipientVerificationStatus, + Recipient, + recipientHasAddress, + recipientHasNumber, +} from 'src/recipients/recipient' import useSelector from 'src/redux/useSelector' import { getFeeType, useDailyTransferLimitValidator } from 'src/send/utils' import DisconnectBanner from 'src/shared/DisconnectBanner' @@ -114,11 +119,11 @@ function SendAmount(props: Props) { useEffect(() => { dispatch(fetchDollarBalance()) - if (recipient.kind === RecipientKind.QrCode || recipient.kind === RecipientKind.Address) { + if (recipientHasAddress(recipient)) { return } - if (!recipient.e164PhoneNumber) { + if (!recipientHasNumber(recipient)) { throw Error('Recipient phone number is required if not sending via QR Code or address') } @@ -256,11 +261,7 @@ function SendAmount(props: Props) { dispatch(hideAlert()) - if ( - addressValidationType !== AddressValidationType.NONE && - recipient.kind !== RecipientKind.QrCode && - recipient.kind !== RecipientKind.Address - ) { + if (addressValidationType !== AddressValidationType.NONE && !recipientHasAddress(recipient)) { navigate(Screens.ValidateRecipientIntro, { transactionData, addressValidationType, @@ -288,11 +289,7 @@ function SendAmount(props: Props) { const transactionData = getTransactionData(TokenTransactionType.PayRequest) - if ( - addressValidationType !== AddressValidationType.NONE && - recipient.kind !== RecipientKind.QrCode && - recipient.kind !== RecipientKind.Address - ) { + if (addressValidationType !== AddressValidationType.NONE && !recipientHasAddress(recipient)) { navigate(Screens.ValidateRecipientIntro, { transactionData, addressValidationType, diff --git a/packages/mobile/src/send/SendConfirmation.tsx b/packages/mobile/src/send/SendConfirmation.tsx index 7438f92cfcf..6621b5c02bd 100644 --- a/packages/mobile/src/send/SendConfirmation.tsx +++ b/packages/mobile/src/send/SendConfirmation.tsx @@ -52,7 +52,7 @@ import { navigate } from 'src/navigator/NavigationService' import { modalScreenOptions } from 'src/navigator/Navigator' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' -import { getDisplayName, getRecipientThumbnail } from 'src/recipients/recipient' +import { getDisplayName } from 'src/recipients/recipient' import { isAppConnected } from 'src/redux/selectors' import { sendPaymentOrInvite } from 'src/send/actions' import { isSendingSelector } from 'src/send/selectors' @@ -259,8 +259,6 @@ function SendConfirmation(props: Props) { const isInvite = type === TokenTransactionType.InviteSent const inviteFee = getInvitationVerificationFeeInDollars() - const { displayName, e164PhoneNumber } = transactionData.recipient - const subtotalAmount = { value: amount.isGreaterThan(0) ? amount : inviteFee, currencyCode: CURRENCIES[CURRENCY_ENUM.DOLLAR].code, @@ -354,18 +352,12 @@ function SendConfirmation(props: Props) { {isInvite && {t('inviteMoneyEscrow')}} - + {t('sending')} - - {getDisplayName({ recipient, recipientAddress, t })} - + {getDisplayName(recipient, t)} {validatedRecipientAddress && ( @@ -403,8 +395,7 @@ function SendConfirmation(props: Props) { {features.KOMENCI ? ( diff --git a/packages/mobile/src/send/ValidateRecipientAccount.tsx b/packages/mobile/src/send/ValidateRecipientAccount.tsx index 06495fb7781..707cdd7892b 100644 --- a/packages/mobile/src/send/ValidateRecipientAccount.tsx +++ b/packages/mobile/src/send/ValidateRecipientAccount.tsx @@ -28,7 +28,7 @@ import { emptyHeader } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' -import { Recipient } from 'src/recipients/recipient' +import { getDisplayName, Recipient } from 'src/recipients/recipient' import { RootState } from 'src/redux/reducers' import { TransactionDataInput } from 'src/send/SendAmount' @@ -67,7 +67,7 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => { const { route } = ownProps const transactionData = route.params.transactionData const { recipient } = transactionData - const { e164PhoneNumber } = recipient + const e164PhoneNumber = recipient.e164PhoneNumber const error = state.alert ? state.alert.underlyingError : null const validationSuccessful = e164PhoneNumber ? !!state.identity.secureSendPhoneNumberMapping[e164PhoneNumber]?.validationSuccessful @@ -96,7 +96,7 @@ export class ValidateRecipientAccount extends React.Component { } componentDidMount = () => { - const { e164PhoneNumber } = this.props.recipient + const e164PhoneNumber = this.props.recipient.e164PhoneNumber if (e164PhoneNumber) { this.props.validateRecipientAddressReset(e164PhoneNumber) } @@ -174,7 +174,7 @@ export class ValidateRecipientAccount extends React.Component { renderInstructionsAndInputField = () => { const { t, recipient, addressValidationType } = this.props const { inputValue, singleDigitInputValueArr } = this.state - const { displayName } = recipient + const displayName = getDisplayName(recipient, t) if (addressValidationType === AddressValidationType.FULL) { return ( @@ -226,7 +226,7 @@ export class ValidateRecipientAccount extends React.Component { render = () => { const { t, recipient, error } = this.props - const { displayName } = recipient + const displayName = getDisplayName(recipient, t) return ( diff --git a/packages/mobile/src/send/ValidateRecipientIntro.tsx b/packages/mobile/src/send/ValidateRecipientIntro.tsx index 8c221d322b6..352cbf1ebb6 100644 --- a/packages/mobile/src/send/ValidateRecipientIntro.tsx +++ b/packages/mobile/src/send/ValidateRecipientIntro.tsx @@ -14,7 +14,7 @@ import { emptyHeader } from 'src/navigator/Headers' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' -import { getRecipientThumbnail } from 'src/recipients/recipient' +import { getDisplayName } from 'src/recipients/recipient' const AVATAR_SIZE = 64 @@ -64,25 +64,22 @@ class ValidateRecipientIntro extends React.Component { render() { const { t } = this.props const { recipient } = this.props.route.params.transactionData - const { displayName, e164PhoneNumber } = recipient + const displayName = getDisplayName(recipient, t) + const e164PhoneNumber = recipient.e164PhoneNumber return ( - + - {displayName === 'Mobile #' + {!recipient.name ? t('confirmAccount.headerNoDisplayName') : t('confirmAccount.header', { displayName })} - {displayName === 'Mobile #' || !e164PhoneNumber + {!recipient.name ? t('secureSendExplanation.body1NoDisplayName') : t('secureSendExplanation.body1', { e164PhoneNumber, displayName })} diff --git a/packages/mobile/src/send/__snapshots__/Send.test.tsx.snap b/packages/mobile/src/send/__snapshots__/Send.test.tsx.snap index 39c53714841..3c3ef9d78d7 100644 --- a/packages/mobile/src/send/__snapshots__/Send.test.tsx.snap +++ b/packages/mobile/src/send/__snapshots__/Send.test.tsx.snap @@ -161,11 +161,9 @@ exports[`Send renders correctly with invite rewards disabled 1`] = ` Object { "address": "0x0000000000000000000000000000000000007E57", "contactId": "contactId", - "displayId": "14155550000", - "displayName": "John Doe", + "displayNumber": "14155550000", "e164PhoneNumber": "+14155550000", - "kind": "Contact", - "phoneNumberLabel": "phoneNumLabel", + "name": "John Doe", }, ], "key": "recent", @@ -175,16 +173,14 @@ exports[`Send renders correctly with invite rewards disabled 1`] = ` Object { "address": "0x9335BaFcE54cAa0D6690d1D4DA6406568b52488F", "contactId": "contactId", - "displayId": "13105550000", - "displayName": "Jane Doe", + "displayNumber": "13105550000", "e164PhoneNumber": "+13105550000", - "kind": "Contact", - "phoneNumberLabel": "phoneNumLabel", + "name": "Jane Doe", }, Object { "contactId": "contactId4", - "displayName": "Zebra Zone", - "kind": "Contact", + "e164PhoneNumber": "+14163957395", + "name": "Zebra Zone", }, ], "key": "contacts", @@ -354,18 +350,6 @@ exports[`Send renders correctly with invite rewards disabled 1`] = ` > John Doe - - 14155550000 - Jane Doe - - 13105550000 - Zebra Zone - { await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], - [select(e164NumberToAddressSelector), {}], + [select(e164NumberToAddressSelector), mockE164NumberToAddress], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch({ type: Actions.BARCODE_DETECTED, data }) .silentRun() @@ -74,12 +69,9 @@ describe(watchQrCodeDetections, () => { isFromScan: true, recipient: { address: mockAccount.toLowerCase(), - displayName: mockName, - displayId: mockE164Number, + name: mockName, e164PhoneNumber: mockE164Number, - kind: RecipientKind.QrCode, contactId: undefined, - phoneNumberLabel: undefined, thumbnailPath: undefined, }, }) @@ -96,9 +88,8 @@ describe(watchQrCodeDetections, () => { await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), {}], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch({ type: Actions.BARCODE_DETECTED, data }) .silentRun() @@ -107,12 +98,8 @@ describe(watchQrCodeDetections, () => { isFromScan: true, recipient: { address: mockAccount.toLowerCase(), - displayName: 'anonymous', - displayId: mockE164Number, e164PhoneNumber: mockE164Number, - kind: RecipientKind.QrCode, contactId: undefined, - phoneNumberLabel: undefined, thumbnailPath: undefined, }, }) @@ -129,9 +116,8 @@ describe(watchQrCodeDetections, () => { await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), {}], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch({ type: Actions.BARCODE_DETECTED, data }) .silentRun() @@ -140,13 +126,11 @@ describe(watchQrCodeDetections, () => { isFromScan: true, recipient: { address: mockAccount.toLowerCase(), - displayName: mockName, - displayId: undefined, + name: mockName, + displayNumber: undefined, e164PhoneNumber: undefined, contactId: undefined, - phoneNumberLabel: undefined, thumbnailPath: undefined, - kind: RecipientKind.QrCode, }, }) }) @@ -157,9 +141,8 @@ describe(watchQrCodeDetections, () => { await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), {}], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch({ type: Actions.BARCODE_DETECTED, data }) .put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS)) @@ -177,9 +160,8 @@ describe(watchQrCodeDetections, () => { await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), {}], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch({ type: Actions.BARCODE_DETECTED, data }) .put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS)) @@ -197,9 +179,8 @@ describe(watchQrCodeDetections, () => { } await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), mockE164NumberToAddress], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch(qrAction) .put(validateRecipientAddressSuccess(mockE164NumberInvite, mockAccount2Invite.toLowerCase())) @@ -222,9 +203,8 @@ describe(watchQrCodeDetections, () => { } await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), mockE164NumberToAddress], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch(qrAction) .put(validateRecipientAddressSuccess(mockE164NumberInvite, mockAccount2Invite.toLowerCase())) @@ -245,9 +225,8 @@ describe(watchQrCodeDetections, () => { } await expectSaga(watchQrCodeDetections) .provide([ - [select(addressToE164NumberSelector), {}], - [select(recipientCacheSelector), {}], [select(e164NumberToAddressSelector), mockE164NumberToAddress], + [select(recipientInfoSelector), mockRecipientInfo], ]) .dispatch(qrAction) .put(showMessage(ErrorMessages.QR_FAILED_INVALID_RECIPIENT)) diff --git a/packages/mobile/src/send/saga.ts b/packages/mobile/src/send/saga.ts index cb505b626cd..de7bf424a0e 100644 --- a/packages/mobile/src/send/saga.ts +++ b/packages/mobile/src/send/saga.ts @@ -9,13 +9,14 @@ import { ErrorMessages } from 'src/app/ErrorMessages' import { calculateFee, FeeInfo } from 'src/fees/saga' import { transferGoldToken } from 'src/goldToken/actions' import { encryptComment } from 'src/identity/commentEncryption' -import { addressToE164NumberSelector, e164NumberToAddressSelector } from 'src/identity/reducer' +import { e164NumberToAddressSelector } from 'src/identity/reducer' import { InviteBy } from 'src/invite/actions' import { sendInvite } from 'src/invite/saga' import { navigateBack, navigateHome } from 'src/navigator/NavigationService' import { completePaymentRequest } from 'src/paymentRequest/actions' import { handleBarcode, shareSVGImage } from 'src/qrcode/utils' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { recipientHasNumber, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { Actions, HandleBarcodeDetectedAction, @@ -97,8 +98,8 @@ export function* watchQrCodeDetections() { while (true) { const action: HandleBarcodeDetectedAction = yield take(Actions.BARCODE_DETECTED) Logger.debug(TAG, 'Barcode detected in watcher') - const addressToE164Number = yield select(addressToE164NumberSelector) - const recipientCache = yield select(recipientCacheSelector) + const recipientInfo: RecipientInfo = yield select(recipientInfoSelector) + const e164NumberToAddress = yield select(e164NumberToAddressSelector) const isOutgoingPaymentRequest = action.isOutgoingPaymentRequest let secureSendTxData @@ -113,9 +114,8 @@ export function* watchQrCodeDetections() { yield call( handleBarcode, action.data, - addressToE164Number, - recipientCache, e164NumberToAddress, + recipientInfo, secureSendTxData, isOutgoingPaymentRequest, requesterAddress @@ -209,13 +209,9 @@ export function* sendPaymentOrInviteSaga({ try { yield call(getConnectedUnlockedAccount) - if (!recipient?.e164PhoneNumber && !recipient?.address) { - throw new Error("Can't send to recipient without valid e164PhoneNumber or address") - } - if (recipientAddress) { yield call(sendPayment, recipientAddress, amount, comment, CURRENCY_ENUM.DOLLAR, feeInfo) - } else if (recipient.e164PhoneNumber) { + } else if (recipientHasNumber(recipient)) { yield call( sendInvite, recipient.e164PhoneNumber, diff --git a/packages/mobile/src/send/utils.ts b/packages/mobile/src/send/utils.ts index 0b187b53c16..377611a00fe 100644 --- a/packages/mobile/src/send/utils.ts +++ b/packages/mobile/src/send/utils.ts @@ -25,11 +25,12 @@ import { import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { UriData, uriDataFromUrl } from 'src/qrcode/schema' +import { updateValoraRecipientCache } from 'src/recipients/actions' import { + AddressRecipient, Recipient, - RecipientKind, - RecipientWithContact, - RecipientWithQrCode, + recipientHasAddress, + recipientHasNumber, } from 'src/recipients/recipient' import { storeLatestInRecents } from 'src/send/actions' import { PaymentInfo } from 'src/send/reducers' @@ -58,14 +59,14 @@ export const getConfirmationInput = ( const { recipient } = transactionData let recipientAddress: string | null | undefined - if (recipient.kind === RecipientKind.QrCode || recipient.kind === RecipientKind.Address) { + if (recipientHasAddress(recipient)) { recipientAddress = recipient.address - } else if (recipient.e164PhoneNumber) { + } else if (recipientHasNumber(recipient)) { recipientAddress = getAddressFromPhoneNumber( recipient.e164PhoneNumber, e164NumberToAddress, secureSendPhoneNumberMapping, - recipient.address + undefined ) } @@ -192,21 +193,24 @@ export function showLimitReachedError( export function* handleSendPaymentData( data: UriData, - cachedRecipient?: RecipientWithContact, + cachedRecipient?: Recipient, isOutgoingPaymentRequest?: true, isFromScan?: true ) { - const recipient: RecipientWithQrCode = { - kind: RecipientKind.QrCode, + const recipient: AddressRecipient = { address: data.address.toLowerCase(), - displayId: data.e164PhoneNumber, - displayName: data.displayName || cachedRecipient?.displayName || 'anonymous', + name: data.displayName || cachedRecipient?.name, e164PhoneNumber: data.e164PhoneNumber, - phoneNumberLabel: cachedRecipient?.phoneNumberLabel, + displayNumber: cachedRecipient?.displayNumber, thumbnailPath: cachedRecipient?.thumbnailPath, contactId: cachedRecipient?.contactId, } yield put(storeLatestInRecents(recipient)) + yield put( + updateValoraRecipientCache({ + [data.address.toLowerCase()]: recipient, + }) + ) if (data.amount) { if (data.token === 'CELO') { diff --git a/packages/mobile/src/transactions/CeloTransferFeedItem.test.tsx b/packages/mobile/src/transactions/CeloTransferFeedItem.test.tsx index 56df94ba106..633161fee92 100644 --- a/packages/mobile/src/transactions/CeloTransferFeedItem.test.tsx +++ b/packages/mobile/src/transactions/CeloTransferFeedItem.test.tsx @@ -41,6 +41,7 @@ describe('CeloTransferFeedItem', () => { hash={'0x'} amount={{ value: '-1.005', currencyCode: 'cGLD', localAmount }} address={SAMPLE_ADDRESS} + account={''} comment={''} timestamp={1} {...getMockI18nProps()} @@ -71,6 +72,7 @@ describe('CeloTransferFeedItem', () => { hash={'0x'} amount={{ value: '1.005', currencyCode: 'cGLD', localAmount }} address={mockAccount} + account={''} comment={''} timestamp={1} {...getMockI18nProps()} diff --git a/packages/mobile/src/transactions/TransactionFeed.tsx b/packages/mobile/src/transactions/TransactionFeed.tsx index c986854b824..fdbbf4e05b2 100644 --- a/packages/mobile/src/transactions/TransactionFeed.tsx +++ b/packages/mobile/src/transactions/TransactionFeed.tsx @@ -6,7 +6,8 @@ import { FlatList, SectionList, SectionListData } from 'react-native' import { useSelector } from 'react-redux' import { TransactionFeedFragment } from 'src/apollo/types' import { inviteesSelector } from 'src/invite/reducer' -import { recipientCacheSelector } from 'src/recipients/reducer' +import { RecipientInfo } from 'src/recipients/recipient' +import { phoneRecipientCacheSelector, recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' import CeloTransferFeedItem from 'src/transactions/CeloTransferFeedItem' import ExchangeFeedItem from 'src/transactions/ExchangeFeedItem' @@ -40,9 +41,10 @@ interface Props { function TransactionFeed({ kind, loading, error, data }: Props) { const commentKey = useSelector(dataEncryptionKeySelector) const addressToE164Number = useSelector((state: RootState) => state.identity.addressToE164Number) - const recipientCache = useSelector(recipientCacheSelector) + const phoneRecipientCache = useSelector(phoneRecipientCacheSelector) const recentTxRecipientsCache = useSelector(recentTxRecipientsCacheSelector) const invitees = useSelector(inviteesSelector) + const recipientInfo: RecipientInfo = useSelector(recipientInfoSelector) const renderItem = ({ item: tx }: { item: FeedItem; index: number }) => { switch (tx.__typename) { @@ -53,10 +55,11 @@ function TransactionFeed({ kind, loading, error, data }: Props) { return ( ) diff --git a/packages/mobile/src/transactions/TransactionReview.tsx b/packages/mobile/src/transactions/TransactionReview.tsx index 7a068373beb..66f3ca28cf3 100644 --- a/packages/mobile/src/transactions/TransactionReview.tsx +++ b/packages/mobile/src/transactions/TransactionReview.tsx @@ -10,6 +10,8 @@ import { addressToDisplayNameSelector, SecureSendPhoneNumberMapping } from 'src/ import { HeaderTitleWithSubtitle, headerWithBackButton } from 'src/navigator/Headers' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' +import { getRecipientFromAddress, RecipientInfo } from 'src/recipients/recipient' +import { recipientInfoSelector } from 'src/recipients/reducer' import { RootState } from 'src/redux/reducers' import useSelector from 'src/redux/useSelector' import TransferConfirmationCard, { @@ -20,6 +22,7 @@ import { getDatetimeDisplayString } from 'src/utils/time' interface StateProps { addressHasChanged: boolean + recipientInfo: RecipientInfo } export interface ReviewProps { type: TokenTransactionType @@ -59,8 +62,9 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => { const { confirmationProps } = ownProps.route.params const { secureSendPhoneNumberMapping } = state.identity const addressHasChanged = hasAddressChanged(confirmationProps, secureSendPhoneNumberMapping) + const recipientInfo = recipientInfoSelector(state) - return { addressHasChanged } + return { addressHasChanged, recipientInfo } } function isExchange( @@ -69,7 +73,7 @@ function isExchange( return (confirmationProps as ExchangeConfirmationCardProps).makerAmount !== undefined } -function TransactionReview({ navigation, route, addressHasChanged }: Props) { +function TransactionReview({ navigation, route, addressHasChanged, recipientInfo }: Props) { const { reviewProps: { type, timestamp }, confirmationProps, @@ -88,7 +92,11 @@ function TransactionReview({ navigation, route, addressHasChanged }: Props) { }, [type, confirmationProps, addressToDisplayName]) if (isTransferConfirmationCardProps(confirmationProps)) { - const props = { ...confirmationProps, addressHasChanged } + // @ts-ignore, address should never be undefined + const recipient = getRecipientFromAddress(confirmationProps.address, recipientInfo) + Object.assign(recipient, { e164PhoneNumber: confirmationProps.e164PhoneNumber }) + + const props = { ...confirmationProps, addressHasChanged, recipient } return } diff --git a/packages/mobile/src/transactions/TransactionsList.test.tsx b/packages/mobile/src/transactions/TransactionsList.test.tsx index 22b78ec7fa5..6b9b0eb323a 100644 --- a/packages/mobile/src/transactions/TransactionsList.test.tsx +++ b/packages/mobile/src/transactions/TransactionsList.test.tsx @@ -107,6 +107,7 @@ const mockQueryData: UserTransactionsQuery = { }, timestamp: 1578530538, address: '0xce10ce10ce10ce10ce10ce10ce10ce10ce10ce10', + account: '0xsflkj', comment: 'Hi', }, }, diff --git a/packages/mobile/src/transactions/TransactionsList.tsx b/packages/mobile/src/transactions/TransactionsList.tsx index c2a3871e1b6..4eb07a3afe5 100644 --- a/packages/mobile/src/transactions/TransactionsList.tsx +++ b/packages/mobile/src/transactions/TransactionsList.tsx @@ -177,6 +177,8 @@ function mapTransferStandbyToFeedItem( ), comment, address, + // the account address is NOT the same as "address", but the correct info isn't needed for the standby transactions + account: address, } } diff --git a/packages/mobile/src/transactions/TransferAvatars.tsx b/packages/mobile/src/transactions/TransferAvatars.tsx index 1e71e2b83ad..72a8cf2e2cb 100644 --- a/packages/mobile/src/transactions/TransferAvatars.tsx +++ b/packages/mobile/src/transactions/TransferAvatars.tsx @@ -1,29 +1,17 @@ import React from 'react' import { StyleSheet, View } from 'react-native' -import { useSelector } from 'react-redux' import ContactCircle from 'src/components/ContactCircle' import ContactCircleSelf from 'src/components/ContactCircleSelf' import CircleArrowIcon from 'src/icons/CircleArrowIcon' -import { addressToDisplayNameSelector } from 'src/identity/reducer' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { Recipient } from 'src/recipients/recipient' interface Props { type: 'sent' | 'received' - address?: string - recipient?: Recipient + recipient: Recipient } -export default function TransferAvatars({ type, address, recipient }: Props) { - const addressToDisplayName = useSelector(addressToDisplayNameSelector) - const userPicture = addressToDisplayName[address || '']?.imageUrl - - const userAvatar = ( - - ) +export default function TransferAvatars({ type, recipient }: Props) { + const userAvatar = const selfAvatar = diff --git a/packages/mobile/src/transactions/TransferConfirmationCard.test.tsx b/packages/mobile/src/transactions/TransferConfirmationCard.test.tsx index a2197e0ae62..a8ed05e8ddb 100644 --- a/packages/mobile/src/transactions/TransferConfirmationCard.test.tsx +++ b/packages/mobile/src/transactions/TransferConfirmationCard.test.tsx @@ -14,6 +14,7 @@ import { mockContactWithPhone, mockCountryCode, mockE164Number, + mockRecipient, } from 'test/values' const celoRewardSenderAddress = '0x123456' @@ -40,6 +41,7 @@ describe('TransferConfirmationCard', () => { address: mockAccount, comment: '', amount: { value: '-0.3', currencyCode: 'cUSD', localAmount: null }, + recipient: mockRecipient, } const tree = renderer.create( @@ -57,6 +59,7 @@ describe('TransferConfirmationCard', () => { address: mockAccount, comment: '', amount: { value: '100', currencyCode: 'cUSD', localAmount: null }, + recipient: mockRecipient, } const tree = renderer.create( @@ -76,6 +79,7 @@ describe('TransferConfirmationCard', () => { amount: { value: '100', currencyCode: 'cUSD', localAmount: null }, contact: mockContactWithPhone, e164PhoneNumber: mockE164Number, + recipient: mockRecipient, } const tree = renderer.create( @@ -93,6 +97,7 @@ describe('TransferConfirmationCard', () => { address: celoRewardSenderAddress, comment: '', amount: { value: '100', currencyCode: 'cUSD', localAmount: null }, + recipient: mockRecipient, } const tree = render( @@ -114,6 +119,7 @@ describe('TransferConfirmationCard', () => { amount: { value: '100', currencyCode: 'cUSD', localAmount: null }, contact: mockContactWithPhone, e164PhoneNumber: mockE164Number, + recipient: mockRecipient, } const tree = renderer.create( @@ -133,6 +139,7 @@ describe('TransferConfirmationCard', () => { amount: { value: '-100', currencyCode: 'cUSD', localAmount: null }, contact: mockContactWithPhone, e164PhoneNumber: mockE164Number, + recipient: mockRecipient, fee: new BigNumber(0.01), } @@ -152,6 +159,7 @@ describe('TransferConfirmationCard', () => { comment: mockComment, amount: { value: '-100', currencyCode: 'cGLD', localAmount: null }, fee: new BigNumber(0.01), + recipient: mockRecipient, } const tree = renderer.create( @@ -172,6 +180,7 @@ describe('TransferConfirmationCard', () => { contact: mockContactWithPhone, e164PhoneNumber: mockE164Number, fee: new BigNumber(0.01), + recipient: mockRecipient, } const tree = renderer.create( diff --git a/packages/mobile/src/transactions/TransferConfirmationCard.tsx b/packages/mobile/src/transactions/TransferConfirmationCard.tsx index 74f6a32dde4..873f64e7eff 100644 --- a/packages/mobile/src/transactions/TransferConfirmationCard.tsx +++ b/packages/mobile/src/transactions/TransferConfirmationCard.tsx @@ -24,7 +24,7 @@ import { addressToDisplayNameSelector } from 'src/identity/reducer' import { getInvitationVerificationFeeInDollars } from 'src/invite/saga' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { Recipient } from 'src/recipients/recipient' import useTypedSelector from 'src/redux/useSelector' import BottomText from 'src/transactions/BottomText' import CommentSection from 'src/transactions/CommentSection' @@ -44,6 +44,7 @@ export interface TransferConfirmationCardProps { type Props = TransferConfirmationCardProps & { addressHasChanged: boolean + recipient: Recipient } const onPressGoToFaq = () => { @@ -74,13 +75,7 @@ function VerificationContent({ amount }: Props) { ) } -function InviteSentContent({ - address, - addressHasChanged, - recipient, - e164PhoneNumber, - amount, -}: Props) { +function InviteSentContent({ addressHasChanged, recipient, amount }: Props) { const { t } = useTranslation(Namespaces.sendFlow7) const totalAmount = amount const inviteFee = getInvitationVerificationFeeInDollars() @@ -92,17 +87,9 @@ function InviteSentContent({ <> - } + avatar={} /> - } + avatar={} /> @@ -166,14 +139,7 @@ function NetworkFeeContent({ amount }: Props) { ) } -function PaymentSentContent({ - address, - addressHasChanged, - recipient, - e164PhoneNumber, - amount, - comment, -}: Props) { +function PaymentSentContent({ addressHasChanged, recipient, amount, comment }: Props) { const { t } = useTranslation(Namespaces.sendFlow7) const sentAmount = amount // TODO: Use real fee @@ -187,11 +153,9 @@ function PaymentSentContent({ <> } + avatar={} /> @@ -223,10 +187,8 @@ function PaymentReceivedContent({ address, recipient, e164PhoneNumber, amount, c <> } + avatar={} /> {isCeloTx && celoEducationUri && ( @@ -240,7 +202,7 @@ function PaymentReceivedContent({ address, recipient, e164PhoneNumber, amount, c ) } -function CeloRewardContent({ address, amount, recipient }: Props) { +function CeloRewardContent({ amount, recipient }: Props) { const { t } = useTranslation(Namespaces.sendFlow7) const openLearnMore = () => { @@ -254,9 +216,9 @@ function CeloRewardContent({ address, amount, recipient }: Props) { <> } + recipient={recipient} + avatar={} /> {t('learnMore')} diff --git a/packages/mobile/src/transactions/TransferFeedIcon.tsx b/packages/mobile/src/transactions/TransferFeedIcon.tsx index c7e83378f4f..b7c09d4a891 100644 --- a/packages/mobile/src/transactions/TransferFeedIcon.tsx +++ b/packages/mobile/src/transactions/TransferFeedIcon.tsx @@ -4,19 +4,17 @@ import { Image, StyleSheet, View } from 'react-native' import { TokenTransactionType } from 'src/apollo/types' import ContactCircle from 'src/components/ContactCircle' import { transactionNetwork } from 'src/images/Images' -import { getRecipientThumbnail, Recipient } from 'src/recipients/recipient' +import { Recipient } from 'src/recipients/recipient' const AVATAR_SIZE = 40 interface Props { type: TokenTransactionType - recipient?: Recipient - address?: string - imageUrl?: string | null + recipient: Recipient } export default function TransferFeedIcon(props: Props) { - const { recipient, address, type, imageUrl } = props + const { recipient, type } = props switch (type) { case TokenTransactionType.VerificationFee: // fallthrough @@ -38,14 +36,7 @@ export default function TransferFeedIcon(props: Props) { case TokenTransactionType.EscrowSent: case TokenTransactionType.EscrowReceived: default: { - return ( - - ) + return } } } diff --git a/packages/mobile/src/transactions/TransferFeedItem.test.tsx b/packages/mobile/src/transactions/TransferFeedItem.test.tsx index e7c447472de..2094af0dac0 100644 --- a/packages/mobile/src/transactions/TransferFeedItem.test.tsx +++ b/packages/mobile/src/transactions/TransferFeedItem.test.tsx @@ -4,6 +4,7 @@ import { render } from 'react-native-testing-library' import { Provider } from 'react-redux' import * as renderer from 'react-test-renderer' import { TokenTransactionType } from 'src/apollo/types' +import { RecipientInfo } from 'src/recipients/recipient' import { TransferFeedItem } from 'src/transactions/TransferFeedItem' import { TransactionStatus } from 'src/transactions/types' import { createMockStore, getMockI18nProps } from 'test/utils' @@ -15,9 +16,10 @@ import { mockE164Number, mockInviteDetails, mockInviteDetails2, + mockPhoneRecipientCache, mockPrivateDEK, mockPrivateDEK2, - mockRecipientCache, + mockRecipientInfo, } from 'test/values' // Mock encrypted comment from account 1 to account 2. @@ -42,9 +44,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -65,9 +69,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={mockPrivateDEK} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -88,9 +94,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={mockPrivateDEK2} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -111,9 +119,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -134,9 +144,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -157,9 +169,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -180,9 +194,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -203,9 +219,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -226,9 +244,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -258,9 +278,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={mockAddressToE164Number} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[mockStoredInviteDetails]} + account={''} {...getMockI18nProps()} /> @@ -281,9 +303,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -304,9 +328,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -327,9 +353,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={mockAddressToE164Number} - recipientCache={mockRecipientCache} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -350,9 +378,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={{}} - recipientCache={{}} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -373,9 +403,11 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={mockAddressToE164Number} - recipientCache={mockRecipientCache} + phoneRecipientCache={mockPhoneRecipientCache} + recipientInfo={mockRecipientInfo} recentTxRecipientsCache={{}} invitees={[]} + account={''} {...getMockI18nProps()} /> @@ -396,16 +428,18 @@ describe('transfer feed item renders correctly', () => { timestamp={1} commentKey={null} addressToE164Number={mockAddressToE164Number} - recipientCache={{}} - recentTxRecipientsCache={mockRecipientCache} + phoneRecipientCache={mockPhoneRecipientCache} + recentTxRecipientsCache={mockPhoneRecipientCache} invitees={[]} + recipientInfo={mockRecipientInfo} + account={''} {...getMockI18nProps()} /> ) expect(tree).toMatchSnapshot() }) - const renderFeedItemForSendWithoutCaches = (address: string) => ( + const renderFeedItemForSendWithoutCaches = (address: string, recipientInfo: RecipientInfo) => ( { address={address} timestamp={1} commentKey={null} - addressToE164Number={mockAddressToE164Number} - recipientCache={{}} + addressToE164Number={{}} + phoneRecipientCache={{}} + recipientInfo={recipientInfo} recentTxRecipientsCache={{}} + account={''} invitees={[]} /> ) it('for known address display name show stored name on feed item', () => { const contactName = 'Some name' + const recipientInfo = { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number: {}, + addressToDisplayName: { + [mockAccount]: { + name: contactName, + imageUrl: '', + }, + }, + } const tree = render( - - {renderFeedItemForSendWithoutCaches(mockAccount)} + + {renderFeedItemForSendWithoutCaches(mockAccount, recipientInfo)} ) expect(tree.queryByText(contactName)).toBeTruthy() @@ -444,19 +481,20 @@ describe('transfer feed item renders correctly', () => { }) it('for unknown address display name show phone number on feed item', () => { const contactName = 'Some name' + const recipientInfo = { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + addressToE164Number: mockAddressToE164Number, + addressToDisplayName: { + [mockAccount2]: { + name: contactName, + imageUrl: '', + }, + }, + } const tree = render( - - {renderFeedItemForSendWithoutCaches(mockAccount)} + + {renderFeedItemForSendWithoutCaches(mockAccount, recipientInfo)} ) expect(tree.queryByText(contactName)).toBeFalsy() diff --git a/packages/mobile/src/transactions/TransferFeedItem.tsx b/packages/mobile/src/transactions/TransferFeedItem.tsx index ed287eec15b..8056b735dea 100644 --- a/packages/mobile/src/transactions/TransferFeedItem.tsx +++ b/packages/mobile/src/transactions/TransferFeedItem.tsx @@ -9,7 +9,7 @@ import { txHashToFeedInfoSelector } from 'src/fiatExchanges/reducer' import { Namespaces } from 'src/i18n' import { addressToDisplayNameSelector, AddressToE164NumberType } from 'src/identity/reducer' import { InviteDetails } from 'src/invite/actions' -import { getRecipientFromAddress, NumberToRecipient } from 'src/recipients/recipient' +import { getRecipientFromAddress, NumberToRecipient, RecipientInfo } from 'src/recipients/recipient' import { navigateToPaymentTransferReview } from 'src/transactions/actions' import TransactionFeedItem from 'src/transactions/TransactionFeedItem' import TransferFeedIcon from 'src/transactions/TransferFeedIcon' @@ -23,10 +23,11 @@ type Props = TransferItemFragment & { type: TokenTransactionType status: TransactionStatus addressToE164Number: AddressToE164NumberType - recipientCache: NumberToRecipient + phoneRecipientCache: NumberToRecipient recentTxRecipientsCache: NumberToRecipient invitees: InviteDetails[] commentKey: string | null + recipientInfo: RecipientInfo } function navigateToTransactionReview({ @@ -36,16 +37,14 @@ function navigateToTransactionReview({ commentKey, timestamp, amount, - addressToE164Number, - recipientCache, + recipientInfo, }: Props) { // TODO: remove this when verification reward drilldown is supported if (type === TokenTransactionType.VerificationReward) { return } - const recipient = getRecipientFromAddress(address, addressToE164Number, recipientCache) - const e164PhoneNumber = addressToE164Number[address] || undefined + const recipient = getRecipientFromAddress(address, recipientInfo) navigateToPaymentTransferReview(type, timestamp, { address, @@ -53,7 +52,6 @@ function navigateToTransactionReview({ amount, recipient, type, - e164PhoneNumber, // fee TODO: add fee here. }) } @@ -78,27 +76,28 @@ export function TransferFeedItem(props: Props) { commentKey, status, addressToE164Number, - recipientCache, + phoneRecipientCache, recentTxRecipientsCache, invitees, + recipientInfo, } = props const txInfo = txHashToFeedInfo[hash] const { title, info, recipient } = getTransferFeedParams( type, t, - recipientCache, + phoneRecipientCache, recentTxRecipientsCache, - txInfo?.name || addressToDisplayName[address]?.name, address, addressToE164Number, comment, commentKey, timestamp, invitees, - addressToDisplayName[address]?.isCeloRewardSender ?? false + recipientInfo, + addressToDisplayName[address]?.isCeloRewardSender ?? false, + txInfo ) - const imageUrl = (txInfo?.icon || addressToDisplayName[address]?.imageUrl) ?? null return ( - } + icon={} timestamp={timestamp} status={status} onPress={onPress} @@ -133,6 +130,7 @@ TransferFeedItem.fragments = { } timestamp address + account comment } `, diff --git a/packages/mobile/src/transactions/UserSection.tsx b/packages/mobile/src/transactions/UserSection.tsx index f06716b6593..d98bd223079 100644 --- a/packages/mobile/src/transactions/UserSection.tsx +++ b/packages/mobile/src/transactions/UserSection.tsx @@ -2,84 +2,43 @@ import Expandable from '@celo/react-components/components/Expandable' import Touchable from '@celo/react-components/components/Touchable' import colors from '@celo/react-components/styles/colors' import fontStyles from '@celo/react-components/styles/fonts' -import { getAddressChunks } from '@celo/utils/lib/address' import { getDisplayNumberInternational } from '@celo/utils/lib/phoneNumbers' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { LayoutAnimation, StyleSheet, Text, View } from 'react-native' -import { useSelector } from 'react-redux' import AccountNumber from 'src/components/AccountNumber' import { Namespaces } from 'src/i18n' -import { addressToDisplayNameSelector } from 'src/identity/reducer' import { Screens } from 'src/navigator/Screens' -import { Recipient } from 'src/recipients/recipient' - -function getDisplayName( - recipient?: Recipient, - cachedName?: string, - e164Number?: string, - address?: string -) { - if (recipient && recipient.displayName) { - return recipient.displayName - } - if (cachedName) { - return cachedName - } - const number = getDisplayNumber(e164Number, recipient) - if (number) { - return number - } - if (address) { - // TODO: extract this into a reusable getShortAddressDisplay function - const addressChunks = getAddressChunks(address) - return `0x ${addressChunks[0]}…${addressChunks[addressChunks.length - 1]}` - } - - return undefined -} - -function getDisplayNumber(e164Number?: string, recipient?: Recipient) { - const number = e164Number || recipient?.e164PhoneNumber - if (!number) { - return undefined - } - return getDisplayNumberInternational(number) -} +import { getDisplayName, Recipient, recipientHasNumber } from 'src/recipients/recipient' interface Props { type: 'sent' | 'received' | 'withdrawn' - address?: string addressHasChanged?: boolean - e164PhoneNumber?: string - recipient?: Recipient + recipient: Recipient avatar: React.ReactNode expandable?: boolean } export default function UserSection({ type, - address, addressHasChanged = false, recipient, - e164PhoneNumber, avatar, expandable = true, }: Props) { const { t } = useTranslation(Namespaces.sendFlow7) const [expanded, setExpanded] = useState(expandable && addressHasChanged) - const addressToDisplayName = useSelector(addressToDisplayNameSelector) - const userName = addressToDisplayName[address || '']?.name - const toggleExpanded = () => { LayoutAnimation.easeInEaseOut() setExpanded(!expanded) } - const displayName = getDisplayName(recipient, userName, e164PhoneNumber, address) - const displayNumber = getDisplayNumber(e164PhoneNumber, recipient) - const e164Number = displayName !== displayNumber ? displayNumber : undefined + const displayName = getDisplayName(recipient, t) + const displayNumber = recipientHasNumber(recipient) + ? getDisplayNumberInternational(recipient.e164PhoneNumber) + : undefined + const address = recipient.address || '' const sectionLabel = { received: t('receivedFrom'), @@ -94,12 +53,12 @@ export default function UserSection({ {sectionLabel} <> - + {displayName} - {e164Number && ( - - {e164Number} + {displayNumber && ( + + {displayNumber} )} diff --git a/packages/mobile/src/transactions/__snapshots__/TransactionsList.test.tsx.snap b/packages/mobile/src/transactions/__snapshots__/TransactionsList.test.tsx.snap index 63d0af36eff..5bed3db4076 100644 --- a/packages/mobile/src/transactions/__snapshots__/TransactionsList.test.tsx.snap +++ b/packages/mobile/src/transactions/__snapshots__/TransactionsList.test.tsx.snap @@ -8,6 +8,7 @@ exports[`ignores failed standby transactions 1`] = ` "data": Array [ Object { "__typename": "TokenTransfer", + "account": "0xsflkj", "address": "0xce10ce10ce10ce10ce10ce10ce10ce10ce10ce10", "amount": Object { "__typename": "MoneyAmount", @@ -231,7 +232,7 @@ exports[`ignores failed standby transactions 1`] = ` } } > - 0xce10...ce10 + walletFlow5:feedItemAddress - 0xce10...ce10 + walletFlow5:feedItemAddress - 0xce10...ce10 + walletFlow5:feedItemAddress - 0072bv...o23u + walletFlow5:feedItemAddress - 0x 0000…7E57 + John Doe + + + + + +1 415-555-0000 - + + J + - CELO Rewards + John Doe + + + + + +1 415-555-0000 @@ -1034,7 +1094,7 @@ exports[`TransferConfirmationCard renders correctly for received CELO reward 1`] "justifyContent": "center", }, Object { - "backgroundColor": "hsl(291, 53%, 93%)", + "backgroundColor": "hsl(0, 53%, 93%)", "borderRadius": 20, "height": 40, "width": 40, @@ -1051,13 +1111,13 @@ exports[`TransferConfirmationCard renders correctly for received CELO reward 1`] "fontSize": 16, }, Object { - "color": "hsl(291, 67%, 24%)", + "color": "hsl(0, 67%, 24%)", "fontSize": 20, }, ] } > - C + J @@ -1460,6 +1520,31 @@ exports[`TransferConfirmationCard renders correctly for received escrow transact "marginRight": 7, } } + > + John Doe + + + + +1 415-555-0000 @@ -1535,13 +1620,23 @@ exports[`TransferConfirmationCard renders correctly for received escrow transact ] } > - + + J + + John Doe + + + + +1 415-555-0000 @@ -1984,13 +2104,23 @@ exports[`TransferConfirmationCard renders correctly for received transaction dri ] } > - + + J + + John Doe + + + + +1 415-555-0000 @@ -2435,13 +2590,23 @@ exports[`TransferConfirmationCard renders correctly for sent escrow transaction ] } > - + + J + + John Doe + + + + +1 415-555-0000 @@ -3133,13 +3323,23 @@ exports[`TransferConfirmationCard renders correctly for sent transaction drilldo ] } > - + + J + - + + J + @@ -1326,13 +1336,23 @@ exports[`transfer feed item renders correctly for received with encrypted commen ] } > - + + J + @@ -1476,13 +1496,23 @@ exports[`transfer feed item renders correctly for sent 1`] = ` ] } > - + + J + @@ -1512,7 +1542,7 @@ exports[`transfer feed item renders correctly for sent 1`] = ` } } > - 0x0000...7E57 + John Doe - + + J + @@ -1786,7 +1826,7 @@ exports[`transfer feed item renders correctly for sent transaction 1`] = ` } } > - 0x0000...7E57 + John Doe - 0x1Ff4...Bc42 + walletFlow5:feedItemAddress ( } function* refreshRecentTxRecipients() { - const addressToE164Number = yield select(addressToE164NumberSelector) - const recipientCache = yield select(recipientCacheSelector) + const addressToE164Number: AddressToE164NumberType = yield select(addressToE164NumberSelector) + const recipientCache: NumberToRecipient = yield select(phoneRecipientCacheSelector) const knownFeedTransactions: KnownFeedTransactionsType = yield select( knownFeedTransactionsSelector ) @@ -148,21 +152,52 @@ function* refreshRecentTxRecipients() { } const e164PhoneNumber = addressToE164Number[address] - const cachedRecipient = recipientCache[e164PhoneNumber] - // Skip if there is no recipient to cache or we've already cached them - if (!cachedRecipient || recentTxRecipientsCache[e164PhoneNumber]) { - continue + if (e164PhoneNumber) { + const cachedRecipient = recipientCache[e164PhoneNumber] + // Skip if there is no recipient to cache or we've already cached them + if (!cachedRecipient || recentTxRecipientsCache[e164PhoneNumber]) { + continue + } + + recentTxRecipientsCache[e164PhoneNumber] = cachedRecipient + remainingCacheStorage -= 1 } - - recentTxRecipientsCache[e164PhoneNumber] = cachedRecipient - remainingCacheStorage -= 1 } yield put(updateRecentTxRecipientsCache(recentTxRecipientsCache)) } +function* addProfile(transaction: TransferItemFragment) { + const address = transaction.account + const newProfile: AddressToRecipient = {} + if (transaction.type === TokenTransactionType.Received) { + const info = yield call(getProfileInfo, address) + if (info) { + newProfile[address] = { + address, + name: info?.name, + thumbnailPath: info?.thumbnailPath, + } + + yield put(updateValoraRecipientCache(newProfile)) + Logger.info(TAG, `added ${newProfile} to valoraRecipientCache`) + } + } +} + +function* addRecipientProfiles({ transactions }: NewTransactionsInFeedAction) { + yield all( + transactions.map((trans) => { + if (isTransferTransaction(trans)) { + return call(addProfile, trans) + } + }) + ) +} + function* watchNewFeedTransactions() { yield takeEvery(Actions.NEW_TRANSACTIONS_IN_FEED, cleanupStandbyTransactions) + yield takeEvery(Actions.NEW_TRANSACTIONS_IN_FEED, addRecipientProfiles) yield takeLatest(Actions.NEW_TRANSACTIONS_IN_FEED, refreshRecentTxRecipients) } diff --git a/packages/mobile/src/transactions/transferFeedUtils.ts b/packages/mobile/src/transactions/transferFeedUtils.ts index 77381e8cfe9..8df8c377c1e 100644 --- a/packages/mobile/src/transactions/transferFeedUtils.ts +++ b/packages/mobile/src/transactions/transferFeedUtils.ts @@ -6,12 +6,19 @@ import { TransferItemFragment, UserTransactionsQuery, } from 'src/apollo/types' -import { formatShortenedAddress } from 'src/components/ShortenedAddress' import { DEFAULT_TESTNET } from 'src/config' +import { ProviderFeedInfo } from 'src/fiatExchanges/reducer' import { decryptComment } from 'src/identity/commentEncryption' import { AddressToE164NumberType } from 'src/identity/reducer' import { InviteDetails } from 'src/invite/actions' -import { NumberToRecipient } from 'src/recipients/recipient' +import { + getDisplayName, + getRecipientFromAddress, + NumberToRecipient, + Recipient, + recipientHasNumber, + RecipientInfo, +} from 'src/recipients/recipient' import { KnownFeedTransactionsType } from 'src/transactions/reducer' import { isPresent } from 'src/utils/typescript' @@ -51,53 +58,70 @@ function getRecipient( recipientCache: NumberToRecipient, recentTxRecipientsCache: NumberToRecipient, txTimestamp: number, - invitees: InviteDetails[] -) { + invitees: InviteDetails[], + address: string, + recipientInfo: RecipientInfo, + providerInfo: ProviderFeedInfo | undefined +): Recipient { let phoneNumber = e164PhoneNumber + let recipient: Recipient if (type === TokenTransactionType.EscrowSent) { phoneNumber = getEscrowSentRecipientPhoneNumber(invitees, txTimestamp) } - if (!phoneNumber) { - return undefined + if (phoneNumber) { + // Use the recentTxRecipientCache until the full cache is populated + recipient = Object.keys(recipientCache).length + ? recipientCache[phoneNumber] + : recentTxRecipientsCache[phoneNumber] + + if (recipient) { + return recipient + } else { + recipient = { e164PhoneNumber: phoneNumber } + return recipient + } } - // Use the recentTxRecipientCache until the full cache is populated - return Object.keys(recipientCache).length - ? recipientCache[phoneNumber] - : recentTxRecipientsCache[phoneNumber] + recipient = getRecipientFromAddress(address, recipientInfo) + if (providerInfo) { + Object.assign(recipient, { name: providerInfo.name, thumbnailPath: providerInfo.icon }) + } + return recipient } export function getTransferFeedParams( type: TokenTransactionType, t: TFunction, - recipientCache: NumberToRecipient, + phoneRecipientCache: NumberToRecipient, recentTxRecipientsCache: NumberToRecipient, - name: string | undefined, address: string, addressToE164Number: AddressToE164NumberType, rawComment: string | null, commentKey: string | null, timestamp: number, invitees: InviteDetails[], - isCeloRewardSender: boolean + recipientInfo: RecipientInfo, + isCeloRewardSender: boolean, + providerInfo: ProviderFeedInfo | undefined ) { const e164PhoneNumber = addressToE164Number[address] const recipient = getRecipient( type, e164PhoneNumber, - recipientCache, + phoneRecipientCache, recentTxRecipientsCache, timestamp, - invitees + invitees, + address, + recipientInfo, + providerInfo ) - const nameOrNumber = name || recipient?.displayName || e164PhoneNumber - const displayName = - nameOrNumber || - t('feedItemAddress', { - address: formatShortenedAddress(address), - }) + Object.assign(recipient, { address }) + const nameOrNumber = + recipient?.name || (recipientHasNumber(recipient) ? recipient.e164PhoneNumber : e164PhoneNumber) + const displayName = getDisplayName(recipient, t) const comment = getDecryptedTransferFeedComment(rawComment, commentKey, type) let title, info diff --git a/packages/mobile/src/transactions/types.ts b/packages/mobile/src/transactions/types.ts index 7803a4441f8..15d7d71fc21 100644 --- a/packages/mobile/src/transactions/types.ts +++ b/packages/mobile/src/transactions/types.ts @@ -1,3 +1,4 @@ +import { Address } from '@celo/base' import { TokenTransactionType } from 'src/apollo/types' import { CURRENCY_ENUM } from 'src/geth/consts' import { v4 as uuidv4 } from 'uuid' @@ -22,7 +23,7 @@ export interface TransferStandby { comment: string symbol: CURRENCY_ENUM timestamp: number - address: string + address: Address hash?: string } diff --git a/packages/mobile/src/transactions/utils.test.ts b/packages/mobile/src/transactions/utils.test.ts index 4f4d92c8ba7..5e549f0f117 100644 --- a/packages/mobile/src/transactions/utils.test.ts +++ b/packages/mobile/src/transactions/utils.test.ts @@ -16,6 +16,7 @@ const mockFeedItem = (timestamp: number, comment: string): FeedItem => { }, timestamp, address: '0xanything', + account: '', comment, status: TransactionStatus.Complete, } diff --git a/packages/mobile/test/schemas.ts b/packages/mobile/test/schemas.ts index e079bf899b8..136a2a79c69 100644 --- a/packages/mobile/test/schemas.ts +++ b/packages/mobile/test/schemas.ts @@ -489,6 +489,10 @@ export const v7Schema = { loading: false, notifications: {}, }, + recipients: { + phoneRecipientCache: {}, + valoraRecipientCache: {}, + }, fiatExchanges: { lastUsedProvider: null, txHashToProvider: {}, diff --git a/packages/mobile/test/utils.ts b/packages/mobile/test/utils.ts index 3a6ef658353..c82693d2708 100644 --- a/packages/mobile/test/utils.ts +++ b/packages/mobile/test/utils.ts @@ -13,6 +13,8 @@ import { mockContractAddress, mockE164NumberToAddress, mockNavigation, + mockPhoneRecipientCache, + mockValoraRecipientCache, } from 'test/values' // Sleep for a number of ms @@ -93,7 +95,18 @@ export function getMockStoreData(overrides: RecursivePartial = {}): R e164NumberToAddress: mockE164NumberToAddress, }, } - const mockStoreData: any = { ...defaultSchema, ...appConnectedData, ...contactMappingData } + const recipientData = { + recipients: { + phoneRecipientCache: mockPhoneRecipientCache, + valoraRecipientCache: mockValoraRecipientCache, + }, + } + const mockStoreData: any = { + ...defaultSchema, + ...appConnectedData, + ...contactMappingData, + ...recipientData, + } // Apply overrides. Note: only merges one level deep for (const key of Object.keys(overrides)) { diff --git a/packages/mobile/test/values.ts b/packages/mobile/test/values.ts index 232630fd15b..4f6505c246c 100644 --- a/packages/mobile/test/values.ts +++ b/packages/mobile/test/values.ts @@ -14,10 +14,12 @@ import { NotificationTypes } from 'src/notifications/types' import { PaymentRequest, PaymentRequestStatus } from 'src/paymentRequest/types' import { UriData } from 'src/qrcode/schema' import { - RecipientKind, - RecipientWithContact, - RecipientWithMobileNumber, - RecipientWithQrCode, + AddressRecipient, + AddressToRecipient, + ContactRecipient, + MobileRecipient, + NumberToRecipient, + RecipientInfo, } from 'src/recipients/recipient' export const nullAddress = '0x0' @@ -112,22 +114,18 @@ export const mockInviteDetails3 = { inviteLink: 'http://celo.page.link/PARAMS', } -export const mockInvitableRecipient: RecipientWithContact = { - kind: RecipientKind.Contact, - displayName: mockName, - displayId: '14155550000', +export const mockInvitableRecipient: ContactRecipient = { + name: mockName, + displayNumber: '14155550000', e164PhoneNumber: mockE164Number, contactId: 'contactId', - phoneNumberLabel: 'phoneNumLabel', } -export const mockInvitableRecipient2: RecipientWithContact = { - kind: RecipientKind.Contact, - displayName: mockNameInvite, - displayId: mockDisplayNumberInvite, +export const mockInvitableRecipient2: ContactRecipient = { + name: mockNameInvite, + displayNumber: mockDisplayNumberInvite, e164PhoneNumber: mockE164NumberInvite, contactId: 'contactId', - phoneNumberLabel: 'phoneNumLabel', } export const mockTransactionData = { @@ -142,34 +140,32 @@ export const mockInviteTransactionData = { type: TokenTransactionType.InviteSent, } -export const mockInvitableRecipient3: RecipientWithContact = { - kind: RecipientKind.Contact, - displayName: mockName2Invite, - displayId: mockDisplayNumber2Invite, +export const mockInvitableRecipient3: ContactRecipient = { + name: mockName2Invite, + displayNumber: mockDisplayNumber2Invite, e164PhoneNumber: mockE164Number2Invite, contactId: 'contactId', - phoneNumberLabel: 'phoneNumLabel', } -export const mockRecipient: RecipientWithContact = { +export const mockRecipient: ContactRecipient & AddressRecipient = { ...mockInvitableRecipient, address: mockAccount, } -export const mockRecipient2: RecipientWithContact = { +export const mockRecipient2: ContactRecipient & AddressRecipient = { ...mockInvitableRecipient2, address: mockAccountInvite, } -export const mockRecipient3: RecipientWithContact = { +export const mockRecipient3: ContactRecipient & AddressRecipient = { ...mockInvitableRecipient3, address: mockAccount2Invite, } -export const mockRecipient4: RecipientWithContact = { - kind: RecipientKind.Contact, - displayName: 'Zebra Zone', +export const mockRecipient4: ContactRecipient = { + name: 'Zebra Zone', contactId: 'contactId4', + e164PhoneNumber: '+14163957395', } export const mockE164NumberToInvitableRecipient = { @@ -178,17 +174,22 @@ export const mockE164NumberToInvitableRecipient = { [mockE164Number2Invite]: mockInvitableRecipient3, } -export const mockRecipientCache = { +export const mockPhoneRecipientCache: NumberToRecipient = { [mockE164Number]: mockRecipient, [mockE164NumberInvite]: mockInvitableRecipient2, [mockE164Number2Invite]: mockInvitableRecipient3, } -export const mockRecipientWithPhoneNumber: RecipientWithMobileNumber = { - kind: RecipientKind.MobileNumber, +export const mockValoraRecipientCache: AddressToRecipient = { + [mockAccount]: mockRecipient, + [mockAccountInvite]: mockRecipient2, + [mockAccount2Invite]: mockRecipient3, +} + +export const mockRecipientWithPhoneNumber: MobileRecipient = { address: mockAccount, - displayName: mockName, - displayId: '14155550000', + name: mockName, + displayNumber: '14155550000', e164PhoneNumber: mockE164Number, } @@ -358,17 +359,22 @@ export const mockUriData: UriData[] = [ }, ] -export const mockQRCodeRecipient: RecipientWithQrCode = { - kind: RecipientKind.QrCode, +export const mockQRCodeRecipient: AddressRecipient = { address: mockUriData[3].address.toLowerCase(), - displayId: mockUriData[3].e164PhoneNumber, - displayName: mockUriData[3].displayName || 'anonymous', + displayNumber: mockUriData[3].e164PhoneNumber, + name: mockUriData[3].displayName, e164PhoneNumber: mockUriData[3].e164PhoneNumber, - phoneNumberLabel: undefined, thumbnailPath: undefined, contactId: undefined, } +export const mockRecipientInfo: RecipientInfo = { + phoneRecipientCache: mockPhoneRecipientCache, + valoraRecipientCache: mockValoraRecipientCache, + addressToE164Number: mockAddressToE164Number, + addressToDisplayName: {}, +} + export const mockWallet: UnlockableWallet = { unlockAccount: jest.fn(), isAccountUnlocked: jest.fn(),