-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[No QA][Free trial] Implement all Free Trials utility functions #43844
Changes from 23 commits
127d350
3aaaac1
12fa116
91affbd
a8f194f
9f41e9b
3127c9d
4eb8d41
92ba0c8
ce591a3
ac22ed8
c012ed3
ccd5758
5fa1f17
8e27a71
52074be
dbec623
0cf96d7
35e9bba
a3e1c4a
7eb5a07
cfd25ee
7b5456d
efe02e1
903a6c6
9954f49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -82,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'; | ||
|
@@ -1035,12 +1037,15 @@ function isGroupChat(report: OnyxEntry<Report> | Partial<Report>): boolean { | |
return getChatType(report) === CONST.REPORT.CHAT_TYPE.GROUP; | ||
} | ||
|
||
/** | ||
* Only returns true if this is the Expensify DM report. | ||
*/ | ||
function isSystemChat(report: OnyxEntry<Report>): 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<Report>): boolean { | ||
const participantAccountIDs = Object.keys(report?.participants ?? {}) | ||
|
@@ -2285,6 +2290,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry<Report> | 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) | ||
|
@@ -2315,6 +2321,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry<Report> | Op | |
return true; | ||
} | ||
|
||
if (isChatUsedForOnboarding(optionOrReport) && SubscriptionUtils.hasUserFreeTrialEnded() && !SubscriptionUtils.doesUserHavePaymentCardAdded()) { | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
|
@@ -6964,6 +6974,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<Report>): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: we should add some context that these are used for A/B testing because it could be rather confusing for anyone reading this that we're doing different logic depending on whether accountID is odd or even. |
||
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<Report> { | ||
return Object.values(allReports ?? {}).find(isChatUsedForOnboarding); | ||
} | ||
|
||
export { | ||
addDomainToShortMention, | ||
areAllRequestsBeingSmartScanned, | ||
|
@@ -7236,6 +7260,8 @@ export { | |
isDraftReport, | ||
changeMoneyRequestHoldStatus, | ||
createDraftWorkspaceAndNavigateToConfirmationScreen, | ||
isChatUsedForOnboarding, | ||
getChatUsedForOnboarding, | ||
}; | ||
|
||
export type { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
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<string>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, | ||
callback: (value) => (firstDayFreeTrial = value), | ||
}); | ||
|
||
let lastDayFreeTrial: OnyxEntry<string>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, | ||
callback: (value) => (lastDayFreeTrial = value), | ||
}); | ||
|
||
let userBillingFundID: OnyxEntry<number>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.NVP_BILLING_FUND_ID, | ||
callback: (value) => (userBillingFundID = value), | ||
}); | ||
|
||
let userBillingGraceEndPeriodCollection: OnyxCollection<BillingGraceEndPeriod>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END, | ||
callback: (value) => (userBillingGraceEndPeriodCollection = value), | ||
waitForCollectionCallback: true, | ||
}); | ||
|
||
let ownerBillingGraceEndPeriod: OnyxEntry<number>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END, | ||
callback: (value) => (ownerBillingGraceEndPeriod = value), | ||
}); | ||
|
||
let amountOwed: OnyxEntry<number>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.NVP_PRIVATE_AMOUNT_OWNED, | ||
callback: (value) => (amountOwed = value), | ||
}); | ||
|
||
let allPolicies: OnyxCollection<Policy>; | ||
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. | ||
*/ | ||
function calculateRemainingFreeTrialDays(): number { | ||
if (!lastDayFreeTrial) { | ||
return 0; | ||
} | ||
|
||
const difference = differenceInCalendarDays(parseDate(lastDayFreeTrial, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING, new Date()), new Date()); | ||
fabioh8010 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return difference < 0 ? 0 : difference; | ||
} | ||
|
||
/** | ||
* Whether the workspace's owner is on its free trial period. | ||
*/ | ||
function isUserOnFreeTrial(): boolean { | ||
if (!firstDayFreeTrial || !lastDayFreeTrial) { | ||
return true; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's an incorrect value when either of those values hasn't been set/loaded yet or user hasn't started the free trial yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right, adjusted! cc @chiragsalian |
||
|
||
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); | ||
} | ||
|
||
/** | ||
* 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()); | ||
fabioh8010 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return isAfter(currentDate, lastDayFreeTrialDate); | ||
} | ||
|
||
/** | ||
* Whether the user has a payment card added to its account. | ||
*/ | ||
function doesUserHavePaymentCardAdded(): boolean { | ||
return userBillingFundID !== undefined; | ||
} | ||
|
||
/** | ||
* 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))) { | ||
// 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); | ||
|
||
const ownerPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; | ||
if (String(ownerPolicy?.ownerAccountID ?? -1) === ownerAccountID) { | ||
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}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this sound better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rojiphil That is not true, each object represents the workspace owner where the user is member of. I've changed to
Collection of objects where each object represents the owner of the workspace that is past due billing AND the user is a member of.
Wdyt?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! You are right. It makes sense too.