Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,19 @@ const ONYXKEYS = {
/** Information about loading states while talking with AI sales */
TALK_TO_AI_SALES: 'talkToAISales',

/** Set when we are loading bill when downgrade */
IS_LOADING_BILL_WHEN_DOWNGRADE: 'isLoadingBillWhenDowngrade',

/**
* Determines whether billing is required when the user downgrades their plan.
* If true, the "Pay & Downgrade" RHP will be displayed to guide the user
* through the payment process before downgrading.
*/
SHOULD_BILL_WHEN_DOWNGRADING: 'shouldBillWhenDowngrading',
Comment thread
blimpich marked this conversation as resolved.

/** Billing receipt details */
BILLING_RECEIPT_DETAILS: 'billingReceiptDetails',

/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
Expand Down Expand Up @@ -1124,6 +1137,9 @@ type OnyxValuesMapping = {
[ONYXKEYS.CORPAY_ONBOARDING_FIELDS]: OnyxTypes.CorpayOnboardingFields;
[ONYXKEYS.LAST_FULL_RECONNECT_TIME]: string;
[ONYXKEYS.TRAVEL_PROVISIONING]: OnyxTypes.TravelProvisioning;
[ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE]: boolean | undefined;
[ONYXKEYS.SHOULD_BILL_WHEN_DOWNGRADING]: boolean | undefined;
[ONYXKEYS.BILLING_RECEIPT_DETAILS]: OnyxTypes.BillingReceiptDetails;
[ONYXKEYS.NVP_SIDE_PANEL]: OnyxTypes.SidePanel;
};

Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,10 @@ const ROUTES = {
getRoute: (policyID?: string, backTo?: string) =>
getUrlWithBackToParam(policyID ? (`settings/workspaces/${policyID}/downgrade/` as const) : (`settings/workspaces/downgrade` as const), backTo),
},
WORKSPACE_PAY_AND_DOWNGRADE: {
route: 'settings/workspaces/pay-and-downgrade/',
getRoute: (backTo?: string) => getUrlWithBackToParam(`settings/workspaces/pay-and-downgrade` as const, backTo),
},
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ const SCREENS = {
DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit',
UPGRADE: 'Workspace_Upgrade',
DOWNGRADE: 'Workspace_Downgrade',
PAY_AND_DOWNGRADE: 'Workspace_Pay_And_Downgrade',
RULES: 'Policy_Rules',
RULES_CUSTOM_NAME: 'Rules_Custom_Name',
RULES_AUTO_APPROVE_REPORTS_UNDER: 'Rules_Auto_Approve_Reports_Under',
Expand Down
3 changes: 3 additions & 0 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type PopoverMenuItem = MenuItemProps & {

key?: string;

/** Whether to keep the modal open after clicking on the menu item */
shouldKeepModalOpen?: boolean;

/** Test identifier used to find elements in unit and e2e tests */
testID?: string;
};
Expand Down
16 changes: 13 additions & 3 deletions src/components/ThreeDotsMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import {getButtonRole} from '@components/Button/utils';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
Expand Down Expand Up @@ -38,8 +39,9 @@ function ThreeDotsMenu({
renderProductTrainingTooltipContent,
shouldShowProductTrainingTooltip = false,
isNested = false,
threeDotsMenuRef,
}: ThreeDotsMenuProps) {
const [modal] = useOnyx(ONYXKEYS.MODAL);
const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: true});

const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -53,10 +55,18 @@ function ThreeDotsMenu({
setPopupMenuVisible(true);
};

const hidePopoverMenu = () => {
const hidePopoverMenu = (selectedItem?: PopoverMenuItem) => {
if (selectedItem && selectedItem.shouldKeepModalOpen) {
return;
}
setPopupMenuVisible(false);
};

useImperativeHandle(threeDotsMenuRef, () => ({
isPopupMenuVisible,
hidePopoverMenu,
}));

useEffect(() => {
if (!isBehindModal || !isPopupMenuVisible) {
return;
Expand Down
3 changes: 3 additions & 0 deletions src/components/ThreeDotsMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type ThreeDotsMenuProps = {

/** Is the menu nested? This prop is used to omit html warning when we are nesting a button inside another button */
isNested?: boolean;

/** Ref to the menu */
threeDotsMenuRef?: React.RefObject<{hidePopoverMenu: () => void; isPopupMenuVisible: boolean}>;
};

type ThreeDotsMenuWithOptionalAnchorProps =
Expand Down
33 changes: 33 additions & 0 deletions src/hooks/usePayAndDowngrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useEffect, useRef} from 'react';
import {useOnyx} from 'react-native-onyx';
import Navigation from '@libs/Navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

function usePayAndDowngrade(setIsDeleteModalOpen: (value: boolean) => void) {
const [isLoadingBill] = useOnyx(ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE, {canBeMissing: true});
const [shouldBillWhenDowngrading] = useOnyx(ONYXKEYS.SHOULD_BILL_WHEN_DOWNGRADING, {canBeMissing: true});
const isDeletingPaidWorkspaceRef = useRef(false);

const setIsDeletingPaidWorkspace = (value: boolean) => {
isDeletingPaidWorkspaceRef.current = value;
};

useEffect(() => {
if (!isDeletingPaidWorkspaceRef.current || isLoadingBill) {
return;
}

if (!shouldBillWhenDowngrading) {
setIsDeleteModalOpen(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line created a bug. More details - #61040

} else {
Navigation.navigate(ROUTES.WORKSPACE_PAY_AND_DOWNGRADE.getRoute(Navigation.getActiveRoute()));
}

isDeletingPaidWorkspaceRef.current = false;
}, [isLoadingBill, shouldBillWhenDowngrading, setIsDeleteModalOpen]);

return {setIsDeletingPaidWorkspace, isLoadingBill};
}

export default usePayAndDowngrade;
9 changes: 9 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4800,6 +4800,15 @@ const translations = {
gotIt: 'Got it, thanks',
},
},
payAndDowngrade: {
title: 'Pay & downgrade',
headline: 'Your final payment',
description1: 'Your final bill for this subscription will be',
description2: ({date}: DateParams) => `See your breakdown below for ${date}:`,
subscription:
'This will end your subscription with Expensify, delete your remaining workspace and all members will lose access moving forward. If you want to remove just yourself, have another admin take over billing, and at that point, you can remove yourself from this workspace.',
genericFailureMessage: 'An error occurred while paying your bill. Please try again.',
},
restrictedAction: {
restricted: 'Restricted',
actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Actions on the ${workspaceName} workspace are currently restricted`,
Expand Down
9 changes: 9 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4869,6 +4869,15 @@ const translations = {
gotIt: 'Entendido, gracias.',
},
},
payAndDowngrade: {
title: 'Pagar y bajar de categoría',
headline: 'Tu pago final',
description1: 'Tu factura final por esta suscripción será',
description2: ({date}: DateParams) => `Consulta el desglose a continuación para ${date}:`,
subscription:
'Esto finalizará tu suscripción a Expensify, eliminará tu espacio de trabajo restante y todos los miembros perderán acceso de ahora en adelante. Si solo deseas eliminarte a ti mismo, haz que otro administrador se encargue de la facturación, y en ese momento podrás salir de este espacio de trabajo.',
genericFailureMessage: 'Ocurrió un error al pagar tu factura. Por favor, inténtalo de nuevo.',
},
restrictedAction: {
restricted: 'Restringido',
actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Las acciones en el espacio de trabajo ${workspaceName} están actualmente restringidas`,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ const WRITE_COMMANDS = {
CHANGE_REPORT_POLICY: 'ChangeReportPolicy',
CHANGE_TRANSACTIONS_REPORT: 'ChangeTransactionsReport',
SEND_RECAP_IN_ADMINS_ROOM: 'SendRecapInAdminsRoom',
PAY_AND_DOWNGRADE: 'PayAndDowngrade',
COMPLETE_CONCIERGE_CALL: 'CompleteConciergeCall',
} as const;

Expand Down Expand Up @@ -952,6 +953,8 @@ type WriteCommandParameters = {
// Change report policy
[WRITE_COMMANDS.CHANGE_REPORT_POLICY]: Parameters.ChangeReportPolicyParams;

[WRITE_COMMANDS.PAY_AND_DOWNGRADE]: null;

// Change transaction report
[WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT]: Parameters.ChangeTransactionsReportParams;
};
Expand Down Expand Up @@ -1023,6 +1026,7 @@ const READ_COMMANDS = {
GET_CORPAY_ONBOARDING_FIELDS: 'GetCorpayOnboardingFields',
OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage',
OPEN_SECURITY_SETTINGS_PAGE: 'OpenSecuritySettingsPage',
CALCULATE_BILL_NEW_DOT: 'CalculateBillNewDot',
} as const;

type ReadCommand = ValueOf<typeof READ_COMMANDS>;
Expand Down Expand Up @@ -1094,6 +1098,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.GET_CORPAY_ONBOARDING_FIELDS]: Parameters.GetCorpayOnboardingFieldsParams;
[READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams;
[READ_COMMANDS.OPEN_SECURITY_SETTINGS_PAGE]: null;
[READ_COMMANDS.CALCULATE_BILL_NEW_DOT]: null;
};

const SIDE_EFFECT_REQUEST_COMMANDS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: () => require<ReactComponentModule>('../../../../pages/workspace/categories/ImportedCategoriesPage').default,
[SCREENS.WORKSPACE.UPGRADE]: () => require<ReactComponentModule>('../../../../pages/workspace/upgrade/WorkspaceUpgradePage').default,
[SCREENS.WORKSPACE.DOWNGRADE]: () => require<ReactComponentModule>('../../../../pages/workspace/downgrade/WorkspaceDowngradePage').default,
[SCREENS.WORKSPACE.PAY_AND_DOWNGRADE]: () => require<ReactComponentModule>('../../../../pages/workspace/downgrade/PayAndDowngradePage').default,
[SCREENS.WORKSPACE.MEMBER_DETAILS]: () => require<ReactComponentModule>('../../../../pages/workspace/members/WorkspaceMemberDetailsPage').default,
[SCREENS.WORKSPACE.MEMBER_NEW_CARD]: () => require<ReactComponentModule>('../../../../pages/workspace/members/WorkspaceMemberNewCardPage').default,
[SCREENS.WORKSPACE.OWNER_CHANGE_CHECK]: () => require<ReactComponentModule>('@pages/workspace/members/WorkspaceOwnerChangeWrapperPage').default,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.WORKSPACE.DOWNGRADE]: {
path: ROUTES.WORKSPACE_DOWNGRADE.route,
},
[SCREENS.WORKSPACE.PAY_AND_DOWNGRADE]: {
path: ROUTES.WORKSPACE_PAY_AND_DOWNGRADE.route,
},
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ type SettingsNavigatorParamList = {
policyID?: string;
backTo?: Routes;
};
[SCREENS.WORKSPACE.PAY_AND_DOWNGRADE]: {
policyID?: string;
};
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
backTo?: Routes;
Expand Down
14 changes: 14 additions & 0 deletions src/libs/SubscriptionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ Onyx.connect({
waitForCollectionCallback: true,
});

// Indicates if downgrading the current subscription plan is allowed for the user.
let canDowngrade = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NAB: comment improvement

/** Indicates if downgrading the current subscription plan is allowed for the user. */

Onyx.connect({
key: ONYXKEYS.ACCOUNT,
callback: (val) => {
canDowngrade = val?.canDowngrade ?? false;
},
});

/**
* @returns The date when the grace period ends.
*/
Expand Down Expand Up @@ -569,6 +578,10 @@ function shouldRestrictUserBillableActions(policyID: string): boolean {
return false;
}

function shouldCalculateBillNewDot(): boolean {
return canDowngrade && getOwnedPaidPolicies(allPolicies, currentUserAccountID).length === 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@dukenv0307 @blimpich, Just confirming—canDowngrade will be true if the user has at least 3 purchases, and we can rely on this value, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can downgrade doesn't concern the 3 purchases logic, it has other logic. The three purchases logic is covered by the shouldBillWhenDowngrading variable.

}

function checkIfHasTeam2025Pricing() {
if (hasManualTeamPricing2025) {
return true;
Expand Down Expand Up @@ -676,6 +689,7 @@ export {
shouldShowPreTrialBillingBanner,
shouldShowDiscountBanner,
getEarlyDiscountInfo,
shouldCalculateBillNewDot,
getSubscriptionPlanInfo,
getSubscriptionPrice,
};
75 changes: 75 additions & 0 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5080,6 +5080,78 @@ function getAssignedSupportData(policyID: string) {
API.read(READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA, parameters);
}

/**
* Call the API to calculate the bill for the new dot
*/
function calculateBillNewDot() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let’s add a brief comment for the calculateBillNewDot and payAndDowngrade APIs Commands.

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE,
value: true,
},
];
const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE,
value: false,
},
];
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE,
value: false,
},
];
API.read(READ_COMMANDS.CALCULATE_BILL_NEW_DOT, null, {
optimisticData,
successData,
failureData,
});
}

/**
* Call the API to pay and downgrade
*/
function payAndDowngrade() {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.BILLING_RECEIPT_DETAILS,
value: {
errors: null,
isLoading: true,
},
},
];
const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.BILLING_RECEIPT_DETAILS,
value: {
isLoading: false,
},
},
];

const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.BILLING_RECEIPT_DETAILS,
value: {
isLoading: false,
},
},
];
API.write(WRITE_COMMANDS.PAY_AND_DOWNGRADE, null, {optimisticData, successData, failureData});
}

function clearBillingReceiptDetailsErrors() {
Onyx.merge(ONYXKEYS.BILLING_RECEIPT_DETAILS, {errors: null});
}

export {
leaveWorkspace,
addBillingCardAndRequestPolicyOwnerChange,
Expand Down Expand Up @@ -5180,6 +5252,9 @@ export {
updateDefaultPolicy,
getAssignedSupportData,
downgradeToTeam,
calculateBillNewDot,
payAndDowngrade,
clearBillingReceiptDetailsErrors,
clearQuickbooksOnlineAutoSyncErrorField,
updateLastAccessedWorkspaceSwitcher,
};
Expand Down
Loading