diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index fb67a0682599..aa2205be8425 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -397,13 +397,14 @@ function calculateRemainingFreeTrialDays(lastDayFreeTrial: string | undefined): * @returns The free trial badge text . */ function getFreeTrialText( + currentUserAccountID: number | undefined, translate: LocalizedTranslate, policies: OnyxCollection | null, introSelected: OnyxEntry, firstDayFreeTrial: string | undefined, lastDayFreeTrial: string | undefined, ): string | undefined { - const ownedPaidPolicies = getOwnedPaidPolicies(policies, deprecatedCurrentUserAccountID); + const ownedPaidPolicies = getOwnedPaidPolicies(policies, currentUserAccountID); if (isEmptyObject(ownedPaidPolicies)) { return undefined; } @@ -585,6 +586,7 @@ function getSubscriptionPlanInfo( } function shouldShowTrialEndedUI( + currentUserAccountID: number | undefined, lastDayFreeTrial: string | undefined, userBillingFundID: number | undefined, policies: OnyxCollection, @@ -592,7 +594,7 @@ function shouldShowTrialEndedUI( isFromInternalDomain: boolean | undefined, privateSubscriptionType: SubscriptionType | undefined, ): boolean { - if (!getOwnedPaidPolicies(policies, deprecatedCurrentUserAccountID)?.length) { + if (!getOwnedPaidPolicies(policies, currentUserAccountID)?.length) { return false; } if (isGrandfatheredFree || isFromInternalDomain) { diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard.ts index 2e3726a7228f..a0b364785088 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard.ts @@ -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(); @@ -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, diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 4f04e97d3ec5..c52e19f0ca2a 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -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; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 16f5338ce6cc..5c7fa2338eef 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -188,7 +188,7 @@ function CardSection() { BillingBanner = ; } else if (isUserOnFreeTrial(firstDayFreeTrial, lastDayFreeTrial)) { BillingBanner = ; - } 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 = ; } if (billingStatus) { diff --git a/src/pages/settings/Subscription/FreeTrial.tsx b/src/pages/settings/Subscription/FreeTrial.tsx index 6da1ad93a8cf..50f8496714ac 100644 --- a/src/pages/settings/Subscription/FreeTrial.tsx +++ b/src/pages/settings/Subscription/FreeTrial.tsx @@ -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'; @@ -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; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index e856ecabba44..a65612e715f0 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -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, @@ -1185,6 +1187,72 @@ describe('SubscriptionUtils', () => { expect(shouldShowPreTrialBillingBanner(introSelected, firstDayFreeTrial, lastDayFreeTrial)).toBeFalsy(); }); }); + + describe('getFreeTrialText', () => { + 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 = { + 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 = { + 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'; @@ -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(); }); }); }); diff --git a/tests/unit/hooks/useTimeSensitiveAddPaymentCard.test.ts b/tests/unit/hooks/useTimeSensitiveAddPaymentCard.test.ts index 6986911c38a7..d2088d7f8305 100644 --- a/tests/unit/hooks/useTimeSensitiveAddPaymentCard.test.ts +++ b/tests/unit/hooks/useTimeSensitiveAddPaymentCard.test.ts @@ -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'; @@ -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); }); }); });