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: Subscription settings UI #42990

Merged
merged 20 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
18 changes: 18 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4815,6 +4815,24 @@ const CONST = {
},

SUBSCRIPTION_SIZE_LIMIT: 20000,
FEEDBACK_SURVEY_OPTIONS: {
TOO_LIMITED: {
ID: 'tooLimited',
TRANSLATION_KEY: 'feedbackSurvey.tooLimited',
},
TOO_EXPENSIVE: {
ID: 'tooExpensive',
TRANSLATION_KEY: 'feedbackSurvey.tooExpensive',
},
INADEQUATE_SUPPORT: {
ID: 'inadequateSupport',
TRANSLATION_KEY: 'feedbackSurvey.inadequateSupport',
},
BUSINESS_CLOSING: {
ID: 'businessClosing',
TRANSLATION_KEY: 'feedbackSurvey.businessClosing',
},
},
} as const;

type Country = keyof typeof CONST.ALL_COUNTRIES;
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const ROUTES = {
SETTINGS_SUBSCRIPTION: 'settings/subscription',
SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size',
SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card',
SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey',
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 @@ -108,6 +108,7 @@ const SCREENS = {
ROOT: 'Settings_Subscription',
SIZE: 'Settings_Subscription_Size',
ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card',
DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey',
},
},
SAVE_THE_WORLD: {
Expand Down
90 changes: 90 additions & 0 deletions src/components/FeedbackSurvey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, {useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import FixedFooter from './FixedFooter';
import FormAlertWithSubmitButton from './FormAlertWithSubmitButton';
import SingleOptionSelector from './SingleOptionSelector';
import Text from './Text';

type FeedbackSurveyProps = {
/** Title of the survey */
title: string;

/** Description of the survey */
description: string;

/** Callback to be called when the survey is submitted */
onSubmit: (reason: Option) => void;

/** Styles for the option row element */
optionRowStyles?: StyleProp<ViewStyle>;
};

type Option = {
key: string;
label: TranslationPaths;
};

const OPTIONS: Option[] = [
{key: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.TRANSLATION_KEY},
{key: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.TRANSLATION_KEY},
{key: CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.TRANSLATION_KEY},
{key: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY},
];

function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: FeedbackSurveyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();

const selectCircleStyles: StyleProp<ViewStyle> = {borderColor: theme.border};
const [reason, setReason] = useState<Option>();
const [shouldShowReasonError, setShouldShowReasonError] = useState(false);

const handleOptionSelect = (option: Option) => {
setShouldShowReasonError(false);
setReason(option);
};

const handleSubmit = () => {
if (!reason) {
setShouldShowReasonError(true);
return;
}

onSubmit(reason);
};

return (
<View style={[styles.flexGrow1, styles.justifyContentBetween]}>
<View style={styles.mh5}>
<Text style={styles.textHeadline}>{title}</Text>
<Text style={[styles.mt1, styles.mb3, styles.textNormalThemeText]}>{description}</Text>
<SingleOptionSelector
options={OPTIONS}
optionRowStyles={[styles.mb7, optionRowStyles]}
selectCircleStyles={selectCircleStyles}
selectedOptionKey={reason?.key}
onSelectOption={handleOptionSelect}
/>
</View>
<FixedFooter>
<FormAlertWithSubmitButton
isAlertVisible={shouldShowReasonError}
onSubmit={handleSubmit}
message="common.error.pleaseCompleteForm"
Copy link
Contributor

Choose a reason for hiding this comment

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

Error message here was left hardcoded, it needed translate function. #44075

buttonText={translate('common.submit')}
/>
</FixedFooter>
</View>
);
}

FeedbackSurvey.displayName = 'FeedbackSurvey';

export default FeedbackSurvey;
13 changes: 10 additions & 3 deletions src/components/SingleOptionSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -22,9 +23,15 @@ type SingleOptionSelectorProps = {

/** Function to be called when an option is selected */
onSelectOption?: (item: Item) => void;

/** Styles for the option row element */
optionRowStyles?: StyleProp<ViewStyle>;

/** Styles for the select circle */
selectCircleStyles?: StyleProp<ViewStyle>;
};

function SingleOptionSelector({options = [], selectedOptionKey, onSelectOption = () => {}}: SingleOptionSelectorProps) {
function SingleOptionSelector({options = [], selectedOptionKey, onSelectOption = () => {}, optionRowStyles, selectCircleStyles}: SingleOptionSelectorProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
return (
Expand All @@ -35,7 +42,7 @@ function SingleOptionSelector({options = [], selectedOptionKey, onSelectOption =
key={option.key}
>
<PressableWithoutFeedback
style={styles.singleOptionSelectorRow}
style={[styles.singleOptionSelectorRow, optionRowStyles]}
onPress={() => onSelectOption(option)}
role={CONST.ROLE.BUTTON}
accessibilityState={{checked: selectedOptionKey === option.key}}
Expand All @@ -44,7 +51,7 @@ function SingleOptionSelector({options = [], selectedOptionKey, onSelectOption =
>
<SelectCircle
isChecked={selectedOptionKey ? selectedOptionKey === option.key : false}
selectCircleStyles={[styles.ml0, styles.singleOptionSelectorCircle]}
selectCircleStyles={[styles.ml0, styles.singleOptionSelectorCircle, selectCircleStyles]}
/>
<Text>{translate(option.label)}</Text>
</PressableWithoutFeedback>
Expand Down
20 changes: 20 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export default {
enterAmount: 'Enter an amount.',
enterDate: 'Enter a date.',
invalidTimeRange: 'Please enter a time using the 12-hour clock format (e.g., 2:30 PM).',
pleaseCompleteForm: 'Please complete the form above to continue.',
},
comma: 'comma',
semicolon: 'semicolon',
Expand Down Expand Up @@ -3264,5 +3265,24 @@ export default {
security: 'Expensify is PCI-DSS compliant, uses bank-level encryption, and utilizes redundant infrastructure to protect your data.',
learnMoreAboutSecurity: 'Learn more about our security.',
},
subscriptionSettings: {
title: 'Subscription settings',
autoRenew: 'Auto-renew',
yourAnnual: 'Your annual subscription will automatically renew on (date). You can switch to pay-per-use starting 30 days before renewal.',
Copy link
Contributor

Choose a reason for hiding this comment

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

(date) should be a variable here, equal to nvp_private_subscription.endDate + 1 month

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually this translation key is not used anywhere 😅 It was present in the doc so I've added it but its not used anywhere so Im removing it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will however adjust the renewal date we display under auto-renew toggle

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah okay good catch, I think that initial text was from a previous iteration (I see it in the high level portion of the doc, but not Figma or the mocks in Detailed, so I think we're good to remove it).

autoIncrease: 'Auto-increase annual seats',
saveUpTo: 'Save up to $10/month per active member',
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be slightly variable: it should be 10 for a Collect plan and 18 for a Control plan. I'm also asking in the doc if it should always be in USD or not

Copy link
Contributor

Choose a reason for hiding this comment

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

Just gonna tag you here directly too @MitchExpensify 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be localised to the billing currency, personally. It doesn't make sense if you're billed in GBP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment all prices on main Subscription page have fixed $ currency. Do you want me to fix them all as part of this PR or do we leave the currency as it is for now and change it later in a separate issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's fine to be a follow-up, but let's make sure we do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so should we create new issue for it? If so, who's job is it? :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Polish GH for later follow-up: #43277

automaticallyIncrease:
'Automatically increase your annual seats to accommodate for active members that exceed your subscription size. Note: This will extend your annual subscription end date.',
disableAutoRenew: 'Disable auto-renew',
helpUsImprove: 'Help us improve Expensify',
whatsMainReason: 'What’s the main reason you’re disabling auto-renew on your subscription?',
renewsOn: ({date}) => `Renews on ${date}`,
},
},
feedbackSurvey: {
tooLimited: 'Functionality needs improvement',
tooExpensive: 'Too expensive',
inadequateSupport: 'Inadequate customer support',
businessClosing: 'Company closing, downsizing, or acquired',
},
} satisfies TranslationBase;
20 changes: 20 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export default {
enterAmount: 'Introduce un importe.',
enterDate: 'Introduce una fecha.',
invalidTimeRange: 'Por favor, introduce una hora entre 1 y 12 (por ejemplo, 2:30 PM).',
pleaseCompleteForm: 'Por favor complete el formulario de arriba para continuar..',
},
comma: 'la coma',
semicolon: 'el punto y coma',
Expand Down Expand Up @@ -3771,5 +3772,24 @@ export default {
security: 'Expensify es PCI-DSS obediente, utiliza cifrado a nivel bancario, y emplea infraestructura redundante para proteger tus datos.',
learnMoreAboutSecurity: 'Conozca más sobre nuestra seguridad.',
},
subscriptionSettings: {
title: 'Configuración de suscripción',
autoRenew: 'Auto-renovación',
yourAnnual: 'Tu suscripción anual se renovará el (date). Puedes cambiar a pago-por-uso a partir de los 30 días previos a la renovación.',
autoIncrease: 'Auto-incremento',
saveUpTo: 'Ahorre hasta $10 al mes por miembro activo',
automaticallyIncrease:
'Aumenta automáticamente tus plazas anuales para dar lugar a los miembros activos que superen el tamaño de tu suscripción. Nota: Esto ampliará la fecha de finalización de tu suscripción anual.',
disableAutoRenew: 'Desactivar auto-renovación',
helpUsImprove: 'Ayúdanos a mejorar Expensify',
whatsMainReason: '¿Cuál es la razón principal por la que deseas desactivar la auto-renovación de tu suscripción?',
renewsOn: ({date}) => `Se renovará el ${date}`,
},
},
feedbackSurvey: {
tooLimited: 'Hay que mejorar la funcionalidad',
tooExpensive: 'Demasiado caro',
inadequateSupport: 'Atención al cliente inadecuada',
businessClosing: 'Cierre, reducción, o adquisición de la empresa',
},
} satisfies EnglishTranslation;
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[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.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: () => require('../../../../pages/settings/Subscription/DisableAutoRenewSurveyPage').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.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE],
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE, SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY],
};

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 @@ -284,6 +284,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: {
path: ROUTES.SETTINGS_SUBSCRIPTION_SIZE,
},
[SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: {
path: ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY,
},
[SCREENS.WORKSPACE.CURRENCY]: {
path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
},
Expand Down
43 changes: 43 additions & 0 deletions src/pages/settings/Subscription/DisableAutoRenewSurveyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import FeedbackSurvey from '@components/FeedbackSurvey';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';

function DisableAutoRenewSurveyPage() {
const {translate} = useLocalize();
const styles = useThemeStyles();

const handleSubmit = () => {
// TODO API call to submit feedback will be implemented in next phase
};

return (
<ScreenWrapper
testID={DisableAutoRenewSurveyPage.displayName}
includeSafeAreaPaddingBottom={false}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason to set this to false? It doesn't looks good on iOS without the safe area padding.

Simulator Screenshot - iPhone 15 Pro 17 2 - 2024-06-05 at 22 44 12

Copy link
Contributor

Choose a reason for hiding this comment

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

Could this in theory still be accessed on native via deep link? Should we implement return null for native?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe a better option than returning null would be to display FullPageNotFoundView?

shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('subscription.subscriptionSettings.disableAutoRenew')}
onBackButtonPress={Navigation.goBack}
/>
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.pt3]}>
<FeedbackSurvey
title={translate('subscription.subscriptionSettings.helpUsImprove')}
description={translate('subscription.subscriptionSettings.whatsMainReason')}
onSubmit={handleSubmit}
optionRowStyles={styles.flex1}
/>
</ScrollView>
</ScreenWrapper>
);
}

DisableAutoRenewSurveyPage.displayName = 'DisableAutoRenewSurveyPage';

export default DisableAutoRenewSurveyPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function SubscriptionSettings() {
return null;
}

export default SubscriptionSettings;
Loading
Loading