From 127d350993ddded4c3dc09d4a2c6453b021d528c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 16:57:15 +0100 Subject: [PATCH 01/22] Add NVP_FIRST_DAY_FREE_TRIAL Onyx key --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0d22d3714fe6..99b88df19ee8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -169,6 +169,9 @@ const ONYXKEYS = { /** The NVP with the last action taken (for the Quick Action Button) */ NVP_QUICK_ACTION_GLOBAL_CREATE: 'nvp_quickActionGlobalCreate', + /** The start date of the workspace owner’s free trial period. */ + NVP_FIRST_DAY_FREE_TRIAL: 'nvp_private_firstDayFreeTrial', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -699,6 +702,7 @@ type OnyxValuesMapping = { [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; From 3aaaac125c9378f78caf1dba950a22a735cd8b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 16:58:04 +0100 Subject: [PATCH 02/22] Add NVP_LAST_DAY_FREE_TRIAL Onyx key --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 99b88df19ee8..04f62b0ac7b3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -172,6 +172,9 @@ const ONYXKEYS = { /** The start date of the workspace owner’s free trial period. */ NVP_FIRST_DAY_FREE_TRIAL: 'nvp_private_firstDayFreeTrial', + /** The end date of the workspace owner’s free trial period. */ + NVP_LAST_DAY_FREE_TRIAL: 'nvp_private_lastDayFreeTrial', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -703,6 +706,7 @@ type OnyxValuesMapping = { [ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record; [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; From 12fa1167b8ac92487784b963863620ec1dca456b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 16:59:41 +0100 Subject: [PATCH 03/22] Add NVP_BILLING_FUND_ID Onyx key --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 04f62b0ac7b3..840abf9f571a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -175,6 +175,9 @@ const ONYXKEYS = { /** The end date of the workspace owner’s free trial period. */ NVP_LAST_DAY_FREE_TRIAL: 'nvp_private_lastDayFreeTrial', + /** ID associated with the payment card added by the user. */ + NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -707,6 +710,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE]: OnyxTypes.QuickAction; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; + [ONYXKEYS.NVP_BILLING_FUND_ID]: number; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; From 91affbd223778d2d4081a15c8bb8bd287e7f48fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 17:01:02 +0100 Subject: [PATCH 04/22] Add NVP_PRIVATE_AMOUNT_OWNED Onyx key --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 840abf9f571a..a8e85845f617 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -178,6 +178,9 @@ const ONYXKEYS = { /** ID associated with the payment card added by the user. */ NVP_BILLING_FUND_ID: 'nvp_expensify_billingFundID', + /** The amount owed by the workspace’s owner. */ + NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -711,6 +714,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_BILLING_FUND_ID]: number; + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; From a8f194f3f47b053f54be34c598815fe2a3602f63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 17:02:21 +0100 Subject: [PATCH 05/22] Add NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END Onyx key --- src/ONYXKEYS.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a8e85845f617..35acfb06d1d2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -181,6 +181,9 @@ const ONYXKEYS = { /** The amount owed by the workspace’s owner. */ NVP_PRIVATE_AMOUNT_OWNED: 'nvp_private_amountOwed', + /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ + NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -715,6 +718,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: number; + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; From 9f41e9b471149945f37019fa1f16da95669634b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 13 Jun 2024 17:27:06 +0100 Subject: [PATCH 06/22] Add SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END Onyx key --- src/ONYXKEYS.ts | 5 +++++ src/types/onyx/BillingGraceEndPeriod.ts | 13 +++++++++++++ src/types/onyx/index.ts | 2 ++ 3 files changed, 20 insertions(+) create mode 100644 src/types/onyx/BillingGraceEndPeriod.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 35acfb06d1d2..d5289d97b8ab 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -391,6 +391,10 @@ const ONYXKEYS = { // Search Page related SNAPSHOT: 'snapshot_', + + // Shared NVPs + /** Collection of objects where each objects represents a workspace’s owner which is past due billing AND the user is a member of. */ + SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_' }, /** List of Form ids */ @@ -604,6 +608,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; }; type OnyxValuesMapping = { diff --git a/src/types/onyx/BillingGraceEndPeriod.ts b/src/types/onyx/BillingGraceEndPeriod.ts new file mode 100644 index 000000000000..e66bc92c59df --- /dev/null +++ b/src/types/onyx/BillingGraceEndPeriod.ts @@ -0,0 +1,13 @@ +/** Model of BillingGraceEndPeriod's Shared NVP record */ +type BillingGraceEndPeriod = { + /** The name of the NVP key. */ + name: string; + + /** The permission associated with the NVP key. */ + permissions: string; + + /** The grace period end date (epoch timestamp) of the workspace's owner where the user is a member of. */ + value: number; +}; + +export default BillingGraceEndPeriod; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 5a70ddd48d9e..47e0f34045ec 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -3,6 +3,7 @@ import type AccountData from './AccountData'; import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; +import type BillingGraceEndPeriod from './BillingGraceEndPeriod'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; import type {CardList} from './Card'; @@ -187,4 +188,5 @@ export type { CapturedLogs, SearchResults, PrivateSubscription, + BillingGraceEndPeriod, }; From 3127c9d10db8d94017cad014c99480c1f926baf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 12:19:40 +0100 Subject: [PATCH 07/22] Implement isChatUsedForOnboarding() and getChatUsedForOnboarding() functions --- src/libs/AccountUtils.ts | 1 + src/libs/ReportUtils.ts | 17 ++++++++++++++ tests/unit/ReportUtilsTest.ts | 43 +++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/src/libs/AccountUtils.ts b/src/libs/AccountUtils.ts index 8bc7037e9682..b926e20ca59c 100644 --- a/src/libs/AccountUtils.ts +++ b/src/libs/AccountUtils.ts @@ -5,6 +5,7 @@ import type {Account} from '@src/types/onyx'; const isValidateCodeFormSubmitting = (account: OnyxEntry) => !!account?.isLoading && account.loadingForm === (account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); +/** Whether the accound ID is an odd number, useful for A/B testing. */ const isAccountIDOddNumber = (accountID: number) => accountID % 2 === 1; export default {isValidateCodeFormSubmitting, isAccountIDOddNumber}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 914c653b6f91..5f17b4125f14 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -58,6 +58,7 @@ import type {Comment, Receipt, TransactionChanges, WaypointCollection} from '@sr import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; +import AccountUtils from './AccountUtils'; import * as IOU from './actions/IOU'; import * as PolicyActions from './actions/Policy/Policy'; import * as store from './actions/ReimbursementAccount/store'; @@ -6946,6 +6947,20 @@ function shouldShowMerchantColumn(transactions: Transaction[]) { return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? {})); } +/** + * Whether the report is a system chat or concierge chat, depending on the user's account ID. + */ +function isChatUsedForOnboarding(report: OnyxEntry): boolean { + return AccountUtils.isAccountIDOddNumber(currentUserAccountID ?? -1) ? isSystemChat(report) : isConciergeChatReport(report); +} + +/** + * Get the report (system or concierge chat) used for the user's onboarding process. + */ +function getChatUsedForOnboarding(): OnyxEntry { + return Object.values(allReports ?? {}).find(isChatUsedForOnboarding); +} + export { addDomainToShortMention, areAllRequestsBeingSmartScanned, @@ -7217,6 +7232,8 @@ export { isDraftReport, changeMoneyRequestHoldStatus, createDraftWorkspaceAndNavigateToConfirmationScreen, + isChatUsedForOnboarding, + getChatUsedForOnboarding, }; export type { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 326e020464e0..c678a6fed6cc 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -821,4 +821,47 @@ describe('ReportUtils', () => { expect(ReportUtils.getAllAncestorReportActions(reports[4])).toEqual(resultAncestors); }); }); + + describe('isChatUsedForOnboarding', () => { + afterAll(() => Onyx.clear()); + + it('should return true if the user account ID is odd and report is the system chat', async () => { + const accountID = 1; + + return Onyx.multiSet({ + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [accountID]: { + accountID, + }, + }, + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID}, + }).then(() => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); + }); + }); + + it('should return true if the user account ID is even and report is the concierge chat', async () => { + const accountID = 2; + + return Onyx.multiSet({ + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [accountID]: { + accountID, + }, + }, + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID}, + }).then(() => { + const report: Report = { + ...LHNTestUtils.getFakeReport([CONST.ACCOUNT_ID.CONCIERGE]), + }; + + expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); + }); + }); + }); }); From 4eb8d41bcb69d7a4fa8dc06e0066bf65fe535937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 12:19:56 +0100 Subject: [PATCH 08/22] Add descriptive comment to isSystemChat() function --- src/libs/ReportUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5f17b4125f14..f53e625295d0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1035,12 +1035,15 @@ function isGroupChat(report: OnyxEntry | Partial): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.GROUP; } +/** + * Only returns true if this is the Expensify DM report. + */ function isSystemChat(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.SYSTEM; } /** - * Only returns true if this is our main 1:1 DM report with Concierge + * Only returns true if this is our main 1:1 DM report with Concierge. */ function isConciergeChatReport(report: OnyxInputOrEntry): boolean { const participantAccountIDs = Object.keys(report?.participants ?? {}) From 92ba0c8986336d82c3304daebfa325c37a7ebdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 17:19:52 +0100 Subject: [PATCH 09/22] Implement SubscriptionUtils.calculateRemainingFreeTrialDays() function --- src/ONYXKEYS.ts | 4 ++-- src/libs/SubscriptionUtils.ts | 22 +++++++++++++++++++ tests/unit/SubscriptionUtilsTest.ts | 33 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/libs/SubscriptionUtils.ts create mode 100644 tests/unit/SubscriptionUtilsTest.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d5289d97b8ab..bcaa1fe0e0e1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -169,10 +169,10 @@ const ONYXKEYS = { /** The NVP with the last action taken (for the Quick Action Button) */ NVP_QUICK_ACTION_GLOBAL_CREATE: 'nvp_quickActionGlobalCreate', - /** The start date of the workspace owner’s free trial period. */ + /** The start date (yyyy-MM-dd HH:mm:ss) of the workspace owner’s free trial period. */ NVP_FIRST_DAY_FREE_TRIAL: 'nvp_private_firstDayFreeTrial', - /** The end date of the workspace owner’s free trial period. */ + /** The end date (yyyy-MM-dd HH:mm:ss) of the workspace owner’s free trial period. */ NVP_LAST_DAY_FREE_TRIAL: 'nvp_private_lastDayFreeTrial', /** ID associated with the payment card added by the user. */ diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts new file mode 100644 index 000000000000..022fe4cd6fe2 --- /dev/null +++ b/src/libs/SubscriptionUtils.ts @@ -0,0 +1,22 @@ +import {differenceInCalendarDays, parse as parseDate} from 'date-fns'; +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +let lastDayFreeTrial: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, + callback: (value) => (lastDayFreeTrial = value), +}); + +function calculateRemainingFreeTrialDays(): number { + if (!lastDayFreeTrial) { + return 0; + } + + const difference = differenceInCalendarDays(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()), new Date()); + return difference < 0 ? 0 : difference; +} + +export {calculateRemainingFreeTrialDays}; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts new file mode 100644 index 000000000000..c990bb0a6324 --- /dev/null +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -0,0 +1,33 @@ +import {addDays, format as formatDate, subDays} from 'date-fns'; +import Onyx from 'react-native-onyx'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +Onyx.init({keys: ONYXKEYS}); + +describe('SubscriptionUtils', () => { + describe('calculateRemainingFreeTrialDays', () => { + afterEach(() => Onyx.clear()); + + it('should return 0 if the Onyx key is not set', () => { + expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(0); + }); + + it('should return 0 if the current date is after the free trial end date', async () => { + const date = formatDate(subDays(new Date(), 8), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, date); + + expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(0); + }); + + it('should return the remaining days if the current date is before the free trial end date', async () => { + const date = formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); + + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, date); + + expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(5); + }); + }); +}); From ce591a307fc83422ac20a93781ea3f89293d8ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 17:20:08 +0100 Subject: [PATCH 10/22] Improve ReportUtilsTest tests --- tests/unit/ReportUtilsTest.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index c678a6fed6cc..f9a815345693 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -823,45 +823,45 @@ describe('ReportUtils', () => { }); describe('isChatUsedForOnboarding', () => { - afterAll(() => Onyx.clear()); + afterEach(() => Onyx.clear()); it('should return true if the user account ID is odd and report is the system chat', async () => { const accountID = 1; - return Onyx.multiSet({ + await Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [accountID]: { accountID, }, }, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID}, - }).then(() => { - const report: Report = { - ...LHNTestUtils.getFakeReport(), - chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, - }; - - expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); }); + + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); }); it('should return true if the user account ID is even and report is the concierge chat', async () => { const accountID = 2; - return Onyx.multiSet({ + await Onyx.multiSet({ [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [accountID]: { accountID, }, }, [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID}, - }).then(() => { - const report: Report = { - ...LHNTestUtils.getFakeReport([CONST.ACCOUNT_ID.CONCIERGE]), - }; - - expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); }); + + const report: Report = { + ...LHNTestUtils.getFakeReport([CONST.ACCOUNT_ID.CONCIERGE]), + }; + + expect(ReportUtils.isChatUsedForOnboarding(report)).toBeTruthy(); }); }); }); From ac22ed8f661fe31bf4d2f012380b7d3e1f166f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 17:32:41 +0100 Subject: [PATCH 11/22] Implement SubscriptionUtils.isUserOnFreeTrial() function --- src/libs/SubscriptionUtils.ts | 28 ++++++++++++++++++++++++++-- tests/unit/SubscriptionUtilsTest.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 022fe4cd6fe2..fb63cf8b2d9e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -1,15 +1,24 @@ -import {differenceInCalendarDays, parse as parseDate} from 'date-fns'; +import {differenceInCalendarDays, isAfter, isBefore, parse as parseDate} from 'date-fns'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +let firstDayFreeTrial: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, + callback: (value) => (firstDayFreeTrial = value), +}); + let lastDayFreeTrial: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, callback: (value) => (lastDayFreeTrial = value), }); +/** + * Calculates the remaining number of days of the workspace owner's free trial before it ends. + */ function calculateRemainingFreeTrialDays(): number { if (!lastDayFreeTrial) { return 0; @@ -19,4 +28,19 @@ function calculateRemainingFreeTrialDays(): number { return difference < 0 ? 0 : difference; } -export {calculateRemainingFreeTrialDays}; +/** + * Whether the workspace's owner is on its free trial period. + */ +function isUserOnFreeTrial(): boolean { + if (!firstDayFreeTrial || !lastDayFreeTrial) { + return true; + } + + const currentDate = new Date(); + const firstDayFreeTrialDate = parseDate(firstDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); + const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); + + return isAfter(currentDate, firstDayFreeTrialDate) && isBefore(currentDate, lastDayFreeTrialDate); +} + +export {calculateRemainingFreeTrialDays, isUserOnFreeTrial}; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index c990bb0a6324..4f1ab09254e4 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -30,4 +30,30 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(5); }); }); + + describe('isUserOnFreeTrial', () => { + afterEach(() => Onyx.clear()); + + it('should return true if the Onyx keys are not set', () => { + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + }); + + it('should return true if the current date is between the free trial start and end dates', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + }); + + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + }); + + it('should return false if the current date is after the free trial end date', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 10), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + }); + + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeFalsy(); + }); + }); }); From c012ed3978aabaf7cfc1902efc1ae307d62257f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 18:01:31 +0100 Subject: [PATCH 12/22] Implement SubscriptionUtils.hasUserFreeTrialEnded() function --- src/libs/SubscriptionUtils.ts | 16 +++++++++- tests/unit/SubscriptionUtilsTest.ts | 49 +++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index fb63cf8b2d9e..0dce8c01e3f3 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -43,4 +43,18 @@ function isUserOnFreeTrial(): boolean { return isAfter(currentDate, firstDayFreeTrialDate) && isBefore(currentDate, lastDayFreeTrialDate); } -export {calculateRemainingFreeTrialDays, isUserOnFreeTrial}; +/** + * Whether the workspace owner's free trial period has ended. + */ +function hasUserFreeTrialEnded(): boolean { + if (!lastDayFreeTrial) { + return false; + } + + const currentDate = new Date(); + const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); + + return isAfter(currentDate, lastDayFreeTrialDate); +} + +export {calculateRemainingFreeTrialDays, isUserOnFreeTrial, hasUserFreeTrialEnded}; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 4f1ab09254e4..4c0f4c03e32e 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -8,31 +8,36 @@ Onyx.init({keys: ONYXKEYS}); describe('SubscriptionUtils', () => { describe('calculateRemainingFreeTrialDays', () => { - afterEach(() => Onyx.clear()); + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: null, + }); + }); it('should return 0 if the Onyx key is not set', () => { expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(0); }); it('should return 0 if the current date is after the free trial end date', async () => { - const date = formatDate(subDays(new Date(), 8), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); - - await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, date); - + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(subDays(new Date(), 8), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(0); }); it('should return the remaining days if the current date is before the free trial end date', async () => { - const date = formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING); - - await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, date); - + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(5); }); }); describe('isUserOnFreeTrial', () => { - afterEach(() => Onyx.clear()); + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: null, + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: null, + }); + }); it('should return true if the Onyx keys are not set', () => { expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); @@ -56,4 +61,28 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.isUserOnFreeTrial()).toBeFalsy(); }); }); + + describe('hasUserFreeTrialEnded', () => { + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: null, + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: null, + }); + }); + + it('should return false if the Onyx key is not set', () => { + expect(SubscriptionUtils.hasUserFreeTrialEnded()).toBeFalsy(); + }); + + it('should return false if the current date is before the free trial end date', async () => { + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(addDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); + expect(SubscriptionUtils.hasUserFreeTrialEnded()).toBeFalsy(); + }); + + it('should return true if the current date is after the free trial end date', async () => { + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); + expect(SubscriptionUtils.hasUserFreeTrialEnded()).toBeTruthy(); + }); + }); }); From ccd5758b2ebb94079187fc18ac590d01f0a57d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 18:09:14 +0100 Subject: [PATCH 13/22] Implement SubscriptionUtils.doesUserHavePaymentCardAdded() function --- src/libs/SubscriptionUtils.ts | 15 ++++++++++++++- tests/unit/SubscriptionUtilsTest.ts | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 0dce8c01e3f3..881b89e97df6 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -16,6 +16,12 @@ Onyx.connect({ callback: (value) => (lastDayFreeTrial = value), }); +let userBillingFundID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_BILLING_FUND_ID, + callback: (value) => (userBillingFundID = value), +}); + /** * Calculates the remaining number of days of the workspace owner's free trial before it ends. */ @@ -57,4 +63,11 @@ function hasUserFreeTrialEnded(): boolean { return isAfter(currentDate, lastDayFreeTrialDate); } -export {calculateRemainingFreeTrialDays, isUserOnFreeTrial, hasUserFreeTrialEnded}; +/** + * Whether the user has a payment card added to its account. + */ +function doesUserHavePaymentCardAdded(): boolean { + return userBillingFundID !== undefined; +} + +export {calculateRemainingFreeTrialDays, isUserOnFreeTrial, hasUserFreeTrialEnded, doesUserHavePaymentCardAdded}; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 4c0f4c03e32e..ae0436d59f23 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -85,4 +85,22 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.hasUserFreeTrialEnded()).toBeTruthy(); }); }); + + describe('doesUserHavePaymentCardAdded', () => { + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.NVP_BILLING_FUND_ID]: null, + }); + }); + + it('should return false if the Onyx key is not set', () => { + expect(SubscriptionUtils.doesUserHavePaymentCardAdded()).toBeFalsy(); + }); + + it('should return true if the Onyx key is set', async () => { + await Onyx.set(ONYXKEYS.NVP_BILLING_FUND_ID, 8010); + expect(SubscriptionUtils.doesUserHavePaymentCardAdded()).toBeTruthy(); + }); + }); }); From 5fa1f176a1c6eb85838f11e1f36cc23d6b8eb1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 22:59:38 +0100 Subject: [PATCH 14/22] Implement SubscriptionUtils.shouldRestrictUserBillableActions() function --- src/libs/SubscriptionUtils.ts | 63 ++++++++++++++++- tests/unit/SubscriptionUtilsTest.ts | 101 +++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 881b89e97df6..8c234385ac70 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -1,8 +1,9 @@ -import {differenceInCalendarDays, isAfter, isBefore, parse as parseDate} from 'date-fns'; -import type {OnyxEntry} from 'react-native-onyx'; +import {differenceInCalendarDays, fromUnixTime, isAfter, isBefore, parse as parseDate} from 'date-fns'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {BillingGraceEndPeriod, Policy} from '@src/types/onyx'; let firstDayFreeTrial: OnyxEntry; Onyx.connect({ @@ -22,6 +23,32 @@ Onyx.connect({ callback: (value) => (userBillingFundID = value), }); +let userBillingGraceEndPeriodCollection: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END, + callback: (value) => (userBillingGraceEndPeriodCollection = value), + waitForCollectionCallback: true, +}); + +let ownerBillingGraceEndPeriod: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, + callback: (value) => (ownerBillingGraceEndPeriod = value), +}); + +let amountOwed: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED, + callback: (value) => (amountOwed = value), +}); + +let allPolicies: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (value) => (allPolicies = value), + waitForCollectionCallback: true, +}); + /** * Calculates the remaining number of days of the workspace owner's free trial before it ends. */ @@ -70,4 +97,34 @@ function doesUserHavePaymentCardAdded(): boolean { return userBillingFundID !== undefined; } -export {calculateRemainingFreeTrialDays, isUserOnFreeTrial, hasUserFreeTrialEnded, doesUserHavePaymentCardAdded}; +/** + * Whether the user's billable actions should be restricted. + */ +function shouldRestrictUserBillableActions(policyID: string): boolean { + // This logic will be executed if the user is a workspace's non-owner (normal user or admin). + // We should restrict the workspace's non-owner actions if it's member of a workspace where the owner is + // past due and is past its grace period end. + for (const userBillingGraceEndPeriodEntry of Object.entries(userBillingGraceEndPeriodCollection ?? {})) { + const [entryKey, userBillingGracePeriodEnd] = userBillingGraceEndPeriodEntry; + + if (userBillingGracePeriodEnd && isAfter(new Date(), fromUnixTime(userBillingGracePeriodEnd.value))) { + const ownerPolicy = Object.values(allPolicies ?? {}).find( + (policy) => policy?.id === policyID && String(policy.ownerAccountID ?? -1) === entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length), + ); + + if (ownerPolicy) { + return true; + } + } + } + + // If it reached here it means that the user is actually the workspace's owner. + // We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount. + if (ownerBillingGraceEndPeriod && amountOwed !== undefined && isAfter(new Date(), fromUnixTime(ownerBillingGraceEndPeriod))) { + return true; + } + + return false; +} + +export {calculateRemainingFreeTrialDays, doesUserHavePaymentCardAdded, hasUserFreeTrialEnded, isUserOnFreeTrial, shouldRestrictUserBillableActions}; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index ae0436d59f23..8e4c99c5151f 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -1,8 +1,16 @@ -import {addDays, format as formatDate, subDays} from 'date-fns'; +import {addDays, format as formatDate, getUnixTime, subDays} from 'date-fns'; import Onyx from 'react-native-onyx'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {BillingGraceEndPeriod} from '@src/types/onyx'; +import createRandomPolicy from '../utils/collections/policies'; + +const billingGraceEndPeriod: BillingGraceEndPeriod = { + name: 'owner@email.com', + permissions: 'read', + value: 0, +}; Onyx.init({keys: ONYXKEYS}); @@ -103,4 +111,95 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.doesUserHavePaymentCardAdded()).toBeTruthy(); }); }); + + describe('shouldRestrictUserBillableActions', () => { + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: null, + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: null, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: null, + [ONYXKEYS.COLLECTION.POLICY]: null, + }); + }); + + it("should return false if the user isn't a workspace's owner or isn't a member of any past due billing workspace", () => { + expect(SubscriptionUtils.shouldRestrictUserBillableActions('1')).toBeFalsy(); + }); + + it('should return false if the user is a non-owner of a workspace that is not in the shared NVP collection', async () => { + const policyID = '1001'; + const ownerAccountID = 2001; + + await Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END}${ownerAccountID}` as const]: { + ...billingGraceEndPeriod, + value: getUnixTime(subDays(new Date(), 3)), // past due + }, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: 2002, // owner not in the shared NVP collection + }, + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); + }); + + it("should return false if the user is a workspace's non-owner that is not past due billing", async () => { + const policyID = '1001'; + const ownerAccountID = 2001; + + await Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END}${ownerAccountID}` as const]: { + ...billingGraceEndPeriod, + value: getUnixTime(addDays(new Date(), 3)), // not past due + }, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID, // owner in the shared NVP collection + }, + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); + }); + + it("should return true if the user is a workspace's non-owner that is past due billing", async () => { + const policyID = '1001'; + const ownerAccountID = 2001; + + await Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END}${ownerAccountID}` as const]: { + ...billingGraceEndPeriod, + value: getUnixTime(subDays(new Date(), 3)), // past due + }, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID, // owner in the shared NVP collection + }, + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); + }); + + it('should return false if the user is a workspace owner but is not past due billing', async () => { + const policyID = '1001'; + + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(addDays(new Date(), 3)), // not past due + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); + }); + + it('should return true if the user is a workspace owner but is past due billing', async () => { + const policyID = '1001'; + + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, // owing some amount + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy(); + }); + }); }); From 8e27a71b20b8619f8892939ae28f5a823a02f68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 23:06:04 +0100 Subject: [PATCH 15/22] Improve shouldRestrictUserBillableActions() logic --- src/libs/SubscriptionUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 8c234385ac70..93811de6fffe 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -108,11 +108,11 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { const [entryKey, userBillingGracePeriodEnd] = userBillingGraceEndPeriodEntry; if (userBillingGracePeriodEnd && isAfter(new Date(), fromUnixTime(userBillingGracePeriodEnd.value))) { - const ownerPolicy = Object.values(allPolicies ?? {}).find( - (policy) => policy?.id === policyID && String(policy.ownerAccountID ?? -1) === entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length), - ); + // Extracts the owner account ID from the collection member key. + const ownerAccountID = entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length); - if (ownerPolicy) { + const ownerPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + if (String(ownerPolicy?.ownerAccountID ?? -1) === ownerAccountID) { return true; } } From 52074be1e8a71c3fde14ee2a81421b19c216ff25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 23:22:43 +0100 Subject: [PATCH 16/22] Fix TS errors --- src/libs/actions/Policy/Member.ts | 17 +++---- src/libs/actions/Policy/Policy.ts | 83 +++++++++++++++++-------------- src/libs/actions/Report.ts | 37 +++++++------- 3 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index fc28a01b043c..74d1831f32b9 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -247,8 +247,8 @@ function removeMembers(accountIDs: number[], policyID: string) { key: policyKey, value: {employeeList: optimisticMembersState}, }, - ...announceRoomMembers.onyxOptimisticData, ]; + optimisticData.push(...announceRoomMembers.onyxOptimisticData); const successData: OnyxUpdate[] = [ { @@ -256,8 +256,8 @@ function removeMembers(accountIDs: number[], policyID: string) { key: policyKey, value: {employeeList: successMembersState}, }, - ...announceRoomMembers.onyxSuccessData, ]; + successData.push(...announceRoomMembers.onyxSuccessData); const failureData: OnyxUpdate[] = [ { @@ -265,8 +265,8 @@ function removeMembers(accountIDs: number[], policyID: string) { key: policyKey, value: {employeeList: failureMembersState}, }, - ...announceRoomMembers.onyxFailureData, ]; + failureData.push(...announceRoomMembers.onyxFailureData); const pendingChatMembers = ReportUtils.getPendingChatMembers(accountIDs, [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); @@ -551,10 +551,8 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount employeeList: optimisticMembersState, }, }, - ...newPersonalDetailsOnyxData.optimisticData, - ...membersChats.onyxOptimisticData, - ...announceRoomMembers.onyxOptimisticData, ]; + optimisticData.push(...newPersonalDetailsOnyxData.optimisticData, ...membersChats.onyxOptimisticData, ...announceRoomMembers.onyxOptimisticData); const successData: OnyxUpdate[] = [ { @@ -564,10 +562,8 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount employeeList: successMembersState, }, }, - ...newPersonalDetailsOnyxData.finallyData, - ...membersChats.onyxSuccessData, - ...announceRoomMembers.onyxSuccessData, ]; + successData.push(...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxSuccessData, ...announceRoomMembers.onyxSuccessData); const failureData: OnyxUpdate[] = [ { @@ -580,9 +576,8 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount employeeList: failureMembersState, }, }, - ...membersChats.onyxFailureData, - ...announceRoomMembers.onyxFailureData, ]; + failureData.push(...membersChats.onyxFailureData, ...announceRoomMembers.onyxFailureData); const params: AddMembersToWorkspaceParams = { employees: JSON.stringify(logins.map((login) => ({email: login}))), diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8f82ca5881fc..23bdfaf67e96 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2197,8 +2197,8 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string pendingAction: null, }, }, - ...employeeWorkspaceChat.onyxOptimisticData, ]; + optimisticData.push(...employeeWorkspaceChat.onyxOptimisticData); const successData: OnyxUpdate[] = [ { @@ -2634,49 +2634,56 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { }; const policy = getPolicy(policyID); const shouldAddDefaultTaxRatesData = (!policy?.taxRates || isEmptyObject(policy.taxRates)) && enabled; - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - tax: { - trackingEnabled: enabled, - }, - pendingFields: { - tax: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + tax: { + trackingEnabled: enabled, + }, + pendingFields: { + tax: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, - ...(shouldAddDefaultTaxRatesData ? taxRatesData.optimisticData ?? [] : []), - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - pendingFields: { - tax: null, - }, + }, + ]; + optimisticData.push(...(shouldAddDefaultTaxRatesData ? taxRatesData.optimisticData ?? [] : [])); + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + tax: null, }, }, - ...(shouldAddDefaultTaxRatesData ? taxRatesData.successData ?? [] : []), - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - tax: { - trackingEnabled: !enabled, - }, - pendingFields: { - tax: null, - }, + }, + ]; + successData.push(...(shouldAddDefaultTaxRatesData ? taxRatesData.successData ?? [] : [])); + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + tax: { + trackingEnabled: !enabled, + }, + pendingFields: { + tax: null, }, }, - ...(shouldAddDefaultTaxRatesData ? taxRatesData.failureData ?? [] : []), - ], + }, + ]; + failureData.push(...(shouldAddDefaultTaxRatesData ? taxRatesData.failureData ?? [] : [])); + + const onyxData: OnyxData = { + optimisticData, + successData, + failureData, }; const parameters: EnablePolicyTaxesParams = {policyID, enabled}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3adf48046936..01557b21897a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2734,8 +2734,8 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails pendingChatMembers, }, }, - ...newPersonalDetailsOnyxData.optimisticData, ]; + optimisticData.push(...newPersonalDetailsOnyxData.optimisticData); const successPendingChatMembers = report?.pendingChatMembers ? report?.pendingChatMembers?.filter( @@ -2750,8 +2750,9 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails pendingChatMembers: successPendingChatMembers, }, }, - ...newPersonalDetailsOnyxData.finallyData, ]; + successData.push(...newPersonalDetailsOnyxData.finallyData); + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3342,8 +3343,8 @@ function completeOnboarding( return acc; }, []); - const optimisticData: OnyxUpdate[] = [ - ...tasksForOptimisticData, + const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData]; + optimisticData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, @@ -3364,18 +3365,18 @@ function completeOnboarding( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: engagementChoice}, }, - ]; - const successData: OnyxUpdate[] = [ - ...tasksForSuccessData, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, - value: { - [introductionCommentAction.reportActionID]: {pendingAction: null}, - [textCommentAction.reportActionID]: {pendingAction: null}, - }, + ); + + const successData: OnyxUpdate[] = [...tasksForSuccessData]; + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, + value: { + [introductionCommentAction.reportActionID]: {pendingAction: null}, + [textCommentAction.reportActionID]: {pendingAction: null}, }, - ]; + }); + let failureReport: Partial = { lastMessageTranslationKey: '', lastMessageText: '', @@ -3394,8 +3395,8 @@ function completeOnboarding( }; } - const failureData: OnyxUpdate[] = [ - ...tasksForFailureData, + const failureData: OnyxUpdate[] = [...tasksForFailureData]; + failureData.push( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, @@ -3418,7 +3419,7 @@ function completeOnboarding( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: null}, }, - ]; + ); const guidedSetupData: GuidedSetupData = [ {type: 'message', ...introductionMessage}, From 0cf96d70b093a4391e889072df5ec00bc0498ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 14 Jun 2024 23:32:31 +0100 Subject: [PATCH 17/22] Prettier --- src/ONYXKEYS.ts | 2 +- src/types/onyx/BillingGraceEndPeriod.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bcaa1fe0e0e1..34537f65dfbf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -394,7 +394,7 @@ const ONYXKEYS = { // Shared NVPs /** Collection of objects where each objects represents a workspace’s owner which is past due billing AND the user is a member of. */ - SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_' + SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', }, /** List of Form ids */ diff --git a/src/types/onyx/BillingGraceEndPeriod.ts b/src/types/onyx/BillingGraceEndPeriod.ts index e66bc92c59df..44235694adb9 100644 --- a/src/types/onyx/BillingGraceEndPeriod.ts +++ b/src/types/onyx/BillingGraceEndPeriod.ts @@ -5,7 +5,7 @@ type BillingGraceEndPeriod = { /** The permission associated with the NVP key. */ permissions: string; - + /** The grace period end date (epoch timestamp) of the workspace's owner where the user is a member of. */ value: number; }; From 35e9bba5afcc2b418b35d05dd5e11f10b01ef36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 17 Jun 2024 12:29:51 +0100 Subject: [PATCH 18/22] Changes ReportUtils.requiresAttentionFromCurrentUser() to include free trial logic --- src/libs/ReportUtils.ts | 6 +++ tests/unit/ReportUtilsTest.ts | 75 ++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 06a336171023..eb6d332f0271 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -83,6 +83,7 @@ import * as PolicyUtils from './PolicyUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import StringUtils from './StringUtils'; +import * as SubscriptionUtils from './SubscriptionUtils'; import * as TransactionUtils from './TransactionUtils'; import * as Url from './Url'; import type {AvatarSource} from './UserUtils'; @@ -2289,6 +2290,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo * - is unread and the user was mentioned in one of the unread comments * - is for an outstanding task waiting on the user * - has an outstanding child expense that is waiting for an action from the current user (e.g. pay, approve, add bank account) + * - is either the system or concierge chat, the user free trial has ended and it didn't add a payment card yet * * @param option (report or optionItem) * @param parentReportAction (the report action the current report is a thread of) @@ -2319,6 +2321,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op return true; } + if (isChatUsedForOnboarding(optionOrReport) && SubscriptionUtils.hasUserFreeTrialEnded() && !SubscriptionUtils.doesUserHavePaymentCardAdded()) { + return true; + } + return false; } diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index f9a815345693..139a15cb9f1d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import {addDays, format as formatDate, subDays} from 'date-fns'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as ReportUtils from '@libs/ReportUtils'; @@ -266,9 +267,15 @@ describe('ReportUtils', () => { }); describe('requiresAttentionFromCurrentUser', () => { + afterEach(async () => { + await Onyx.clear(); + await Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); + }); + it('returns false when there is no report', () => { expect(ReportUtils.requiresAttentionFromCurrentUser(undefined)).toBe(false); }); + it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -276,6 +283,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); + it('returns false when the linked iou report has an oustanding IOU', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -288,6 +296,7 @@ describe('ReportUtils', () => { expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); }); + it('returns false when the report has no outstanding IOU but is waiting for a bank account and the logged user is the report owner', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -296,6 +305,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); + it('returns false when the report has outstanding IOU and is not waiting for a bank account and the logged user is the report owner', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -304,6 +314,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); + it('returns false when the report has no oustanding IOU but is waiting for a bank account and the logged user is not the report owner', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -312,6 +323,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); }); + it('returns true when the report has an unread mention', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -319,6 +331,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); + it('returns true when the report is an outstanding task', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -330,6 +343,7 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); + it('returns true when the report has oustanding child expense', () => { const report = { ...LHNTestUtils.getFakeReport(), @@ -339,6 +353,62 @@ describe('ReportUtils', () => { }; expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); + + it("returns false if the user free trial hasn't ended yet", async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), // trial not ended + [ONYXKEYS.NVP_BILLING_FUND_ID]: null, // no payment card added + }); + + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); + }); + + it('returns false if the user free trial has ended and it added a payment card', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), // trial not ended + [ONYXKEYS.NVP_BILLING_FUND_ID]: 8010, // payment card added + }); + + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); + }); + + it("returns true if the report is the system chat, the user free trial has ended and it didn't add a payment card yet", async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), // trial ended + [ONYXKEYS.NVP_BILLING_FUND_ID]: null, // no payment card added + }); + + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); + }); + + it("returns true if the report is the concierge chat, the user free trial has ended and it didn't add a payment card yet", async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), // trial ended + [ONYXKEYS.NVP_BILLING_FUND_ID]: null, // no payment card added + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: 8}, // even account id + }); + + const report: Report = { + ...LHNTestUtils.getFakeReport([CONST.ACCOUNT_ID.CONCIERGE]), + }; + + expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); + }); }); describe('getMoneyRequestOptions', () => { @@ -823,7 +893,10 @@ describe('ReportUtils', () => { }); describe('isChatUsedForOnboarding', () => { - afterEach(() => Onyx.clear()); + afterEach(async () => { + await Onyx.clear(); + await Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); + }); it('should return true if the user account ID is odd and report is the system chat', async () => { const accountID = 1; From 7eb5a07a8e18559e53685df65759f57637561af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 17 Jun 2024 15:54:10 +0100 Subject: [PATCH 19/22] Add another test for ReportUtils.requiresAttentionFromCurrentUser() --- tests/unit/ReportUtilsTest.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 139a15cb9f1d..5f8902c5c076 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -354,6 +354,20 @@ describe('ReportUtils', () => { expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); + it("returns false if the user is not on free trial", async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: null, // not on free trial + [ONYXKEYS.NVP_BILLING_FUND_ID]: null, // no payment card added + }); + + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.SYSTEM, + }; + + expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(false); + }); + it("returns false if the user free trial hasn't ended yet", async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), // trial not ended From 7b5456deeab5095c0f8f5e2faeecf57ddc0ab67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 17 Jun 2024 16:24:48 +0100 Subject: [PATCH 20/22] Prettier fix --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 5f8902c5c076..a04f26e70c5b 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -354,7 +354,7 @@ describe('ReportUtils', () => { expect(ReportUtils.requiresAttentionFromCurrentUser(report)).toBe(true); }); - it("returns false if the user is not on free trial", async () => { + it('returns false if the user is not on free trial', async () => { await Onyx.multiSet({ [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: null, // not on free trial [ONYXKEYS.NVP_BILLING_FUND_ID]: null, // no payment card added From efe02e16486e71dd6c8eb5a70f3c21b0f02b665f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Mon, 17 Jun 2024 16:35:29 +0100 Subject: [PATCH 21/22] Fix dates --- src/libs/SubscriptionUtils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 93811de6fffe..721bb9b1aa6c 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -57,7 +57,8 @@ function calculateRemainingFreeTrialDays(): number { return 0; } - const difference = differenceInCalendarDays(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()), new Date()); + const currentDate = new Date(); + const difference = differenceInCalendarDays(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate), currentDate); return difference < 0 ? 0 : difference; } @@ -70,8 +71,8 @@ function isUserOnFreeTrial(): boolean { } const currentDate = new Date(); - const firstDayFreeTrialDate = parseDate(firstDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); - const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); + const firstDayFreeTrialDate = parseDate(firstDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate); + const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate); return isAfter(currentDate, firstDayFreeTrialDate) && isBefore(currentDate, lastDayFreeTrialDate); } @@ -85,7 +86,7 @@ function hasUserFreeTrialEnded(): boolean { } const currentDate = new Date(); - const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()); + const lastDayFreeTrialDate = parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate); return isAfter(currentDate, lastDayFreeTrialDate); } @@ -101,13 +102,15 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { + const currentDate = new Date(); + // This logic will be executed if the user is a workspace's non-owner (normal user or admin). // We should restrict the workspace's non-owner actions if it's member of a workspace where the owner is // past due and is past its grace period end. for (const userBillingGraceEndPeriodEntry of Object.entries(userBillingGraceEndPeriodCollection ?? {})) { const [entryKey, userBillingGracePeriodEnd] = userBillingGraceEndPeriodEntry; - if (userBillingGracePeriodEnd && isAfter(new Date(), fromUnixTime(userBillingGracePeriodEnd.value))) { + if (userBillingGracePeriodEnd && isAfter(currentDate, fromUnixTime(userBillingGracePeriodEnd.value))) { // Extracts the owner account ID from the collection member key. const ownerAccountID = entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length); @@ -120,7 +123,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { // If it reached here it means that the user is actually the workspace's owner. // We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount. - if (ownerBillingGraceEndPeriod && amountOwed !== undefined && isAfter(new Date(), fromUnixTime(ownerBillingGraceEndPeriod))) { + if (ownerBillingGraceEndPeriod && amountOwed !== undefined && isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod))) { return true; } From 9954f493176bc37597c2abb1fb0ed919dce7f1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 18 Jun 2024 17:49:08 +0100 Subject: [PATCH 22/22] Address review comments --- src/ONYXKEYS.ts | 2 +- src/libs/SubscriptionUtils.ts | 12 +++--- tests/unit/ReportUtilsTest.ts | 4 ++ tests/unit/SubscriptionUtilsTest.ts | 65 ++++++++++++++++++++++++----- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 34537f65dfbf..5267cd1fdf55 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -393,7 +393,7 @@ const ONYXKEYS = { SNAPSHOT: 'snapshot_', // Shared NVPs - /** Collection of objects where each objects represents a workspace’s owner which is past due billing AND the user is a member of. */ + /** Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of. */ SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END: 'sharedNVP_private_billingGracePeriodEnd_', }, diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 721bb9b1aa6c..50dc4f99eec0 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -1,4 +1,4 @@ -import {differenceInCalendarDays, fromUnixTime, isAfter, isBefore, parse as parseDate} from 'date-fns'; +import {differenceInSeconds, fromUnixTime, isAfter, isBefore, parse as parseDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -58,8 +58,10 @@ function calculateRemainingFreeTrialDays(): number { } const currentDate = new Date(); - const difference = differenceInCalendarDays(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate), currentDate); - return difference < 0 ? 0 : difference; + const diffInSeconds = differenceInSeconds(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, currentDate), currentDate); + const diffInDays = Math.ceil(diffInSeconds / 86400); + + return diffInDays < 0 ? 0 : diffInDays; } /** @@ -67,7 +69,7 @@ function calculateRemainingFreeTrialDays(): number { */ function isUserOnFreeTrial(): boolean { if (!firstDayFreeTrial || !lastDayFreeTrial) { - return true; + return false; } const currentDate = new Date(); @@ -123,7 +125,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean { // If it reached here it means that the user is actually the workspace's owner. // We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount. - if (ownerBillingGraceEndPeriod && amountOwed !== undefined && isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod))) { + if (ownerBillingGraceEndPeriod && amountOwed !== undefined && amountOwed > 0 && isAfter(currentDate, fromUnixTime(ownerBillingGraceEndPeriod))) { return true; } diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index a04f26e70c5b..98b92c77663b 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -912,6 +912,10 @@ describe('ReportUtils', () => { await Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID}); }); + it('should return false if the report is neither the system or concierge chat', () => { + expect(ReportUtils.isChatUsedForOnboarding(LHNTestUtils.getFakeReport())).toBeFalsy(); + }); + it('should return true if the user account ID is odd and report is the system chat', async () => { const accountID = 1; diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 8e4c99c5151f..7767ae9f387b 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -1,4 +1,4 @@ -import {addDays, format as formatDate, getUnixTime, subDays} from 'date-fns'; +import {addDays, addMinutes, format as formatDate, getUnixTime, subDays} from 'date-fns'; import Onyx from 'react-native-onyx'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; @@ -32,6 +32,11 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(0); }); + it('should return 1 if the current date is on the same day of the free trial end date, but some minutes earlier', async () => { + await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(addMinutes(new Date(), 30), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); + expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(1); + }); + it('should return the remaining days if the current date is before the free trial end date', async () => { await Onyx.set(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, formatDate(addDays(new Date(), 5), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)); expect(SubscriptionUtils.calculateRemainingFreeTrialDays()).toBe(5); @@ -47,27 +52,54 @@ describe('SubscriptionUtils', () => { }); }); - it('should return true if the Onyx keys are not set', () => { - expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + it('should return false if the Onyx keys are not set', () => { + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeFalsy(); }); - it('should return true if the current date is between the free trial start and end dates', async () => { + it('should return false if the current date is before the free trial start date', async () => { await Onyx.multiSet({ - [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), - [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 4), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), }); - expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeFalsy(); }); it('should return false if the current date is after the free trial end date', async () => { await Onyx.multiSet({ - [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 10), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), - [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 4), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), }); expect(SubscriptionUtils.isUserOnFreeTrial()).toBeFalsy(); }); + + it('should return true if the current date is on the same date of free trial start date', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(new Date(), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + }); + + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + }); + + it('should return true if the current date is on the same date of free trial end date, but some minutes earlier', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 2), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addMinutes(new Date(), 30), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + }); + + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + }); + + it('should return true if the current date is between the free trial start and end dates', async () => { + await Onyx.multiSet({ + [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: formatDate(subDays(new Date(), 1), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: formatDate(addDays(new Date(), 3), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING), + }); + + expect(SubscriptionUtils.isUserOnFreeTrial()).toBeTruthy(); + }); }); describe('hasUserFreeTrialEnded', () => { @@ -191,12 +223,23 @@ describe('SubscriptionUtils', () => { expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); }); - it('should return true if the user is a workspace owner but is past due billing', async () => { + it("should return false if the user is a workspace owner but is past due billing but isn't owning any amount", async () => { + const policyID = '1001'; + + await Onyx.multiSet({ + [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 0, + }); + + expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeFalsy(); + }); + + it('should return true if the user is a workspace owner but is past due billing and is owning some amount', async () => { const policyID = '1001'; await Onyx.multiSet({ [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: getUnixTime(subDays(new Date(), 3)), // past due - [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, // owing some amount + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED]: 8010, }); expect(SubscriptionUtils.shouldRestrictUserBillableActions(policyID)).toBeTruthy();