diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg
new file mode 100644
index 000000000000..e158bc5588cb
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg
@@ -0,0 +1,82 @@
+
+
+
diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg
new file mode 100644
index 000000000000..d70d2d1ef552
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg
@@ -0,0 +1,27 @@
+
+
+
diff --git a/assets/images/subscription-details__approvedlogo--light.svg b/assets/images/subscription-details__approvedlogo--light.svg
new file mode 100644
index 000000000000..580ee60c597c
--- /dev/null
+++ b/assets/images/subscription-details__approvedlogo--light.svg
@@ -0,0 +1,91 @@
+
+
+
diff --git a/assets/images/subscription-details__approvedlogo.svg b/assets/images/subscription-details__approvedlogo.svg
new file mode 100644
index 000000000000..7722e2526657
--- /dev/null
+++ b/assets/images/subscription-details__approvedlogo.svg
@@ -0,0 +1,94 @@
+
+
+
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index e59d73aee192..96104932c899 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -80,6 +80,8 @@ import SendMoney from '@assets/images/simple-illustrations/simple-illustration__
import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg';
import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg';
import SplitBill from '@assets/images/simple-illustrations/simple-illustration__splitbill.svg';
+import SubscriptionAnnual from '@assets/images/simple-illustrations/simple-illustration__subscription-annual.svg';
+import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg';
import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg';
import TeachersUnite from '@assets/images/simple-illustrations/simple-illustration__teachers-unite.svg';
import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg';
@@ -88,6 +90,8 @@ import TrashCan from '@assets/images/simple-illustrations/simple-illustration__t
import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg';
import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg';
+import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg';
+import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo.svg';
export {
Abracadabra,
@@ -178,6 +182,10 @@ export {
Tag,
CarIce,
Lightbulb,
+ SubscriptionAnnual,
+ SubscriptionPPU,
+ ExpensifyApprovedLogo,
+ ExpensifyApprovedLogoLight,
SendMoney,
CheckmarkCircle,
};
diff --git a/src/components/OptionsPicker/OptionItem.tsx b/src/components/OptionsPicker/OptionItem.tsx
new file mode 100644
index 000000000000..a787c20f515c
--- /dev/null
+++ b/src/components/OptionsPicker/OptionItem.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import {PressableWithFeedback} from '@components/Pressable';
+import SelectCircle from '@components/SelectCircle';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type OptionItemProps = {
+ /** Text to be rendered */
+ title: TranslationPaths;
+
+ /** Icon to be displayed above the title */
+ icon: IconAsset;
+
+ /** Press handler */
+ onPress?: () => void;
+
+ /** Indicates whether the option is currently selected (active) */
+ isSelected?: boolean;
+
+ /** Indicates whether the option is disabled */
+ isDisabled?: boolean;
+
+ /** Optional style prop */
+ style?: StyleProp;
+};
+
+function OptionItem({title, icon, onPress, isSelected = false, isDisabled, style}: OptionItemProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ return (
+
+
+
+
+
+ {!isDisabled && (
+
+
+
+ )}
+
+
+ {translate(title)}
+
+
+
+
+ );
+}
+
+OptionItem.displayName = 'OptionItem';
+
+export default OptionItem;
diff --git a/src/components/OptionsPicker/index.tsx b/src/components/OptionsPicker/index.tsx
new file mode 100644
index 000000000000..621b8465adba
--- /dev/null
+++ b/src/components/OptionsPicker/index.tsx
@@ -0,0 +1,61 @@
+import React, {Fragment} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {TranslationPaths} from '@src/languages/types';
+import type IconAsset from '@src/types/utils/IconAsset';
+import OptionItem from './OptionItem';
+
+type OptionsPickerItem = {
+ /** A unique identifier for each option */
+ key: TKey;
+
+ /** Text to be displayed */
+ title: TranslationPaths;
+
+ /** Icon to be displayed above the title */
+ icon: IconAsset;
+};
+
+type OptionsPickerProps = {
+ /** Options list */
+ options: Array>;
+
+ /** Selected option's identifier */
+ selectedOption: TKey;
+
+ /** Option select handler */
+ onOptionSelected: (option: TKey) => void;
+
+ /** Indicates whether the picker is disabled */
+ isDisabled?: boolean;
+
+ /** Optional style */
+ style?: StyleProp;
+};
+
+function OptionsPicker({options, selectedOption, onOptionSelected, style, isDisabled}: OptionsPickerProps) {
+ const styles = useThemeStyles();
+
+ return (
+
+ {options.map((option, index) => (
+
+ onOptionSelected(option.key)}
+ />
+ {index < options.length - 1 && }
+
+ ))}
+
+ );
+}
+
+OptionsPicker.displayName = 'OptionsPicker';
+
+export default OptionsPicker;
+export type {OptionsPickerItem};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c69531a7ab13..8f8fae9fc8d8 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -3191,6 +3191,16 @@ export default {
saveWithExpensifyDescription: 'Use our savings calculator to see how cash back from the Expensify Card can reduce your Expensify bill.',
saveWithExpensifyButton: 'Learn more',
},
+ details: {
+ title: 'Subscription details',
+ annual: 'Annual subscription',
+ payPerUse: 'Pay-per-use',
+ subscriptionSize: 'Subscription size',
+ headsUpTitle: 'Heads up: ',
+ headsUpBody:
+ "If you don’t set your subscription size now, we’ll set it automatically to your first month's active member count. You’ll then be committed to paying for at least this number of members for the next 12 months. You can increase your subscription size at any time, but you can’t decrease it until your subscription is over.",
+ zeroCommitment: 'Zero commitment at the discounted annual subscription rate',
+ },
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.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 3e8a4a00d4f2..8acc353d4752 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3697,6 +3697,16 @@ export default {
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',
},
+ details: {
+ title: 'Datos de suscripción',
+ annual: 'Suscripción anual',
+ payPerUse: 'Pago por uso',
+ subscriptionSize: 'Tamaño de suscripción',
+ headsUpTitle: 'Atención: ',
+ headsUpBody:
+ 'Si no estableces ahora el tamaño de tu suscripción, lo haremos automáticamente con el número de suscriptores activos del primer mes. A partir de ese momento, estarás suscrito para pagar al menos por ese número de afiliados durante los 12 meses siguientes. Puedes aumentar el tamaño de tu suscripción en cualquier momento, pero no puedes reducirlo hasta que finalice tu suscripción.',
+ zeroCommitment: 'Compromiso cero con la tarifa de suscripción anual reducida',
+ },
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.',
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index c91ea4475403..6ca80d525682 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -529,7 +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;
+ [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null;
};
const SIDE_EFFECT_REQUEST_COMMANDS = {
diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts
index f28c395f1b40..c7732575aaa6 100644
--- a/src/libs/actions/Subscription.ts
+++ b/src/libs/actions/Subscription.ts
@@ -5,7 +5,7 @@ import {READ_COMMANDS} from '@libs/API/types';
* Fetches data when the user opens the SubscriptionSettingsPage
*/
function openSubscriptionPage() {
- API.read(READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE, {});
+ API.read(READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE, null);
}
export {
diff --git a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx b/src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx
index be12d66a4f9a..6eef4331c304 100644
--- a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx
+++ b/src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx
@@ -3,4 +3,6 @@ function SaveWithExpensifyButton() {
return null;
}
+SaveWithExpensifyButton.displayName = 'SaveWithExpensifyButton';
+
export default SaveWithExpensifyButton;
diff --git a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx b/src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx
index f6aff02c801c..ea12afe90440 100644
--- a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx
+++ b/src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx
@@ -20,4 +20,6 @@ function SaveWithExpensifyButton() {
);
}
+SaveWithExpensifyButton.displayName = 'SaveWithExpensifyButton';
+
export default SaveWithExpensifyButton;
diff --git a/src/pages/settings/Subscription/SubscriptionDetails/index.native.tsx b/src/pages/settings/Subscription/SubscriptionDetails/index.native.tsx
new file mode 100644
index 000000000000..0eed6fd99ea5
--- /dev/null
+++ b/src/pages/settings/Subscription/SubscriptionDetails/index.native.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Icon from '@components/Icon';
+import * as Illustrations from '@components/Icon/Illustrations';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OptionItem from '@components/OptionsPicker/OptionItem';
+import Section from '@components/Section';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function SubscriptionDetails() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
+
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+
+ // This section is only shown when the subscription is annual
+ let subscriptionSizeSection: React.JSX.Element | null = null;
+
+ if (privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL) {
+ subscriptionSizeSection = privateSubscription?.userCount ? (
+
+ ) : (
+
+ {translate('subscription.details.headsUpTitle')}
+ {translate('subscription.details.headsUpBody')}
+
+ );
+ }
+
+ return (
+
+ {!!account?.isApprovedAccountant || !!account?.isApprovedAccountantClient ? (
+
+
+ {translate('subscription.details.zeroCommitment')}
+
+ ) : (
+ <>
+ {privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.PAYPERUSE ? (
+
+ ) : (
+
+ )}
+ {subscriptionSizeSection}
+ >
+ )}
+
+ );
+}
+
+SubscriptionDetails.displayName = 'SubscriptionDetails';
+
+export default SubscriptionDetails;
diff --git a/src/pages/settings/Subscription/SubscriptionDetails/index.tsx b/src/pages/settings/Subscription/SubscriptionDetails/index.tsx
new file mode 100644
index 000000000000..350d84d00a46
--- /dev/null
+++ b/src/pages/settings/Subscription/SubscriptionDetails/index.tsx
@@ -0,0 +1,107 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import Icon from '@components/Icon';
+import * as Illustrations from '@components/Icon/Illustrations';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import type {OptionsPickerItem} from '@components/OptionsPicker';
+import OptionsPicker from '@components/OptionsPicker';
+import Section from '@components/Section';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type SubscriptionVariant = ValueOf;
+
+const options: Array> = [
+ {
+ key: CONST.SUBSCRIPTION.TYPE.ANNUAL,
+ title: 'subscription.details.annual',
+ icon: Illustrations.SubscriptionAnnual,
+ },
+ {
+ key: CONST.SUBSCRIPTION.TYPE.PAYPERUSE,
+ title: 'subscription.details.payPerUse',
+ icon: Illustrations.SubscriptionPPU,
+ },
+];
+
+function SubscriptionDetails() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
+
+ const [selectedOption, setSelectedOption] = useState(privateSubscription?.type ?? CONST.SUBSCRIPTION.TYPE.ANNUAL);
+
+ const onOptionSelected = (option: SubscriptionVariant) => {
+ setSelectedOption(option);
+ };
+
+ // This section is only shown when the subscription is annual
+ // An onPress action is going to be assigned to these buttons in phase 2
+ let subscriptionSizeSection: React.JSX.Element | null = null;
+
+ if (privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL) {
+ subscriptionSizeSection = privateSubscription?.userCount ? (
+
+ ) : (
+ <>
+
+
+ {translate('subscription.details.headsUpTitle')}
+ {translate('subscription.details.headsUpBody')}
+
+ >
+ );
+ }
+
+ return (
+
+ {!!account?.isApprovedAccountant || !!account?.isApprovedAccountantClient ? (
+
+
+ {translate('subscription.details.zeroCommitment')}
+
+ ) : (
+ <>
+
+ {subscriptionSizeSection}
+ >
+ )}
+
+ );
+}
+
+SubscriptionDetails.displayName = 'SubscriptionDetails';
+
+export default SubscriptionDetails;
diff --git a/src/pages/settings/Subscription/SubscriptionPlan.tsx b/src/pages/settings/Subscription/SubscriptionPlan.tsx
index 28da111f298c..0834d2b89e15 100644
--- a/src/pages/settings/Subscription/SubscriptionPlan.tsx
+++ b/src/pages/settings/Subscription/SubscriptionPlan.tsx
@@ -69,6 +69,8 @@ function SubscriptionPlan() {
{benefit}
@@ -91,4 +93,6 @@ function SubscriptionPlan() {
);
}
+SubscriptionPlan.displayName = 'SubscriptionPlan';
+
export default SubscriptionPlan;
diff --git a/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
index be65fb8aa355..932c83c1b7d2 100644
--- a/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
+++ b/src/pages/settings/Subscription/SubscriptionSettingsPage.tsx
@@ -2,17 +2,21 @@ import React, {useEffect} from 'react';
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 useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
+import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import * as Subscription from '@userActions/Subscription';
+import SubscriptionDetails from './SubscriptionDetails';
import SubscriptionPlan from './SubscriptionPlan';
function SubscriptionSettingsPage() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const subscriptionPlan = useSubscriptionPlan();
useEffect(() => {
@@ -31,7 +35,10 @@ function SubscriptionSettingsPage() {
shouldShowBackButton={shouldUseNarrowLayout}
icon={Illustrations.CreditCardsNew}
/>
-
+
+
+
+
);
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index a9c9c12335a1..bacaa66cb729 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2799,7 +2799,7 @@ const styles = (theme: ThemeColors) =>
borderedContentCard: {
borderWidth: 1,
borderColor: theme.border,
- borderRadius: variables.componentBorderRadiusMedium,
+ borderRadius: variables.componentBorderRadiusNormal,
},
sectionMenuItem: {
@@ -2809,6 +2809,10 @@ const styles = (theme: ThemeColors) =>
alignItems: 'center',
},
+ sectionSelectCircle: {
+ backgroundColor: colors.productDark200,
+ },
+
qrShareSection: {
width: 264,
},
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index c53d7ea816f8..46eac1e94aa4 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -63,6 +63,12 @@ type Account = {
/** Object containing all account information necessary to connect with Spontana */
travelSettings?: TravelSettings;
+
+ /** Indicates whether the user is an approved accountant */
+ isApprovedAccountant?: boolean;
+
+ /** Indicates whether the user is a client of an approved accountant */
+ isApprovedAccountantClient?: boolean;
};
export default Account;