Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / payments / SubscriptionsSection.tsx
blob8a8cafed54f3788e01660116cf04e203c5d3a86a
1 import { c, msgid } from 'ttag';
3 import { usePlans } from '@proton/account/plans/hooks';
4 import { useSubscription } from '@proton/account/subscription/hooks';
5 import { useUser } from '@proton/account/user/hooks';
6 import type { DropdownActionProps } from '@proton/components/components/dropdown/DropdownActions';
7 import DropdownActions from '@proton/components/components/dropdown/DropdownActions';
8 import Icon from '@proton/components/components/icon/Icon';
9 import Loader from '@proton/components/components/loader/Loader';
10 import Price from '@proton/components/components/price/Price';
11 import Table from '@proton/components/components/table/Table';
12 import TableBody from '@proton/components/components/table/TableBody';
13 import TableCell from '@proton/components/components/table/TableCell';
14 import TableHeader from '@proton/components/components/table/TableHeader';
15 import TableRow from '@proton/components/components/table/TableRow';
16 import Time from '@proton/components/components/time/Time';
17 import Tooltip from '@proton/components/components/tooltip/Tooltip';
18 import MozillaInfoPanel from '@proton/components/containers/account/MozillaInfoPanel';
19 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
20 import useCancellationTelemetry from '@proton/components/containers/payments/subscription/cancellationFlow/useCancellationTelemetry';
21 import useApi from '@proton/components/hooks/useApi';
22 import useEventManager from '@proton/components/hooks/useEventManager';
23 import { usePreferredPlansMap } from '@proton/components/hooks/usePreferredPlansMap';
24 import { useLoading } from '@proton/hooks';
25 import { PLANS, onSessionMigrationPaymentsVersion } from '@proton/payments';
26 import { changeRenewState } from '@proton/shared/lib/api/payments';
27 import { getCheckout, getOptimisticCheckResult } from '@proton/shared/lib/helpers/checkout';
28 import { getOptimisticRenewCycleAndPrice } from '@proton/shared/lib/helpers/renew';
29 import {
30     getHas2023OfferCoupon,
31     getNormalCycleFromCustomCycle,
32     getPlanIDs,
33     getPlanTitle,
34 } from '@proton/shared/lib/helpers/subscription';
35 import { Renew } from '@proton/shared/lib/interfaces';
36 import isTruthy from '@proton/utils/isTruthy';
37 import noop from '@proton/utils/noop';
39 import type { BadgeType } from '../../components/badge/Badge';
40 import { default as Badge } from '../../components/badge/Badge';
41 import { subscriptionExpires } from './subscription/helpers';
43 export const getMonths = (n: number) => c('Billing cycle').ngettext(msgid`${n} month`, `${n} months`, n);
45 const SubscriptionsSection = () => {
46     const [plansResult, loadingPlans] = usePlans();
47     const plans = plansResult?.plans;
48     const [current, loadingSubscription] = useSubscription();
49     const upcoming = current?.UpcomingSubscription ?? undefined;
50     const api = useApi();
51     const eventManager = useEventManager();
52     const [reactivating, withReactivating] = useLoading();
53     const [user] = useUser();
55     const { sendDashboardReactivateReport } = useCancellationTelemetry();
56     const searchParams = new URLSearchParams(location.search);
57     const reactivationSource = searchParams.get('source');
59     const { plansMap, plansMapLoading } = usePreferredPlansMap();
61     if (!current || !plans || loadingSubscription || loadingPlans || plansMapLoading) {
62         return <Loader />;
63     }
65     if (current.isManagedByMozilla) {
66         return <MozillaInfoPanel />;
67     }
69     const planTitle = getPlanTitle(current);
71     const { renewEnabled, subscriptionExpiresSoon } = subscriptionExpires(current);
73     const reactivateAction: DropdownActionProps[] = [
74         !renewEnabled && {
75             text: c('Action subscription').t`Reactivate`,
76             loading: reactivating,
77             onClick: () => {
78                 sendDashboardReactivateReport(reactivationSource || 'default');
80                 withReactivating(async () => {
81                     await api(
82                         changeRenewState(
83                             {
84                                 RenewalState: Renew.Enabled,
85                             },
86                             onSessionMigrationPaymentsVersion(user, current)
87                         )
88                     );
90                     await eventManager.call();
91                 }).catch(noop);
92             },
93         },
94     ].filter(isTruthy);
96     const latestSubscription = upcoming ?? current;
97     // That's the case for AddonDowngrade subscription mode. If user with addons decreases the number of addons
98     // then in might fall under the AddonDowngrade subscription mode. In this case, user doesn't immediately.
99     // The upcoming subscription will be created, it will have the same cycle as the current subscription
100     // and user will be charged  at the beginning of the upcoming subscription.
101     // see PAY-2060 and PAY-2080
102     const isUpcomingSubscriptionUnpaid = !!current && !!upcoming && current.Cycle === upcoming.Cycle;
104     const { renewPrice, renewalLength } = (() => {
105         const latestPlanIDs = getPlanIDs(latestSubscription);
106         if (
107             getHas2023OfferCoupon(latestSubscription.CouponCode) &&
108             (latestPlanIDs[PLANS.VPN] || latestPlanIDs[PLANS.VPN_PASS_BUNDLE])
109         ) {
110             const nextCycle = getNormalCycleFromCustomCycle(latestSubscription.Cycle);
111             const latestCheckout = getCheckout({
112                 plansMap,
113                 planIDs: latestPlanIDs,
114                 checkResult: getOptimisticCheckResult({
115                     planIDs: latestPlanIDs,
116                     plansMap,
117                     cycle: nextCycle,
118                     currency: latestSubscription.Currency,
119                 }),
120             });
121             return {
122                 // The API doesn't return the correct next cycle or RenewAmount for the VPN or VPN+Pass bundle plan since we don't have chargebee
123                 // So we calculate it with the cycle discount here
124                 renewPrice: (
125                     <Price key="renewal-price" currency={latestSubscription.Currency}>
126                         {latestCheckout.withDiscountPerCycle}
127                     </Price>
128                 ),
129                 renewalLength: getMonths(nextCycle),
130             };
131         }
133         if (latestPlanIDs[PLANS.VPN2024]) {
134             const result = getOptimisticRenewCycleAndPrice({
135                 plansMap,
136                 planIDs: latestPlanIDs,
137                 cycle: latestSubscription.Cycle,
138                 currency: latestSubscription.Currency,
139             })!;
140             return {
141                 renewPrice: (
142                     <Price key="renewal-price" currency={latestSubscription.Currency}>
143                         {result.renewPrice}
144                     </Price>
145                 ),
146                 renewalLength: getMonths(result.renewalLength),
147             };
148         }
150         if (isUpcomingSubscriptionUnpaid) {
151             return {
152                 renewPrice: (
153                     <Price key="renewal-price" currency={upcoming.Currency}>
154                         {upcoming.Amount}
155                     </Price>
156                 ),
157                 renewalLength: getMonths(upcoming.Cycle),
158             };
159         }
161         return {
162             renewPrice: (
163                 <Price key="renewal-price" currency={latestSubscription.Currency}>
164                     {latestSubscription.RenewAmount}
165                 </Price>
166             ),
167             renewalLength: getMonths(latestSubscription.Cycle),
168         };
169     })();
171     const renewalText = (
172         <span data-testid="renewalNotice">{c('Billing cycle')
173             .jt`Renews automatically at ${renewPrice}, for ${renewalLength}`}</span>
174     );
176     const status = subscriptionExpiresSoon
177         ? {
178               type: 'error' as BadgeType,
179               label: c('Subscription status').t`Expiring`,
180           }
181         : { type: 'success' as BadgeType, label: c('Subscription status').t`Active` };
183     const renewalDate = isUpcomingSubscriptionUnpaid ? upcoming.PeriodStart : latestSubscription.PeriodEnd;
185     return (
186         <SettingsSectionWide>
187             <div style={{ overflow: 'auto' }}>
188                 <Table className="table-auto" responsive="cards">
189                     <TableHeader>
190                         <TableRow>
191                             <TableCell type="header">{c('Title subscription').t`Plan`}</TableCell>
192                             <TableCell type="header">{c('Title subscription').t`Status`}</TableCell>
193                             <TableCell type="header">{c('Title subscription').t`End date`}</TableCell>
194                             <TableCell type="header"> </TableCell>
195                         </TableRow>
196                     </TableHeader>
197                     <TableBody colSpan={4}>
198                         <TableRow>
199                             <TableCell label={c('Title subscription').t`Plan`}>
200                                 <span data-testid="planNameId">{planTitle}</span>
201                             </TableCell>
202                             <TableCell data-testid="subscriptionStatusId">
203                                 <Badge type={status.type}>{status.label}</Badge>
204                             </TableCell>
205                             <TableCell label={c('Title subscription').t`End date`}>
206                                 <Time format="PP" sameDayFormat={false} data-testid="planEndTimeId">
207                                     {renewalDate}
208                                 </Time>
209                                 {subscriptionExpiresSoon && (
210                                     <Tooltip
211                                         title={c('Info subscription')
212                                             .t`You can prevent expiry by reactivating the subscription`}
213                                         data-testid="periodEndWarning"
214                                     >
215                                         <Icon
216                                             name="exclamation-circle-filled"
217                                             className="color-danger ml-1"
218                                             size={4.5}
219                                         />
220                                     </Tooltip>
221                                 )}
222                             </TableCell>
223                             <TableCell data-testid="subscriptionActionsId">
224                                 {subscriptionExpiresSoon ? (
225                                     <DropdownActions size="small" list={reactivateAction} />
226                                 ) : (
227                                     renewalText
228                                 )}
229                             </TableCell>
230                         </TableRow>
231                     </TableBody>
232                 </Table>
233             </div>
234         </SettingsSectionWide>
235     );
237 export default SubscriptionsSection;