1 import { type ReactNode } from 'react';
3 import { addMonths } from 'date-fns';
4 import { c, msgid } from 'ttag';
6 import Time from '@proton/components/components/time/Time';
7 import { type Currency, PLANS, type PlanIDs } from '@proton/payments';
8 import { CYCLE, PASS_SHORT_APP_NAME } from '@proton/shared/lib/constants';
9 import { type SubscriptionCheckoutData, getCheckout } from '@proton/shared/lib/helpers/checkout';
10 import { getPlanFromPlanIDs, getPlanNameFromIDs, isLifetimePlanSelected } from '@proton/shared/lib/helpers/planIDs';
11 import { getOptimisticRenewCycleAndPrice, isSpecialRenewPlan } from '@proton/shared/lib/helpers/renew';
13 getHas2024OfferCoupon,
17 } from '@proton/shared/lib/helpers/subscription';
18 import type { Coupon, PlansMap, Subscription, SubscriptionCheckResponse } from '@proton/shared/lib/interfaces';
20 import Price from '../../components/price/Price';
21 import { getMonths } from './SubscriptionsSection';
22 import type { CheckoutModifiers } from './subscription/useCheckoutModifiers';
24 type RenewalNoticeProps = {
26 subscription?: Subscription;
27 } & Partial<CheckoutModifiers>;
29 export const getBlackFridayRenewalNoticeText = ({
42 const { renewPrice: renewAmount, renewalLength } = getOptimisticRenewCycleAndPrice({
49 const plan = getPlanFromPlanIDs(plansMap, planIDs);
50 const discountedPrice = (
51 <Price key="a" currency={currency}>
55 const nextPrice = plan ? (
56 <Price key="b" currency={currency}>
61 if (renewalLength === CYCLE.MONTHLY) {
62 // translator: The specially discounted price of $8.99 is valid for the first month. Then it will automatically be renewed at $9.99 every month. You can cancel at any time.
63 return c('bf2023: renew')
64 .jt`The specially discounted price of ${discountedPrice} is valid for the first month. Then it will automatically be renewed at ${nextPrice} every month. You can cancel at any time.`;
67 const discountedMonths = ((n: number) => {
68 if (n === CYCLE.MONTHLY) {
69 // translator: This string is a special case for 1 month billing cycle, together with the string "The specially discounted price of ... is valid for the first 'month' ..."
70 return c('bf2023: renew').t`the first month`;
72 // translator: The singular is not handled in this string. The month part of the string "The specially discounted price of EUR XX is valid for the first 30 months. Then it will automatically be renewed at the discounted price of EUR XX for 24 months. You can cancel at any time."
73 return c('bf2023: renew').ngettext(msgid`${n} month`, `the first ${n} months`, n);
76 const nextMonths = getMonths(renewalLength);
78 // translator: The specially discounted price of EUR XX is valid for the first 30 months. Then it will automatically be renewed at the discounted price of EUR XX for 24 months. You can cancel at any time.
79 return c('bf2023: renew')
80 .jt`The specially discounted price of ${discountedPrice} is valid for ${discountedMonths}. Then it will automatically be renewed at the discounted price of ${nextPrice} for ${nextMonths}. You can cancel at any time.`;
83 const getRegularRenewalNoticeText = ({
86 isScheduledSubscription,
89 }: RenewalNoticeProps) => {
90 let unixRenewalTime: number = +addMonths(new Date(), cycle) / 1000;
91 // custom billings are renewed at the end of the current subscription.
92 // addon downgrades are more tricky. On the first glance they behave like scheduled subscriptions,
93 // because they indeed create an upcoming subscription. But when subscription/check returns addon
94 // downgrade then user pays nothing now, and the scheduled subscription will still be created.
95 // The payment happens when the upcoming subscription becomes the current one. So the next billing date is still
96 // the end of the current subscription.
97 if ((isCustomBilling || isAddonDowngrade) && subscription) {
98 unixRenewalTime = subscription.PeriodEnd;
101 if (isScheduledSubscription && subscription) {
102 const periodEndMilliseconds = subscription.PeriodEnd * 1000;
103 unixRenewalTime = +addMonths(periodEndMilliseconds, cycle) / 1000;
106 const renewalTime = (
107 <Time format="P" key="auto-renewal-time">
113 cycle === CYCLE.MONTHLY
114 ? c('Info').t`Subscription auto-renews every month.`
115 : c('Info').t`Subscription auto-renews every ${cycle} months.`;
117 return [start, ' ', c('Info').jt`Your next billing date is ${renewalTime}.`];
120 const getSpecialLengthRenewNoticeText = ({
131 const { renewPrice: renewAmount, renewalLength } = getOptimisticRenewCycleAndPrice({
138 if (renewalLength === CYCLE.YEARLY) {
139 const first = c('vpn_2024: renew').ngettext(
140 msgid`Your subscription will automatically renew in ${cycle} month.`,
141 `Your subscription will automatically renew in ${cycle} months.`,
146 <Price key="renewal-price" currency={currency}>
151 const second = c('vpn_2024: renew').jt`You'll then be billed every 12 months at ${renewPrice}.`;
153 return [first, ' ', second];
157 const getRenewNoticeTextForLimitedCoupons = ({
171 checkout: SubscriptionCheckoutData;
174 if (!coupon || !coupon.MaximumRedemptionsPerUser) {
178 const couponRedemptions = coupon.MaximumRedemptionsPerUser;
180 const priceWithDiscount = (
181 <Price key="price-with-discount" currency={currency}>
182 {checkout.withDiscountPerCycle}
186 const { renewPrice } = getOptimisticRenewCycleAndPrice({ planIDs, plansMap, cycle, currency });
187 const months = getMonths(cycle);
190 <Price key="price" currency={currency}>
195 if (couponRedemptions === 1) {
197 return c('Payments').jt`Renews at ${price}, cancel anytime.`;
200 if (cycle === CYCLE.MONTHLY) {
202 .jt`The specially discounted price of ${priceWithDiscount} is valid for the first month. Then it will automatically be renewed at ${price} every month. You can cancel at any time.`;
205 .jt`The specially discounted price of ${priceWithDiscount} is valid for the first ${months}. Then it will automatically be renewed at ${price} for ${months}. You can cancel at any time.`;
210 .jt`The specially discounted price of ${priceWithDiscount} is valid for the first ${months}. The coupon is valid for ${couponRedemptions} renewals. Then it will automatically be renewed at ${price} for ${months} months. You can cancel at any time.`;
213 export const getPassLifetimeRenewNoticeText = ({ subscription }: { subscription?: Subscription }) => {
214 const planName = getPlanName(subscription);
215 if (!planName || planName === PLANS.FREE) {
217 .t`${PASS_SHORT_APP_NAME} lifetime deal has no renewal price, it's a one-time payment for lifetime access to ${PASS_SHORT_APP_NAME}.`;
220 if (planName === PLANS.PASS) {
222 .t`Your ${PASS_SHORT_APP_NAME} Plus subscription will be replaced with ${PASS_SHORT_APP_NAME} Lifetime. The remaining balance of your subscription will be added to your account. ${PASS_SHORT_APP_NAME} lifetime deal has no renewal price, it's a one-time payment for lifetime access to ${PASS_SHORT_APP_NAME}.`;
225 const planTitle = getPlanTitle(subscription);
227 .t`${PASS_SHORT_APP_NAME} lifetime deal has no renewal price, it's a one-time payment for lifetime access to ${PASS_SHORT_APP_NAME}. Your ${planTitle} subscription renewal price and date remain unchanged.`;
230 export const getLifetimeRenewNoticeText = ({
235 subscription?: Subscription;
237 const planName = getPlanNameFromIDs(planIDs);
239 if (isLifetimePlan(planName)) {
240 return getPassLifetimeRenewNoticeText({ subscription });
244 export const getCheckoutRenewNoticeText = ({
252 ...renewalNoticeProps
259 checkout: SubscriptionCheckoutData;
261 } & RenewalNoticeProps): ReactNode => {
262 if (isLifetimePlanSelected(planIDs)) {
263 return getLifetimeRenewNoticeText({ ...renewalNoticeProps, planIDs });
266 if (getHas2024OfferCoupon(coupon?.Code)) {
267 return getBlackFridayRenewalNoticeText({
272 price: checkout.withDiscountPerCycle,
276 const isSpeciallyRenewedPlan = isSpecialRenewPlan(planIDs);
277 if (isSpeciallyRenewedPlan) {
278 const specialLengthRenewNotice = getSpecialLengthRenewNoticeText({
285 if (specialLengthRenewNotice) {
286 return specialLengthRenewNotice;
290 const limitedCouponsNotice = getRenewNoticeTextForLimitedCoupons({
300 if (limitedCouponsNotice) {
301 return limitedCouponsNotice;
304 return getRegularRenewalNoticeText({
306 ...renewalNoticeProps,
310 export const getCheckoutRenewNoticeTextFromCheckResult = ({
316 checkResult: SubscriptionCheckResponse;
321 return getCheckoutRenewNoticeText({
324 cycle: checkResult.Cycle,
325 checkout: getCheckout({
330 currency: checkResult.Currency,
331 coupon: checkResult.Coupon,