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';
20 getIsB2BAudienceFromPlan,
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';
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);
54 plan: params.get('plan') || undefined,
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();
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({
85 plan: searchParams.plan,
86 planIDs: getPlanIDs(subscription),
87 cycle: subscription.Cycle,
92 const hasInboxDesktopInAppPayments = useHasInboxDesktopInAppPayments();
94 const [cycle, setCycle] = useState(searchParams.cycle ?? DEFAULT_CYCLE);
95 const { CouponCode } = subscription;
99 const handleModal = async (newPlanIDs: PlanIDs, newCycle: Cycle, currency: Currency) => {
100 if (!hasPlanIDs(newPlanIDs)) {
101 throw new Error('Downgrade not supported');
104 const couponCode = CouponCode || undefined; // From current subscription; CouponCode can be null
105 const { Coupon } = await paymentsApi.checkWithAutomaticVersion({
109 CouponCode: couponCode,
112 const plan = getPlanFromCheckout(newPlanIDs, plansMap);
115 defaultSelectedProductPlans: selectedProductPlans,
117 coupon: Coupon?.Code,
118 step: SUBSCRIPTION_STEPS.CHECKOUT,
120 currency: plan?.Currency,
121 defaultAudience: Object.keys(newPlanIDs).some((planID) => getIsB2BAudienceFromPlan(planID as any))
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);
139 void withLoading(handleModal(newPlanIDs, newCycle, currency));
146 const cycle = subscription.Cycle || DEFAULT_CYCLE;
148 setSelectedProductPlans(
149 getDefaultSelectedProductPlans({
151 planIDs: getPlanIDs(subscription),
152 plan: searchParams.plan,
154 cycle: subscription.Cycle,
157 }, [isLoading, subscription, app]);
160 if (subscription.isManagedByMozilla) {
161 return <MozillaInfoPanel />;
174 onChangeAudience={setAudience}
178 vpnServers={vpnServers}
180 paymentsStatus={status}
182 onChangeCycle={setCycle}
183 planIDs={currentPlanIDs}
185 hasPlanSelectionComparison={false}
186 subscription={subscription}
187 onChangePlanIDs={handlePlanChange}
188 onChangeCurrency={setCurrency}
189 selectedProductPlans={selectedProductPlans}
190 onChangeSelectedProductPlans={setSelectedProductPlans}
191 organization={organization}
193 {app !== APPS.PROTONWALLET && (
197 className="flex mx-auto items-center mb-4"
199 if (isElectronApp && !hasInboxDesktopInAppPayments) {
200 openLinkInBrowser(getAppHref(`mail/upgrade`, APPS.PROTONACCOUNT));
204 step: SUBSCRIPTION_STEPS.PLAN_SELECTION,
205 defaultAudience: audience,
206 defaultSelectedProductPlans: selectedProductPlans,
213 {c('Action').t`View plans details`}
214 <Icon name="arrow-right" className="ml-2 rtl:mirror" />
221 export default PlansSection;