From 183e74e876620e9374ac6da4d44f5238256e2308 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 29 May 2024 21:00:48 +0200 Subject: [PATCH 01/14] feat: implement card section for subscription --- assets/images/credit-card-exclamation.svg | 14 ++++ src/components/Icon/Expensicons.ts | 2 + src/languages/en.ts | 15 +++++ src/languages/es.ts | 15 +++++ .../Subscription/CardSection/CardSection.tsx | 64 +++++++++++++++++++ .../CardSectionActions/index.native.tsx | 5 ++ .../CardSection/CardSectionActions/index.tsx | 56 ++++++++++++++++ .../CardSectionDataEmpty/index.native.tsx | 30 +++++++++ .../CardSectionDataEmpty/index.tsx | 18 ++++++ .../index.native.tsx | 12 ++++ .../ReducedFunctionalityMessage/index.tsx | 5 ++ .../Subscription/SubscriptionSettingsPage.tsx | 12 ++++ src/styles/index.ts | 26 ++++++++ 13 files changed, 274 insertions(+) create mode 100644 assets/images/credit-card-exclamation.svg create mode 100644 src/pages/settings/Subscription/CardSection/CardSection.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionActions/index.native.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.native.tsx create mode 100644 src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx create mode 100644 src/pages/settings/Subscription/ReducedFunctionalityMessage/index.native.tsx create mode 100644 src/pages/settings/Subscription/ReducedFunctionalityMessage/index.tsx diff --git a/assets/images/credit-card-exclamation.svg b/assets/images/credit-card-exclamation.svg new file mode 100644 index 000000000000..67e686516baa --- /dev/null +++ b/assets/images/credit-card-exclamation.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c127fbda1bd5..d3ed683eeddf 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -45,6 +45,7 @@ import Concierge from '@assets/images/concierge.svg'; import Connect from '@assets/images/connect.svg'; import ConnectionComplete from '@assets/images/connection-complete.svg'; import Copy from '@assets/images/copy.svg'; +import CreditCardExclamation from '@assets/images/credit-card-exclamation.svg'; import CreditCardHourglass from '@assets/images/credit-card-hourglass.svg'; import CreditCard from '@assets/images/creditcard.svg'; import DocumentPlus from '@assets/images/document-plus.svg'; @@ -212,6 +213,7 @@ export { Copy, CreditCard, CreditCardHourglass, + CreditCardExclamation, DeletedRoomAvatar, Document, DocumentSlash, diff --git a/src/languages/en.ts b/src/languages/en.ts index 0fbfa88b7fae..f3fea79d22e1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3113,4 +3113,19 @@ export default { systemMessage: { mergedWithCashTransaction: 'matched a receipt to this transaction.', }, + subscription: { + mobileReducedFunctionalityMessage: 'You can’t make changes to your subscription in the mobile app.', + cardSection: { + title: 'Payment', + subtitle: 'Add a payment card to pay for your Expensify subscription.', + addCardButton: 'Add payment card', + cardNextPayment: 'Your next payment date is', + cardEnding: ({cardNumber}) => `Card ending in ${cardNumber}`, + cardInfo: ({name, expiration, currency}) => `Name: ${name}, Expiration: ${expiration}, Currency: ${currency}`, + changeCard: 'Change payment card', + changeCurrency: 'Change payment currency', + cardNotFound: 'No payment card added', + retryPaymentButton: 'Retry payment', + }, + }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index d6d393213bc7..bace59cc66e7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3617,4 +3617,19 @@ export default { systemMessage: { mergedWithCashTransaction: 'encontró un recibo para esta transacción.', }, + subscription: { + mobileReducedFunctionalityMessage: 'No puedes hacer cambios en tu suscripción en la aplicación móvil.', + cardSection: { + title: 'Pago', + subtitle: 'Añade una tarjeta de pago para abonar tu suscripción a Expensify', + addCardButton: 'Añade tarjeta de pago', + cardNextPayment: 'Your next payment date is', + cardEnding: ({cardNumber}) => `Tarjeta terminada en ${cardNumber}`, + cardInfo: ({name, expiration, currency}) => `Nombre: ${name}, Expiración: ${expiration}, Moneda: ${currency}`, + changeCard: 'Cambiar tarjeta de pago', + changeCurrency: 'Cambiar moneda de pago', + cardNotFound: 'No se ha añadido ninguna tarjeta de pago', + retryPaymentButton: 'Reintentar el pago', + }, + }, } satisfies EnglishTranslation; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx new file mode 100644 index 000000000000..6a5b72bcc172 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import CardSectionActions from './CardSectionActions'; +import CardSectionDataEmpty from './CardSectionDataEmpty'; + +function CardSection() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + + const defaultCard = Object.values(fundList ?? {}).find((card) => card.isDefault); + + return ( +
+ + + {!isEmptyObject(defaultCard?.accountData) && ( + <> + + + + {translate('subscription.cardSection.cardEnding', {cardNumber: defaultCard?.accountData?.cardNumber})} + + {translate('subscription.cardSection.cardInfo', { + name: defaultCard?.accountData?.addressName, + expiration: `${defaultCard?.accountData?.cardMonth}/${defaultCard?.accountData?.cardYear}`, + currency: defaultCard?.accountData?.currency, + })} + + + + + + )} + {isEmptyObject(defaultCard?.accountData) && } + + +
+ ); +} + +export default CardSection; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.native.tsx b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.native.tsx new file mode 100644 index 000000000000..c934a8f84aa8 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.native.tsx @@ -0,0 +1,5 @@ +function CardSectionActions() { + return null; +} + +export default CardSectionActions; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx new file mode 100644 index 000000000000..b21071615690 --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx @@ -0,0 +1,56 @@ +import React, {useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; +import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; +import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {AnchorPosition} from '@styles/index'; +import CONST from '@src/CONST'; + +function CardSectionActions() { + const {isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0}); + const threeDotsMenuContainerRef = useRef(null); + + const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo( + () => [ + { + icon: Expensicons.CreditCard, + text: translate('subscription.cardSection.changeCard'), + onSelected: () => {}, // TODO: update with navigation to add card screen + }, + { + icon: Expensicons.MoneyCircle, + text: translate('subscription.cardSection.changeCurrency'), + onSelected: () => {}, // TODO: update with navigation to change currency screen + }, + ], + [translate], + ); + + return ( + + { + if (isSmallScreenWidth) { + return; + } + threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => { + setThreeDotsMenuPosition({ + horizontal: x + width, + vertical: y + height, + }); + }); + }} + menuItems={overflowMenu} + anchorPosition={threeDotsMenuPosition} + anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} + shouldOverlay + /> + + ); +} + +export default CardSectionActions; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.native.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.native.tsx new file mode 100644 index 000000000000..f75439d512cc --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.native.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function CardSectionDataEmpty() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + + + + {translate('subscription.cardSection.cardNotFound')} + + + ); +} + +export default CardSectionDataEmpty; diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx new file mode 100644 index 000000000000..7997d400f7ec --- /dev/null +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Button from '@components/Button'; +import useLocalize from '@hooks/useLocalize'; + +function CardSectionDataEmpty() { + const {translate} = useLocalize(); + + return ( +