diff --git a/src/CONST.ts b/src/CONST.ts index de4e3305eddc..4dcd65780002 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4791,6 +4791,8 @@ const CONST = { ASC: 'asc', DESC: 'desc', }, + + SUBSCRIPTION_SIZE_LIMIT: 20000, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 86a4a0a31716..145be07fae0c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -479,6 +479,8 @@ const ONYXKEYS = { WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', NEW_CHAT_NAME_FORM: 'newChatNameForm', NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', + SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', + SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', }, } as const; @@ -536,6 +538,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; + [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 82acc26e3100..61034382fefd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -102,6 +102,7 @@ const ROUTES = { SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_SUBSCRIPTION: 'settings/subscription', + SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1ec1462da32c..6f32f980d6c2 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -106,6 +106,7 @@ const SCREENS = { SUBSCRIPTION: { ROOT: 'Settings_Subscription', + SIZE: 'Settings_Subscription_Size', }, }, SAVE_THE_WORLD: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 59f5a8ce869d..0d8b845e2dfc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3143,4 +3143,23 @@ export default { systemMessage: { mergedWithCashTransaction: 'matched a receipt to this transaction.', }, + subscription: { + 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.', + eachMonth: + 'Each month, your subscription covers up to the number of active members set above. Any time you increase your subscription size, you’ll start a new 12-month subscription at that new size.', + note: 'Note: An active member is anyone who has created, edited, submitted, approved, reimbursed, or exported expense data tied to your company workspace.', + confirmDetails: 'Confirm your new annual subscription details', + subscriptionSize: 'Subscription size', + activeMembers: ({size}) => `${size} active members/month`, + subscriptionRenews: 'Subscription renews', + youCantDowngrade: 'You can’t downgrade during your annual subscription', + youAlreadyCommitted: ({size, date}) => + `You already committed to an annual subscription size of ${size} active members per month until ${date}. You can switch to a pay-per-use subscription on ${date} by disabling auto-renew.`, + error: { + size: 'Please enter a valid subscription size.', + }, + }, + }, } satisfies TranslationBase; diff --git a/src/languages/es.ts b/src/languages/es.ts index 3eba60c5ef8e..2cb905d3d258 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3649,4 +3649,23 @@ export default { systemMessage: { mergedWithCashTransaction: 'encontró un recibo para esta transacción.', }, + subscription: { + 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.', + eachMonth: + 'Cada mes, tu suscripción cubre hasta el número de miembros activos establecido anteriormente. Cada vez que aumentes el tamaño de tu suscripción, iniciarás una nueva suscripción de 12 meses con ese nuevo tamaño.', + note: 'Nota: Un miembro activo es cualquiera que haya creado, editado, enviado, aprobado, reembolsado, o exportado datos de gastos vinculados al espacio de trabajo de tu empresa.', + confirmDetails: 'Confirma los datos de tu nueva suscripción anual', + subscriptionSize: 'Tamaño de suscripción', + activeMembers: ({size}) => `${size} miembros activos/mes`, + subscriptionRenews: 'Renovación de la suscripción', + youCantDowngrade: 'No puedes bajar de categoría durante tu suscripción anual', + youAlreadyCommitted: ({size, date}) => + `Ya se ha comprometido a un tamaño de suscripción anual de ${size} miembros activos al mes hasta el ${date}. Puede cambiar a una suscripción de pago por uso en ${date} desactivando la auto-renovación.`, + error: { + size: 'Por favor ingrese un tamaño de suscripción valido.', + }, + }, + }, } satisfies EnglishTranslation; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index dad9178aaf45..67890a132d2d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -222,6 +222,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/CustomStatus/StatusClearAfterPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: () => require('../../../../pages/settings/Profile/CustomStatus/SetDatePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: () => require('../../../../pages/settings/Profile/CustomStatus/SetTimePage').default as React.ComponentType, + [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: () => require('../../../../pages/settings/Subscription/SubscriptionSize/SubscriptionSizePage').default as React.ComponentType, [SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage').default as React.ComponentType, [SCREENS.WORKSPACE.RATE_AND_UNIT_RATE]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage').default as React.ComponentType, [SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 154ab63aad77..4e77edeaa633 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -39,7 +39,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER], [SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE], [SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP], - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [], + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.SIZE], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2bd3642ce912..b19fbc4c38e0 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -277,6 +277,9 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: { path: ROUTES.SETTINGS_STATUS_CLEAR_AFTER_TIME, }, + [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_SIZE, + }, [SCREENS.WORKSPACE.CURRENCY]: { path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route, }, diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 13fe326c2c1c..19e35e236e24 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -480,6 +480,14 @@ function isExistingTaxName(taxName: string, taxRates: TaxRates): boolean { return !!Object.values(taxRates).find((taxRate) => taxRate.name === trimmedTaxName); } +/** + * Validates the given value if it is correct subscription size. + */ +function isValidSubscriptionSize(subscriptionSize: string): boolean { + const parsedSubscriptionSize = Number(subscriptionSize); + return !Number.isNaN(parsedSubscriptionSize) && parsedSubscriptionSize > 0 && parsedSubscriptionSize <= CONST.SUBSCRIPTION_SIZE_LIMIT; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -521,4 +529,5 @@ export { isValidPercentage, isValidReportName, isExistingTaxName, + isValidSubscriptionSize, }; diff --git a/src/pages/settings/Subscription/SubscriptionSize/SubscriptionSizePage.tsx b/src/pages/settings/Subscription/SubscriptionSize/SubscriptionSizePage.tsx new file mode 100644 index 000000000000..4589d04a774d --- /dev/null +++ b/src/pages/settings/Subscription/SubscriptionSize/SubscriptionSizePage.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Confirmation from './substeps/Confirmation'; +import Size from './substeps/Size'; + +const bodyContent: Array> = [Size, Confirmation]; + +function SubscriptionSizePage() { + const [subscriptionSizeFormDraft] = useOnyx(ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM_DRAFT); + const {translate} = useLocalize(); + // TODO startFrom variable will get it's value based on ONYX data, it will be implemented in next phase (account?.canDowngrade field) + const CAN_DOWNGRADE = true; + const startFrom = CAN_DOWNGRADE ? 0 : 1; + + const onFinished = () => { + if (CAN_DOWNGRADE) { + // TODO this is temporary solution for the time being, API call will be implemented in next phase + // eslint-disable-next-line no-console + console.log(subscriptionSizeFormDraft); + return; + } + + Navigation.goBack(); + }; + + const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom, onFinished}); + + const onBackButtonPress = () => { + if (screenIndex !== 0 && startFrom === 0) { + prevScreen(); + return; + } + + Navigation.goBack(); + }; + + return ( + + + + + ); +} + +SubscriptionSizePage.displayName = 'SubscriptionSizePage'; + +export default SubscriptionSizePage; diff --git a/src/pages/settings/Subscription/SubscriptionSize/substeps/Confirmation.tsx b/src/pages/settings/Subscription/SubscriptionSize/substeps/Confirmation.tsx new file mode 100644 index 000000000000..b54f21726b6f --- /dev/null +++ b/src/pages/settings/Subscription/SubscriptionSize/substeps/Confirmation.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getNewSubscriptionRenewalDate} from '@pages/settings/Subscription/SubscriptionSize/utils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/SubscriptionSizeForm'; + +type ConfirmationProps = SubStepProps; + +function Confirmation({onNext}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const [subscriptionSizeFormDraft] = useOnyx(ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM_DRAFT); + const subscriptionRenewalDate = getNewSubscriptionRenewalDate(); + + // TODO this is temporary and will be replaced in next phase once data in ONYX is ready + // we will have to check if the amount of active members is less than the current amount of active members and if account?.canDowngrade is true - if so then we can't downgrade + const CAN_DOWNGRADE = true; + // TODO this is temporary and will be replaced in next phase once data in ONYX is ready + const SUBSCRIPTION_UNTIL = subscriptionRenewalDate; + + return ( + + {CAN_DOWNGRADE ? ( + <> + {translate('subscription.subscriptionSize.confirmDetails')} + + + + ) : ( + <> + {translate('subscription.subscriptionSize.youCantDowngrade')} + + {translate('subscription.subscriptionSize.youAlreadyCommitted', { + size: subscriptionSizeFormDraft ? subscriptionSizeFormDraft[INPUT_IDS.SUBSCRIPTION_SIZE] : 0, + date: SUBSCRIPTION_UNTIL, + })} + + + )} + +