start removing account
[ProtonMail-WebClient.git] / packages / components / containers / payments / PlansSection.tsx
blobb89e0c42bb7d0d411c97154bb2d2f84e0ea92925
1 import { useEffect, useState } from 'react';
2 import { useLocation } from 'react-router-dom';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import Icon from '@proton/components/components/icon/Icon';
8 import Loader from '@proton/components/components/loader/Loader';
9 import MozillaInfoPanel from '@proton/components/containers/account/MozillaInfoPanel';
10 import { useAutomaticCurrency } from '@proton/components/payments/client-extensions';
11 import { usePaymentsApi } from '@proton/components/payments/react-extensions/usePaymentsApi';
12 import { useLoading } from '@proton/hooks';
13 import { getPlansMap } from '@proton/payments';
14 import { getAppHref } from '@proton/shared/lib/apps/helper';
15 import type { APP_NAMES } from '@proton/shared/lib/constants';
16 import { APPS, DEFAULT_CYCLE, FREE_SUBSCRIPTION, isStringPLAN } from '@proton/shared/lib/constants';
17 import { isElectronApp } from '@proton/shared/lib/helpers/desktop';
18 import { getPlanFromCheckout, hasPlanIDs } from '@proton/shared/lib/helpers/planIDs';
19 import {
20     getIsB2BAudienceFromPlan,
21     getPlanIDs,
22     getValidAudience,
23     getValidCycle,
24 } from '@proton/shared/lib/helpers/subscription';
25 import type { Currency, Cycle, PlanIDs } from '@proton/shared/lib/interfaces';
26 import { Audience } from '@proton/shared/lib/interfaces';
27 import { FREE_PLAN } from '@proton/shared/lib/subscription/freePlans';
29 import {
30     useApi,
31     useLoad,
32     useOrganization,
33     usePaymentStatus,
34     usePlans,
35     useSubscription,
36     useVPNServersCount,
37 } from '../../hooks';
38 import { openLinkInBrowser, upgradeButtonClick } from '../desktop/openExternalLink';
39 import { useHasInboxDesktopInAppPayments } from '../desktop/useHasInboxDesktopInAppPayments';
40 import PlanSelection from './subscription/PlanSelection';
41 import { useSubscriptionModal } from './subscription/SubscriptionModalProvider';
42 import { SUBSCRIPTION_STEPS } from './subscription/constants';
43 import { getDefaultSelectedProductPlans } from './subscription/helpers';
45 const getSearchParams = (search: string) => {
46     const params = new URLSearchParams(search);
47     const maybeCycle = Number(params.get('cycle'));
48     const cycle = getValidCycle(maybeCycle);
49     const maybeAudience = params.get('audience');
50     const audience = getValidAudience(maybeAudience);
52     return {
53         audience,
54         plan: params.get('plan') || undefined,
55         cycle,
56     };
59 const PlansSection = ({ app }: { app: APP_NAMES }) => {
60     const [loading, withLoading] = useLoading();
61     const [subscription = FREE_SUBSCRIPTION, loadingSubscription] = useSubscription();
62     const [organization, loadingOrganization] = useOrganization();
63     const [plansResult, loadingPlans] = usePlans();
64     const [status, statusLoading] = usePaymentStatus();
65     const plans = plansResult?.plans || [];
66     const freePlan = plansResult?.freePlan || FREE_PLAN;
67     const [vpnServers] = useVPNServersCount();
68     const api = useApi();
69     const { paymentsApi } = usePaymentsApi(api);
70     const location = useLocation();
71     const preferredCurrency = useAutomaticCurrency();
72     const currentPlanIDs = getPlanIDs(subscription);
73     const searchParams = getSearchParams(location.search);
74     const [audience, setAudience] = useState(searchParams.audience || Audience.B2C);
76     const [open] = useSubscriptionModal();
77     const isLoading = loadingPlans || loadingSubscription || loadingOrganization || statusLoading || !status;
78     const [selectedCurrency, setCurrency] = useState<Currency>();
79     const currency = selectedCurrency || preferredCurrency;
80     const plansMap = getPlansMap(plans, currency);
82     const [selectedProductPlans, setSelectedProductPlans] = useState(() => {
83         return getDefaultSelectedProductPlans({
84             appName: app,
85             plan: searchParams.plan,
86             planIDs: getPlanIDs(subscription),
87             cycle: subscription.Cycle,
88             plansMap,
89         });
90     });
92     const hasInboxDesktopInAppPayments = useHasInboxDesktopInAppPayments();
94     const [cycle, setCycle] = useState(searchParams.cycle ?? DEFAULT_CYCLE);
95     const { CouponCode } = subscription;
97     useLoad();
99     const handleModal = async (newPlanIDs: PlanIDs, newCycle: Cycle, currency: Currency) => {
100         if (!hasPlanIDs(newPlanIDs)) {
101             throw new Error('Downgrade not supported');
102         }
104         const couponCode = CouponCode || undefined; // From current subscription; CouponCode can be null
105         const { Coupon } = await paymentsApi.checkWithAutomaticVersion({
106             Plans: newPlanIDs,
107             Currency: currency,
108             Cycle: newCycle,
109             CouponCode: couponCode,
110         });
112         const plan = getPlanFromCheckout(newPlanIDs, plansMap);
114         open({
115             defaultSelectedProductPlans: selectedProductPlans,
116             planIDs: newPlanIDs,
117             coupon: Coupon?.Code,
118             step: SUBSCRIPTION_STEPS.CHECKOUT,
119             cycle: newCycle,
120             currency: plan?.Currency,
121             defaultAudience: Object.keys(newPlanIDs).some((planID) => getIsB2BAudienceFromPlan(planID as any))
122                 ? Audience.B2B
123                 : Audience.B2C,
124             metrics: {
125                 source: 'plans',
126             },
127         });
128     };
130     // Clicking the "Select Plan" button opens the browser on Electron or the modal on the web
131     const handlePlanChange = (newPlanIDs: PlanIDs, newCycle: Cycle, currency: Currency) => {
132         const newPlanName = Object.keys(newPlanIDs)[0];
133         const isNewPlanCorrect = isStringPLAN(newPlanName);
134         if (isElectronApp && !hasInboxDesktopInAppPayments && newPlanName && isNewPlanCorrect) {
135             upgradeButtonClick(newCycle, newPlanName);
136             return;
137         }
139         void withLoading(handleModal(newPlanIDs, newCycle, currency));
140     };
142     useEffect(() => {
143         if (isLoading) {
144             return;
145         }
146         const cycle = subscription.Cycle || DEFAULT_CYCLE;
147         setCycle(cycle);
148         setSelectedProductPlans(
149             getDefaultSelectedProductPlans({
150                 appName: app,
151                 planIDs: getPlanIDs(subscription),
152                 plan: searchParams.plan,
153                 plansMap,
154                 cycle: subscription.Cycle,
155             })
156         );
157     }, [isLoading, subscription, app]);
159     // @ts-ignore
160     if (subscription.isManagedByMozilla) {
161         return <MozillaInfoPanel />;
162     }
164     if (isLoading) {
165         return <Loader />;
166     }
168     return (
169         <>
170             <PlanSelection
171                 app={app}
172                 mode="settings"
173                 audience={audience}
174                 onChangeAudience={setAudience}
175                 loading={loading}
176                 freePlan={freePlan}
177                 plans={plans}
178                 vpnServers={vpnServers}
179                 currency={currency}
180                 paymentsStatus={status}
181                 cycle={cycle}
182                 onChangeCycle={setCycle}
183                 planIDs={currentPlanIDs}
184                 hasFreePlan={false}
185                 hasPlanSelectionComparison={false}
186                 subscription={subscription}
187                 onChangePlanIDs={handlePlanChange}
188                 onChangeCurrency={setCurrency}
189                 selectedProductPlans={selectedProductPlans}
190                 onChangeSelectedProductPlans={setSelectedProductPlans}
191                 organization={organization}
192             />
193             {app !== APPS.PROTONWALLET && (
194                 <Button
195                     color="norm"
196                     shape="ghost"
197                     className="flex mx-auto items-center mb-4"
198                     onClick={() => {
199                         if (isElectronApp && !hasInboxDesktopInAppPayments) {
200                             openLinkInBrowser(getAppHref(`mail/upgrade`, APPS.PROTONACCOUNT));
201                             return;
202                         }
203                         open({
204                             step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
205                             defaultAudience: audience,
206                             defaultSelectedProductPlans: selectedProductPlans,
207                             metrics: {
208                                 source: 'plans',
209                             },
210                         });
211                     }}
212                 >
213                     {c('Action').t`View plans details`}
214                     <Icon name="arrow-right" className="ml-2 rtl:mirror" />
215                 </Button>
216             )}
217         </>
218     );
221 export default PlansSection;