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

feat: Payment card subscription size screen UI #42683

Merged
3 changes: 3 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,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;

Expand Down Expand Up @@ -533,6 +535,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 = {
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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',
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const SCREENS = {

SUBSCRIPTION: {
ROOT: 'Settings_Subscription',
SIZE: 'Settings_Subscription_Size',
},
},
SAVE_THE_WORLD: {
Expand Down
19 changes: 19 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3106,4 +3106,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;
19 changes: 19 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3610,4 +3610,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.',
MrMuzyk marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
} satisfies EnglishTranslation;
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: () => 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial<Record<CentralPaneName, string[]>> =
[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;
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ const config: LinkingOptions<RootStackParamList>['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,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<React.ComponentType<SubStepProps>> = [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 (
<ScreenWrapper
testID={SubscriptionSizePage.displayName}
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('subscription.subscriptionSize.title')}
onBackButtonPress={onBackButtonPress}
/>
<SubStep
isEditing={isEditing}
onNext={nextScreen}
onMove={moveTo}
/>
</ScreenWrapper>
);
}

SubscriptionSizePage.displayName = 'SubscriptionSizePage';

export default SubscriptionSizePage;
Original file line number Diff line number Diff line change
@@ -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 (
<View style={[styles.flexGrow1]}>
{CAN_DOWNGRADE ? (
<>
<Text style={[styles.ph5, styles.pb3]}>{translate('subscription.subscriptionSize.confirmDetails')}</Text>
<MenuItemWithTopDescription
interactive={false}
description={translate('subscription.subscriptionSize.subscriptionSize')}
title={translate('subscription.subscriptionSize.activeMembers', {size: subscriptionSizeFormDraft ? subscriptionSizeFormDraft[INPUT_IDS.SUBSCRIPTION_SIZE] : 0})}
/>
<MenuItemWithTopDescription
interactive={false}
description={translate('subscription.subscriptionSize.subscriptionRenews')}
title={subscriptionRenewalDate}
/>
</>
) : (
<>
<Text style={[styles.ph5, styles.pb5, styles.textNormalThemeText]}>{translate('subscription.subscriptionSize.youCantDowngrade')}</Text>
<Text style={[styles.ph5, styles.textLabel]}>
{translate('subscription.subscriptionSize.youAlreadyCommitted', {
size: subscriptionSizeFormDraft ? subscriptionSizeFormDraft[INPUT_IDS.SUBSCRIPTION_SIZE] : 0,
date: SUBSCRIPTION_UNTIL,
})}
</Text>
</>
)}
<FixedFooter style={[styles.mtAuto]}>
<Button
isDisabled={isOffline}
success
large
onPress={onNext}
text={translate(CAN_DOWNGRADE ? 'common.save' : 'common.close')}
/>
</FixedFooter>
</View>
);
}

Confirmation.displayName = 'ConfirmationStep';

export default Confirmation;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import {View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import type {SubStepProps} from '@hooks/useSubStep/types';
import useThemeStyles from '@hooks/useThemeStyles';
import {validate} from '@pages/settings/Subscription/SubscriptionSize/utils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/SubscriptionSizeForm';

type SizeProps = SubStepProps;

function Size({onNext}: SizeProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();

const defaultValues = {
// TODO this is temporary and default value will be replaced in next phase once data in ONYX is ready
[INPUT_IDS.SUBSCRIPTION_SIZE]: '0',
};

return (
<FormProvider
formID={ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM}
submitButtonText={translate('common.next')}
onSubmit={onNext}
validate={validate}
style={[styles.mh5, styles.flexGrow1]}
>
<View>
<Text style={[styles.textNormalThemeText, styles.mb5]}>{translate('subscription.subscriptionSize.yourSize')}</Text>
<InputWrapper
InputComponent={TextInput}
inputID={INPUT_IDS.SUBSCRIPTION_SIZE}
label={translate('subscription.subscriptionSize.subscriptionSize')}
aria-label={translate('subscription.subscriptionSize.subscriptionSize')}
role={CONST.ROLE.PRESENTATION}
defaultValue={defaultValues[INPUT_IDS.SUBSCRIPTION_SIZE]}
shouldSaveDraft
/>
<Text style={[styles.textLabel, styles.mt5]}>{translate('subscription.subscriptionSize.eachMonth')}</Text>
<Text style={[styles.textLabel, styles.mt5]}>{translate('subscription.subscriptionSize.note')}</Text>
</View>
</FormProvider>
);
}

Size.displayName = 'SizeStep';

export default Size;
19 changes: 19 additions & 0 deletions src/pages/settings/Subscription/SubscriptionSize/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {addMonths, format, startOfMonth} from 'date-fns';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import type ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/SubscriptionSizeForm';

const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM> => {
const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.SUBSCRIPTION_SIZE]);
if (values[INPUT_IDS.SUBSCRIPTION_SIZE] && !ValidationUtils.isNumeric(values[INPUT_IDS.SUBSCRIPTION_SIZE])) {
MrMuzyk marked this conversation as resolved.
Show resolved Hide resolved
errors.subscriptionSize = 'subscription.subscriptionSize.error.size';
}

return errors;
};

const getNewSubscriptionRenewalDate = (): string => format(startOfMonth(addMonths(new Date(), 11)), CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const getNewSubscriptionRenewalDate = (): string => format(startOfMonth(addMonths(new Date(), 11)), CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT);
const getNewSubscriptionRenewalDate = (): string => format(startOfMonth(addMonths(new Date(), 12)), CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT);

12 months matches the backend logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing it. I've found 11 in doc. Must've been a mistake then
Screenshot 2024-06-03 at 09 59 56


export {validate, getNewSubscriptionRenewalDate};
13 changes: 13 additions & 0 deletions src/types/form/SubscriptionSizeForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {ValueOf} from 'type-fest';
import type Form from './Form';

const INPUT_IDS = {
SUBSCRIPTION_SIZE: 'subscriptionSize',
} as const;

type InputID = ValueOf<typeof INPUT_IDS>;

type SubscriptionSizeForm = Form<InputID, {[INPUT_IDS.SUBSCRIPTION_SIZE]: string}>;

export type {SubscriptionSizeForm};
export default INPUT_IDS;
1 change: 1 addition & 0 deletions src/types/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm';
export type {WalletAdditionalDetailsForm} from './WalletAdditionalDetailsForm';
export type {NewChatNameForm} from './NewChatNameForm';
export type {WorkForm} from './WorkForm';
export type {SubscriptionSizeForm} from './SubscriptionSizeForm';
export type {default as Form} from './Form';
Loading