Skip to content

Commit

Permalink
Merge pull request #42690 from JKobrynski/createYourPlanSection
Browse files Browse the repository at this point in the history
[Payment card / Subscription] Implement “Your plan” section (UI)
  • Loading branch information
amyevans authored Jun 4, 2024
2 parents 92a7b15 + 3e1c63b commit eb03ad2
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 4 deletions.
7 changes: 7 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ const CONST = {
ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'),
MANAGE_CARDS_URL: 'domain_companycards',
FEES_URL: `${USE_EXPENSIFY_URL}/fees`,
SAVE_WITH_EXPENSIFY_URL: `${USE_EXPENSIFY_URL}/savings-calculator`,
CFPB_PREPAID_URL: 'https://cfpb.gov/prepaid',
STAGING_NEW_EXPENSIFY_URL: 'https://staging.new.expensify.com',
NEWHELP_URL: 'https://help.expensify.com',
Expand Down Expand Up @@ -1897,6 +1898,12 @@ const CONST = {
COMPACT: 'compact',
DEFAULT: 'default',
},
SUBSCRIPTION: {
TYPE: {
ANNUAL: 'yearly2018',
PAYPERUSE: 'monthly2018',
},
},
REGEX: {
SPECIAL_CHARS_WITHOUT_NEWLINE: /((?!\n)[()-\s\t])/g,
DIGITS_AND_PLUS: /^\+?[0-9]*$/,
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ const ONYXKEYS = {
/** Whether the user has been shown the hold educational interstitial yet */
NVP_HOLD_USE_EXPLAINED: 'holdUseExplained',

/** Store the state of the subscription */
NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription',

/** Store preferred skintone for emoji */
PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone',

Expand Down Expand Up @@ -643,6 +646,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string;
[ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners;
[ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean;
[ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription;
[ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet;
[ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido;
[ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails;
Expand Down
32 changes: 32 additions & 0 deletions src/hooks/useSubscriptionPlan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useMemo} from 'react';
import {useOnyx} from 'react-native-onyx';
import {isPolicyOwner} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

function useSubscriptionPlan() {
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [session] = useOnyx(ONYXKEYS.SESSION);

// Filter workspaces in which user is the owner and the type is either corporate (control) or team (collect)
const ownerPolicies = useMemo(
() =>
Object.values(policies ?? {}).filter(
(policy) => isPolicyOwner(policy, session?.accountID ?? -1) && (CONST.POLICY.TYPE.CORPORATE === policy?.type || CONST.POLICY.TYPE.TEAM === policy?.type),
),
[policies, session?.accountID],
);

if (isEmptyObject(ownerPolicies)) {
return null;
}

// Check if user has corporate (control) workspace
const hasControlWorkspace = ownerPolicies.some((policy) => policy?.type === CONST.POLICY.TYPE.CORPORATE);

// Corporate (control) workspace is supposed to be the higher priority
return hasControlWorkspace ? CONST.POLICY.TYPE.CORPORATE : CONST.POLICY.TYPE.TEAM;
}

export default useSubscriptionPlan;
29 changes: 29 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3162,6 +3162,35 @@ export default {
mergedWithCashTransaction: 'matched a receipt to this transaction.',
},
subscription: {
yourPlan: {
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.',
benefit1: 'Unlimited SmartScans and distance tracking',
benefit2: 'Expensify Cards with Smart Limits',
benefit3: 'Bill pay and invoicing',
benefit4: 'Expense approvals',
benefit5: 'ACH reimbursement',
benefit6: 'QuickBooks and Xero integrations',
benefit7: 'Custom insights and reporting',
},
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.',
benefit1: 'Everything in Collect, plus:',
benefit2: 'NetSuite and Sage Intacct integrations',
benefit3: 'Certinia and Workday sync',
benefit4: 'Multiple expense approvers',
benefit5: 'SAML/SSO',
benefit6: 'Budgeting',
},
saveWithExpensifyTitle: 'Save with the Expensify Card',
saveWithExpensifyDescription: 'Use our savings calculator to see how cash back from the Expensify Card can reduce your Expensify bill.',
saveWithExpensifyButton: 'Learn more',
},
subscriptionSize: {
title: 'Subscription size',
yourSize: 'Your subscription size is the number of open seats that can be filled by any active member in a given month.',
Expand Down
29 changes: 29 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3668,6 +3668,35 @@ export default {
mergedWithCashTransaction: 'encontró un recibo para esta transacción.',
},
subscription: {
yourPlan: {
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.',
benefit1: 'SmartScans ilimitados y seguimiento de la distancia',
benefit2: 'Tarjetas Expensify con Límites Inteligentes',
benefit3: 'Pago de facturas y facturación',
benefit4: 'Aprobación de gastos',
benefit5: 'Reembolso ACH',
benefit6: 'Integraciones con QuickBooks y Xero',
benefit7: 'Reportes e informes personalizados',
},
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.',
benefit1: 'Todo en Recolectar, más:',
benefit2: 'Integraciones con NetSuite y Sage Intacct',
benefit3: 'Sincronización de Certinia y Workday',
benefit4: 'Varios aprobadores de gastos',
benefit5: 'SAML/SSO',
benefit6: 'Presupuestos',
},
saveWithExpensifyTitle: 'Ahorra con la Tarjeta Expensify',
saveWithExpensifyDescription: 'Utiliza nuestra calculadora de ahorro para ver cómo el reembolso en efectivo de la Tarjeta Expensify puede reducir tu factura de Expensify',
saveWithExpensifyButton: 'Más información',
},
subscriptionSize: {
title: 'Tamaño de suscripción',
yourSize: 'El tamaño de tu suscripción es el número de plazas abiertas que puede ocupar cualquier miembro activo en un mes determinado.',
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ const READ_COMMANDS = {
OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage',
OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage',
SEARCH: 'Search',
OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage',
} as const;

type ReadCommand = ValueOf<typeof READ_COMMANDS>;
Expand Down Expand Up @@ -528,6 +529,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams;
[READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams;
[READ_COMMANDS.SEARCH]: Parameters.SearchParams;
[READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: EmptyObject;
};

const SIDE_EFFECT_REQUEST_COMMANDS = {
Expand Down
14 changes: 14 additions & 0 deletions src/libs/actions/Subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as API from '@libs/API';
import {READ_COMMANDS} from '@libs/API/types';

/**
* Fetches data when the user opens the SubscriptionSettingsPage
*/
function openSubscriptionPage() {
API.read(READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE, {});
}

export {
// eslint-disable-next-line import/prefer-default-export
openSubscriptionPage,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Return null because this button is not supposed to be rendered in the native apps */
function SaveWithExpensifyButton() {
return null;
}

export default SaveWithExpensifyButton;
23 changes: 23 additions & 0 deletions src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import {Linking} from 'react-native';
import Button from '@components/Button';
import useLocalize from '@hooks/useLocalize';
import CONST from '@src/CONST';

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

const onLinkPress = () => {
Linking.openURL(CONST.SAVE_WITH_EXPENSIFY_URL);
};

return (
<Button
text={translate('subscription.yourPlan.saveWithExpensifyButton')}
onPress={onLinkPress}
medium
/>
);
}

export default SaveWithExpensifyButton;
94 changes: 94 additions & 0 deletions src/pages/settings/Subscription/SubscriptionPlan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 * as Illustrations from '@components/Icon/Illustrations';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import SaveWithExpensifyButton from './SaveWithExpensifyButton';

function SubscriptionPlan() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();

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

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

const benefitsList = isCollect
? [
translate('subscription.yourPlan.collect.benefit1'),
translate('subscription.yourPlan.collect.benefit2'),
translate('subscription.yourPlan.collect.benefit3'),
translate('subscription.yourPlan.collect.benefit4'),
translate('subscription.yourPlan.collect.benefit5'),
translate('subscription.yourPlan.collect.benefit6'),
translate('subscription.yourPlan.collect.benefit7'),
]
: [
translate('subscription.yourPlan.control.benefit1'),
translate('subscription.yourPlan.control.benefit2'),
translate('subscription.yourPlan.control.benefit3'),
translate('subscription.yourPlan.control.benefit4'),
translate('subscription.yourPlan.control.benefit5'),
translate('subscription.yourPlan.control.benefit6'),
];

return (
<Section
title={translate('subscription.yourPlan.title')}
isCentralPane
titleStyles={styles.textStrong}
>
<View style={[styles.borderedContentCard, styles.mt5, styles.p5]}>
<Icon
src={isCollect ? Illustrations.Mailbox : Illustrations.ShieldYellow}
width={variables.iconHeader}
height={variables.iconHeader}
/>
<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'}`)}
</Text>
{benefitsList.map((benefit) => (
<View
style={[styles.flexRow, styles.alignItemsCenter, styles.mt2]}
key={benefit}
>
<Icon
src={Expensicons.Checkmark}
fill={theme.iconSuccessFill}
/>
<Text style={[styles.textMicroSupporting, styles.ml2]}>{benefit}</Text>
</View>
))}
</View>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt6]}>
<Icon
src={Illustrations.HandCard}
width={variables.iconHeader}
height={variables.iconHeader}
additionalStyles={styles.mr2}
/>
<View style={[styles.flexColumn, styles.justifyContentCenter, styles.flex1, styles.mr2]}>
<Text style={[styles.headerText, styles.mt2]}>{translate('subscription.yourPlan.saveWithExpensifyTitle')}</Text>
<Text style={[styles.textLabelSupporting, styles.mb2]}>{translate('subscription.yourPlan.saveWithExpensifyDescription')}</Text>
</View>
<SaveWithExpensifyButton />
</View>
</Section>
);
}

export default SubscriptionPlan;
22 changes: 18 additions & 4 deletions src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import React from 'react';
import React, {useEffect} from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import useLocalize from '@hooks/useLocalize';
import useWindowDimensions from '@hooks/useWindowDimensions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
import Navigation from '@libs/Navigation/Navigation';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as Subscription from '@userActions/Subscription';
import SubscriptionPlan from './SubscriptionPlan';

function SubscriptionSettingsPage() {
const {isSmallScreenWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
const subscriptionPlan = useSubscriptionPlan();

useEffect(() => {
Subscription.openSubscriptionPage();
}, []);

if (!subscriptionPlan) {
return <NotFoundPage />;
}

return (
<ScreenWrapper testID={SubscriptionSettingsPage.displayName}>
<HeaderWithBackButton
title={translate('workspace.common.subscription')}
onBackButtonPress={() => Navigation.goBack()}
shouldShowBackButton={isSmallScreenWidth}
shouldShowBackButton={shouldUseNarrowLayout}
icon={Illustrations.CreditCardsNew}
/>
<SubscriptionPlan />
</ScreenWrapper>
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2796,6 +2796,12 @@ const styles = (theme: ThemeColors) =>
fontWeight: FontUtils.fontWeight.bold,
},

borderedContentCard: {
borderWidth: 1,
borderColor: theme.border,
borderRadius: variables.componentBorderRadiusMedium,
},

sectionMenuItem: {
borderRadius: 8,
paddingHorizontal: 8,
Expand Down
30 changes: 30 additions & 0 deletions src/types/onyx/PrivateSubscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';

type PrivateSubscription = {
/** "auto increase annual seats" setting */
addNewUsersAutomatically: boolean;

/** "auto renew" setting */
autoRenew: boolean;

/** The date "auto renew" was last edited */
autoRenewLastChangedDate: string;

/** "corporate karma" setting */
donateToExpensifyOrg?: true;

/** Subscription end date */
endDate: string;

/** Subscription start date */
startDate: string;

/** Subscription variant. "yearly2018" - annual, "monthly2018" - pay-per-use */
type: ValueOf<typeof CONST.SUBSCRIPTION.TYPE>;

/** Subscription size */
userCount?: number;
};

export default PrivateSubscription;
Loading

0 comments on commit eb03ad2

Please sign in to comment.