Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3172,6 +3172,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 @@ -3678,6 +3678,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