Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement card section for subscription #42787

Merged
merged 17 commits into from
Jun 6, 2024
Merged
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);
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved

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;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function CardSectionActions() {
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

export default CardSectionActions;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
},
{
icon: Expensicons.MoneyCircle,
text: translate('subscription.cardSection.changeCurrency'),
onSelected: () => {}, // TODO: update with navigation to change currency screen
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
},
],
[translate],
);

return (
<View ref={threeDotsMenuContainerRef}>
<ThreeDotsMenu
onIconPress={() => {
if (isSmallScreenWidth) {
return;
}
threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
setThreeDotsMenuPosition({
horizontal: x + width,
vertical: y + height,
});
});
}}
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
menuItems={overflowMenu}
anchorPosition={threeDotsMenuPosition}
anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
shouldOverlay
/>
</View>
);
}

export default CardSectionActions;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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]}>
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
<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>
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
</View>
);
}

export default CardSectionDataEmpty;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
success
large
/>
);
}

export default CardSectionDataEmpty;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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>;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
}

export default ReducedFunctionalityMessage;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function ReducedFunctionalityMessage() {
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

export default ReducedFunctionalityMessage;
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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: {
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
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: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show a screenshot of what these styles do exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed and updated to existing styles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was used for the message above "Add card" block

image

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are these values coming from? cc @dannymcclain @dubielzyk-expensify

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It maybe looks like these styles are for this little guy?

image

And I'm assuming that the 10px padding is coming from the fact that the container is 40px and the icon is 20px, though I would probably just set an explicit width and height on the container and let the icon sit in the center (but maybe that's just me).

The border radius should definitely be using one of our variables—and it should be equal to 8px.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I agree that the border radius should be 8 and that we should just vertically/horizontally center the icon within the space if we can.

},

subscriptionEmptyCardIcon: {
pasyukevich marked this conversation as resolved.
Show resolved Hide resolved
padding: 10,
backgroundColor: theme.border,
borderRadius: 10,
},

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