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

[Payment card / Subscription] Integrate “Your plan” section with backend data and related screens #43128

Merged
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
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 = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it better to move it to CONST.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Its values are based on CONST, so it's impossible to put it in that object, and I also can't see any other const being exported from that file, only types

Copy link
Contributor

Choose a reason for hiding this comment

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

@JKobrynski Hmm, I was thinking if we should code subscription prices in CONST.ts too - then it'll be easier for the team to maintain them, like team members from business team

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree this is not ideal but I'm also not seeing a better way to handle it so I think it is what it is

[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 {
JKobrynski marked this conversation as resolved.
Show resolved Hide resolved
JKobrynski marked this conversation as resolved.
Show resolved Hide resolved
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents);

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

// There will be no decimals displayed (e.g. $9)
minimumFractionDigits: 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

Just out of curiosity, does adding this line fix the crash on Android? If so, why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it does fix it! Hard to say why, it just doesn't work without it, the app crashes and doesn't show any useful crash details, even when using adb logcat with filters

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();
JKobrynski marked this conversation as resolved.
Show resolved Hide resolved
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)),
);
});
});
Loading