Skip to content

Commit

Permalink
Merge pull request #42787 from pasyukevich/feature/card-section
Browse files Browse the repository at this point in the history
  • Loading branch information
blimpich authored Jun 6, 2024
2 parents eddb696 + ec88580 commit 05bcf8a
Show file tree
Hide file tree
Showing 13 changed files with 282 additions and 2 deletions.
14 changes: 14 additions & 0 deletions assets/images/credit-card-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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 Crosshair from '@assets/images/crosshair.svg';
Expand Down Expand Up @@ -224,6 +225,7 @@ export {
Copy,
CreditCard,
CreditCardHourglass,
CreditCardExclamation,
DeletedRoomAvatar,
Document,
DocumentSlash,
Expand Down
13 changes: 13 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3202,6 +3202,19 @@ export default {
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',
},
yourPlan: {
title: 'Your plan',
collect: {
Expand Down
13 changes: 13 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3709,6 +3709,19 @@ export default {
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',
},
yourPlan: {
title: 'Tu plan',
collect: {
Expand Down
67 changes: 67 additions & 0 deletions src/pages/settings/Subscription/CardSection/CardSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, {useMemo} 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 DateUtils from '@libs/DateUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import CardSectionActions from './CardSectionActions';
import CardSectionDataEmpty from './CardSectionDataEmpty';

function CardSection() {
const {translate, preferredLocale} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);

const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.isDefault), [fundList]);

const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]);

return (
<Section
title={translate('subscription.cardSection.title')}
subtitle={translate('subscription.cardSection.subtitle')}
isCentralPane
titleStyles={styles.textStrong}
subtitleMuted
>
<View style={[styles.mt8, styles.mb3, styles.flexRow]}>
{!isEmptyObject(defaultCard?.accountData) && (
<>
<View style={[styles.flexRow, styles.flex1, styles.gap3]}>
<Icon
src={Expensicons.CreditCard}
additionalStyles={styles.subscriptionAddedCardIcon}
fill={theme.text}
medium
/>
<View style={styles.flex1}>
<Text style={styles.textStrong}>{translate('subscription.cardSection.cardEnding', {cardNumber: defaultCard?.accountData?.cardNumber})}</Text>
<Text style={styles.mutedNormalTextLabel}>
{translate('subscription.cardSection.cardInfo', {
name: defaultCard?.accountData?.addressName,
expiration: `${cardMonth} ${defaultCard?.accountData?.cardYear}`,
currency: defaultCard?.accountData?.currency,
})}
</Text>
</View>
</View>
<CardSectionActions />
</>
)}
{isEmptyObject(defaultCard?.accountData) && <CardSectionDataEmpty />}
</View>
</Section>
);
}

CardSection.displayName = 'CardSection';

export default CardSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function CardSectionActions() {
return null; // We need to disable actions on mobile
}

CardSectionActions.displayName = 'CardSectionActions';

export default CardSectionActions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, {useCallback, 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';

const anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
};

function CardSectionActions() {
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState<AnchorPosition>({horizontal: 0, vertical: 0});
const threeDotsMenuContainerRef = useRef<View>(null);

const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
() => [
{
icon: Expensicons.CreditCard,
text: translate('subscription.cardSection.changeCard'),
onSelected: () => {}, // TODO: update with navigation to "add card" screen (https://github.com/Expensify/App/issues/38621)
},
{
icon: Expensicons.MoneyCircle,
text: translate('subscription.cardSection.changeCurrency'),
onSelected: () => {}, // TODO: update with navigation to "change currency" screen (https://github.com/Expensify/App/issues/38621)
},
],
[translate],
);

const calculateAndSetThreeDotsMenuPosition = useCallback(() => {
if (isSmallScreenWidth) {
return;
}
threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
setThreeDotsMenuPosition({
horizontal: x + width,
vertical: y + height,
});
});
}, [isSmallScreenWidth]);

return (
<View ref={threeDotsMenuContainerRef}>
<ThreeDotsMenu
onIconPress={calculateAndSetThreeDotsMenuPosition}
menuItems={overflowMenu}
anchorPosition={threeDotsMenuPosition}
anchorAlignment={anchorAlignment}
shouldOverlay
/>
</View>
);
}

CardSectionActions.displayName = 'CardSectionActions';

export default CardSectionActions;
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap3]}>
<Icon
src={Expensicons.CreditCardExclamation}
additionalStyles={styles.subscriptionCardIcon}
fill={theme.icon}
medium
/>
<Text style={[styles.mutedNormalTextLabel, styles.textStrong]}>{translate('subscription.cardSection.cardNotFound')}</Text>
</View>
);
}

CardSectionDataEmpty.displayName = 'CardSectionDataEmpty';

export default CardSectionDataEmpty;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import Button from '@components/Button';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

function CardSectionDataEmpty() {
const {translate} = useLocalize();
const styles = useThemeStyles();

return (
<Button
text={translate('subscription.cardSection.addCardButton')}
onPress={() => {}} // TODO: update with navigation to "add card" screen (https://github.com/Expensify/App/issues/38621)
style={styles.w100}
success
large
/>
);
}

CardSectionDataEmpty.displayName = 'CardSectionDataEmpty';

export default CardSectionDataEmpty;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

function ReducedFunctionalityMessage() {
const styles = useThemeStyles();
const {translate} = useLocalize();

return <Text style={[styles.ph5, styles.pb5, styles.textSupporting]}>{translate('subscription.mobileReducedFunctionalityMessage')}</Text>;
}

ReducedFunctionalityMessage.displayName = 'ReducedFunctionalityMessage';

export default ReducedFunctionalityMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function ReducedFunctionalityMessage() {
return null; // We do not need to show this message on web and desktop
}

ReducedFunctionalityMessage.displayName = 'ReducedFunctionalityMessage';

export default ReducedFunctionalityMessage;
13 changes: 11 additions & 2 deletions src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -7,14 +8,18 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as Subscription from '@userActions/Subscription';
import CardSection from './CardSection/CardSection';
import ReducedFunctionalityMessage from './ReducedFunctionalityMessage';
import SubscriptionDetails from './SubscriptionDetails';
import SubscriptionPlan from './SubscriptionPlan';

function SubscriptionSettingsPage() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const styles = useThemeStyles();
const subscriptionPlan = useSubscriptionPlan();
Expand All @@ -36,8 +41,12 @@ function SubscriptionSettingsPage() {
icon={Illustrations.CreditCardsNew}
/>
<ScrollView style={styles.pt3}>
<SubscriptionPlan />
<SubscriptionDetails />
<View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}>
<ReducedFunctionalityMessage />
<CardSection />
<SubscriptionPlan />
<SubscriptionDetails />
</View>
</ScrollView>
</ScreenWrapper>
);
Expand Down
16 changes: 16 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2832,6 +2832,22 @@ const styles = (theme: ThemeColors) =>
width: 'auto',
},

subscriptionCardIcon: {
padding: 10,
backgroundColor: theme.border,
borderRadius: variables.componentBorderRadius,
height: variables.iconSizeExtraLarge,
width: variables.iconSizeExtraLarge,
},

subscriptionAddedCardIcon: {
padding: 10,
backgroundColor: theme.icon,
borderRadius: variables.componentBorderRadius,
height: variables.iconSizeExtraLarge,
width: variables.iconSizeExtraLarge,
},

selectCircle: {
width: variables.componentSizeSmall,
height: variables.componentSizeSmall,
Expand Down

0 comments on commit 05bcf8a

Please sign in to comment.