From fc2aa2160b568964e198bcacfdf728e9eeccf0cc Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Fri, 3 Nov 2023 00:37:41 -0700 Subject: [PATCH 1/4] feat(recipient): add new recipient picker --- src/components/ContactCircle.tsx | 26 ++++-- src/icons/QuestionIcon.tsx | 39 +++------ src/recipients/RecipientItem.tsx | 3 + src/recipients/RecipientItemV2.test.tsx | 60 +++++++++++++ src/recipients/RecipientItemV2.tsx | 111 ++++++++++++++++++++++++ test/values.ts | 4 +- 6 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 src/recipients/RecipientItemV2.test.tsx create mode 100644 src/recipients/RecipientItemV2.tsx diff --git a/src/components/ContactCircle.tsx b/src/components/ContactCircle.tsx index d2d22774b30..6c6a4dbbd2c 100644 --- a/src/components/ContactCircle.tsx +++ b/src/components/ContactCircle.tsx @@ -2,12 +2,17 @@ import * as React from 'react' import { Image, StyleSheet, Text, View, ViewStyle } from 'react-native' import DefaultAvatar from 'src/icons/DefaultAvatar' import { Recipient } from 'src/recipients/recipient' +import Colors from 'src/styles/colors' import fontStyles from 'src/styles/fonts' interface Props { style?: ViewStyle size?: number recipient: Recipient + backgroundColor?: Colors + foregroundColor?: Colors + borderColor?: Colors + DefaultIcon?: React.ComponentType<{ foregroundColor: string; backgroundColor: string }> } const DEFAULT_ICON_SIZE = 40 @@ -18,10 +23,17 @@ const getAddressForegroundColor = (address: string) => `hsl(${parseInt(address.substring(0, 5), 16) % 360}, 67%, 24%)` const getNameInitial = (name: string) => name.charAt(0).toLocaleUpperCase() -function ContactCircle({ size, recipient, style }: Props) { +function ContactCircle({ + size: iconSize = DEFAULT_ICON_SIZE, + recipient, + style, + backgroundColor, + foregroundColor, + borderColor, + DefaultIcon = DefaultAvatar, +}: Props) { const address = recipient.address - const iconSize = size || DEFAULT_ICON_SIZE - const iconBackgroundColor = getAddressBackgroundColor(address || '0x0') + const iconBackgroundColor = backgroundColor ?? getAddressBackgroundColor(address || '0x0') const renderThumbnail = () => { if (recipient.thumbnailPath) { @@ -37,7 +49,7 @@ function ContactCircle({ size, recipient, style }: Props) { ) } - const fontColor = getAddressForegroundColor(address || '0x0') + const fontColor = foregroundColor ?? getAddressForegroundColor(address || '0x0') if (recipient.name) { const initial = getNameInitial(recipient.name) return ( @@ -50,7 +62,7 @@ function ContactCircle({ size, recipient, style }: Props) { ) } - return + return } return ( @@ -64,6 +76,10 @@ function ContactCircle({ size, recipient, style }: Props) { width: iconSize, borderRadius: iconSize / 2, }, + borderColor && { + borderColor, + borderWidth: 1, + }, ]} > {renderThumbnail()} diff --git a/src/icons/QuestionIcon.tsx b/src/icons/QuestionIcon.tsx index eed2f265f5b..5854f86ff54 100644 --- a/src/icons/QuestionIcon.tsx +++ b/src/icons/QuestionIcon.tsx @@ -1,34 +1,17 @@ import * as React from 'react' import colors from 'src/styles/colors' -import Svg, { Circle, Path } from 'svgs' +import Svg, { Path } from 'svgs' interface Props { - size?: number - color?: string + color?: colors } +const QuestionIcon = ({ color = colors.dark }: Props) => ( + + + +) -export default class QuestionIcon extends React.PureComponent { - static defaultProps = { - size: 32, - color: colors.gray5, - } - - render() { - return ( - - - - - - ) - } -} +export default QuestionIcon diff --git a/src/recipients/RecipientItem.tsx b/src/recipients/RecipientItem.tsx index 0fd56ea47f4..78920399be9 100644 --- a/src/recipients/RecipientItem.tsx +++ b/src/recipients/RecipientItem.tsx @@ -16,6 +16,9 @@ interface Props { loading: boolean } +/** + * @deprecated new recipient screen should use RecipientItemV2 + */ function RecipientItem({ recipient, onSelectRecipient, loading }: Props) { const { t } = useTranslation() diff --git a/src/recipients/RecipientItemV2.test.tsx b/src/recipients/RecipientItemV2.test.tsx new file mode 100644 index 00000000000..7c95dcf65f1 --- /dev/null +++ b/src/recipients/RecipientItemV2.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render } from '@testing-library/react-native' +import * as React from 'react' +import 'react-native' +import RecipientItem from 'src/recipients/RecipientItemV2' +import { mockRecipient } from 'test/values' + +describe('RecipientItemV2', () => { + it('renders correctly with no icon or loading state', () => { + const { queryByTestId, getByText } = render( + + ) + expect(getByText(mockRecipient.name)).toBeTruthy() + expect(getByText(mockRecipient.displayNumber)).toBeTruthy() + expect(queryByTestId('RecipientItem/ValoraIcon')).toBeFalsy() + expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy() + }) + + it('renders correctly with icon and no loading state', () => { + const { queryByTestId, getByText, getByTestId } = render( + + ) + expect(getByText(mockRecipient.name)).toBeTruthy() + expect(getByText(mockRecipient.displayNumber)).toBeTruthy() + expect(getByTestId('RecipientItem/ValoraIcon')).toBeTruthy() + expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy() + }) + + it('renders correctly with no icon and loading state', () => { + const { queryByTestId, getByText, getByTestId } = render( + + ) + expect(getByText(mockRecipient.name)).toBeTruthy() + expect(getByText(mockRecipient.displayNumber)).toBeTruthy() + expect(getByTestId('RecipientItem/ActivityIndicator')).toBeTruthy() + expect(queryByTestId('RecipientItem/ValoraIcon')).toBeFalsy() + }) + + it('tapping item invokes onSelectRecipient', () => { + const mockSelectRecipient = jest.fn() + const { getByTestId } = render( + + ) + fireEvent.press(getByTestId('RecipientItem')) + expect(mockSelectRecipient).toHaveBeenLastCalledWith(mockRecipient) + }) +}) diff --git a/src/recipients/RecipientItemV2.tsx b/src/recipients/RecipientItemV2.tsx new file mode 100644 index 00000000000..4d4f1e28d8a --- /dev/null +++ b/src/recipients/RecipientItemV2.tsx @@ -0,0 +1,111 @@ +import React, { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' +import ContactCircle from 'src/components/ContactCircle' +import Touchable from 'src/components/Touchable' +import Logo, { LogoTypes } from 'src/icons/Logo' +import QuestionIcon from 'src/icons/QuestionIcon' +import { Recipient, getDisplayDetail, getDisplayName } from 'src/recipients/recipient' +import colors, { Colors } from 'src/styles/colors' +import { typeScale } from 'src/styles/fonts' +import { Spacing } from 'src/styles/styles' + +interface Props { + recipient: Recipient + onSelectRecipient(recipient: Recipient): void + loading: boolean + showValoraIcon?: boolean + selected?: boolean +} + +const ICON_SIZE = 10 + +function RecipientItem({ recipient, onSelectRecipient, loading, showValoraIcon, selected }: Props) { + const { t } = useTranslation() + + const onPress = () => { + onSelectRecipient(recipient) + } + + return ( + + + + } // no need to honor color props here since the color we need match the defaults + /> + {showValoraIcon && ( + + )} + + + + {getDisplayName(recipient, t)} + + {recipient.name && {getDisplayDetail(recipient)}} + + {loading && ( + + + + )} + + + ) +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + paddingVertical: Spacing.Regular16, + paddingHorizontal: Spacing.Thick24, + alignItems: 'center', + }, + rowSelected: { + backgroundColor: colors.gray1, + }, + avatar: { + marginRight: Spacing.Small12, + }, + contentContainer: { + flex: 1, + }, + name: { ...typeScale.labelMedium, color: colors.dark }, + phone: { + ...typeScale.bodySmall, + color: colors.gray4, + }, + rightIconContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + valoraIcon: { + position: 'absolute', + top: 22, + left: 22, + backgroundColor: '#42D689', + padding: 4, + borderRadius: 100, + // To override the default shadow props on the logo + shadowColor: undefined, + shadowOpacity: undefined, + shadowRadius: undefined, + shadowOffset: undefined, + }, +}) + +export default memo(RecipientItem) diff --git a/test/values.ts b/test/values.ts index b84dfcab84b..70eb5a68917 100644 --- a/test/values.ts +++ b/test/values.ts @@ -164,7 +164,7 @@ export const mockInviteDetails3 = { inviteLink: 'http://celo.page.link/PARAMS', } -export const mockInvitableRecipient: ContactRecipient = { +export const mockInvitableRecipient: ContactRecipient & { displayNumber: string } = { name: mockName, displayNumber: '14155550000', e164PhoneNumber: mockE164Number, @@ -222,7 +222,7 @@ export const mockTokenInviteTransactionData: TransactionDataInput = { tokenAmount: new BigNumber(1), } -export const mockRecipient: ContactRecipient & AddressRecipient = { +export const mockRecipient: ContactRecipient & AddressRecipient & { displayNumber: string } = { ...mockInvitableRecipient, address: mockAccount, recipientType: RecipientType.Address, From 25b91fb9d3efc42c018b14edba8fe09e8a7bed2f Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 6 Nov 2023 11:05:34 -0800 Subject: [PATCH 2/4] fix build --- branding/celo/src/icons/Logo.tsx | 2 ++ src/account/__snapshots__/Profile.test.tsx.snap | 1 + src/components/__snapshots__/Avatar.test.tsx.snap | 4 ++++ src/components/__snapshots__/ContactCircle.test.tsx.snap | 1 + .../__snapshots__/EscrowedPaymentListItem.test.tsx.snap | 1 + .../__snapshots__/EscrowedPaymentListScreen.test.tsx.snap | 3 +++ .../EscrowedPaymentReminderSummaryNotification.test.tsx.snap | 1 + .../ReclaimPaymentConfirmationCard.test.tsx.snap | 1 + .../registration/__snapshots__/PictureInput.test.tsx.snap | 1 + src/qrcode/__snapshots__/QRCode.test.tsx.snap | 2 ++ src/recipients/__snapshots__/RecipientItem.test.tsx.snap | 1 + src/send/__snapshots__/SendConfirmation.test.tsx.snap | 1 + src/send/__snapshots__/ValidateRecipientIntro.test.tsx.snap | 1 + .../feed/__snapshots__/TransactionFeed.test.tsx.snap | 1 + 14 files changed, 21 insertions(+) diff --git a/branding/celo/src/icons/Logo.tsx b/branding/celo/src/icons/Logo.tsx index 0b5217c1a54..84aa9a74029 100644 --- a/branding/celo/src/icons/Logo.tsx +++ b/branding/celo/src/icons/Logo.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { ViewStyle } from 'react-native' import colors from 'src/styles/colors' import Svg, { Path } from 'svgs' @@ -12,6 +13,7 @@ export enum LogoTypes { interface Props { height?: number type?: LogoTypes + style?: ViewStyle testID?: string } diff --git a/src/account/__snapshots__/Profile.test.tsx.snap b/src/account/__snapshots__/Profile.test.tsx.snap index f826f9f007e..53a94f3d2ca 100644 --- a/src/account/__snapshots__/Profile.test.tsx.snap +++ b/src/account/__snapshots__/Profile.test.tsx.snap @@ -106,6 +106,7 @@ exports[`Profile renders correctly 1`] = ` "height": 64, "width": 64, }, + undefined, ] } > diff --git a/src/components/__snapshots__/Avatar.test.tsx.snap b/src/components/__snapshots__/Avatar.test.tsx.snap index 2fe070b0413..4fff51a2618 100644 --- a/src/components/__snapshots__/Avatar.test.tsx.snap +++ b/src/components/__snapshots__/Avatar.test.tsx.snap @@ -36,6 +36,7 @@ exports[`Avatar renders correctly with address and name but no number 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > @@ -140,6 +141,7 @@ exports[`Avatar renders correctly with address but no name nor number 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > @@ -222,6 +224,7 @@ exports[`Avatar renders correctly with just number 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > @@ -343,6 +346,7 @@ exports[`Avatar renders correctly with number and name 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/components/__snapshots__/ContactCircle.test.tsx.snap b/src/components/__snapshots__/ContactCircle.test.tsx.snap index a61e0a69659..ae5f47fedd2 100644 --- a/src/components/__snapshots__/ContactCircle.test.tsx.snap +++ b/src/components/__snapshots__/ContactCircle.test.tsx.snap @@ -26,6 +26,7 @@ exports[`ContactCircle when given recipient with only address uses DefaultAvatar "height": 30, "width": 30, }, + undefined, ] } > diff --git a/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap b/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap index 898dacf2280..4acd71ddec7 100644 --- a/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap +++ b/src/escrow/__snapshots__/EscrowedPaymentListItem.test.tsx.snap @@ -116,6 +116,7 @@ exports[`EscrowedPaymentReminderNotification renders correctly 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap b/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap index dd5d95f484f..7b3f5b79f73 100644 --- a/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap +++ b/src/escrow/__snapshots__/EscrowedPaymentListScreen.test.tsx.snap @@ -186,6 +186,7 @@ exports[`EscrowedPaymentListScreen renders correctly with payments 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > @@ -464,6 +465,7 @@ exports[`EscrowedPaymentListScreen renders correctly with payments 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > @@ -742,6 +744,7 @@ exports[`EscrowedPaymentListScreen renders correctly with payments 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap b/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap index 527af4a40b2..43acb0d73ca 100644 --- a/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap +++ b/src/escrow/__snapshots__/EscrowedPaymentReminderSummaryNotification.test.tsx.snap @@ -285,6 +285,7 @@ exports[`EscrowedPaymentReminderSummaryNotification when more 1 requests renders "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap b/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap index 298d0f21fea..2cdaef70907 100644 --- a/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap +++ b/src/escrow/__snapshots__/ReclaimPaymentConfirmationCard.test.tsx.snap @@ -44,6 +44,7 @@ exports[`ReclaimPaymentConfirmationCard renders correctly for send payment confi "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/onboarding/registration/__snapshots__/PictureInput.test.tsx.snap b/src/onboarding/registration/__snapshots__/PictureInput.test.tsx.snap index ae950abd694..69f9de9a4fc 100644 --- a/src/onboarding/registration/__snapshots__/PictureInput.test.tsx.snap +++ b/src/onboarding/registration/__snapshots__/PictureInput.test.tsx.snap @@ -75,6 +75,7 @@ exports[`PictureInputScreen renders correctly 1`] = ` "height": 64, "width": 64, }, + undefined, ] } > diff --git a/src/qrcode/__snapshots__/QRCode.test.tsx.snap b/src/qrcode/__snapshots__/QRCode.test.tsx.snap index 1a4d800f637..4baf1407c83 100644 --- a/src/qrcode/__snapshots__/QRCode.test.tsx.snap +++ b/src/qrcode/__snapshots__/QRCode.test.tsx.snap @@ -54,6 +54,7 @@ exports[`QRCode renders correctly 1`] = ` "height": 64, "width": 64, }, + undefined, ] } > @@ -219,6 +220,7 @@ exports[`QRCode renders correctly 2`] = ` "height": 64, "width": 64, }, + undefined, ] } > diff --git a/src/recipients/__snapshots__/RecipientItem.test.tsx.snap b/src/recipients/__snapshots__/RecipientItem.test.tsx.snap index d5b83bbb113..c2114ba4ca6 100644 --- a/src/recipients/__snapshots__/RecipientItem.test.tsx.snap +++ b/src/recipients/__snapshots__/RecipientItem.test.tsx.snap @@ -74,6 +74,7 @@ exports[`RecipientItem renders correctly 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/send/__snapshots__/SendConfirmation.test.tsx.snap b/src/send/__snapshots__/SendConfirmation.test.tsx.snap index 70cb5abc98e..ffcb419916b 100644 --- a/src/send/__snapshots__/SendConfirmation.test.tsx.snap +++ b/src/send/__snapshots__/SendConfirmation.test.tsx.snap @@ -191,6 +191,7 @@ exports[`SendConfirmation renders correctly 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > diff --git a/src/send/__snapshots__/ValidateRecipientIntro.test.tsx.snap b/src/send/__snapshots__/ValidateRecipientIntro.test.tsx.snap index db0e8937c6f..b0c351acc9a 100644 --- a/src/send/__snapshots__/ValidateRecipientIntro.test.tsx.snap +++ b/src/send/__snapshots__/ValidateRecipientIntro.test.tsx.snap @@ -61,6 +61,7 @@ exports[`ValidateRecipientIntro renders correctly 1`] = ` "height": 64, "width": 64, }, + undefined, ] } > diff --git a/src/transactions/feed/__snapshots__/TransactionFeed.test.tsx.snap b/src/transactions/feed/__snapshots__/TransactionFeed.test.tsx.snap index 260e08c8032..cd9ad817bc0 100644 --- a/src/transactions/feed/__snapshots__/TransactionFeed.test.tsx.snap +++ b/src/transactions/feed/__snapshots__/TransactionFeed.test.tsx.snap @@ -156,6 +156,7 @@ exports[`TransactionFeed renders correctly when there is a response 1`] = ` "height": 40, "width": 40, }, + undefined, ] } > From e0fb74e63fac74ac7c8f0120b221e2c0d8a4e187 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 6 Nov 2023 12:14:52 -0800 Subject: [PATCH 3/4] use style prop in celo logo --- branding/celo/src/icons/Logo.tsx | 34 ++++++++-------- src/recipients/RecipientPicker.tsx | 15 ++++++- .../__snapshots__/RecipientItem.test.tsx.snap | 40 ++++++++++--------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/branding/celo/src/icons/Logo.tsx b/branding/celo/src/icons/Logo.tsx index 84aa9a74029..c9f22c34b52 100644 --- a/branding/celo/src/icons/Logo.tsx +++ b/branding/celo/src/icons/Logo.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { ViewStyle } from 'react-native' +import { View, ViewStyle } from 'react-native' import colors from 'src/styles/colors' import Svg, { Path } from 'svgs' @@ -17,7 +17,7 @@ interface Props { testID?: string } -export default function Logo({ height = 25, type = LogoTypes.COLOR, testID }: Props) { +export default function Logo({ height = 25, type = LogoTypes.COLOR, testID, style }: Props) { let mainColor switch (type) { case LogoTypes.DARK: @@ -31,19 +31,21 @@ export default function Logo({ height = 25, type = LogoTypes.COLOR, testID }: Pr } return ( - - - - - + + + + + + + ) } diff --git a/src/recipients/RecipientPicker.tsx b/src/recipients/RecipientPicker.tsx index 062dc8e7632..77ffe538e09 100644 --- a/src/recipients/RecipientPicker.tsx +++ b/src/recipients/RecipientPicker.tsx @@ -22,6 +22,7 @@ import KeyboardSpacer from 'src/components/KeyboardSpacer' import SectionHead from 'src/components/SectionHead' import { RecipientVerificationStatus } from 'src/identity/types' import RecipientItem from 'src/recipients/RecipientItem' +import RecipientItemV2 from 'src/recipients/RecipientItemV2' import { MobileRecipient, Recipient, @@ -116,7 +117,19 @@ function RecipientPicker(props: RecipientProps) { ) } - const renderItem = ({ item }: SectionListRenderItemInfo) => { + const renderItem = ({ item, index }: SectionListRenderItemInfo) => { + if (index >= 0) { + return ( + + ) + } + return ( - - - - + - + viewBox="0 0 25 25" + width={25} + > + + + + + From c8e60a586c6cbb9eb52ff3aa1bd093c57f5208b7 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 6 Nov 2023 14:17:32 -0800 Subject: [PATCH 4/4] revert testing changes --- src/recipients/RecipientPicker.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/recipients/RecipientPicker.tsx b/src/recipients/RecipientPicker.tsx index 77ffe538e09..062dc8e7632 100644 --- a/src/recipients/RecipientPicker.tsx +++ b/src/recipients/RecipientPicker.tsx @@ -22,7 +22,6 @@ import KeyboardSpacer from 'src/components/KeyboardSpacer' import SectionHead from 'src/components/SectionHead' import { RecipientVerificationStatus } from 'src/identity/types' import RecipientItem from 'src/recipients/RecipientItem' -import RecipientItemV2 from 'src/recipients/RecipientItemV2' import { MobileRecipient, Recipient, @@ -117,19 +116,7 @@ function RecipientPicker(props: RecipientProps) { ) } - const renderItem = ({ item, index }: SectionListRenderItemInfo) => { - if (index >= 0) { - return ( - - ) - } - + const renderItem = ({ item }: SectionListRenderItemInfo) => { return (