Skip to content

Commit

Permalink
feat: implement card section for subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
pasyukevich committed May 29, 2024
1 parent 23232b4 commit 183e74e
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 0 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 @@ -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';
Expand Down Expand Up @@ -212,6 +213,7 @@ export {
Copy,
CreditCard,
CreditCardHourglass,
CreditCardExclamation,
DeletedRoomAvatar,
Document,
DocumentSlash,
Expand Down
15 changes: 15 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
15 changes: 15 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
64 changes: 64 additions & 0 deletions src/pages/settings/Subscription/CardSection/CardSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Section
title={translate('subscription.cardSection.title')}
subtitle={translate('subscription.cardSection.subtitle')}
isCentralPane
titleStyles={styles.textStrong}
subtitleMuted
>
<View style={styles.mt5}>
<View style={[styles.flexRow, styles.flex1]}>
{!isEmptyObject(defaultCard?.accountData) && (
<>
<View style={[styles.flexRow, styles.flex1, styles.gap3]}>
<Icon
src={Expensicons.CreditCard}
additionalStyles={styles.subscriptionCardIcon}
fill={theme.text}
large
/>
<View>
<Text>{translate('subscription.cardSection.cardEnding', {cardNumber: defaultCard?.accountData?.cardNumber})}</Text>
<Text style={styles.mutedNormalTextLabel}>
{translate('subscription.cardSection.cardInfo', {
name: defaultCard?.accountData?.addressName,
expiration: `${defaultCard?.accountData?.cardMonth}/${defaultCard?.accountData?.cardYear}`,
currency: defaultCard?.accountData?.currency,
})}
</Text>
</View>
</View>
<CardSectionActions />
</>
)}
{isEmptyObject(defaultCard?.accountData) && <CardSectionDataEmpty />}
</View>
</View>
</Section>
);
}

export default CardSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function CardSectionActions() {
return null;
}

export default CardSectionActions;
Original file line number Diff line number Diff line change
@@ -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<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
},
{
icon: Expensicons.MoneyCircle,
text: translate('subscription.cardSection.changeCurrency'),
onSelected: () => {}, // TODO: update with navigation to change currency screen
},
],
[translate],
);

return (
<View ref={threeDotsMenuContainerRef}>
<ThreeDotsMenu
onIconPress={() => {
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
/>
</View>
);
}

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.flex1, styles.gap3]}>
<Icon
src={Expensicons.CreditCardExclamation}
additionalStyles={styles.subscriptionCardIcon}
fill={theme.icon}
large
/>
<View style={styles.alignSelfCenter}>
<Text style={[styles.mutedNormalTextLabel, styles.textBold]}>{translate('subscription.cardSection.cardNotFound')}</Text>
</View>
</View>
);
}

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

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

return (
<Button
text={translate('subscription.cardSection.addCardButton')}
onPress={() => {}} // TODO: update with navigation to add card screen
success
large
/>
);
}

export default CardSectionDataEmpty;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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.sectionListMutedInfo}>{translate('subscription.mobileReducedFunctionalityMessage')}</Text>;
}

export default ReducedFunctionalityMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function ReducedFunctionalityMessage() {
return null;
}

export default ReducedFunctionalityMessage;
12 changes: 12 additions & 0 deletions src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import CardSection from './CardSection/CardSection';
import ReducedFunctionalityMessage from './ReducedFunctionalityMessage';

function SubscriptionSettingsPage() {
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const styles = useThemeStyles();

return (
<ScreenWrapper testID={SubscriptionSettingsPage.displayName}>
Expand All @@ -18,6 +24,12 @@ function SubscriptionSettingsPage() {
shouldShowBackButton={isSmallScreenWidth}
icon={Illustrations.CreditCardsNew}
/>
<ScrollView style={styles.pt3}>
<View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}>
<ReducedFunctionalityMessage />
<CardSection />
</View>
</ScrollView>
</ScreenWrapper>
);
}
Expand Down
26 changes: 26 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ const styles = (theme: ThemeColors) =>
verticalAlign: 'top',
},

textBold: {
fontWeight: FontUtils.fontWeight.bold,
},

label: {
fontSize: variables.fontSizeLabel,
lineHeight: variables.lineHeightLarge,
Expand Down Expand Up @@ -2791,6 +2795,16 @@ const styles = (theme: ThemeColors) =>
color: theme.textSupporting,
},

sectionListMutedInfo: {
color: theme.textSupporting,
fontSize: variables.fontSizeNormal,
lineHeight: variables.lineHeightNormal,
fontFamily: FontUtils.fontFamily.platform.EXP_NEUE,
width: '100%',
alignItems: 'center',
padding: 20,
},

accountSettingsSectionTitle: {
fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD,
fontWeight: FontUtils.fontWeight.bold,
Expand All @@ -2813,6 +2827,18 @@ const styles = (theme: ThemeColors) =>
width: 'auto',
},

subscriptionCardIcon: {
padding: 10,
backgroundColor: theme.border,
borderRadius: 10,
},

subscriptionEmptyCardIcon: {
padding: 10,
backgroundColor: theme.border,
borderRadius: 10,
},

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

0 comments on commit 183e74e

Please sign in to comment.