Skip to content
Merged
6 changes: 4 additions & 2 deletions src/libs/SubscriptionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
};

let deprecatedCurrentUserAccountID = -1;
Onyx.connect({

Check warning on line 56 in src/libs/SubscriptionUtils.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: (value) => {
deprecatedCurrentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
Expand All @@ -61,13 +61,13 @@
});

let privateAmountOwed: OnyxEntry<number>;
Onyx.connect({

Check warning on line 64 in src/libs/SubscriptionUtils.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.NVP_PRIVATE_AMOUNT_OWED,
callback: (value) => (privateAmountOwed = value),
});

let deprecatedAllPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 70 in src/libs/SubscriptionUtils.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: (value) => (deprecatedAllPolicies = value),
waitForCollectionCallback: true,
Expand Down Expand Up @@ -397,13 +397,14 @@
* @returns The free trial badge text .
*/
function getFreeTrialText(
currentUserAccountID: number | undefined,
translate: LocalizedTranslate,
policies: OnyxCollection<Policy> | null,
introSelected: OnyxEntry<IntroSelected>,
firstDayFreeTrial: string | undefined,
lastDayFreeTrial: string | undefined,
): string | undefined {
const ownedPaidPolicies = getOwnedPaidPolicies(policies, deprecatedCurrentUserAccountID);
const ownedPaidPolicies = getOwnedPaidPolicies(policies, currentUserAccountID);
if (isEmptyObject(ownedPaidPolicies)) {
return undefined;
}
Expand Down Expand Up @@ -585,14 +586,15 @@
}

function shouldShowTrialEndedUI(
currentUserAccountID: number | undefined,
lastDayFreeTrial: string | undefined,
userBillingFundID: number | undefined,
policies: OnyxCollection<Policy>,
isGrandfatheredFree: boolean | undefined,
isFromInternalDomain: boolean | undefined,
privateSubscriptionType: SubscriptionType | undefined,
): boolean {
if (!getOwnedPaidPolicies(policies, deprecatedCurrentUserAccountID)?.length) {
if (!getOwnedPaidPolicies(policies, currentUserAccountID)?.length) {
return false;
}
if (isGrandfatheredFree || isFromInternalDomain) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {isFromInternalDomainSelector} from '@selectors/Account';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing';
import useOnyx from '@hooks/useOnyx';
import {shouldShowTrialEndedUI} from '@libs/SubscriptionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

function useTimeSensitiveAddPaymentCard() {
const {accountID} = useCurrentUserPersonalDetails();
const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL);
const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID);
const hasTeam2025Pricing = useHasTeam2025Pricing();
Expand All @@ -15,7 +18,8 @@ function useTimeSensitiveAddPaymentCard() {

// Show add payment card for users whose trial ended and haven't added a payment card
const shouldShowAddPaymentCard =
hasTeam2025Pricing && shouldShowTrialEndedUI(lastDayFreeTrial, userBillingFundID, allPolicies, isGrandfatheredFree, isFromInternalDomain, privateSubscription?.type);
hasTeam2025Pricing &&
shouldShowTrialEndedUI(accountID ?? CONST.DEFAULT_NUMBER_ID, lastDayFreeTrial, userBillingFundID, allPolicies, isGrandfatheredFree, isFromInternalDomain, privateSubscription?.type);

return {
shouldShowAddPaymentCard,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT);
const isLoadingTryNewDot = isLoadingOnyxValue(tryNewDotMetadata);

const freeTrialText = getFreeTrialText(translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial);
const freeTrialText = getFreeTrialText(currentUserPersonalDetails.accountID, translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial);

const shouldDisplayLHB = !shouldUseNarrowLayout;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ function CardSection() {
BillingBanner = <PreTrialBillingBanner />;
} else if (isUserOnFreeTrial(firstDayFreeTrial, lastDayFreeTrial)) {
BillingBanner = <TrialStartedBillingBanner />;
} else if (shouldShowTrialEndedUI(lastDayFreeTrial, userBillingFundID, allPolicies, isGrandfatheredFree, account?.isFromInternalDomain, privateSubscription?.type)) {
} else if (shouldShowTrialEndedUI(session?.accountID, lastDayFreeTrial, userBillingFundID, allPolicies, isGrandfatheredFree, account?.isFromInternalDomain, privateSubscription?.type)) {
BillingBanner = <TrialEndedBillingBanner />;
}
if (billingStatus) {
Expand Down
4 changes: 3 additions & 1 deletion src/pages/settings/Subscription/FreeTrial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import Badge from '@components/Badge';
import Button from '@components/Button';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -30,11 +31,12 @@ function FreeTrial({badgeStyles, pressable = false, addSpacing = false, success
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const privateSubscription = usePrivateSubscription();

const {accountID} = useCurrentUserPersonalDetails();
const {isOffline} = useNetwork();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Star']);

const freeTrialText = privateSubscription || isOffline ? getFreeTrialText(translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial) : undefined;
const freeTrialText = privateSubscription || isOffline ? getFreeTrialText(accountID, translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial) : undefined;

if (!freeTrialText) {
return null;
Expand Down
84 changes: 76 additions & 8 deletions tests/unit/SubscriptionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {act} from '@testing-library/react-native';
import {addDays, addMinutes, format as formatDate, getUnixTime, subDays} from 'date-fns';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import {
calculateRemainingFreeTrialDays,
doesUserHavePaymentCardAdded,
getEarlyDiscountInfo,
getFreeTrialText,
getSubscriptionStatus,
hasCardAuthenticatedError,
hasGracePeriodOverdue,
Expand Down Expand Up @@ -1185,6 +1187,72 @@ describe('SubscriptionUtils', () => {
expect(shouldShowPreTrialBillingBanner(introSelected, firstDayFreeTrial, lastDayFreeTrial)).toBeFalsy();
});
});

describe('getFreeTrialText', () => {
Comment thread
hungvu193 marked this conversation as resolved.
const accountID = 42;
const paidPolicyID = '200099';
const ownedPaidPolicies = {
[`${ONYXKEYS.COLLECTION.POLICY}${paidPolicyID}`]: {
...createRandomPolicy(Number(paidPolicyID)),
ownerAccountID: accountID,
type: CONST.POLICY.TYPE.CORPORATE,
},
};

const translate = jest.fn((key: string, ...parameters: unknown[]) => {
const remainingDays = parameters.at(0) as number | undefined;
if (key === 'subscription.billingBanner.trialStarted.title' && remainingDays !== undefined) {
return `trialStarted:${remainingDays}`;
}
if (key === 'subscription.billingBanner.preTrial.title') {
return 'preTrial';
}
return key;
}) as unknown as LocalizedTranslate;

beforeEach(() => {
jest.clearAllMocks();
});

it('returns undefined when the user has no owned paid workspace', () => {
expect(getFreeTrialText(accountID, translate, {}, undefined, undefined, undefined)).toBeUndefined();
expect(translate).not.toHaveBeenCalled();
});

it('returns pre-trial billing copy when the trial has not started and has not ended', () => {
const firstDayFreeTrial = formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
const lastDayFreeTrial = formatDate(addDays(new Date(), 15), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
const introSelected: OnyxEntry<IntroSelected> = {
choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
};

expect(getFreeTrialText(accountID, translate, ownedPaidPolicies, introSelected, firstDayFreeTrial, lastDayFreeTrial)).toBe('preTrial');
expect(translate).toHaveBeenCalledWith('subscription.billingBanner.preTrial.title');
});

it('returns trial-started copy with remaining days when the user is currently on a free trial', () => {
const firstDayFreeTrial = formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
const lastDayFreeTrial = formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
const introSelected: OnyxEntry<IntroSelected> = {
choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
};

const expectedRemainingDays = calculateRemainingFreeTrialDays(lastDayFreeTrial);
const result = getFreeTrialText(accountID, translate, ownedPaidPolicies, introSelected, firstDayFreeTrial, lastDayFreeTrial);

expect(translate).toHaveBeenCalledWith('subscription.billingBanner.trialStarted.title', expectedRemainingDays);
expect(result).toBe(`trialStarted:${expectedRemainingDays}`);
});

it('returns undefined when the free trial has ended', () => {
const firstDayFreeTrial = formatDate(subDays(new Date(), 20), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
const lastDayFreeTrial = formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);

expect(getFreeTrialText(accountID, translate, ownedPaidPolicies, undefined, firstDayFreeTrial, lastDayFreeTrial)).toBeUndefined();
expect(translate).not.toHaveBeenCalled();
});
});

describe('shouldCalculateBillNewDot', () => {
const testUserAccountID = 1; // A consistent account ID for tests
const paidPolicyID = '12345';
Expand Down Expand Up @@ -1320,35 +1388,35 @@ describe('SubscriptionUtils', () => {
});

it('should return true for a regular user whose trial ended, no card, with owned workspace', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, undefined, policies, undefined, undefined, undefined)).toBeTruthy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, undefined, policies, undefined, undefined, undefined)).toBeTruthy();
});

it('should return false if the user has no owned paid policies', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, undefined, {}, undefined, undefined, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, undefined, {}, undefined, undefined, undefined)).toBeFalsy();
});

it('should return false if the user is grandfathered free', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, undefined, policies, true, undefined, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, undefined, policies, true, undefined, undefined)).toBeFalsy();
});

