diff --git a/assets/images/calendar-solid.svg b/assets/images/calendar-solid.svg
new file mode 100644
index 000000000000..168aabaa4a49
--- /dev/null
+++ b/assets/images/calendar-solid.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index e03f42b282e4..e354ba663923 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -5332,6 +5332,12 @@ const CONST = {
},
EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[],
+
+ CANCELLATION_TYPE: {
+ MANUAL: 'manual',
+ AUTOMATIC: 'automatic',
+ NONE: 'none',
+ },
EMPTY_STATE_MEDIA: {
ANIMATION: 'animation',
ILLUSTRATION: 'illustration',
@@ -5400,7 +5406,8 @@ type IOURequestType = ValueOf;
type FeedbackSurveyOptionID = ValueOf, 'ID'>>;
type SubscriptionType = ValueOf;
+type CancellationType = ValueOf;
-export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID};
+export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType};
export default CONST;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 62c32e15f3b6..615d0217f0d4 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -385,6 +385,8 @@ const ONYXKEYS = {
/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',
+ NVP_PRIVATE_CANCELLATION_DETAILS: 'nvp_private_cancellationDetails',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -850,6 +852,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_BILLING_FUND_ID]: number;
[ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number;
[ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number;
+ [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[];
};
type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index ae7781318c52..3bda25d4699b 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -123,6 +123,7 @@ const ROUTES = {
SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency',
SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY: 'settings/subscription/add-payment-card/change-payment-currency',
SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey',
+ SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION: 'settings/subscription/request-early-cancellation-survey',
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 d7d56089876d..44ac2677db68 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -111,6 +111,7 @@ const SCREENS = {
DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey',
CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency',
CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency',
+ REQUEST_EARLY_CANCELLATION: 'Settings_Subscription_RequestEarlyCancellation',
},
},
SAVE_THE_WORLD: {
diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx
index a7b0732be1fb..db5db3ee447d 100644
--- a/src/components/FeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey.tsx
@@ -25,6 +25,15 @@ type FeedbackSurveyProps = {
/** Styles for the option row element */
optionRowStyles?: StyleProp;
+
+ /** Optional text to render over the submit button */
+ footerText?: React.ReactNode;
+
+ /** Indicates whether note field is required */
+ isNoteRequired?: boolean;
+
+ /** Indicates whether a loading indicator should be shown */
+ isLoading?: boolean;
};
type Option = {
@@ -39,7 +48,7 @@ const OPTIONS: Option[] = [
{key: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY},
];
-function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: FeedbackSurveyProps) {
+function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading}: FeedbackSurveyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
@@ -55,7 +64,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: Feedbac
};
const handleSubmit = () => {
- if (!reason) {
+ if (!reason || (isNoteRequired && !note.trim())) {
setShouldShowReasonError(true);
return;
}
@@ -89,12 +98,15 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: Feedbac
)}
+ {!!footerText && footerText}
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 487df5594212..0b9306d1c977 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -27,6 +27,7 @@ import Bolt from '@assets/images/bolt.svg';
import Briefcase from '@assets/images/briefcase.svg';
import Bug from '@assets/images/bug.svg';
import Building from '@assets/images/building.svg';
+import CalendarSolid from '@assets/images/calendar-solid.svg';
import Calendar from '@assets/images/calendar.svg';
import Camera from '@assets/images/camera.svg';
import CarWithKey from '@assets/images/car-with-key.svg';
@@ -372,4 +373,5 @@ export {
CheckCircle,
CheckmarkCircle,
NetSuiteSquare,
+ CalendarSolid,
};
diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts
new file mode 100644
index 000000000000..bc34f5feea6f
--- /dev/null
+++ b/src/hooks/useCancellationType.ts
@@ -0,0 +1,43 @@
+import {useEffect, useMemo, useRef, useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {CancellationType} from '@src/CONST';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+function useCancellationType(): CancellationType | undefined {
+ const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS);
+
+ const [cancellationType, setCancellationType] = useState();
+
+ // Store initial cancellation details array in a ref for comparison
+ const previousCancellationDetails = useRef(cancellationDetails);
+
+ const memoizedCancellationType = useMemo(() => {
+ const pendingManualCancellation = cancellationDetails?.filter((detail) => detail.cancellationType === CONST.CANCELLATION_TYPE.MANUAL).find((detail) => !detail.cancellationDate);
+
+ // There is a pending manual cancellation - return manual cancellation type
+ if (pendingManualCancellation) {
+ return CONST.CANCELLATION_TYPE.MANUAL;
+ }
+
+ // There are no new items in the cancellation details NVP
+ if (previousCancellationDetails.current?.length === cancellationDetails?.length) {
+ return;
+ }
+
+ // There is a new item in the cancellation details NVP, it has to be an automatic cancellation, as pending manual cancellations are handled above
+ return CONST.CANCELLATION_TYPE.AUTOMATIC;
+ }, [cancellationDetails]);
+
+ useEffect(() => {
+ if (!memoizedCancellationType) {
+ return;
+ }
+
+ setCancellationType(memoizedCancellationType);
+ }, [memoizedCancellationType]);
+
+ return cancellationType;
+}
+
+export default useCancellationType;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 204fc71d8923..ea7acfcbda6a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -4224,6 +4224,33 @@ export default {
whatsMainReason: "What's the main reason you're disabling auto-renew?",
renewsOn: ({date}) => `Renews on ${date}.`,
},
+ requestEarlyCancellation: {
+ title: 'Request early cancellation',
+ subtitle: 'What’s the main reason you’re requesting early cancellation?',
+ subscriptionCanceled: {
+ title: 'Subscription canceled',
+ subtitle: 'Your annual subscription has been canceled.',
+ info: 'If you want to keep using your workspace(s) on a pay-per-use basis, you’re all set.',
+ preventFutureActivity: {
+ part1: 'If you’d like to prevent future activity and charges, you must ',
+ link: 'delete your workspace(s)',
+ part2: '. Note that when you delete your workspace(s), you’ll be charged for any outstanding activity that was incurred during the current calendar month.',
+ },
+ },
+ requestSubmitted: {
+ title: 'Request submitted',
+ subtitle: {
+ part1: 'Thanks for letting us know you’re interested in canceling your subscription. We’re reviewing your request and will be in touch soon via your chat with ',
+ link: 'Concierge',
+ part2: '.',
+ },
+ },
+ acknowledgement: {
+ part1: 'By requesting early cancellation, I acknowledge and agree that Expensify has no obligation to grant such request under the Expensify ',
+ link: 'Terms of Service',
+ part2: ' or other applicable services agreement between me and Expensify and that Expensify retains sole discretion with regard to granting any such request.',
+ },
+ },
},
feedbackSurvey: {
tooLimited: 'Functionality needs improvement',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 934e0de335a2..53d5f8a8dc26 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -4747,6 +4747,33 @@ export default {
whatsMainReason: '¿Cuál es la razón principal por la que deseas desactivar la auto-renovación?',
renewsOn: ({date}) => `Se renovará el ${date}.`,
},
+ requestEarlyCancellation: {
+ title: 'Solicitar cancelación anticipada',
+ subtitle: '¿Cuál es la razón principal por la que solicitas la cancelación anticipada?',
+ subscriptionCanceled: {
+ title: 'Suscripción cancelada',
+ subtitle: 'Tu suscripción anual ha sido cancelada.',
+ info: 'Ya puedes seguir utilizando tu(s) espacio(s) de trabajo en la modalidad de pago por uso.',
+ preventFutureActivity: {
+ part1: 'Si quieres evitar actividad y cargos futuros, debes ',
+ link: 'eliminar tu(s) espacio(s) de trabajo.',
+ part2: ' Ten en cuenta que cuando elimines tu(s) espacio(s) de trabajo, se te cobrará cualquier actividad pendienteque se haya incurrido durante el mes en curso.',
+ },
+ },
+ requestSubmitted: {
+ title: 'Solicitud enviada',
+ subtitle: {
+ part1: 'Gracias por hacernos saber que deseas cancelar tu suscripción. Estamos revisando tu solicitud y nos comunicaremos contigo en breve a través de tu chat con ',
+ link: 'Concierge',
+ part2: '.',
+ },
+ },
+ acknowledgement: {
+ part1: 'Al solicitar la cancelación anticipada, reconozco y acepto que Expensify no tiene ninguna obligación de conceder dicha solicitud en virtud de las ',
+ link: 'Condiciones de Servicio',
+ part2: ' de Expensify u otro acuerdo de servicios aplicable entre Expensify y yo, y que Expensify se reserva el derecho exclusivo a conceder dicha solicitud.',
+ },
+ },
},
feedbackSurvey: {
tooLimited: 'Hay que mejorar la funcionalidad',
diff --git a/src/libs/API/parameters/CancelBillingSubscriptionParams.ts b/src/libs/API/parameters/CancelBillingSubscriptionParams.ts
new file mode 100644
index 000000000000..252a7420d27d
--- /dev/null
+++ b/src/libs/API/parameters/CancelBillingSubscriptionParams.ts
@@ -0,0 +1,8 @@
+import type {FeedbackSurveyOptionID} from '@src/CONST';
+
+type CancelBillingSubscriptionParams = {
+ cancellationReason: FeedbackSurveyOptionID;
+ cancellationNote: string;
+};
+
+export default CancelBillingSubscriptionParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 0b3b0e1ece93..997eb415a848 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -260,6 +260,7 @@ export type {default as RemoveWorkspaceReportFieldListValueParams} from './Remov
export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams';
export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams';
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
+export type {default as CancelBillingSubscriptionParams} from './CancelBillingSubscriptionParams';
export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams';
export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams';
export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 5c9d4a9e93b9..70a0e91aba10 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -300,6 +300,7 @@ const WRITE_COMMANDS = {
UPDATE_SAGE_INTACCT_SYNC_REIMBURSEMENT_ACCOUNT_ID: 'UpdateSageIntacctSyncReimbursementAccountID',
CONNECT_POLICY_TO_NETSUITE: 'ConnectPolicyToNetSuite',
CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance',
+ CANCEL_BILLING_SUBSCRIPTION: 'CancelBillingSubscriptionNewDot',
UPDATE_SAGE_INTACCT_ENTITY: 'UpdateSageIntacctEntity',
UPDATE_SAGE_INTACCT_BILLABLE: 'UpdateSageIntacctBillable',
UPDATE_SAGE_INTACCT_DEPARTMENT_MAPPING: 'UpdateSageIntacctDepartmentsMapping',
@@ -532,6 +533,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.MARK_AS_EXPORTED]: Parameters.MarkAsExportedParams;
[WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE]: Parameters.RequestExpensifyCardLimitIncreaseParams;
[WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE]: null;
+ [WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION]: Parameters.CancelBillingSubscriptionParams;
[WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams;
[WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index d22efe973eeb..b612d4a3f0cd 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -218,6 +218,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/CustomStatus/SetTimePage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: () => require('../../../../pages/settings/Subscription/SubscriptionSize').default,
[SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: () => require('../../../../pages/settings/Subscription/DisableAutoRenewSurveyPage').default,
+ [SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION]: () => require('../../../../pages/settings/Subscription/RequestEarlyCancellationPage').default,
[SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage').default,
[SCREENS.WORKSPACE.RATE_AND_UNIT_RATE]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage').default,
[SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage').default,
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 c6f3aa2f17b5..8aa259b769b0 100755
--- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts
@@ -43,6 +43,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> =
SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD,
SCREENS.SETTINGS.SUBSCRIPTION.SIZE,
SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY,
+ SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION,
SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY,
SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY,
],
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 77fafb9fba4d..47a9d3f70d6a 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -294,6 +294,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: {
path: ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY,
},
+ [SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION]: {
+ path: ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION,
+ },
[SCREENS.WORKSPACE.CURRENCY]: {
path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route,
},
diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts
index beed2b1b2962..aaec38dbf5f8 100644
--- a/src/libs/actions/Subscription.ts
+++ b/src/libs/actions/Subscription.ts
@@ -1,7 +1,7 @@
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
-import type {UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters';
+import type {CancelBillingSubscriptionParams, UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import CONST from '@src/CONST';
import type {FeedbackSurveyOptionID, SubscriptionType} from '@src/CONST';
@@ -279,6 +279,15 @@ function clearOutstandingBalance() {
API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData);
}
+function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, cancellationNote: string) {
+ const parameters: CancelBillingSubscriptionParams = {
+ cancellationReason,
+ cancellationNote,
+ };
+
+ API.write(WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION, parameters);
+}
+
export {
openSubscriptionPage,
updateSubscriptionAutoRenew,
@@ -287,4 +296,5 @@ export {
clearUpdateSubscriptionSizeError,
updateSubscriptionType,
clearOutstandingBalance,
+ cancelBillingSubscription,
};
diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx
index d139339da39a..ccf838127a5e 100644
--- a/src/pages/settings/Subscription/CardSection/CardSection.tsx
+++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx
@@ -29,6 +29,7 @@ import TrialEndedBillingBanner from './BillingBanner/TrialEndedBillingBanner';
import TrialStartedBillingBanner from './BillingBanner/TrialStartedBillingBanner';
import CardSectionActions from './CardSectionActions';
import CardSectionDataEmpty from './CardSectionDataEmpty';
+import RequestEarlyCancellationMenuItem from './RequestEarlyCancellationMenuItem';
import type {BillingStatusResult} from './utils';
import CardSectionUtils from './utils';
@@ -129,7 +130,6 @@ function CardSection() {
{isEmptyObject(defaultCard?.accountData) && }
-
{billingStatus?.isRetryAvailable !== undefined && (