1 import { render } from '@testing-library/react';
2 import { addMonths } from 'date-fns';
4 import { ADDON_NAMES, PLANS, PLAN_TYPES, type PlanIDs } from '@proton/payments';
5 import { CYCLE } from '@proton/shared/lib/constants';
6 import { type RequiredCheckResponse, getCheckout } from '@proton/shared/lib/helpers/checkout';
7 import { toMap } from '@proton/shared/lib/helpers/object';
8 import type { PlansMap, Subscription } from '@proton/shared/lib/interfaces';
9 import { getFreeCheckResult } from '@proton/shared/lib/subscription/freePlans';
11 import { getCheckoutRenewNoticeText } from './RenewalNotice';
13 const RenewalNotice = (...props: Parameters<typeof getCheckoutRenewNoticeText>) => {
14 return <div>{getCheckoutRenewNoticeText(...props)}</div>;
17 const getDefaultPlansMap = (): PlansMap => {
20 ID: 'KV8mjImpquR4tux9teokTx9bRB3GDgJYgDV2r1PCaCVD9o8InZs99CZr_q2G0qyP8QES4FZyxdO5gU1K-2Jv7Q==',
22 'hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==',
71 CustomerID: 'cus_google_gOjTbjOE83fpVtO8A7ZY',
80 ID: 'vXOiCO533JxXlqJsrhvREDwaFhGOXXV7NYjsz06VDJGSnes72blaG8hA9547xU1NLrMsDbA3lywhjPZ7oNYNcA==',
82 'hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==',
89 MaxSpace: 16106127360,
119 CustomerID: 'cus_google_gOjTbjOE83fpVtO8A7ZY',
128 ID: 'yu50U8Rf9dhPHDcG3KTX6Nx3Euupk4uskAj9V9YVCFSB3oQk8_pTGfWwRFkRPYziGL5EsEx42ZyRHdcZxpO8CA==',
130 'hUcV0_EeNwUmXA6EoyNrtO-ZTD8H8F6LvNaSjMaPxB5ecFkA7y-5kc3q38cGumJENGHjtSoUndkYFUx0_xlJeg==',
133 Title: 'VPN and Pass bundle',
173 CustomerID: 'cus_google_gOjTbjOE83fpVtO8A7ZY',
182 return toMap(plans, 'Name');
185 const defaultPlansMap = getDefaultPlansMap();
189 plansMap = defaultPlansMap,
190 planIDs = { [PLANS.MAIL]: 1 },
195 checkResult: RequiredCheckResponse;
197 checkResult: getFreeCheckResult(),
200 Parameters<typeof getCheckoutRenewNoticeText>[0],
201 'planIDs' | 'checkout' | 'plansMap' | 'coupon' | 'currency'
203 const checkout = getCheckout({ planIDs, plansMap, checkResult });
208 coupon: checkResult?.Coupon || null,
209 currency: 'CHF' as const,
213 describe('<RenewalNotice />', () => {
215 jest.clearAllMocks();
216 jest.useFakeTimers();
218 const mockedDate = new Date(2023, 10, 1);
219 jest.setSystemTime(mockedDate);
223 jest.useRealTimers();
226 describe('regular subscription renew', () => {
227 it('should render', () => {
228 const { container } = render(
231 isCustomBilling={false}
232 isScheduledSubscription={false}
233 subscription={undefined}
237 expect(container).not.toBeEmptyDOMElement();
240 it('should display the correct renewal date', () => {
241 const renewCycle = 12;
242 const expectedDateString = '11/01/2024'; // because months are 0-indexed ¯\_(ツ)_/¯
244 const { container } = render(
247 isCustomBilling={false}
248 isScheduledSubscription={false}
249 subscription={undefined}
253 expect(container).toHaveTextContent(
254 `Subscription auto-renews every 12 months. Your next billing date is ${expectedDateString}.`
258 it('should use period end date if custom billing is enabled', () => {
259 const renewCycle = 12;
260 const expectedDateString = '08/11/2025'; // because months are 0-indexed ¯\_(ツ)_/¯
262 const { container } = render(
265 isCustomBilling={true}
266 isScheduledSubscription={false}
269 // the backend returns seconds, not milliseconds
270 PeriodEnd: +new Date(2025, 7, 11) / 1000,
276 expect(container).toHaveTextContent(
277 `Subscription auto-renews every 12 months. Your next billing date is ${expectedDateString}.`
281 it('should use the end of upcoming subscription period if scheduled subscription is enabled', () => {
282 const renewCycle = 24; // the upcoming subscription takes another 24 months
283 const { container } = render(
286 isCustomBilling={false}
287 isScheduledSubscription={true}
290 // the backend returns seconds, not milliseconds
291 PeriodEnd: +new Date(2024, 1, 3) / 1000, // the current subscription period ends on 02/03/2024 (3rd of February 2024)
298 const expectedDateString = '02/03/2026'; // and finally the renewal date is 02/03/2026 (3rd of February 2026)
300 expect(container).toHaveTextContent(
301 `Subscription auto-renews every 24 months. Your next billing date is ${expectedDateString}.`
305 it('should user the end of current subscription period if addon downgrade is enabled', () => {
306 // while addon downgrading also schedules subscription, it doesn't charge user immediately.
307 // User will be charged when the scheduled subscription starts.
308 // That's significant difference from scheduled subscription that charges user immediately.
310 const subscription: Subscription = {
311 ID: 'Z1AOQDZSqE_rZEC01CXGl5fEy2JDRmT0M6WjSOtbvPep5lufR30jNAOKCcYavLK6rdOr_Wo4oID7UCskLdz0lw==',
312 InvoiceID: 'HwphVYhz2wbcT2yQJGyumBEmSe5DJqagrgv-WfC4B9nhuUanbGgwGrVeydqE-e3KdpbkfAFrJ5-T5VF-_pTxyg==',
314 PeriodStart: 1718200044,
315 PeriodEnd: 1749736044,
316 CreateTime: 1718200044,
325 ID: 'sId6XkzULCEzDPTuidkwWOgPInKjmzYJrw4nUYKnZHIwnlkiFqQjg_uHvzGrByCB99th0dfcVW-K5lK0E5tZmg==',
327 Name: PLANS.BUNDLE_PRO_2024,
328 Title: 'Proton Business Suite',
332 MaxSpace: 536870912000,
346 ID: 'vJF3r_xo_gpr-YfwAvEMwg3z1ZX7b4cTYHez1upZzszN3YWfQYjCdXQTnG4_WvXo7PJh-cIo1TGOBBKFrsFVoQ==',
348 Name: ADDON_NAMES.DOMAIN_BUNDLE_PRO_2024,
349 Title: '+1 Domain for Proton Business Suite',
366 ID: 'vJF3r_xo_gpr-YfwAvEMwg3z1ZX7b4cTYHez1upZzszN3YWfQYjCdXQTnG4_WvXo7PJh-cIo1TGOBBKFrsFVoQ==',
368 Name: ADDON_NAMES.DOMAIN_BUNDLE_PRO_2024,
369 Title: '+1 Domain for Proton Business Suite',
386 ID: 'vJF3r_xo_gpr-YfwAvEMwg3z1ZX7b4cTYHez1upZzszN3YWfQYjCdXQTnG4_WvXo7PJh-cIo1TGOBBKFrsFVoQ==',
388 Name: ADDON_NAMES.DOMAIN_BUNDLE_PRO_2024,
389 Title: '+1 Domain for Proton Business Suite',
406 ID: 'vJF3r_xo_gpr-YfwAvEMwg3z1ZX7b4cTYHez1upZzszN3YWfQYjCdXQTnG4_WvXo7PJh-cIo1TGOBBKFrsFVoQ==',
408 Name: ADDON_NAMES.DOMAIN_BUNDLE_PRO_2024,
409 Title: '+1 Domain for Proton Business Suite',
426 ID: 'vJF3r_xo_gpr-YfwAvEMwg3z1ZX7b4cTYHez1upZzszN3YWfQYjCdXQTnG4_WvXo7PJh-cIo1TGOBBKFrsFVoQ==',
428 Name: ADDON_NAMES.DOMAIN_BUNDLE_PRO_2024,
429 Title: '+1 Domain for Proton Business Suite',
446 ID: '4bSUpHmMPr1NvKnJIIQQ_ZkhKqWe5TROcR_IFD0HhZxHaHJQrOZ7nw7H8eoLBbfhTWgw08BD4x5A81i3qJfgIA==',
448 Name: ADDON_NAMES.MEMBER_BUNDLE_PRO_2024,
449 Title: '+1 User for Proton Business Suite',
453 MaxSpace: 536870912000,
467 ID: '4bSUpHmMPr1NvKnJIIQQ_ZkhKqWe5TROcR_IFD0HhZxHaHJQrOZ7nw7H8eoLBbfhTWgw08BD4x5A81i3qJfgIA==',
469 Name: ADDON_NAMES.MEMBER_BUNDLE_PRO_2024,
470 Title: '+1 User for Proton Business Suite',
474 MaxSpace: 536870912000,
488 ID: '4bSUpHmMPr1NvKnJIIQQ_ZkhKqWe5TROcR_IFD0HhZxHaHJQrOZ7nw7H8eoLBbfhTWgw08BD4x5A81i3qJfgIA==',
490 Name: ADDON_NAMES.MEMBER_BUNDLE_PRO_2024,
491 Title: '+1 User for Proton Business Suite',
495 MaxSpace: 536870912000,
509 ID: '4bSUpHmMPr1NvKnJIIQQ_ZkhKqWe5TROcR_IFD0HhZxHaHJQrOZ7nw7H8eoLBbfhTWgw08BD4x5A81i3qJfgIA==',
511 Name: ADDON_NAMES.MEMBER_BUNDLE_PRO_2024,
512 Title: '+1 User for Proton Business Suite',
516 MaxSpace: 536870912000,
530 ID: '4bSUpHmMPr1NvKnJIIQQ_ZkhKqWe5TROcR_IFD0HhZxHaHJQrOZ7nw7H8eoLBbfhTWgw08BD4x5A81i3qJfgIA==',
532 Name: ADDON_NAMES.MEMBER_BUNDLE_PRO_2024,
533 Title: '+1 User for Proton Business Suite',
537 MaxSpace: 536870912000,
557 const { container } = render(
561 isCustomBilling={false}
562 isScheduledSubscription={false}
563 isAddonDowngrade={true}
565 planIDs={{ bundlepro2024: 1, '1domain-bundlepro2024': 5, '1member-bundlepro2024': 5 }}
566 subscription={subscription}
570 // end of the current subscription, NOT upcoming that will be created when user accepts the terms of AddonDowngrade subscription
571 const expectedDateString = '06/12/2025';
573 expect(container).toHaveTextContent(
574 `Subscription auto-renews every 12 months. Your next billing date is ${expectedDateString}.`
579 describe('vpn2024 special renew cycle', () => {
580 [CYCLE.TWO_YEARS, CYCLE.YEARLY, CYCLE.FIFTEEN, CYCLE.THIRTY].forEach((cycle) => {
581 it(`should display special renewal notice for vpn2024 ${cycle} months`, () => {
582 const { container } = render(
584 {...getProps({ planIDs: { [PLANS.VPN2024]: 1 }, checkResult: getFreeCheckResult() })}
589 expect(container).toHaveTextContent(
590 `Your subscription will automatically renew in ${cycle} months. You'll then be billed every 12 months at CHF 79.95.`
595 [CYCLE.THREE, CYCLE.MONTHLY].forEach((cycle) => {
596 it(`should display special renewal notice for vpn2024 ${cycle} months`, () => {
597 const { container } = render(
599 {...getProps({ planIDs: { [PLANS.VPN2024]: 1 }, checkResult: getFreeCheckResult() })}
604 if (cycle != CYCLE.MONTHLY) {
605 const nextDate = addMonths(new Date(), cycle);
606 expect(container).toHaveTextContent(
607 `Subscription auto-renews every ${cycle} months. Your next billing date is ${(nextDate.getMonth() + 1).toString().padStart(2, '0')}/${nextDate.getDate().toString().padStart(2, '0')}/${nextDate.getFullYear()}.`
610 expect(container).toHaveTextContent(
611 `Subscription auto-renews every month. Your next billing date is 12/01/2023.`
618 describe('one time coupons', () => {
619 [CYCLE.TWO_YEARS, CYCLE.YEARLY, CYCLE.FIFTEEN, CYCLE.THIRTY].forEach((cycle) => {
620 it(`should ignore coupon for vpn2024 with a coupon`, () => {
621 const { container } = render(
624 planIDs: { [PLANS.VPN2024]: 1 },
629 CouponDiscount: -1200,
633 Code: 'VPNINTROPRICE2024',
634 Description: 'Introductory price for VPN Plus',
635 MaximumRedemptionsPerUser: 1,
641 Name: 'Mehrwertsteuer (MWST)',
642 Rate: 8.0999999999999996,
653 expect(container).toHaveTextContent(
654 `Your subscription will automatically renew in ${cycle} months. You'll then be billed every 12 months at CHF 79.95.`
659 it(`should apply it for 12m vpnpass bundle`, () => {
660 const { container } = render(
663 planIDs: { [PLANS.VPN_PASS_BUNDLE]: 1 },
668 CouponDiscount: -5607,
672 Code: 'TECHRADARVPNPASS',
674 MaximumRedemptionsPerUser: 1,
680 Name: 'Mehrwertsteuer (MWST)',
681 Rate: 8.0999999999999996,
692 expect(container).toHaveTextContent(
693 'The specially discounted price of CHF 47.88 is valid for the first 12 months. Then it will automatically be renewed at CHF 95.88 for 12 months. You can cancel at any time.'
697 it(`should apply it for 1m mail offer`, () => {
698 const { container } = render(
701 planIDs: { [PLANS.MAIL]: 1 },
706 CouponDiscount: -399,
710 Code: 'MAILPLUSINTRO',
711 Description: 'MAILPLUSINTRO',
712 MaximumRedemptionsPerUser: 1,
718 Name: 'Mehrwertsteuer (MWST)',
719 Rate: 8.0999999999999996,
726 cycle={CYCLE.MONTHLY}
729 expect(container).toHaveTextContent(
730 'The specially discounted price of CHF 1 is valid for the first month. Then it will automatically be renewed at CHF 4.99 every month. You can cancel at any time.'
736 describe('getPassLifetimeRenewNoticeText', () => {
737 it('should show basic lifetime message when no subscription exists', () => {
738 const { container } = render(
742 subscription={undefined}
743 planIDs={{ [PLANS.PASS_LIFETIME]: 1 }}
747 expect(container).toHaveTextContent(
748 "Pass lifetime deal has no renewal price, it's a one-time payment for lifetime access to Pass."
752 it('should show basic lifetime message for free plan', () => {
753 const subscription = {
761 const { container } = render(
765 subscription={subscription}
766 planIDs={{ [PLANS.PASS_LIFETIME]: 1 }}
770 expect(container).toHaveTextContent(
771 "Pass lifetime deal has no renewal price, it's a one-time payment for lifetime access to Pass."
775 it('should show replacement message for Pass Plus subscribers', () => {
776 const subscription = {
779 Type: PLAN_TYPES.PLAN,
785 const { container } = render(
789 subscription={subscription}
790 planIDs={{ [PLANS.PASS_LIFETIME]: 1 }}
794 expect(container).toHaveTextContent(
795 "Your Pass Plus subscription will be replaced with Pass Lifetime. The remaining balance of your subscription will be added to your account. Pass lifetime deal has no renewal price, it's a one-time payment for lifetime access to Pass."
799 it('should show unchanged subscription message for other plan subscribers', () => {
800 const subscription = {
803 Type: PLAN_TYPES.PLAN,
810 const { container } = render(
814 subscription={subscription}
815 planIDs={{ [PLANS.PASS_LIFETIME]: 1 }}
819 expect(container).toHaveTextContent(
820 "Pass lifetime deal has no renewal price, it's a one-time payment for lifetime access to Pass. Your Mail Plus subscription renewal price and date remain unchanged."