Skip to content

Commit

Permalink
Merge pull request Expensify#43128 from JKobrynski/integrateYourPlanS…
Browse files Browse the repository at this point in the history
…ectionWithBackend

[Payment card / Subscription] Integrate “Your plan” section with backend data and related screens
  • Loading branch information
amyevans authored Jun 11, 2024
2 parents 4d07594 + e393f85 commit 1021ae7
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 11 deletions.
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4821,6 +4821,15 @@ const CONST = {
},

SUBSCRIPTION_SIZE_LIMIT: 20000,

PAYMENT_CARD_CURRENCY: {
USD: 'USD',
AUD: 'AUD',
GBP: 'GBP',
NZD: 'NZD',
},

SUBSCRIPTION_PRICE_FACTOR: 2,
SUBSCRIPTION_POSSIBLE_COST_SAVINGS: {
COLLECT_PLAN: 10,
CONTROL_PLAN: 18,
Expand Down
33 changes: 33 additions & 0 deletions src/hooks/usePreferredCurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useMemo} from 'react';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

type PreferredCurrency = ValueOf<typeof CONST.PAYMENT_CARD_CURRENCY>;

/**
* Get user's preferred currency in the following order:
*
* 1. Payment card currency
* 2. User's local currency (if it's a valid payment card currency)
* 3. USD (default currency)
*
*/
function usePreferredCurrency(): PreferredCurrency {
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);

const paymentCardCurrency = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard)?.accountData?.currency, [fundList]);

if (paymentCardCurrency) {
return paymentCardCurrency;
}

const currentUserLocalCurrency = (personalDetails?.[session?.accountID ?? '-1']?.localCurrencyCode ?? CONST.PAYMENT_CARD_CURRENCY.USD) as PreferredCurrency;

return Object.values(CONST.PAYMENT_CARD_CURRENCY).includes(currentUserLocalCurrency) ? currentUserLocalCurrency : CONST.PAYMENT_CARD_CURRENCY.USD;
}

export default usePreferredCurrency;
62 changes: 62 additions & 0 deletions src/hooks/useSubscriptionPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {useOnyx} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import usePreferredCurrency from './usePreferredCurrency';
import useSubscriptionPlan from './useSubscriptionPlan';

const SUBSCRIPTION_PRICES = {
[CONST.PAYMENT_CARD_CURRENCY.USD]: {
[CONST.POLICY.TYPE.CORPORATE]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 900,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1800,
},
[CONST.POLICY.TYPE.TEAM]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 500,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1000,
},
},
[CONST.PAYMENT_CARD_CURRENCY.AUD]: {
[CONST.POLICY.TYPE.CORPORATE]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 1500,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 3000,
},
[CONST.POLICY.TYPE.TEAM]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 700,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400,
},
},
[CONST.PAYMENT_CARD_CURRENCY.GBP]: {
[CONST.POLICY.TYPE.CORPORATE]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 700,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1400,
},
[CONST.POLICY.TYPE.TEAM]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 400,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 800,
},
},
[CONST.PAYMENT_CARD_CURRENCY.NZD]: {
[CONST.POLICY.TYPE.CORPORATE]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 1600,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 3200,
},
[CONST.POLICY.TYPE.TEAM]: {
[CONST.SUBSCRIPTION.TYPE.ANNUAL]: 800,
[CONST.SUBSCRIPTION.TYPE.PAYPERUSE]: 1600,
},
},
} as const;

function useSubscriptionPrice(): number {
const preferredCurrency = usePreferredCurrency();
const subscriptionPlan = useSubscriptionPlan();
const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);

if (!subscriptionPlan || !privateSubscription?.type) {
return 0;
}

return SUBSCRIPTION_PRICES[preferredCurrency][subscriptionPlan][privateSubscription.type];
}

