Skip to content

Commit

Permalink
Merge pull request #54901 from parasharrajat/parasharrajat/early-disc…
Browse files Browse the repository at this point in the history
…ount

Early discount countdown banner
  • Loading branch information
youssef-lr authored Jan 24, 2025
2 parents d930fea + 7a9084e commit 4b8278b
Show file tree
Hide file tree
Showing 17 changed files with 529 additions and 200 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ const CONST = {
MIN_DATE: '0001-01-01',
ORDINAL_DAY_OF_MONTH: 'do',
MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy',
SECONDS_PER_DAY: 24 * 60 * 60,
},
SMS: {
DOMAIN: '@expensify.sms',
Expand Down Expand Up @@ -897,6 +898,7 @@ const CONST = {
get DIRECT_REIMBURSEMENT_CURRENCIES() {
return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.EUR];
},
TRIAL_DURATION_DAYS: 8,
EXAMPLE_PHONE_NUMBER: '+15005550006',
CONCIERGE_CHAT_NAME: 'Concierge',
CLOUDFRONT_URL,
Expand Down
15 changes: 15 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import type {
DeleteActionParams,
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EarlyDiscountSubtitleParams,
EarlyDiscountTitleParams,
EditActionParams,
EditDestinationSubtitleParams,
ElectronicFundsParams,
Expand Down Expand Up @@ -5363,6 +5365,19 @@ const translations = {
title: 'Your free trial has ended',
subtitle: 'Add a payment card to continue using all of your favorite features.',
},
earlyDiscount: {
claimOffer: 'Claim Offer',
noThanks: 'No thanks',
subscriptionPageTitle: {
phrase1: ({discountType}: EarlyDiscountTitleParams) => `${discountType}% off your first year!`,
phrase2: `Just add a payment card and start an annual subscription!`,
},
onboardingChatTitle: {
phrase1: 'Limited time offer:',
phrase2: ({discountType}: EarlyDiscountTitleParams) => `${discountType}% off your first year!`,
},
subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Claim within ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
title: 'Payment',
Expand Down
15 changes: 15 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import type {
DeleteActionParams,
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EarlyDiscountSubtitleParams,
EarlyDiscountTitleParams,
EditActionParams,
EditDestinationSubtitleParams,
ElectronicFundsParams,
Expand Down Expand Up @@ -5880,6 +5882,19 @@ const translations = {
title: 'Tu prueba gratuita ha terminado',
subtitle: 'Añade una tarjeta de pago para seguir utilizando tus funciones favoritas.',
},
earlyDiscount: {
claimOffer: 'Solicitar oferta',
noThanks: 'No, gracias',
subscriptionPageTitle: {
phrase1: ({discountType}: EarlyDiscountTitleParams) => ${discountType}% de descuento en tu primer año!`,
phrase2: `¡Solo añade una tarjeta de pago y comienza una suscripción anual!`,
},
onboardingChatTitle: {
phrase1: 'Oferta por tiempo limitado:',
phrase2: ({discountType}: EarlyDiscountTitleParams) => ${discountType}% de descuento en tu primer año!`,
},
subtitle: ({days, hours, minutes, seconds}: EarlyDiscountSubtitleParams) => `Solicítala en ${days > 0 ? `${days}d : ` : ''}${hours}h : ${minutes}m : ${seconds}s`,
},
},
cardSection: {
title: 'Pago',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,10 @@ type BillingBannerCardOnDisputeParams = {amountOwed: string; cardEnding: string}

type TrialStartedTitleParams = {numOfDays: number};

type EarlyDiscountTitleParams = {discountType: number};

type EarlyDiscountSubtitleParams = {days: number; hours: number; minutes: number; seconds: number};

type CardNextPaymentParams = {nextPaymentDate: string};

type CardEndingParams = {cardNumber: string};
Expand Down Expand Up @@ -644,6 +648,8 @@ export type {
BillingBannerCardExpiredParams,
BillingBannerCardOnDisputeParams,
TrialStartedTitleParams,
EarlyDiscountTitleParams,
EarlyDiscountSubtitleParams,
RemoveMemberPromptParams,
StatementTitleParams,
RenamedWorkspaceNameActionParams,
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/AddPaymentCardParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ type AddPaymentCardParams = {
addressZip: string;
currency: ValueOf<typeof CONST.PAYMENT_CARD_CURRENCY>;
isP2PDebitCard: boolean;
shouldClaimEarlyDiscountOffer?: boolean;
};
export default AddPaymentCardParams;
8 changes: 7 additions & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,12 @@ let isAnonymousUser = false;
// Example case: when we need to get a report name of a thread which is dependent on a report action message.
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeChatReportID: string | undefined;
Onyx.connect({
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeChatReportID = value),
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({
key: ONYXKEYS.SESSION,
Expand Down Expand Up @@ -1456,7 +1462,7 @@ function isConciergeChatReport(report: OnyxInputOrEntry<Report>): boolean {
return false;
}

return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING);
return participantAccountIDs.has(CONCIERGE_ACCOUNT_ID_STRING) || report?.reportID === conciergeChatReportID;
}

function findSelfDMReportID(): string | undefined {
Expand Down
69 changes: 64 additions & 5 deletions src/libs/SubscriptionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {differenceInSeconds, fromUnixTime, isAfter, isBefore} from 'date-fns';
import {fromZonedTime} from 'date-fns-tz';
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, BillingStatus, Fund, FundList, Policy, StripeCustomerID} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {translateLocal} from './Localize';
import * as PolicyUtils from './PolicyUtils';
import {getOwnedPaidPolicies, isPolicyOwner} from './PolicyUtils';

const PAYMENT_STATUS = {
POLICY_OWNER_WITH_AMOUNT_OWED: 'policy_owner_with_amount_owed',
Expand All @@ -22,11 +24,19 @@ const PAYMENT_STATUS = {
GENERIC_API_ERROR: 'generic_api_error',
} as const;

type DiscountInfo = {
days: number;
hours: number;
minutes: number;
seconds: number;
discountType: number;
};

let currentUserAccountID = -1;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserAccountID = value?.accountID ?? -1;
currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
},
});

Expand Down Expand Up @@ -234,6 +244,53 @@ function hasCardExpiringSoon(): boolean {
return isExpiringThisMonth || isExpiringNextMonth;
}

function shouldShowDiscountBanner(): boolean {
if (!isUserOnFreeTrial()) {
return false;
}

if (doesUserHavePaymentCardAdded()) {
return false;
}

const dateNow = Math.floor(Date.now() / 1000);
const firstDayTimestamp = fromZonedTime(`${firstDayFreeTrial}`, 'UTC').getTime() / 1000;
const lastDayTimestamp = fromZonedTime(`${lastDayFreeTrial}`, 'UTC').getTime() / 1000;
if (dateNow > lastDayTimestamp) {
return false;
}

return dateNow <= firstDayTimestamp + CONST.TRIAL_DURATION_DAYS * CONST.DATE.SECONDS_PER_DAY;
}

function getEarlyDiscountInfo(): DiscountInfo | null {
if (!firstDayFreeTrial) {
return null;
}
const dateNow = Math.floor(Date.now() / 1000);
const firstDayTimestamp = fromZonedTime(`${firstDayFreeTrial}`, 'UTC').getTime() / 1000;

let timeLeftInSeconds;
const timeLeft24 = CONST.DATE.SECONDS_PER_DAY - (dateNow - firstDayTimestamp);
if (timeLeft24 > 0) {
timeLeftInSeconds = timeLeft24;
} else {
timeLeftInSeconds = firstDayTimestamp + CONST.TRIAL_DURATION_DAYS * CONST.DATE.SECONDS_PER_DAY - dateNow;
}

if (timeLeftInSeconds <= 0) {
return null;
}

return {
days: Math.floor(timeLeftInSeconds / CONST.DATE.SECONDS_PER_DAY),
hours: Math.floor((timeLeftInSeconds % CONST.DATE.SECONDS_PER_DAY) / 3600),
minutes: Math.floor((timeLeftInSeconds % 3600) / 60),
seconds: Math.floor(timeLeftInSeconds % 60),
discountType: timeLeft24 > 0 ? 50 : 25,
};
}

/**
* @returns Whether there is a retry billing error.
*/
Expand Down Expand Up @@ -385,7 +442,7 @@ function calculateRemainingFreeTrialDays(): number {
* @returns The free trial badge text .
*/
function getFreeTrialText(policies: OnyxCollection<Policy> | null): string | undefined {
const ownedPaidPolicies = PolicyUtils.getOwnedPaidPolicies(policies, currentUserAccountID);
const ownedPaidPolicies = getOwnedPaidPolicies(policies, currentUserAccountID);
if (isEmptyObject(ownedPaidPolicies)) {
return undefined;
}
Expand Down Expand Up @@ -456,7 +513,7 @@ function shouldRestrictUserBillableActions(policyID: string): boolean {
// Extracts the owner account ID from the collection member key.
const ownerAccountID = Number(entryKey.slice(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END.length));

if (PolicyUtils.isPolicyOwner(policy, ownerAccountID)) {
if (isPolicyOwner(policy, ownerAccountID)) {
return true;
}
}
Expand All @@ -465,7 +522,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 (
PolicyUtils.isPolicyOwner(policy, currentUserAccountID) &&
isPolicyOwner(policy, currentUserAccountID) &&
ownerBillingGraceEndPeriod &&
amountOwed !== undefined &&
amountOwed > 0 &&
Expand Down Expand Up @@ -494,4 +551,6 @@ export {
PAYMENT_STATUS,
shouldRestrictUserBillableActions,
shouldShowPreTrialBillingBanner,
shouldShowDiscountBanner,
getEarlyDiscountInfo,
};
1 change: 1 addition & 0 deletions src/libs/actions/PaymentMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ function addSubscriptionPaymentCard(
addressZip,
currency,
isP2PDebitCard: false,
shouldClaimEarlyDiscountOffer: true,
};

const optimisticData: OnyxUpdate[] = [
Expand Down
Loading

0 comments on commit 4b8278b

Please sign in to comment.