Skip to content

Commit 4b8278b

Browse files
authored
Merge pull request #54901 from parasharrajat/parasharrajat/early-discount
Early discount countdown banner
2 parents d930fea + 7a9084e commit 4b8278b

File tree

17 files changed

+529
-200
lines changed

17 files changed

+529
-200
lines changed

src/CONST.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ const CONST = {
512512
MIN_DATE: '0001-01-01',
513513
ORDINAL_DAY_OF_MONTH: 'do',
514514
MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy',
515+
SECONDS_PER_DAY: 24 * 60 * 60,
515516
},
516517
SMS: {
517518
DOMAIN: '@expensify.sms',
@@ -897,6 +898,7 @@ const CONST = {
897898
get DIRECT_REIMBURSEMENT_CURRENCIES() {
898899
return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.EUR];
899900
},
901+
TRIAL_DURATION_DAYS: 8,
900902
EXAMPLE_PHONE_NUMBER: '+15005550006',
901903
CONCIERGE_CHAT_NAME: 'Concierge',
902904
CLOUDFRONT_URL,

src/languages/en.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import type {
6060
DeleteActionParams,
6161
DeleteConfirmationParams,
6262
DidSplitAmountMessageParams,
63+
EarlyDiscountSubtitleParams,
64+
EarlyDiscountTitleParams,
6365
EditActionParams,
6466
EditDestinationSubtitleParams,
6567
ElectronicFundsParams,
@@ -5363,6 +5365,19 @@ const translations = {
53635365
title: 'Your free trial has ended',
53645366
subtitle: 'Add a payment card to continue using all of your favorite features.',
53655367
},
5368+
earlyDiscount: {
5369+
claimOffer: 'Claim Offer',
5370+
noThanks: 'No thanks',
5371+
subscriptionPageTitle: {
5372+
phrase1: ({discountType}: EarlyDiscountTitleParams) => `${discountType}% off your first year!`,
5373+
phrase2: `Just add a payment card and start an annual subscription!`,
5374+
},
5375+
onboardingChatTitle: {
5376+
phrase1: 'Limited time offer:',
5377+
phrase2: ({discountType}: EarlyDiscountTitleParams) => `${discountType}% off your first year!`,
5378+
},
5379+
subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Claim within ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
5380+
},
53665381
},
53675382
cardSection: {
53685383
title: 'Payment',

src/languages/es.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import type {
5959
DeleteActionParams,
6060
DeleteConfirmationParams,
6161
DidSplitAmountMessageParams,
62+
EarlyDiscountSubtitleParams,
63+
EarlyDiscountTitleParams,
6264
EditActionParams,
6365
EditDestinationSubtitleParams,
6466
ElectronicFundsParams,
@@ -5880,6 +5882,19 @@ const translations = {
58805882
title: 'Tu prueba gratuita ha terminado',
58815883
subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.',
58825884
},
5885+
earlyDiscount: {
5886+
claimOffer: 'Solicitar oferta',
5887+
noThanks: 'No, gracias',
5888+
subscriptionPageTitle: {
5889+
phrase1: ({discountType}: EarlyDiscountTitleParams) => ${discountType}% de descuento en tu primer año!`,
5890+
phrase2: `¡Solo añade una tarjeta de pago y comienza una suscripción anual!`,
5891+
},
5892+
onboardingChatTitle: {
5893+
phrase1: 'Oferta por tiempo limitado:',
5894+
phrase2: ({discountType}: EarlyDiscountTitleParams) => ${discountType}% de descuento en tu primer año!`,
5895+
},
5896+
subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Solicítala en ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
5897+
},
58835898
},
58845899
cardSection: {
58855900
title: 'Pago',

src/languages/params.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ type BillingBannerCardOnDisputeParams = {amountOwed: string; cardEnding: string}
419419

420420
type TrialStartedTitleParams = {numOfDays: number};
421421

422+
type EarlyDiscountTitleParams = {discountType: number};
423+
424+
type EarlyDiscountSubtitleParams = {days: number; hours: number; minutes: number; seconds: number};
425+
422426
type CardNextPaymentParams = {nextPaymentDate: string};
423427

424428
type CardEndingParams = {cardNumber: string};
@@ -644,6 +648,8 @@ export type {
644648
BillingBannerCardExpiredParams,
645649
BillingBannerCardOnDisputeParams,
646650
TrialStartedTitleParams,
651+
EarlyDiscountTitleParams,
652+
EarlyDiscountSubtitleParams,
647653
RemoveMemberPromptParams,
648654
StatementTitleParams,
649655
RenamedWorkspaceNameActionParams,

src/libs/API/parameters/AddPaymentCardParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ type AddPaymentCardParams = {
1010
addressZip: string;
1111
currency: ValueOf<typeof CONST.PAYMENT_CARD_CURRENCY>;
1212
isP2PDebitCard: boolean;
13+
shouldClaimEarlyDiscountOffer?: boolean;
1314
};
1415
export default AddPaymentCardParams;

src/libs/ReportUtils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,12 @@ let isAnonymousUser = false;
702702
// Example case: when we need to get a report name of a thread which is dependent on a report action message.
703703
const parsedReportActionMessageCache: Record<string, string> = {};
704704

705+
let conciergeChatReportID: string | undefined;
706+
Onyx.connect({
707+
key: ONYXKEYS.CONCIERGE_REPORT_ID,
708+
callback: (value) => (conciergeChatReportID = value),
709+
});
710+
705711
const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
706712
Onyx.connect({
707713
key: ONYXKEYS.SESSION,
@@ -1456,7 +1462,7 @@ function isConciergeChatReport(report: OnyxInputOrEntry<Report>): boolean {
14561462
return false;
14571463
}
14581464

1459-
return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING);
1465+
return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING) || report?.reportID === conciergeChatReportID;
14601466
}
14611467

14621468
function findSelfDMReportID(): string | undefined {

src/libs/SubscriptionUtils.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import {differenceInSeconds, fromUnixTime, isAfter, isBefore} from 'date-fns';
2+
import {fromZonedTime} from 'date-fns-tz';
23
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
34
import Onyx from 'react-native-onyx';
5+
import CONST from '@src/CONST';
46
import ONYXKEYS from '@src/ONYXKEYS';
57
import type {BillingGraceEndPeriod, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx';
68
import {isEmptyObject} from '@src/types/utils/EmptyObject';
79
import {translateLocal} from './Localize';
8-
import * as PolicyUtils from './PolicyUtils';
10+
import {getOwnedPaidPolicies, isPolicyOwner} from './PolicyUtils';
911

1012
const PAYMENT_STATUS = {
1113
POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed',
@@ -22,11 +24,19 @@ const PAYMENT_STATUS = {
2224
GENERIC_API_ERROR: 'generic_api_error',
2325
} as const;
2426

27+
type DiscountInfo = {
28+
days: number;
29+
hours: number;
30+
minutes: number;
31+
seconds: number;
32+
discountType: number;
33+
};
34+
2535
let currentUserAccountID = -1;
2636
Onyx.connect({
2737
key: ONYXKEYS.SESSION,
2838
callback: (value) => {
29-
currentUserAccountID = value?.accountID ?? -1;
39+
currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
3040
},
3141
});
3242

@@ -234,6 +244,53 @@ function hasCardExpiringSoon(): boolean {
234244
return isExpiringThisMonth || isExpiringNextMonth;
235245
}
236246

247+
function shouldShowDiscountBanner(): boolean {
248+
if (!isUserOnFreeTrial()) {
249+
return false;
250+
}
251+
252+
if (doesUserHavePaymentCardAdded()) {
253+
return false;
254+
}
255+
256+
const dateNow = Math.floor(Date.now() / 1000);
257+
const firstDayTimestamp = fromZonedTime(`${firstDayFreeTrial}`, 'UTC').getTime() / 1000;
258+
const lastDayTimestamp = fromZonedTime(`${lastDayFreeTrial}`, 'UTC').getTime() / 1000;
259+
if (dateNow > lastDayTimestamp) {
260+
return false;
261+
}
262+
263+
return dateNow <= firstDayTimestamp + CONST.TRIAL_DURATION_DAYS * CONST.DATE.SECONDS_PER_DAY;
264+
}
265+
266+
function getEarlyDiscountInfo(): DiscountInfo | null {
267+
if (!firstDayFreeTrial) {
268+
return null;
269+
}
270+
const dateNow = Math.floor(Date.now() / 1000);
271+
const firstDayTimestamp = fromZonedTime(`${firstDayFreeTrial}`, 'UTC').getTime() / 1000;
272+
273+
let timeLeftInSeconds;
274+
const timeLeft24 = CONST.DATE.SECONDS_PER_DAY - (dateNow - firstDayTimestamp);
275+
if (timeLeft24 > 0) {
276+
timeLeftInSeconds = timeLeft24;
277+
} else {
278+
timeLeftInSeconds = firstDayTimestamp + CONST.TRIAL_DURATION_DAYS * CONST.DATE.SECONDS_PER_DAY - dateNow;
279+
}
280+
281+
if (timeLeftInSeconds <= 0) {
282+
return null;
283+
}
284+
285+
return {
286+
days: Math.floor(timeLeftInSeconds / CONST.DATE.SECONDS_PER_DAY),
287+
hours: Math.floor((timeLeftInSeconds % CONST.DATE.SECONDS_PER_DAY) / 3600),
288+
minutes: Math.floor((timeLeftInSeconds % 3600) / 60),
289+
seconds: Math.floor(timeLeftInSeconds % 60),
290+
discountType: timeLeft24 > 0 ? 50 : 25,
291+
};
292+
}
293+
237294
/**
238295
* @returns Whether there is a retry billing error.
239296
*/
@@ -385,7 +442,7 @@ function calculateRemainingFreeTrialDays(): number {
385442
* @returns The free trial badge text .
386443
*/
387444
function getFreeTrialText(policies: OnyxCollection<Policy> | null): string | undefined {
388-
const ownedPaidPolicies = PolicyUtils.getOwnedPaidPolicies(policies, currentUserAccountID);
445+
const ownedPaidPolicies = getOwnedPaidPolicies(policies, currentUserAccountID);
389446
if (isEmptyObject(ownedPaidPolicies)) {
390447
return undefined;
391448
}
@@ -456,7 +513,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean {
456513
// Extracts the owner account ID from the collection member key.
457514
const ownerAccountID = Number(entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length));
458515

459-
if (PolicyUtils.isPolicyOwner(policy, ownerAccountID)) {
516+
if (isPolicyOwner(policy, ownerAccountID)) {
460517
return true;
461518
}
462519
}
@@ -465,7 +522,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean {
465522
// If it reached here it means that the user is actually the workspace's owner.
466523
// We should restrict the workspace's owner actions if it's past its grace period end date and it's owing some amount.
467524
if (
468-
PolicyUtils.isPolicyOwner(policy, currentUserAccountID) &&
525+
isPolicyOwner(policy, currentUserAccountID) &&
469526
ownerBillingGraceEndPeriod &&
470527
amountOwed !== undefined &&
471528
amountOwed > 0 &&
@@ -494,4 +551,6 @@ export {
494551
PAYMENT_STATUS,
495552
shouldRestrictUserBillableActions,
496553
shouldShowPreTrialBillingBanner,
554+
shouldShowDiscountBanner,
555+
getEarlyDiscountInfo,
497556
};

src/libs/actions/PaymentMethods.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ function addSubscriptionPaymentCard(
235235
addressZip,
236236
currency,
237237
isP2PDebitCard: false,
238+
shouldClaimEarlyDiscountOffer: true,
238239
};
239240

240241
const optimisticData: OnyxUpdate[] = [

0 commit comments

Comments
 (0)