Remove payments API routing initialization
[ProtonMail-WebClient.git] / applications / account / src / app / single-signup-v2 / helper.ts
blob93f2a984156d3da2bd7697a9e2d38f8cb9cf5400
1 import { c } from 'ttag';
3 import { getAutoCoupon } from '@proton/components/containers/payments/subscription/helpers';
4 import { getMaybeForcePaymentsVersion } from '@proton/components/payments/client-extensions';
5 import type { BillingAddress, PAYMENT_METHOD_TYPES, PaymentsApi, SavedPaymentMethod } from '@proton/payments';
6 import {
7     type ADDON_NAMES,
8     type Currency,
9     DEFAULT_CURRENCY,
10     FREE_SUBSCRIPTION,
11     PLANS,
12     type PlanIDs,
13     isStringPLAN,
14 } from '@proton/payments';
15 import { getOrganization } from '@proton/shared/lib/api/organization';
16 import { getSubscription, queryPaymentMethods } from '@proton/shared/lib/api/payments';
17 import type { APP_NAMES } from '@proton/shared/lib/constants';
18 import { APPS, COUPON_CODES, CYCLE } from '@proton/shared/lib/constants';
19 import { getOptimisticCheckResult } from '@proton/shared/lib/helpers/checkout';
20 import isDeepEqual from '@proton/shared/lib/helpers/isDeepEqual';
21 import {
22     getPlanFromPlanIDs,
23     getPricingFromPlanIDs,
24     hasPlanIDs,
25     isLifetimePlanSelected,
26     switchPlan,
27 } from '@proton/shared/lib/helpers/planIDs';
28 import {
29     getHas2024OfferCoupon,
30     getIsB2BAudienceFromPlan,
31     getNormalCycleFromCustomCycle,
32     getPlan,
33     getPlanIDs,
34 } from '@proton/shared/lib/helpers/subscription';
35 import type {
36     Api,
37     Cycle,
38     CycleMapping,
39     Organization,
40     Plan,
41     PlansMap,
42     StrictPlan,
43     Subscription,
44     SubscriptionCheckResponse,
45     SubscriptionPlan,
46     User,
47 } from '@proton/shared/lib/interfaces';
48 import { Audience } from '@proton/shared/lib/interfaces';
49 import { FREE_PLAN, getFreeCheckResult } from '@proton/shared/lib/subscription/freePlans';
50 import {
51     canPay as getCanPay,
52     isAdmin as getIsAdmin,
53     hasPaidDrive,
54     hasPaidMail,
55     hasPaidPass,
56 } from '@proton/shared/lib/user/helpers';
58 import { getSubscriptionPrices } from '../signup/helper';
59 import type { SessionData, SignupCacheResult, SubscriptionData } from '../signup/interfaces';
60 import type { PlanCard } from './PlanCardSelector';
61 import type { Options, PlanParameters, SignupConfiguration, SignupParameters2, Upsell } from './interface';
62 import { UpsellTypes } from './interface';
64 export const getFreeTitle = (appName: string) => {
65     return c('Title').t`${appName} Free`;
68 export const getIsProductB2BPlan = (plan: PLANS | ADDON_NAMES | undefined) => {
69     const proPlans = [
70         PLANS.MAIL_PRO,
71         PLANS.MAIL_BUSINESS,
72         PLANS.DRIVE_PRO,
73         PLANS.DRIVE_BUSINESS,
74         PLANS.VPN_PRO,
75         PLANS.VPN_BUSINESS,
76         PLANS.PASS_PRO,
77         PLANS.PASS_BUSINESS,
78     ];
79     return proPlans.some((proPlan) => plan === proPlan);
82 export const getIsBundleB2BPlan = (plan: PLANS | ADDON_NAMES | undefined) => {
83     return [PLANS.BUNDLE_PRO, PLANS.BUNDLE_PRO_2024].some((bundlePlan) => plan === bundlePlan);
86 export const getHasAnyPlusPlan = (subscribedPlan: PLANS | ADDON_NAMES | undefined) => {
87     return [PLANS.MAIL, PLANS.DRIVE, PLANS.VPN, PLANS.VPN2024, PLANS.PASS, PLANS.VPN_PASS_BUNDLE].some(
88         (plan) => plan === subscribedPlan
89     );
92 export const getFreeSubscriptionData = (
93     subscriptionData: Omit<SubscriptionData, 'checkResult' | 'planIDs' | 'payment'>
94 ): SubscriptionData => {
95     return {
96         ...subscriptionData,
97         checkResult: getFreeCheckResult(
98             subscriptionData.currency,
99             // "Reset" the cycle because the custom cycles are only valid with a coupon
100             getNormalCycleFromCustomCycle(subscriptionData.cycle)
101         ),
102         planIDs: {},
103         payment: undefined,
104     };
107 export const getSubscriptionData = async (
108     paymentsApi: PaymentsApi,
109     options: Options & {
110         info?: boolean;
111     }
112 ): Promise<SubscriptionData> => {
113     const { planIDs, checkResult } = await getSubscriptionPrices(
114         paymentsApi,
115         options.planIDs || {},
116         options.currency,
117         options.cycle,
118         options.billingAddress,
119         options.coupon
120     )
121         .then((checkResult) => {
122             return {
123                 checkResult,
124                 planIDs: options.planIDs,
125             };
126         })
127         .catch(() => {
128             if (!options?.info) {
129                 return {
130                     checkResult: getFreeCheckResult(
131                         options.currency,
132                         // "Reset" the cycle because the custom cycles are only valid with a coupon
133                         getNormalCycleFromCustomCycle(options.cycle)
134                     ),
135                     planIDs: undefined,
136                 };
137             }
138             // If this is only an "informational" call, like what we would display in a plan/cycle card at signup, we can calculate the price optimistically
139             return {
140                 checkResult: {
141                     ...getOptimisticCheckResult(options),
142                     Currency: options.currency,
143                     PeriodEnd: 0,
144                 },
145                 planIDs: options.planIDs,
146             };
147         });
148     return {
149         cycle: checkResult.Cycle,
150         currency: checkResult.Currency,
151         checkResult,
152         planIDs: planIDs || {},
153         skipUpsell: options.skipUpsell ?? false,
154         billingAddress: options.billingAddress,
155     };
158 const hasSelectedPlan = (plan: Plan | undefined, plans: (PLANS | ADDON_NAMES)[]): plan is Plan => {
159     return plans.some((planName) => plan?.Name === planName);
162 const getUnlockPlanName = (toApp: APP_NAMES) => {
163     if (toApp === APPS.PROTONPASS) {
164         return PLANS.PASS;
165     }
166     if (toApp === APPS.PROTONMAIL || toApp === APPS.PROTONCALENDAR) {
167         return PLANS.MAIL;
168     }
169     if (toApp === APPS.PROTONDRIVE) {
170         return PLANS.DRIVE;
171     }
172     return PLANS.MAIL;
175 const getSafePlan = (plansMap: PlansMap, planName: PLANS | ADDON_NAMES) => {
176     const plan = plansMap[planName];
177     if (!plan) {
178         throw new Error('Missing plan');
179     }
180     return plan;
183 const getUpsell = ({
184     audience,
185     currentPlan,
186     subscription,
187     plansMap,
188     options,
189     planParameters,
190     toApp,
191 }: {
192     audience: Audience;
193     currentPlan?: SubscriptionPlan | undefined;
194     upsellPlanCard?: PlanCard;
195     subscription?: Subscription;
196     plansMap: PlansMap;
197     options: Options;
198     planParameters: PlanParameters;
199     toApp: APP_NAMES;
200 }): Upsell => {
201     const hasMonthlyCycle = subscription?.Cycle === CYCLE.MONTHLY;
203     const noUpsell = {
204         plan: undefined,
205         unlockPlan: plansMap[getUnlockPlanName(toApp)],
206         currentPlan,
207         mode: UpsellTypes.PLANS,
208         subscriptionOptions: {},
209     };
211     // TODO: WalletEA
212     if (toApp === APPS.PROTONWALLET) {
213         return noUpsell;
214     }
216     const getBlackFridayUpsellData = ({
217         plan,
218         cycle = CYCLE.YEARLY,
219         coupon = COUPON_CODES.BLACK_FRIDAY_2024,
220     }: {
221         plan: Plan;
222         cycle?: CYCLE;
223         coupon?: string;
224     }) => {
225         return {
226             ...noUpsell,
227             plan,
228             subscriptionOptions: {
229                 planIDs: {
230                     [plan.Name]: 1,
231                 },
232                 cycle,
233                 coupon,
234             },
235             mode: UpsellTypes.UPSELL,
236         };
237     };
239     const getUpsellData = (plan: PLANS | ADDON_NAMES) => {
240         return {
241             ...noUpsell,
242             plan: plansMap[plan],
243             mode: UpsellTypes.UPSELL,
244         };
245     };
247     if (currentPlan && planParameters.defined) {
248         if (planParameters.plan.Name === PLANS.VISIONARY) {
249             return getUpsellData(planParameters.plan.Name);
250         }
252         if (isLifetimePlanSelected(options.planIDs ?? {}) && !getIsB2BAudienceFromPlan(currentPlan.Name)) {
253             return getBlackFridayUpsellData({ plan: getSafePlan(plansMap, PLANS.PASS_LIFETIME) });
254         }
256         if (getHas2024OfferCoupon(options.coupon)) {
257             if (getHasAnyPlusPlan(currentPlan.Name)) {
258                 if (currentPlan.Name === PLANS.PASS) {
259                     if (
260                         options.cycle === CYCLE.YEARLY &&
261                         hasSelectedPlan(planParameters.plan, [PLANS.PASS_FAMILY, PLANS.BUNDLE, PLANS.DUO, PLANS.FAMILY])
262                     ) {
263                         return getBlackFridayUpsellData({ plan: planParameters.plan });
264                     }
265                     const plan = getSafePlan(plansMap, PLANS.PASS_FAMILY);
266                     return getBlackFridayUpsellData({ plan });
267                 }
269                 if ((currentPlan.Name === PLANS.VPN2024 || currentPlan.Name === PLANS.VPN) && hasMonthlyCycle) {
270                     if (
271                         (options.cycle === CYCLE.YEARLY || options.cycle === CYCLE.TWO_YEARS) &&
272                         hasSelectedPlan(planParameters.plan, [PLANS.VPN2024])
273                     ) {
274                         return getBlackFridayUpsellData({
275                             plan: planParameters.plan,
276                             cycle: options.cycle,
277                             coupon: options.coupon,
278                         });
279                     }
280                 }
282                 const isValidBundleDuoFamilyFromPlus =
283                     options.cycle === CYCLE.YEARLY &&
284                     hasSelectedPlan(planParameters.plan, [PLANS.BUNDLE, PLANS.DUO, PLANS.FAMILY]);
286                 if (isValidBundleDuoFamilyFromPlus) {
287                     return getBlackFridayUpsellData({ plan: planParameters.plan });
288                 }
289                 // Any other selected plan will give yearly bundle
290                 return getBlackFridayUpsellData({ plan: getSafePlan(plansMap, PLANS.BUNDLE) });
291             }
293             if (currentPlan.Name === PLANS.BUNDLE) {
294                 if (
295                     subscription?.CouponCode === COUPON_CODES.DEGOOGLE &&
296                     hasMonthlyCycle &&
297                     options.cycle === CYCLE.YEARLY &&
298                     hasSelectedPlan(planParameters.plan, [PLANS.BUNDLE])
299                 ) {
300                     return getBlackFridayUpsellData({ plan: planParameters.plan });
301                 }
302                 if (options.cycle === CYCLE.YEARLY && hasSelectedPlan(planParameters.plan, [PLANS.DUO, PLANS.FAMILY])) {
303                     return getBlackFridayUpsellData({ plan: planParameters.plan });
304                 }
305                 return getBlackFridayUpsellData({ plan: getSafePlan(plansMap, PLANS.DUO) });
306             }
308             if (currentPlan.Name === PLANS.PASS_FAMILY || currentPlan.Name === PLANS.DUO) {
309                 if (options.cycle === CYCLE.YEARLY && hasSelectedPlan(planParameters.plan, [PLANS.FAMILY])) {
310                     return getBlackFridayUpsellData({ plan: planParameters.plan });
311                 }
312                 return getBlackFridayUpsellData({ plan: getSafePlan(plansMap, PLANS.FAMILY) });
313             }
315             if (getIsProductB2BPlan(currentPlan.Name) && !getIsBundleB2BPlan(planParameters.plan.Name)) {
316                 return getUpsellData(PLANS.BUNDLE_PRO_2024);
317             }
318         } else {
319             if (audience === Audience.B2B) {
320                 if (getIsProductB2BPlan(planParameters.plan.Name) || getIsBundleB2BPlan(planParameters.plan.Name)) {
321                     return getUpsellData(planParameters.plan.Name);
322                 }
323                 return noUpsell;
324             } else {
325                 if (getHasAnyPlusPlan(currentPlan.Name)) {
326                     if (
327                         hasSelectedPlan(planParameters.plan, [PLANS.PASS_FAMILY, PLANS.BUNDLE, PLANS.DUO, PLANS.FAMILY])
328                     ) {
329                         return getUpsellData(planParameters.plan.Name);
330                     }
331                     return getUpsellData(PLANS.BUNDLE);
332                 }
334                 if (currentPlan.Name === PLANS.BUNDLE) {
335                     if (hasSelectedPlan(planParameters.plan, [PLANS.DUO, PLANS.FAMILY])) {
336                         return getUpsellData(planParameters.plan.Name);
337                     }
338                     return getUpsellData(PLANS.DUO);
339                 }
341                 if (currentPlan.Name === PLANS.PASS_FAMILY || currentPlan.Name === PLANS.DUO) {
342                     if (hasSelectedPlan(planParameters.plan, [PLANS.DUO, PLANS.FAMILY])) {
343                         return getUpsellData(planParameters.plan.Name);
344                     }
345                     return getUpsellData(PLANS.FAMILY);
346                 }
348                 if (getIsProductB2BPlan(currentPlan.Name) && !getIsBundleB2BPlan(planParameters.plan.Name)) {
349                     return getUpsellData(PLANS.BUNDLE_PRO_2024);
350                 }
351             }
352         }
353     }
355     return noUpsell;
358 export const getRelativeUpsellPrice = (
359     upsell: Upsell,
360     plansMap: PlansMap,
361     checkResult: SubscriptionCheckResponse | undefined,
362     subscription: Subscription | undefined,
363     cycle: CYCLE
364 ) => {
365     if (!upsell.currentPlan || !upsell.plan) {
366         return 0;
367     }
369     if (subscription && checkResult) {
370         return (
371             (checkResult.Amount + (checkResult?.CouponDiscount || 0)) / cycle - subscription.Amount / subscription.Cycle
372         );
373     }
375     const pricingCurrentPlan = getPricingFromPlanIDs({ [upsell.currentPlan.Name]: 1 }, plansMap);
376     const pricingUpsell = getPricingFromPlanIDs({ [upsell.plan.Name]: 1 }, plansMap);
378     return pricingUpsell.plans[cycle] / cycle - pricingCurrentPlan.plans[cycle] / cycle;
381 const hasAccess = ({
382     toApp,
383     user,
384     audience,
385     currentPlan,
386 }: {
387     toApp: APP_NAMES;
388     user: User;
389     audience: Audience;
390     currentPlan?: SubscriptionPlan;
391 }) => {
392     if (toApp === APPS.PROTONPASS) {
393         if (audience === Audience.B2B) {
394             return [
395                 PLANS.PASS_BUSINESS,
396                 PLANS.VISIONARY,
397                 PLANS.FAMILY,
398                 PLANS.BUNDLE_PRO,
399                 PLANS.BUNDLE_PRO_2024,
400                 PLANS.ENTERPRISE,
401             ].includes(currentPlan?.Name as any);
402         }
403         return hasPaidPass(user);
404     }
406     if ([APPS.PROTONMAIL, APPS.PROTONCALENDAR].includes(toApp as any)) {
407         return hasPaidMail(user);
408     }
410     if (toApp === APPS.PROTONDRIVE) {
411         return hasPaidDrive(user);
412     }
414     return false;
417 export const getUserInfo = async ({
418     api,
419     audience,
420     paymentsApi,
421     user,
422     options,
423     plansMap,
424     plans,
425     planParameters,
426     signupParameters,
427     upsellPlanCard,
428     toApp,
429 }: {
430     api: Api;
431     paymentsApi: PaymentsApi;
432     user?: User | undefined;
433     options: Options;
434     plansMap: PlansMap;
435     plans: Plan[];
436     audience: Audience;
437     upsellPlanCard?: PlanCard;
438     planParameters: PlanParameters;
439     signupParameters: SignupParameters2;
440     toApp: APP_NAMES;
441 }): Promise<{
442     paymentMethods: SavedPaymentMethod[];
443     subscription: Subscription | undefined;
444     subscriptionData: SubscriptionData;
445     organization: Organization | undefined;
446     state: SessionData['state'];
447     upsell: Upsell;
448     defaultPaymentMethod: PAYMENT_METHOD_TYPES | undefined;
449 }> => {
450     if (!user) {
451         return {
452             paymentMethods: [],
453             subscription: undefined,
454             subscriptionData: await getSubscriptionData(paymentsApi, options),
455             organization: undefined,
456             defaultPaymentMethod: undefined,
457             state: {
458                 payable: true,
459                 subscribed: false,
460                 admin: false,
461                 access: false,
462             },
463             upsell: getUpsell({ audience, plansMap, upsellPlanCard, options, planParameters, toApp }),
464         };
465     }
467     const state = {
468         payable: getCanPay(user),
469         admin: getIsAdmin(user),
470         subscribed: Boolean(user.Subscribed),
471         access: false,
472     };
474     const forcePaymentsVersion = getMaybeForcePaymentsVersion(user);
476     const [paymentMethods, subscription, organization] = await Promise.all([
477         state.payable
478             ? api(queryPaymentMethods(forcePaymentsVersion)).then(({ PaymentMethods }) => PaymentMethods)
479             : [],
480         state.payable && state.admin && state.subscribed
481             ? api(getSubscription(forcePaymentsVersion)).then(
482                   ({ Subscription, UpcomingSubscription }) => UpcomingSubscription ?? Subscription
483               )
484             : (FREE_SUBSCRIPTION as unknown as Subscription),
485         state.subscribed
486             ? api<{
487                   Organization: Organization;
488               }>(getOrganization()).then(({ Organization }) => Organization)
489             : undefined,
490     ]);
492     const currentPlan = (() => {
493         const plan = getPlan(subscription);
494         if (plan) {
495             return plan;
496         }
497         if (organization) {
498             return plansMap[organization.PlanName];
499         }
500     })();
502     const upsell = getUpsell({
503         audience,
504         currentPlan,
505         subscription,
506         upsellPlanCard,
507         plansMap,
508         options,
509         planParameters,
510         toApp,
511     });
513     if (user && hasAccess({ toApp, user, audience, currentPlan })) {
514         state.access = true;
515     }
517     // TODO: WalletEA
518     if (toApp === APPS.PROTONWALLET) {
519         state.access = false;
520     }
522     // Disable the access modal and show the upsell flow instead
523     if (state.payable && upsell.plan?.Name) {
524         state.access = false;
525     }
527     const subscriptionData = await (() => {
528         const optionsWithSubscriptionDefaults = {
529             ...options,
530             // TODO: make this more generic
531             cycle: signupParameters.cycle || subscription.Cycle || options.cycle,
532             currency: options.currency,
533             coupon: subscription.CouponCode || options.coupon,
534         };
536         if (!state.payable || state.access) {
537             return getFreeSubscriptionData(optionsWithSubscriptionDefaults);
538         }
540         if (upsell.plan) {
541             return getSubscriptionData(paymentsApi, {
542                 ...optionsWithSubscriptionDefaults,
543                 ...upsell.subscriptionOptions,
544                 planIDs: switchPlan({
545                     planIDs: getPlanIDs(subscription),
546                     planID: upsell.plan.Name,
547                     organization,
548                     plans,
549                     user,
550                 }),
551             });
552         }
554         return getSubscriptionData(paymentsApi, optionsWithSubscriptionDefaults);
555     })();
557     return {
558         paymentMethods,
559         defaultPaymentMethod: undefined,
560         subscription,
561         subscriptionData,
562         organization,
563         state,
564         upsell,
565     };
568 export const getSessionDataFromSignup = (cache: SignupCacheResult): SessionData => {
569     const setupData = cache.setupData;
570     if (!setupData) {
571         throw new Error('Missing setup data');
572     }
573     return {
574         UID: setupData.authResponse.UID,
575         user: setupData.user,
576         localID: setupData.authResponse.LocalID,
577         clientKey: setupData.clientKey,
578         offlineKey: setupData.offlineKey,
579         keyPassword: setupData.keyPassword,
580         persistent: cache.persistent,
581         trusted: cache.trusted,
582         subscription: undefined,
583         organization: undefined,
584         paymentMethods: [],
585         defaultPaymentMethod: undefined,
586         state: {
587             payable: true,
588             admin: false,
589             subscribed: false,
590             access: false,
591         },
592     };
595 export const runAfterScroll = (el: Element, done: () => void) => {
596     let same = 0;
597     let lastPos = el.scrollTop;
598     let startTime = -1;
599     // Timeout after 1 second
600     const maxTime = 1000;
601     const maxFrames = 4;
603     const cb = (time: number) => {
604         if (startTime === -1) {
605             startTime = time;
606         }
607         if (time - startTime > maxTime) {
608             done();
609             return;
610         }
611         const newPos = el.scrollTop;
612         if (lastPos === newPos) {
613             if (same++ > maxFrames) {
614                 done();
615                 return;
616             }
617         } else {
618             same = 0;
619             lastPos = newPos;
620         }
622         requestAnimationFrame(cb);
623     };
625     requestAnimationFrame(cb);
628 export type SubscriptionDataCycleMapping = Partial<{ [key in PLANS]: CycleMapping<SubscriptionData> }>;
629 export const getPlanCardSubscriptionData = async ({
630     planIDs,
631     plansMap,
632     paymentsApi,
633     coupon: maybeCoupon,
634     cycles,
635     billingAddress,
636 }: {
637     cycles: CYCLE[];
638     planIDs: PlanIDs[];
639     plansMap: PlansMap;
640     paymentsApi: PaymentsApi;
641     coupon?: string | null;
642     billingAddress: BillingAddress;
643 }): Promise<SubscriptionDataCycleMapping> => {
644     const result = await Promise.all(
645         planIDs.flatMap((planIDs) =>
646             cycles
647                 .map((cycle) => [planIDs, cycle] as const)
648                 .map(async ([planIDs, cycle]): Promise<SubscriptionData> => {
649                     const coupon =
650                         maybeCoupon === null ? undefined : getAutoCoupon({ coupon: maybeCoupon, planIDs, cycle });
652                     // make sure that the plan and all its addons exist
653                     const plansToCheck = Object.keys(planIDs) as (PLANS | ADDON_NAMES)[];
654                     const plansExist = plansToCheck.every(
655                         (planName) => plansMap[planName]?.Pricing?.[cycle] !== undefined
656                     );
658                     // we extract the currency of the currently selected plan in plansMap.
659                     const currency = plansMap[plansToCheck[0]]?.Currency ?? DEFAULT_CURRENCY;
661                     // If there's no coupon we can optimistically calculate the price.
662                     // Also always exclude Enterprise (price never shown).
663                     // In addition, if the selected plan doesn't exist, then we don't do the live check call.
664                     if (!coupon || planIDs[PLANS.ENTERPRISE] || !plansExist) {
665                         return {
666                             planIDs,
667                             currency,
668                             cycle,
669                             checkResult: {
670                                 ...getOptimisticCheckResult({ planIDs, plansMap, cycle, currency }),
671                                 Currency: currency,
672                                 PeriodEnd: 0,
673                             },
674                             billingAddress,
675                         };
676                     }
678                     const subscriptionData = await getSubscriptionData(paymentsApi, {
679                         plansMap,
680                         planIDs,
681                         cycle,
682                         coupon,
683                         currency,
684                         billingAddress,
685                         info: true,
686                     });
687                     return subscriptionData;
688                 })
689         )
690     );
692     return result.reduce<SubscriptionDataCycleMapping>((acc, subscriptionData) => {
693         const plan = !hasPlanIDs(subscriptionData.planIDs)
694             ? FREE_PLAN
695             : getPlanFromPlanIDs(plansMap, subscriptionData.planIDs);
696         if (!plan) {
697             return acc;
698         }
699         let cycleMapping = acc[plan.Name as unknown as keyof typeof acc];
700         if (!cycleMapping) {
701             cycleMapping = {};
702             acc[plan.Name as unknown as keyof typeof acc] = cycleMapping;
703         }
704         cycleMapping[subscriptionData.cycle] = subscriptionData;
705         return acc;
706     }, {});
709 export const swapCurrency = (
710     subscriptionDataCycleMapping: SubscriptionDataCycleMapping,
711     currency: Currency
712 ): SubscriptionDataCycleMapping => {
713     return (
714         Object.entries(subscriptionDataCycleMapping) as [PLANS, CycleMapping<SubscriptionData>][]
715     ).reduce<SubscriptionDataCycleMapping>((acc, [planName, cycleMapping]) => {
716         acc[planName] = (Object.entries(cycleMapping) as [string, SubscriptionData][]).reduce<
717             CycleMapping<SubscriptionData>
718         >((acc, [cycle, subscriptionData]) => {
719             acc[+cycle as Cycle] = {
720                 ...subscriptionData,
721                 currency,
722                 checkResult: {
723                     ...subscriptionData.checkResult,
724                     Currency: currency,
725                 },
726             };
727             return acc;
728         }, {});
729         return acc;
730     }, {});
733 export const getSubscriptionMapping = ({
734     subscriptionDataCycleMapping,
735     planName,
736     planIDs: newPlanIDs,
737 }: {
738     subscriptionDataCycleMapping: SubscriptionDataCycleMapping;
739     planName: PLANS | ADDON_NAMES;
740     planIDs: PlanIDs;
741 }) => {
742     // ugh
743     let subscriptionMapping = subscriptionDataCycleMapping?.[planName as unknown as PLANS];
744     if (!subscriptionMapping) {
745         return undefined;
746     }
747     const firstKey = Object.keys(subscriptionMapping)[0] as unknown as keyof typeof subscriptionMapping;
748     const planIDs = subscriptionMapping[firstKey]?.planIDs;
749     if (!isDeepEqual(planIDs, newPlanIDs)) {
750         subscriptionMapping = undefined;
751     }
752     return subscriptionMapping;
755 interface GetAccessiblePlansParams {
756     planCards: SignupConfiguration['planCards'];
757     audience: Audience;
758     plans: Plan[];
759     paramPlanName?: string;
762 export const getAccessiblePlans = ({
763     planCards,
764     audience,
765     plans,
766     paramPlanName,
767 }: GetAccessiblePlansParams): StrictPlan[] => {
768     if (audience !== Audience.B2C && audience !== Audience.B2B) {
769         return [];
770     }
772     const accessiblePlanNames = planCards[audience].map(({ plan }) => plan);
773     if (paramPlanName && isStringPLAN(paramPlanName)) {
774         accessiblePlanNames.push(paramPlanName);
775     }
777     return plans.filter(({ Name }) => accessiblePlanNames.includes(Name as PLANS)) as StrictPlan[];