export default useSubscriptionPrice;
8 changes: 4 additions & 4 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3225,8 +3225,8 @@ export default {
title: 'Your plan',
collect: {
title: 'Collect',
priceAnnual: 'From $5/active member with the Expensify Card, $10/active member without the Expensify Card.',
pricePayPerUse: 'From $10/active member with the Expensify Card, $20/active member without the Expensify Card.',
priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
benefit1: 'Unlimited SmartScans and distance tracking',
benefit2: 'Expensify Cards with Smart Limits',
benefit3: 'Bill pay and invoicing',
Expand All @@ -3237,8 +3237,8 @@ export default {
},
control: {
title: 'Control',
priceAnnual: 'From $9/active member with the Expensify Card, $18/active member without the Expensify Card.',
pricePayPerUse: 'From $18/active member with the Expensify Card, $36/active member without the Expensify Card.',
priceAnnual: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
pricePayPerUse: ({lower, upper}) => `From ${lower}/active member with the Expensify Card, ${upper}/active member without the Expensify Card.`,
benefit1: 'Everything in Collect, plus:',
benefit2: 'NetSuite and Sage Intacct integrations',
benefit3: 'Certinia and Workday sync',
Expand Down
8 changes: 4 additions & 4 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3732,8 +3732,8 @@ export default {
title: 'Tu plan',
collect: {
title: 'Recolectar',
priceAnnual: 'Desde $5/miembro activo con la Tarjeta Expensify, $10/miembro activo sin la Tarjeta Expensify.',
pricePayPerUse: 'Desde $10/miembro activo con la Tarjeta Expensify, $20/miembro activo sin la Tarjeta Expensify.',
priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
benefit1: 'SmartScans ilimitados y seguimiento de la distancia',
benefit2: 'Tarjetas Expensify con Límites Inteligentes',
benefit3: 'Pago de facturas y facturación',
Expand All @@ -3744,8 +3744,8 @@ export default {
},
control: {
title: 'Control',
priceAnnual: 'Desde $9/miembro activo con la Tarjeta Expensify, $18/miembro activo sin la Tarjeta Expensify.',
pricePayPerUse: 'Desde $18/miembro activo con la Tarjeta Expensify, $36/miembro activo sin la Tarjeta Expensify.',
priceAnnual: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
pricePayPerUse: ({lower, upper}) => `Desde ${lower}/miembro activo con la Tarjeta Expensify, ${upper}/miembro activo sin la Tarjeta Expensify.`,
benefit1: 'Todo en Recolectar, más:',
benefit2: 'Integraciones con NetSuite y Sage Intacct',
benefit3: 'Sincronización de Certinia y Workday',
Expand Down
21 changes: 21 additions & 0 deletions src/libs/CurrencyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR
});
}

/**
* Given the amount in the "cents", convert it to a short string (no decimals) for display in the UI.
* The backend always handle things in "cents" (subunit equal to 1/100)
*
* @param amountInCents – should be an integer. Anything after a decimal place will be dropped.
* @param currency - IOU currency
*/
function convertToShortDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string {
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents);

return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,

// There will be no decimals displayed (e.g. $9)
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
}

/**
* Given an amount, convert it to a string for display in the UI.
*
Expand Down Expand Up @@ -184,4 +204,5 @@ export {
convertAmountToDisplayString,
convertToDisplayStringWithoutCurrency,
isValidCurrencyCode,
convertToShortDisplayString,
};
13 changes: 11 additions & 2 deletions src/pages/settings/Subscription/SubscriptionPlan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import usePreferredCurrency from '@hooks/usePreferredCurrency';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
import useSubscriptionPrice from '@hooks/useSubscriptionPrice';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertToShortDisplayString} from '@libs/CurrencyUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -20,9 +23,12 @@ function SubscriptionPlan() {
const styles = useThemeStyles();
const theme = useTheme();

const subscriptionPlan = useSubscriptionPlan();
const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);

const subscriptionPlan = useSubscriptionPlan();
const subscriptionPrice = useSubscriptionPrice();
const preferredCurrency = usePreferredCurrency();

const isCollect = subscriptionPlan === CONST.POLICY.TYPE.TEAM;
const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL;

Expand Down Expand Up @@ -59,7 +65,10 @@ function SubscriptionPlan() {
/>
<Text style={[styles.headerText, styles.mt2]}>{translate(`subscription.yourPlan.${isCollect ? 'collect' : 'control'}.title`)}</Text>
<Text style={[styles.textLabelSupporting, styles.mb2]}>
{translate(`subscription.yourPlan.${isCollect ? 'collect' : 'control'}.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`)}
{translate(`subscription.yourPlan.${isCollect ? 'collect' : 'control'}.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`, {
lower: convertToShortDisplayString(subscriptionPrice, preferredCurrency),
upper: convertToShortDisplayString(subscriptionPrice * CONST.SUBSCRIPTION_PRICE_FACTOR, preferredCurrency),
})}
</Text>
{benefitsList.map((benefit) => (
<View
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/BankAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type BankAccountAdditionalData = {

/** In which country is the bank account */
country?: string;

/** Is billing card */
isBillingCard?: boolean;
};

/** Model of bank account */
Expand Down
3 changes: 2 additions & 1 deletion src/types/onyx/Fund.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {BankName} from './Bank';
import type {BankAccountAdditionalData} from './BankAccount';
Expand Down Expand Up @@ -33,7 +34,7 @@ type AccountData = {
created?: string;

/** Debit card currency */
currency?: string;
currency?: ValueOf<typeof CONST.PAYMENT_CARD_CURRENCY>;

/** Debit card ID number */
fundID?: number;
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/CurrencyUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,31 @@ describe('CurrencyUtils', () => {
Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(CurrencyUtils.convertToDisplayString(amount, currency)).toBe(expectedResult)),
);
});

describe('convertToShortDisplayString', () => {
test.each([
[CONST.CURRENCY.USD, 25, '$0'],
[CONST.CURRENCY.USD, 2500, '$25'],
[CONST.CURRENCY.USD, 150, '$2'],
[CONST.CURRENCY.USD, 250000, '$2,500'],
['JPY', 2500, '¥25'],
['JPY', 250000, '¥2,500'],
['JPY', 2500.5, '¥25'],
['RSD', 100, 'RSD\xa01'],
['RSD', 145, 'RSD\xa01'],
['BHD', 12345, 'BHD\xa0123'],
['BHD', 1, 'BHD\xa00'],
])('Correctly displays %s', (currency, amount, expectedResult) => {
expect(CurrencyUtils.convertToShortDisplayString(amount, currency)).toBe(expectedResult);
});

test.each([
['EUR', 25, '0\xa0€'],
['EUR', 2500, '25\xa0€'],
['EUR', 250000, '2500\xa0€'],
['EUR', 250000000, '2.500.000\xa0€'],
])('Correctly displays %s in ES locale', (currency, amount, expectedResult) =>
Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(CurrencyUtils.convertToShortDisplayString(amount, currency)).toBe(expectedResult)),
);
});
});

0 comments on commit 1021ae7

Please sign in to comment.