it('should return false if the user is from an internal domain', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, undefined, policies, undefined, true, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, undefined, policies, undefined, true, undefined)).toBeFalsy();
});

it('should return false if the user is on invoiced billing', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, undefined, policies, undefined, undefined, CONST.SUBSCRIPTION.TYPE.INVOICING)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, undefined, policies, undefined, undefined, CONST.SUBSCRIPTION.TYPE.INVOICING)).toBeFalsy();
});

it('should return false if the user has a payment card added', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialEnded, 8010, policies, undefined, undefined, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialEnded, 8010, policies, undefined, undefined, undefined)).toBeFalsy();
});

it('should return false if the trial has not ended yet', () => {
expect(shouldShowTrialEndedUI(lastDayFreeTrialActive, undefined, policies, undefined, undefined, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, lastDayFreeTrialActive, undefined, policies, undefined, undefined, undefined)).toBeFalsy();
});

it('should return false if lastDayFreeTrial is undefined', () => {
expect(shouldShowTrialEndedUI(undefined, undefined, policies, undefined, undefined, undefined)).toBeFalsy();
expect(shouldShowTrialEndedUI(ownerAccountID, undefined, undefined, policies, undefined, undefined, undefined)).toBeFalsy();
});
});
});
3 changes: 2 additions & 1 deletion tests/unit/hooks/useTimeSensitiveAddPaymentCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx';
import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing';
import {shouldShowTrialEndedUI} from '@libs/SubscriptionUtils';
import useTimeSensitiveAddPaymentCard from '@pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates';

Expand Down Expand Up @@ -78,7 +79,7 @@ describe('useTimeSensitiveAddPaymentCard', () => {

renderHook(() => useTimeSensitiveAddPaymentCard());

expect(mockedShouldShowTrialEndedUI).toHaveBeenCalledWith(lastDayFreeTrial, userBillingFundID, {}, undefined, undefined, undefined);
expect(mockedShouldShowTrialEndedUI).toHaveBeenCalledWith(CONST.DEFAULT_NUMBER_ID, lastDayFreeTrial, userBillingFundID, {}, undefined, undefined, undefined);
});
});
});
Loading