Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / payments / RenewalNotice.tsx
blob5d768b5a6387dba8230abb168e5f1bdb6f387ecb
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';
12 import {
13     getHas2024OfferCoupon,
14     getPlanName,
15     getPlanTitle,
16     isLifetimePlan,
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 = {
25     cycle: number;
26     subscription?: Subscription;
27 } & Partial<CheckoutModifiers>;
29 export const getBlackFridayRenewalNoticeText = ({
30     price,
31     cycle,
32     plansMap,
33     planIDs,
34     currency,
35 }: {
36     price: number;
37     cycle: CYCLE;
38     plansMap: PlansMap;
39     planIDs: PlanIDs;
40     currency: Currency;
41 }) => {
42     const { renewPrice: renewAmount, renewalLength } = getOptimisticRenewCycleAndPrice({
43         planIDs,
44         plansMap,
45         cycle,
46         currency,
47     });
49     const plan = getPlanFromPlanIDs(plansMap, planIDs);
50     const discountedPrice = (
51         <Price key="a" currency={currency}>
52             {price}
53         </Price>
54     );
55     const nextPrice = plan ? (
56         <Price key="b" currency={currency}>
57             {renewAmount}
58         </Price>
59     ) : null;
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.`;
65     }
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`;
71         }
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);
74     })(cycle);
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 = ({
84     cycle,
85     isCustomBilling,
86     isScheduledSubscription,
87     isAddonDowngrade,
88     subscription,
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;
99     }
101     if (isScheduledSubscription && subscription) {
102         const periodEndMilliseconds = subscription.PeriodEnd * 1000;
103         unixRenewalTime = +addMonths(periodEndMilliseconds, cycle) / 1000;
104     }
106     const renewalTime = (
107         <Time format="P" key="auto-renewal-time">
108             {unixRenewalTime}
109         </Time>
110     );
112     const start =
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 = ({
121     cycle,
122     planIDs,
123     plansMap,
124     currency,
125 }: {
126     cycle: CYCLE;
127     planIDs: PlanIDs;
128     plansMap: PlansMap;
129     currency: Currency;
130 }) => {
131     const { renewPrice: renewAmount, renewalLength } = getOptimisticRenewCycleAndPrice({
132         planIDs,
133         plansMap,
134         cycle,
135         currency,
136     });
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.`,
142             cycle
143         );
145         const renewPrice = (
146             <Price key="renewal-price" currency={currency}>
147                 {renewAmount}
148             </Price>
149         );
151         const second = c('vpn_2024: renew').jt`You'll then be billed every 12 months at ${renewPrice}.`;
153         return [first, ' ', second];
154     }
157 const getRenewNoticeTextForLimitedCoupons = ({
158     coupon,
159     cycle,
160     planIDs,
161     plansMap,
162     currency,
163     checkout,
164     short,
165 }: {
166     cycle: CYCLE;
167     planIDs: PlanIDs;
168     plansMap: PlansMap;
169     currency: Currency;
170     coupon: Coupon;
171     checkout: SubscriptionCheckoutData;
172     short?: boolean;
173 }) => {
174     if (!coupon || !coupon.MaximumRedemptionsPerUser) {
175         return;
176     }
178     const couponRedemptions = coupon.MaximumRedemptionsPerUser;
180     const priceWithDiscount = (
181         <Price key="price-with-discount" currency={currency}>
182             {checkout.withDiscountPerCycle}
183         </Price>
184     );
186     const { renewPrice } = getOptimisticRenewCycleAndPrice({ planIDs, plansMap, cycle, currency });
187     const months = getMonths(cycle);
189     const price = (
190         <Price key="price" currency={currency}>
191             {renewPrice}
192         </Price>
193     );
195     if (couponRedemptions === 1) {
196         if (short) {
197             return c('Payments').jt`Renews at ${price}, cancel anytime.`;
198         }
200         if (cycle === CYCLE.MONTHLY) {
201             return c('Payments')
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.`;
203         } else {
204             return c('Payments')
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.`;
206         }
207     }
209     return c('Payments')
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) {
216         return c('Info')
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}.`;
218     }
220     if (planName === PLANS.PASS) {
221         return c('Info')
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}.`;
223     }
225     const planTitle = getPlanTitle(subscription);
226     return c('Info')
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 = ({
231     subscription,
232     planIDs,
233 }: {
234     planIDs: PlanIDs;
235     subscription?: Subscription;
236 }) => {
237     const planName = getPlanNameFromIDs(planIDs);
239     if (isLifetimePlan(planName)) {
240         return getPassLifetimeRenewNoticeText({ subscription });
241     }
244 export const getCheckoutRenewNoticeText = ({
245     coupon,
246     cycle,
247     planIDs,
248     plansMap,
249     currency,
250     checkout,
251     short,
252     ...renewalNoticeProps
253 }: {
254     coupon: Coupon;
255     cycle: CYCLE;
256     planIDs: PlanIDs;
257     plansMap: PlansMap;
258     currency: Currency;
259     checkout: SubscriptionCheckoutData;
260     short?: boolean;
261 } & RenewalNoticeProps): ReactNode => {
262     if (isLifetimePlanSelected(planIDs)) {
263         return getLifetimeRenewNoticeText({ ...renewalNoticeProps, planIDs });
264     }
266     if (getHas2024OfferCoupon(coupon?.Code)) {
267         return getBlackFridayRenewalNoticeText({
268             currency,
269             planIDs,
270             plansMap,
271             cycle,
272             price: checkout.withDiscountPerCycle,
273         });
274     }
276     const isSpeciallyRenewedPlan = isSpecialRenewPlan(planIDs);
277     if (isSpeciallyRenewedPlan) {
278         const specialLengthRenewNotice = getSpecialLengthRenewNoticeText({
279             cycle,
280             planIDs,
281             plansMap,
282             currency,
283         });
285         if (specialLengthRenewNotice) {
286             return specialLengthRenewNotice;
287         }
288     }
290     const limitedCouponsNotice = getRenewNoticeTextForLimitedCoupons({
291         coupon,
292         cycle,
293         planIDs,
294         plansMap,
295         currency,
296         checkout,
297         short,
298     });
300     if (limitedCouponsNotice) {
301         return limitedCouponsNotice;
302     }
304     return getRegularRenewalNoticeText({
305         cycle,
306         ...renewalNoticeProps,
307     });
310 export const getCheckoutRenewNoticeTextFromCheckResult = ({
311     checkResult,
312     plansMap,
313     planIDs,
314     short,
315 }: {
316     checkResult: SubscriptionCheckResponse;
317     plansMap: PlansMap;
318     planIDs: PlanIDs;
319     short?: boolean;
320 }) => {
321     return getCheckoutRenewNoticeText({
322         plansMap,
323         planIDs,
324         cycle: checkResult.Cycle,
325         checkout: getCheckout({
326             planIDs,
327             checkResult,
328             plansMap,
329         }),
330         currency: checkResult.Currency,
331         coupon: checkResult.Coupon,
332         short,
333     });