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
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,8 @@
"RNCORE",
"Wooo",
"Splittable",
"pgrep"
"pgrep",
"Invoicify"
],
"ignorePaths": [
"src/languages/de.ts",
Expand Down
49 changes: 49 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

let allPolicies: OnyxCollection<Policy>;

Onyx.connect({

Check warning on line 63 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand Down Expand Up @@ -1580,6 +1580,54 @@
return (otherControlWorkspaces?.length ?? 0) > 0;
}

/**
* Determines whether workspace deletion should be blocked for an Invoicify user.
*
* The function normalizes owner policies to a flat `Policy[]` regardless of whether
* they are provided as an `OnyxCollection` or a precomputed array. For Invoicify users,
* it ensures that at least one other controllable workspace exists after excluding
* policies with pending ADD or DELETE actions.
*
* The filtering is done lazily to avoid unnecessary work for non-Invoicify users.
*
* @param isSubscriptionTypeOfInvoicing - Whether the user is on an Invoicify subscription
* @param ownerPolicies - Owner policies as an Onyx collection or a normalized array
* @param currentPolicyID - Policy ID of the workspace being evaluated
* @param accountID - Account ID used to resolve owned paid policies when needed
*
*/
function shouldBlockWorkspaceDeletionForInvoicifyUser(
isSubscriptionTypeOfInvoicing: boolean,
ownerPolicies: OnyxCollection<Policy> | Policy[] | undefined,
currentPolicyID: string | undefined,
accountID?: number,
) {
if (!isSubscriptionTypeOfInvoicing || !currentPolicyID) {
return false;
}
const normalizedOwnerPolicies: Policy[] = (() => {
if (!ownerPolicies) {
return [];
}

if (Array.isArray(ownerPolicies)) {
return ownerPolicies;
}

if (accountID) {
return getOwnedPaidPolicies(ownerPolicies, accountID);
}

return Object.values(ownerPolicies).filter((policy): policy is Policy => !!policy);
})();

const ownerPoliciesWithoutPendingActions = (normalizedOwnerPolicies ?? []).filter(
(policy) => policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
);

return !hasOtherControlWorkspaces(ownerPoliciesWithoutPendingActions, currentPolicyID);
}

// If no policyID is provided, it indicates the workspace upgrade/downgrade URL
// is being accessed from the Subscriptions page without a specific policyID.
// In this case, check if the user is an admin on more than one policy.
Expand Down Expand Up @@ -1876,6 +1924,7 @@
getUserFriendlyWorkspaceType,
isPolicyAccessible,
hasOtherControlWorkspaces,
shouldBlockWorkspaceDeletionForInvoicifyUser,
getManagerAccountEmail,
getRuleApprovers,
canModifyPlan,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/actions/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ type CreateWorkspaceWithPolicyDraftParams = {
activePolicyID: string | undefined;
currentUserAccountIDParam: number;
currentUserEmailParam: string;
shouldCreateControlPolicy?: boolean;
};

/**
Expand All @@ -474,6 +475,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(params: CreateWorkspaceWi
activePolicyID,
currentUserAccountIDParam,
currentUserEmailParam,
shouldCreateControlPolicy,
} = params;

const policyIDWithDefault = policyID || generatePolicyID();
Expand All @@ -497,6 +499,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(params: CreateWorkspaceWi
currentUserAccountIDParam,
currentUserEmailParam,
allReportsParam: allReports,
shouldCreateControlPolicy,
});
Navigation.navigate(routeToNavigate, {forceReplace: !transitionFromOldDot});
});
Expand All @@ -515,6 +518,7 @@ type SavePolicyDraftByNewWorkspaceParams = {
currentUserAccountIDParam: number;
currentUserEmailParam: string;
allReportsParam: OnyxCollection<OnyxTypes.Report>;
shouldCreateControlPolicy?: boolean;
};

/**
Expand All @@ -533,6 +537,7 @@ function savePolicyDraftByNewWorkspace({
currentUserAccountIDParam,
currentUserEmailParam,
allReportsParam,
shouldCreateControlPolicy,
}: SavePolicyDraftByNewWorkspaceParams) {
createWorkspace({
policyOwnerEmail,
Expand All @@ -548,6 +553,7 @@ function savePolicyDraftByNewWorkspace({
currentUserAccountIDParam,
currentUserEmailParam,
allReportsParam,
shouldCreateControlPolicy,
});
}

Expand Down
4 changes: 3 additions & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
allReportsParam?: OnyxCollection<Report>;
onboardingPurposeSelected?: OnboardingPurpose;
shouldAddGuideWelcomeMessage?: boolean;
shouldCreateControlPolicy?: boolean;
};

type DuplicatePolicyDataOptions = {
Expand All @@ -209,7 +210,7 @@
};

const deprecatedAllPolicies: OnyxCollection<Policy> = {};
Onyx.connect({

Check warning on line 213 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
callback: (val, key) => {
if (!key) {
Expand All @@ -225,7 +226,7 @@
});

let deprecatedAllReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 229 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -235,7 +236,7 @@

let deprecatedSessionEmail = '';
let deprecatedSessionAccountID = 0;
Onyx.connect({

Check warning on line 239 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (val) => {
deprecatedSessionEmail = val?.email ?? '';
Expand All @@ -244,7 +245,7 @@
});

let deprecatedAllPersonalDetails: OnyxEntry<PersonalDetailsList>;
Onyx.connect({

Check warning on line 248 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (val) => (deprecatedAllPersonalDetails = val),
});
Expand Down Expand Up @@ -2116,6 +2117,7 @@
allReportsParam,
shouldAddGuideWelcomeMessage = true,
onboardingPurposeSelected,
shouldCreateControlPolicy = false,
} = options;
const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);

Expand Down Expand Up @@ -2147,7 +2149,7 @@
// Determine workspace type based on selected features or user reported integration
const isCorporateFeature = featuresMap?.some((feature) => !feature.enabledByDefault && feature.enabled && feature.requiresUpdate) ?? false;
const isCorporateIntegration = userReportedIntegration && (CONST.POLICY.CONNECTIONS.CORPORATE as readonly string[]).includes(userReportedIntegration);
const workspaceType = isCorporateFeature || isCorporateIntegration ? CONST.POLICY.TYPE.CORPORATE : CONST.POLICY.TYPE.TEAM;
const workspaceType = isCorporateFeature || isCorporateIntegration || shouldCreateControlPolicy ? CONST.POLICY.TYPE.CORPORATE : CONST.POLICY.TYPE.TEAM;

const areDistanceRatesEnabled = !!featuresMap?.find((feature) => feature.id === CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED && feature.enabled);

Expand Down
4 changes: 4 additions & 0 deletions src/pages/workspace/WorkspaceConfirmationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm';
import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
import usePrivateSubscription from '@hooks/usePrivateSubscription';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import {createWorkspaceWithPolicyDraftAndNavigateToIt} from '@libs/actions/App';
import {generatePolicyID} from '@libs/actions/Policy/Policy';
import getCurrentUrl from '@libs/Navigation/currentUrl';
import {isSubscriptionTypeOfInvoicing} from '@libs/SubscriptionUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {LastPaymentMethodType} from '@src/types/onyx';
Expand All @@ -21,6 +23,7 @@ function WorkspaceConfirmationPage() {
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const privateSubscription = usePrivateSubscription();
const onSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => {
const policyID = params.policyID || generatePolicyID();
const routeToNavigate = isSmallScreenWidth ? ROUTES.WORKSPACE_INITIAL.getRoute(policyID) : ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID);
Expand All @@ -39,6 +42,7 @@ function WorkspaceConfirmationPage() {
activePolicyID,
currentUserAccountIDParam: currentUserPersonalDetails.accountID,
currentUserEmailParam: currentUserPersonalDetails.email ?? '',
shouldCreateControlPolicy: isSubscriptionTypeOfInvoicing(privateSubscription?.type),
Comment on lines 43 to +45

Choose a reason for hiding this comment

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

P2 Badge Treat missing subscription as non-invoicing

usePrivateSubscription returns undefined while Onyx is still loading, and isSubscriptionTypeOfInvoicing(undefined) is false. That means an Invoicify user who opens this page before NVP_PRIVATE_SUBSCRIPTION is ready (e.g., fresh login or cache miss/offline) will still create a TEAM/Collect workspace, recreating the optimistic-type bug you’re trying to fix. Consider blocking submission until the subscription is loaded or handling the “loading/unknown” state explicitly.

Useful? React with 👍 / 👎.

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 don't think this applies in this case, because this function is only executed when the user clicks the button, not when the page loads, so this data will already be available at the moment of clicking.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need to set shouldCreateControlPolicy here too?

createWorkspaceWithPolicyDraftAndNavigateToIt({

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'm not even entirely sure how to test this functionality, as it seems that this logic is rarely used, and I think that Invoicy users will almost never use it, especially in offline mode, and in the online backend will return the correct data.

In addition, it seems that this useEffect is called once when the page is mounted after the transition, at a time when privateSettings will still be undefined. I'm afraid that if we add any dependencies to this useEffect, we will cause many more bugs than we will gain benefits, because the chance of such a scenario occurring is very small and only applies to offline mode, which, in my opinion, does not justify the risks.

What do you think about this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. Digged in a little deeper to figure out the usage and it’s here.
I too don’t see this being applicable in our invoicify scenario here. Let us leave this as it is.

});
};
const currentUrl = getCurrentUrl();
Expand Down
Loading
Loading