-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Description
Part of the Early Adoption Discount project
Main issue: https://github.com/Expensify/Expensify/issues/457641
Doc section: https://docs.google.com/document/d/1C8EIyxvoZGhYJOLA8J1QO2EqUkAX0LBFmkVSJaUeGF8/edit?tab=t.0#bookmark=id.cr9hpaweltm6
Project: https://github.com/orgs/Expensify/projects/177/views/7
Feature Description
In order to display the banner, we need to check first that the free trial is ongoing. If yes, we can use the same logic used in the backend to check if there is still time to display the banner. We’ll code a utility function in SubscriptionUtils that does the following (note that this only controls whether or not we show the banner, it doesn’t control which discount is shown):
Utility function to control the display of the banner
function shouldShowDiscountBanner(): boolean {
if (!isUserOnFreeTrial()) {
return false;
}
if (doesUserHavePaymentCardAdded()) {
return false;
}
// We'll use comparisons in seconds in this project
const dateNow = Date.now() / 1000;
// As well as UTC comparisons
const firstDayTimestamp = new Date(`${firstDayFreeTrial} UTC`).getTime() / 1000;
const lastDayTimestamp = new Date(`${lastDayFreeTrial} UTC`).getTime() / 1000;
if (dateNow > lastDayTimestamp) {
return false;
}
return dateNow <= firstDayTimestamp + 8 * CONST.DATE.SECONDS_PER_DAY * 1000;
}
Utility function to calculate the time remaining
function getEarlyDiscountInfo(firstDayFreeTrial: string) {
const dateNow = Date.now() / 1000;
const firstDayTimestamp = new Date(`${firstDayFreeTrial} UTC`).getTime() / 1000;
let timeLeftInSeconds;
const timeLeft24 = CONST.DATE.SECONDS_PER_DAY - (dateNow - firstDayTimestamp);
if (timeLeft24 > 0) {
timeLeftInSeconds = timeLeft24;
} else {
timeLeftInSeconds = firstDayTimestamp + 8 * 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: timeLeftInSeconds % 60,
discountType: timeLeft24 > 0 ? 50 : 25,
};
}
New countdown component
Now that we have everything we need to show a countdown, let’s create a new component that will use the functions above. We’ll simply create a new wrapper around BillingBanner which already has the same styling we need. We’ll also need to modify BilingBanner to take a right component which should host the buttons.
function EarlyDiscountBanner({isSubscriptionPage}) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL);
const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL);
const initialDiscountInfo = getEarlyDiscountInfo();
const [discountInfo, setDiscountInfo] = useState(initialDiscountInfo);
const [isDismissed, setIsDismissed] = useState(false);
useEffect(() => {
const intervalID = setInterval(() => {
setDiscountInfo(getEarlyDiscountInfo());
}, 1000);
return () => clearInterval(intervalID);
}, [firstDayFreeTrial]);
const title = isSubscriptionPage ? (
<Text style={styles.textStrong}>
{discountInfo?.discountType}% off your first year!
<Text>Just add payment card and start an annual subscription!</Text>
</Text>
) : (
<Text style={styles.textStrong}>
Limited time offer:
<Text>{discountInfo?.discountType}% off your first year!</Text>
</Text>
);
const formatTimeRemaining = useCallback(() => {
if (discountInfo?.days === 0) {
return `Claim within ${discountInfo?.hours}h : ${discountInfo?.minutes}m : ${discountInfo?.seconds}s`;
}
return `Claim within ${discountInfo?.days}d : ${discountInfo?.hours}h : ${discountInfo?.minutes}m : ${discountInfo?.seconds}s`;
}, [discountInfo]);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const rightComponent = useMemo(() => {
const smallScreenStyle = shouldUseNarrowLayout ? [styles.flex0, styles.flexBasis100, styles.flexRow, styles.justifyContentCenter] : [];
return (
<View style={[styles.flexRow, styles.gap2, smallScreenStyle]}>
<Button
success
text="Claim offer"
onPress={() => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION)}
/>
// Only allow dismissing the 25% discount
{discountInfo?.discountType === 25 && (
<Button
text="No thanks"
onPress={() => setIsDismissed(true)}
/>
)}
</View>
);
}, [shouldUseNarrowLayout, styles.flex0, styles.flexRow, styles.flexBasis100, styles.gap2, styles.justifyContentCenter, discountInfo]);
if (!firstDayFreeTrial || !lastDayFreeTrial || !discountInfo) {
return null;
}
// Banner is not dismissible in the subscription page
if (isDismissed && !isSubscriptionPage) {
return null;
}
return (
<BillingBanner
title={title}
subtitle={formatTimeRemaining()}
subtitleStyle={[styles.mt1, styles.mutedNormalTextLabel]}
icon={Illustrations.TreasureChest}
rightComponent={!isSubscriptionPage && rightComponent}
/>
);
}
And to display it, we can add the following to HeaderView:
function HeaderView({report, parentReportAction, reportID, onNavigationMenuButtonClicked, ...)
return (
<>
<View
style={[shouldShowBorderBottom && styles.borderBottom]}
dataSet={{dragArea: true}}
>
...
</View>
// The component will be displayed either in the #admins room or concierge.
// Depnding on which chat was used for onboarding
{shouldShowDiscountBanner() && ReportUtils.isChatUsedForOnboarding(report) && <EarlyDiscountBanner />}
</>
);
}
As well as add it to CardSection so we can display it in the Subscriptions page:
if (SubscriptionUtils.shouldShowDiscountBanner()) {
BillingBanner = <EarlyDiscountBanner isSubscriptionPage />;
} else if (SubscriptionUtils.shouldShowPreTrialBillingBanner()) {
BillingBanner = <PreTrialBillingBanner />;
} else if (SubscriptionUtils.isUserOnFreeTrial()) {
BillingBanner = <TrialStartedBillingBanner />;
} else if (SubscriptionUtils.hasUserFreeTrialEnded()) {
BillingBanner = <TrialEndedBillingBanner />;
}
Warning modal when choosing PPU
In the SubscriptionDetails, we’ll create a modal that is displayed whenever the user tries to choose PPU when the discount is applied. We’ll also display this modal if the user is already on a PPU plan and they attempt to claim the offer. The modal should explain that the discount is only applicable to yearly subscriptions.
Manual Test Steps
Claiming the first discount
- Sign up as a new user
- Create a workspace
- Go to your onboarding chat
- Verify you can see a countdown timer starting from 24h.
- Verify the banner is not dismissable.
- Verify click on “Claim offer” takes you to the subscription page.
- Verify the subscription page shows the same timer.
- Verify that after you add a card, a promoCode of 50% has been applied to your account. Verify
Claiming the second discount
- Sign up as a new user
- Create a workspace
- Modify the NVP firstDayFreeTrial such that it's earlier than 24h. Run this in the console: Onyx.merge("nvp_private_firstDayFreeTrial", "2024-12-21 23:36:00");
- Go to your onboarding chat
- Verify you can see a countdown showing days, hours, minutes, and seconds of the time remaining, which should be max 7 days when it starts.
- Verify the banner is dismissable.
- Verify click on “Claim offer” takes you to the subscription page.
- Verify the subscription page shows the same timer.
Period past 8 days
- Sign up as a new user
- Create a workspace
- Modify the NVP firstDayFreeTrial such that it's later than 8 days. Run this in the console: Onyx.merge("nvp_private_firstDayFreeTrial", "2024-12-21 23:36:00");
- Verify no banner is shown in the onboarding chat.
- Verify no banner is shown in the subscription page.
- Verify that in the subscription, we’re showing the usual free trial banner instead.
Automated Tests
The same tests as above coded implemented in unit tests.
Issue Owner
Current Issue Owner: @stephanieelliottMetadata
Metadata
Labels
Type
Projects
Status