@stigg/react-sdk 4.5.3 → 4.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +7 -0
  2. package/dist/components/checkout/CheckoutContainer.d.ts +4 -2
  3. package/dist/components/checkout/hooks/useSubscriptionState.d.ts +2 -2
  4. package/dist/components/checkout/steps/payment/stripe/stripe.utils.d.ts +9 -13
  5. package/dist/components/checkout/steps/payment/stripe/useSubmit.d.ts +2 -1
  6. package/dist/components/customerPortal/paywall/CustomerPortalPaywall.d.ts +0 -1
  7. package/dist/components/customerPortal/paywall/CustomerPortalPaywall.style.d.ts +0 -1
  8. package/dist/components/paywall/Paywall.d.ts +0 -1
  9. package/dist/components/paywall/PlanOffering.d.ts +2 -1
  10. package/dist/components/paywall/PlanOfferingButton.d.ts +1 -2
  11. package/dist/react-sdk.cjs.development.js +287 -260
  12. package/dist/react-sdk.cjs.development.js.map +1 -1
  13. package/dist/react-sdk.cjs.production.min.js +1 -1
  14. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  15. package/dist/react-sdk.esm.js +288 -261
  16. package/dist/react-sdk.esm.js.map +1 -1
  17. package/package.json +2 -2
  18. package/src/components/checkout/CheckoutContainer.tsx +13 -3
  19. package/src/components/checkout/hooks/useSubscriptionState.ts +2 -3
  20. package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +31 -22
  21. package/src/components/checkout/steps/payment/stripe/useSubmit.ts +63 -46
  22. package/src/components/checkout/summary/CheckoutSummary.tsx +1 -0
  23. package/src/components/customerPortal/CustomerPortalContainer.tsx +5 -8
  24. package/src/components/customerPortal/paywall/CustomerPortalPaywall.style.ts +0 -10
  25. package/src/components/customerPortal/paywall/CustomerPortalPaywall.tsx +2 -5
  26. package/src/components/paywall/Paywall.tsx +36 -20
  27. package/src/components/paywall/PaywallContainer.tsx +2 -2
  28. package/src/components/paywall/PlanOffering.tsx +32 -17
  29. package/src/components/paywall/PlanOfferingButton.tsx +1 -3
  30. package/src/components/paywall/utils/mapPaywallData.ts +13 -11
  31. package/src/stories/Checkout.stories.tsx +2 -2
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.5.3",
2
+ "version": "4.5.5",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -109,7 +109,7 @@
109
109
  "@emotion/react": "^11.10.5",
110
110
  "@emotion/styled": "^11.10.5",
111
111
  "@mui/material": "^5.12.0",
112
- "@stigg/js-client-sdk": "2.24.1",
112
+ "@stigg/js-client-sdk": "2.24.2",
113
113
  "@stripe/react-stripe-js": "^2.1.1",
114
114
  "@stripe/stripe-js": "^1.54.1",
115
115
  "@types/styled-components": "^5.1.26",
@@ -1,6 +1,12 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Elements } from '@stripe/react-stripe-js';
3
- import { PricingType, BillingAddress, ApplySubscription, CheckoutStatePlan } from '@stigg/js-client-sdk';
3
+ import {
4
+ PricingType,
5
+ BillingAddress,
6
+ ApplySubscription,
7
+ CheckoutStatePlan,
8
+ ApplySubscriptionResults,
9
+ } from '@stigg/js-client-sdk';
4
10
  import { CheckoutContent, CheckoutLayout, CheckoutPanel } from './CheckoutContainer.style';
5
11
  import { CheckoutProgressBar } from './progressBar/CheckoutProgressBar';
6
12
  import { CheckoutSummary, CheckoutSummarySkeleton } from './summary';
@@ -45,9 +51,13 @@ const getStepProps = (
45
51
  }
46
52
  };
47
53
 
48
- export type CheckoutResult = { success: boolean; errorMessage?: string };
54
+ export type CheckoutResult = { success: boolean; errorMessage?: string; results?: ApplySubscriptionResults };
49
55
 
50
- export type OnCheckoutParams = { checkoutParams: ApplySubscription; checkoutAction: () => Promise<CheckoutResult> };
56
+ export type OnCheckoutParams = {
57
+ customerId: string;
58
+ checkoutParams: ApplySubscription;
59
+ checkoutAction: (params: ApplySubscription) => Promise<CheckoutResult>;
60
+ };
51
61
 
52
62
  export type OnCheckoutCompletedParams = { success: boolean; error?: string };
53
63
 
@@ -1,9 +1,9 @@
1
- import { ApplySubscription } from '@stigg/js-client-sdk';
1
+ import { ApplySubscription, PreviewSubscription } from '@stigg/js-client-sdk';
2
2
  import { useCheckoutContext } from '../CheckoutProvider';
3
3
  import { useCheckoutModel } from './useCheckoutModel';
4
4
  import { useSubscriptionModel } from './useSubscriptionModel';
5
5
 
6
- export function useSubscriptionState(): ApplySubscription | undefined {
6
+ export function useSubscriptionState(): ApplySubscription | PreviewSubscription | undefined {
7
7
  const subscription = useSubscriptionModel();
8
8
  const [{ resourceId }] = useCheckoutContext();
9
9
  const { checkoutState } = useCheckoutModel();
@@ -22,6 +22,5 @@ export function useSubscriptionState(): ApplySubscription | undefined {
22
22
  addons,
23
23
  promotionCode: subscription.promotionCode,
24
24
  billingCountryCode: subscription.billingCountryCode,
25
- ...(subscription.taxPercentage ? { billingInformation: { taxPercentage: subscription.taxPercentage } } : {}),
26
25
  };
27
26
  }
@@ -23,28 +23,6 @@ export async function handleStripeFormValidations({ elements }: Pick<StripeEleme
23
23
  return { success: true };
24
24
  }
25
25
 
26
- export async function handleNewPaymentMethod({
27
- stripe,
28
- elements,
29
- setupIntentClientSecret,
30
- }: {
31
- stripe: Stripe | null;
32
- elements: StripeElements | null;
33
- setupIntentClientSecret?: string;
34
- }) {
35
- if (!stripe || !elements || !setupIntentClientSecret) {
36
- return { success: false };
37
- }
38
-
39
- const { newPaymentMethodId, setupErrorMessage } = await handleStripeSetup({
40
- stripe,
41
- elements,
42
- clientSecret: setupIntentClientSecret,
43
- });
44
-
45
- return { success: !!newPaymentMethodId, paymentMethodId: newPaymentMethodId, errorMessage: setupErrorMessage };
46
- }
47
-
48
26
  export async function handleStripeNextAction({
49
27
  applySubscriptionResults,
50
28
  stripe,
@@ -108,3 +86,34 @@ async function handleStripeSetup({
108
86
 
109
87
  return { newPaymentMethodId, setupErrorMessage };
110
88
  }
89
+
90
+ export async function handleNewPaymentMethod({
91
+ stripe,
92
+ elements,
93
+ setupIntentClientSecret,
94
+ }: {
95
+ stripe: Stripe | null;
96
+ elements: StripeElements | null;
97
+ setupIntentClientSecret?: string;
98
+ }): Promise<{ success: boolean; errorMessage?: string; paymentMethodId?: string }> {
99
+ if (!stripe || !elements || !setupIntentClientSecret) {
100
+ return { success: false };
101
+ }
102
+
103
+ const stripeFormValidationsResults = await handleStripeFormValidations({ elements });
104
+ if (!stripeFormValidationsResults.success) {
105
+ return { success: false, errorMessage: stripeFormValidationsResults.errorMessage };
106
+ }
107
+
108
+ const { newPaymentMethodId, setupErrorMessage } = await handleStripeSetup({
109
+ stripe,
110
+ elements,
111
+ clientSecret: setupIntentClientSecret,
112
+ });
113
+
114
+ if (setupErrorMessage || !newPaymentMethodId) {
115
+ return { success: false, errorMessage: setupErrorMessage };
116
+ }
117
+
118
+ return { success: true, paymentMethodId: newPaymentMethodId };
119
+ }
@@ -5,7 +5,7 @@ import { useCheckoutModel } from '../../../hooks';
5
5
  import { usePaymentStepModel } from '../../../hooks/usePaymentStepModel';
6
6
  import { useSubscriptionState } from '../../../hooks/useSubscriptionState';
7
7
  import { CheckoutContainerProps, CheckoutResult } from '../../../CheckoutContainer';
8
- import { handleNewPaymentMethod, handleStripeFormValidations, handleStripeNextAction } from './stripe.utils';
8
+ import { handleNewPaymentMethod, handleStripeNextAction } from './stripe.utils';
9
9
  import { ANIMATION_DURATION } from '../../../summary/CheckoutSuccess';
10
10
 
11
11
  const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
@@ -14,14 +14,21 @@ export type HandleSubmitResult = { results?: ApplySubscriptionResults; success:
14
14
 
15
15
  export type UseSubmitProps = {
16
16
  onSuccess?: () => void;
17
+ isMocked?: boolean;
17
18
  } & Pick<CheckoutContainerProps, 'onCheckout' | 'onCheckoutCompleted' | 'disableSuccessAnimation'>;
18
19
 
19
- export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess, disableSuccessAnimation }: UseSubmitProps) {
20
+ export function useSubmit({
21
+ isMocked = false,
22
+ onCheckout,
23
+ onCheckoutCompleted,
24
+ onSuccess,
25
+ disableSuccessAnimation,
26
+ }: UseSubmitProps) {
20
27
  const { stigg } = useStiggContext();
21
28
  const { useNewPaymentMethod } = usePaymentStepModel();
22
29
  const subscriptionState = useSubscriptionState();
23
30
  const { checkoutState, widgetState, setWidgetReadOnly } = useCheckoutModel();
24
- const { setupSecret: setupIntentClientSecret } = checkoutState || {};
31
+ const { setupSecret: setupIntentClientSecret, customer } = checkoutState || {};
25
32
  const stripe = useStripe();
26
33
  const elements = useElements();
27
34
 
@@ -32,48 +39,38 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess, disableS
32
39
  return { success: false, errorMessage: 'Unexpected error, please contact support.' };
33
40
  }
34
41
 
35
- let checkoutParams: ApplySubscription = { ...subscriptionState };
36
- let checkoutResults: ApplySubscriptionResults | undefined;
42
+ let success = true;
37
43
  let errorMessage: string | undefined;
44
+ let usedStiggCheckoutAction = false;
38
45
  let paymentMethodId: string | undefined;
39
46
 
40
- const checkoutAction = async (): Promise<CheckoutResult> => {
41
- if (useNewPaymentMethod) {
42
- const { success, errorMessage: stripeValidationsErrorMessage } = await handleStripeFormValidations({
43
- elements,
44
- });
45
-
46
- if (!success) {
47
- return { success: false, errorMessage: stripeValidationsErrorMessage };
48
- }
49
-
50
- const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
51
- if (!paymentMethodResults.success) {
52
- errorMessage = paymentMethodResults.errorMessage;
53
- }
47
+ setWidgetReadOnly(true);
54
48
 
49
+ if (!isMocked && useNewPaymentMethod) {
50
+ const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
51
+ if (!paymentMethodResults.success) {
52
+ errorMessage = paymentMethodResults.errorMessage;
53
+ success = false;
54
+ } else {
55
55
  paymentMethodId = paymentMethodResults.paymentMethodId;
56
56
  }
57
+ }
57
58
 
58
- if (errorMessage) {
59
- return { success: false, errorMessage };
60
- }
59
+ const checkoutParams: ApplySubscription = { ...subscriptionState, paymentMethodId };
61
60
 
62
- checkoutParams = { ...checkoutParams, paymentMethodId };
61
+ const checkoutAction = async (params: ApplySubscription): Promise<CheckoutResult> => {
62
+ usedStiggCheckoutAction = true;
63
63
 
64
64
  try {
65
- const applySubscriptionResults = await stigg.applySubscription(checkoutParams);
66
- const nextActionResults = await handleStripeNextAction({
67
- applySubscriptionResults,
68
- stripe,
69
- });
65
+ const applySubscriptionResults = await stigg.applySubscription(params);
70
66
 
71
- checkoutResults = nextActionResults;
72
- if (nextActionResults.errorMessage) {
73
- errorMessage = nextActionResults.errorMessage;
74
- }
67
+ const nextActionResults = await handleStripeNextAction({ applySubscriptionResults, stripe });
75
68
 
76
- return { success: !nextActionResults.errorMessage, errorMessage: nextActionResults.errorMessage };
69
+ return {
70
+ success: !nextActionResults.errorMessage,
71
+ errorMessage: nextActionResults.errorMessage,
72
+ results: applySubscriptionResults,
73
+ };
77
74
  } catch (e) {
78
75
  console.error(e);
79
76
  errorMessage = (e as any)?.message;
@@ -81,21 +78,41 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess, disableS
81
78
  }
82
79
  };
83
80
 
84
- setWidgetReadOnly(true);
81
+ let checkoutResults: ApplySubscriptionResults | undefined;
85
82
 
86
- let success = false;
87
- if (onCheckout) {
88
- const externalCheckoutResults = await onCheckout({ checkoutParams, checkoutAction });
89
- if (!externalCheckoutResults.success && externalCheckoutResults.errorMessage) {
90
- errorMessage = externalCheckoutResults.errorMessage;
91
- }
92
- success = externalCheckoutResults.success && !errorMessage;
93
- } else {
94
- const checkoutActionResults = await checkoutAction();
95
- if (!checkoutActionResults.success && checkoutActionResults.errorMessage) {
96
- errorMessage = checkoutActionResults.errorMessage;
83
+ if (success) {
84
+ if (onCheckout) {
85
+ const externalCheckoutResults = await onCheckout({
86
+ customerId: customer!.id,
87
+ checkoutParams,
88
+ checkoutAction,
89
+ });
90
+ if (externalCheckoutResults.errorMessage) {
91
+ errorMessage = externalCheckoutResults.errorMessage;
92
+ }
93
+
94
+ if (!usedStiggCheckoutAction && externalCheckoutResults.results && externalCheckoutResults.success) {
95
+ const nextActionResults = await handleStripeNextAction({
96
+ applySubscriptionResults: externalCheckoutResults.results,
97
+ stripe,
98
+ });
99
+
100
+ if (nextActionResults.errorMessage) {
101
+ errorMessage = nextActionResults.errorMessage;
102
+ success = false;
103
+ }
104
+ }
105
+
106
+ success = success && externalCheckoutResults.success && !errorMessage;
107
+ checkoutResults = externalCheckoutResults.results;
108
+ } else {
109
+ const checkoutActionResults = await checkoutAction(checkoutParams);
110
+ if (!checkoutActionResults.success && checkoutActionResults.errorMessage) {
111
+ errorMessage = checkoutActionResults.errorMessage;
112
+ }
113
+ success = checkoutActionResults.success && !errorMessage;
114
+ checkoutResults = checkoutActionResults.results;
97
115
  }
98
- success = checkoutActionResults.success && !errorMessage;
99
116
  }
100
117
 
101
118
  setWidgetReadOnly(false);
@@ -122,6 +122,7 @@ export const CheckoutSummary = ({
122
122
  const { subscriptionPreview, isFetchingSubscriptionPreview } = usePreviewSubscription({ onMockCheckoutPreview });
123
123
 
124
124
  const { handleSubmit, isLoading } = useSubmit({
125
+ isMocked: !!onMockCheckoutPreview, // This is a hack to make the submit button work with mocked data
125
126
  disableSuccessAnimation,
126
127
  onCheckout,
127
128
  onCheckoutCompleted,
@@ -4,7 +4,6 @@ import { CustomerPortalHeader } from './CustomerPortalHeader';
4
4
  import { PaymentDetailsSection } from './billing/PaymentDetailsSection';
5
5
  import { CustomerPortalPaywall } from './paywall/CustomerPortalPaywall';
6
6
  import { useCustomerPortalContext } from './CustomerPortalProvider';
7
- import { PricingType } from '@stigg/js-client-sdk';
8
7
  import { CustomerPortalLayout, CustomerPortalSections } from './CustomerPortal.style';
9
8
  import { CustomerPortalProps } from './CustomerPortal';
10
9
  import { InvoicesSection, useStiggContext } from '../..';
@@ -24,20 +23,19 @@ export function CustomerPortalContainer({
24
23
 
25
24
  const onManageClick = () => {
26
25
  if (onManageSubscription) {
26
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
27
27
  onManageSubscription();
28
28
  } else {
29
29
  customerPortalSectionRef?.current?.scrollIntoView({ behavior: 'smooth' });
30
30
  }
31
31
  };
32
- const hasCustomSubscription = customerPortal?.subscriptions.some(
33
- subscription => subscription.pricingType === PricingType.Custom,
34
- );
35
- const shouldShowUsage = !hiddenSections?.some(section => section === 'usage');
32
+
33
+ const shouldShowUsage = !hiddenSections?.some((section) => section === 'usage');
36
34
  const shouldShowPaymentDetails = !hiddenSections?.some(
37
- section => section === 'paymentDetails' || section === 'billingInformation',
35
+ (section) => section === 'paymentDetails' || section === 'billingInformation',
38
36
  );
39
37
  const shouldShowInvoices = !hiddenSections?.some(
40
- section => section === 'invoices' || section === 'billingInformation',
38
+ (section) => section === 'invoices' || section === 'billingInformation',
41
39
  );
42
40
 
43
41
  return (
@@ -56,7 +54,6 @@ export function CustomerPortalContainer({
56
54
  <CustomerPortalPaywall
57
55
  ref={customerPortalSectionRef}
58
56
  paywallComponent={paywallComponent}
59
- hidePaywallButtons={!!hasCustomSubscription}
60
57
  theme={theme}
61
58
  title={textOverrides.paywallSectionTitle}
62
59
  isLoading={!customerPortal || isLoading}
@@ -1,10 +1,8 @@
1
- import { css } from '@emotion/react';
2
1
  import styled from '@emotion/styled/macro';
3
2
  import { STIGG_WATERMARK_CLASSNAME } from '../../common/PoweredByStigg';
4
3
  import { SectionContainer } from '../common/SectionContainer';
5
4
 
6
5
  export const CustomerPortalPaywallLayout = styled(SectionContainer)<{
7
- $hideButtons: boolean;
8
6
  $backgroundColor: string;
9
7
  $borderColor: string;
10
8
  }>`
@@ -29,14 +27,6 @@ export const CustomerPortalPaywallLayout = styled(SectionContainer)<{
29
27
  margin-right: auto;
30
28
  }
31
29
 
32
- ${({ $hideButtons }) =>
33
- $hideButtons &&
34
- css`
35
- .stigg-paywall-plan-button:not([disabled]) {
36
- display: none;
37
- }
38
- `}
39
-
40
30
  .${STIGG_WATERMARK_CLASSNAME} {
41
31
  display: none;
42
32
  }
@@ -6,14 +6,13 @@ import { SectionHeader } from '../common/SectionHeader';
6
6
 
7
7
  type CustomerPortalPaywallProps = {
8
8
  paywallComponent?: React.ReactNode;
9
- hidePaywallButtons: boolean;
10
9
  theme: CustomerPortalTheme;
11
10
  title: string;
12
11
  isLoading: boolean;
13
12
  };
14
13
 
15
14
  export const CustomerPortalPaywall = React.forwardRef<HTMLDivElement, CustomerPortalPaywallProps>(
16
- ({ paywallComponent, hidePaywallButtons, theme, title, isLoading }, ref) => {
15
+ ({ paywallComponent, theme, title, isLoading }, ref) => {
17
16
  if (!paywallComponent) {
18
17
  return null;
19
18
  }
@@ -22,10 +21,8 @@ export const CustomerPortalPaywall = React.forwardRef<HTMLDivElement, CustomerPo
22
21
  <CustomerPortalPaywallLayout
23
22
  className="stigg-customer-portal-paywall-section"
24
23
  ref={ref}
25
- $hideButtons={hidePaywallButtons}
26
24
  $backgroundColor={theme.backgroundColor}
27
- $borderColor={theme.borderColor}
28
- >
25
+ $borderColor={theme.borderColor}>
29
26
  <SectionHeader className="stigg-customer-portal-paywall-header">
30
27
  <SectionTitle isLoading={isLoading} className="stigg-customer-portal-paywall-section-title" title={title} />
31
28
  </SectionHeader>
@@ -1,5 +1,5 @@
1
- import { BillableFeature, BillingPeriod, Customer, Plan, Subscription } from '@stigg/js-client-sdk';
2
- import React, { useCallback } from 'react';
1
+ import { BillableFeature, BillingPeriod, Customer, Plan, PricingType, Subscription } from '@stigg/js-client-sdk';
2
+ import React, { useCallback, useMemo } from 'react';
3
3
  import styled from '@emotion/styled/macro';
4
4
  import { PlanOffering } from './PlanOffering';
5
5
  import { BillingPeriodPicker } from './BillingPeriodPicker';
@@ -47,7 +47,6 @@ type PaywallProps = {
47
47
  highlightedPlanId?: string;
48
48
  onBillingPeriodChanged: (billingPeriod: BillingPeriod) => void;
49
49
  availableBillingPeriods: BillingPeriod[];
50
- isLoading: boolean;
51
50
  isCustomerOnTrial: boolean;
52
51
  onPlanSelected: OnPlanSelectedCallbackFn;
53
52
  paywallLocale: PaywallLocalization;
@@ -69,7 +68,7 @@ export const Paywall = ({
69
68
  }: PaywallProps) => {
70
69
  const { stigg } = useStiggContext();
71
70
  const discountRate = calculatePaywallDiscountRate(plans);
72
- const shouldShowDescriptionSection = plans.some(plan => !!plan.description);
71
+ const shouldShowDescriptionSection = plans.some((plan) => !!plan.description);
73
72
  const hasMonthlyPrice = hasPricePointsForPlans(plans, BillingPeriod.Monthly);
74
73
  const hasAnnuallyPrice = hasPricePointsForPlans(plans, BillingPeriod.Annually);
75
74
  const plansToShow = getPlansToDisplay(plans, selectedBillingPeriod);
@@ -85,26 +84,42 @@ export const Paywall = ({
85
84
  billableFeatures,
86
85
  });
87
86
  },
88
- [customer, selectedBillingPeriod],
87
+ [customer, selectedBillingPeriod, currentSubscription, onPlanSelected],
89
88
  );
90
89
 
91
- const withStartingAtRow = plansToShow.some(plan => {
92
- const planPrices = plan.pricePoints.filter(pricePoint => pricePoint.billingPeriod === selectedBillingPeriod);
93
- const paywallCalculatedPrice = plan.paywallCalculatedPricePoints?.find(
94
- pricePoint => pricePoint.billingPeriod === selectedBillingPeriod,
95
- );
96
- return planPrices.length > 1 && !!paywallCalculatedPrice?.additionalChargesMayApply;
97
- });
90
+ const isCustomerInCustomPlan = !!currentSubscription && currentSubscription.plan.pricingType === PricingType.Custom;
98
91
 
99
- const withUnitPriceRow = plansToShow.some(plan => {
100
- return !!getPlanPrice(plan, selectedBillingPeriod, paywallLocale, locale, hasMonthlyPrice).unit;
101
- });
92
+ const withStartingAtRow = useMemo(
93
+ () =>
94
+ plansToShow.some((plan) => {
95
+ const planPrices = plan.pricePoints.filter((pricePoint) => pricePoint.billingPeriod === selectedBillingPeriod);
96
+ const paywallCalculatedPrice = plan.paywallCalculatedPricePoints?.find(
97
+ (pricePoint) => pricePoint.billingPeriod === selectedBillingPeriod,
98
+ );
99
+ return planPrices.length > 1 && !!paywallCalculatedPrice?.additionalChargesMayApply;
100
+ }),
101
+ [selectedBillingPeriod, plansToShow],
102
+ );
102
103
 
103
- const withTiersRow = plansToShow.some(plan => {
104
- return !!getSelectedTier(plan, selectedBillingPeriod, currentSubscription, {});
105
- });
104
+ const withUnitPriceRow = useMemo(
105
+ () =>
106
+ plansToShow.some((plan) => {
107
+ return !!getPlanPrice(plan, selectedBillingPeriod, paywallLocale, locale, hasMonthlyPrice).unit;
108
+ }),
109
+ [selectedBillingPeriod, hasMonthlyPrice, locale, paywallLocale, plansToShow],
110
+ );
111
+
112
+ const withTiersRow = useMemo(() => {
113
+ return (
114
+ !isCustomerInCustomPlan &&
115
+ plansToShow.some((plan) => {
116
+ const tiers = getSelectedTier(plan, selectedBillingPeriod, currentSubscription, {});
117
+ return Object.values(tiers).length > 0;
118
+ })
119
+ );
120
+ }, [selectedBillingPeriod, currentSubscription, isCustomerInCustomPlan, plansToShow]);
106
121
 
107
- const withTrialLeftRow = plansToShow.some(plan => {
122
+ const withTrialLeftRow = plansToShow.some((plan) => {
108
123
  return plan.isCurrentCustomerPlan && plan.trialDaysLeft;
109
124
  });
110
125
 
@@ -119,7 +134,7 @@ export const Paywall = ({
119
134
  />
120
135
 
121
136
  <PaywallPlansContainer className="stigg-paywall-plans-layout">
122
- {plansToShow.map(plan => (
137
+ {plansToShow.map((plan) => (
123
138
  <PlanOffering
124
139
  withUnitPriceRow={withUnitPriceRow}
125
140
  withTiersRow={withTiersRow}
@@ -140,6 +155,7 @@ export const Paywall = ({
140
155
  paywallLocale={paywallLocale}
141
156
  locale={locale}
142
157
  customer={customer}
158
+ isCustomerInCustomPlan={isCustomerInCustomPlan}
143
159
  />
144
160
  ))}
145
161
  </PaywallPlansContainer>
@@ -22,7 +22,7 @@ export type PaywallContainerProps = {
22
22
  preferredBillingPeriod?: BillingPeriod;
23
23
  onBillingPeriodChange?: (billingPeriod: BillingPeriod) => void;
24
24
  textOverrides?: DeepPartial<PaywallLocalization>;
25
- billingCountryCode?: string
25
+ billingCountryCode?: string;
26
26
  };
27
27
 
28
28
  export const PaywallContainer = ({
@@ -39,6 +39,7 @@ export const PaywallContainer = ({
39
39
  const hasCustomerPortalContext = useCheckContextExists(CustomerPortalContext);
40
40
  let isCustomerPortalLoading = false;
41
41
  if (hasCustomerPortalContext) {
42
+ // eslint-disable-next-line react-hooks/rules-of-hooks
42
43
  const { isLoading, resourceId: customerPortalResourceId } = useCustomerPortalContext();
43
44
  isCustomerPortalLoading = isLoading;
44
45
  resourceId = customerPortalResourceId;
@@ -81,7 +82,6 @@ export const PaywallContainer = ({
81
82
  onBillingPeriodChanged={handlePeriodChange}
82
83
  availableBillingPeriods={availableBillingPeriods}
83
84
  highlightedPlanId={highlightedPlanId}
84
- isLoading={isLoading}
85
85
  isCustomerOnTrial={isCustomerOnTrial}
86
86
  onPlanSelected={onPlanSelected}
87
87
  paywallLocale={paywallLocale}
@@ -1,18 +1,27 @@
1
1
  import React, { useEffect } from 'react';
2
- import { BillableFeature, BillingPeriod, Customer, PriceTierFragment, Subscription } from '@stigg/js-client-sdk';
2
+ import {
3
+ BillableFeature,
4
+ BillingPeriod,
5
+ Customer,
6
+ PriceTierFragment,
7
+ PricingType,
8
+ Subscription,
9
+ } from '@stigg/js-client-sdk';
3
10
  import styled from '@emotion/styled/macro';
11
+ import classNames from 'classnames';
12
+ import Grid from '@mui/material/Grid';
4
13
  import { PlanEntitlements } from './PlanEntitlements';
5
14
  import { PlanOfferingButton } from './PlanOfferingButton';
6
15
  import { PaywallPlan, SubscribeIntentionType } from './types';
7
16
  import { PaywallLocalization } from './paywallTextOverrides';
8
17
  import { flexLayoutMapper } from '../../theme/getResolvedTheme';
9
18
  import { Typography } from '../common/Typography';
10
- import classNames from 'classnames';
11
- import Grid from '@mui/material/Grid';
12
19
  import MiniSchedule from '../../assets/mini-schedule.svg';
13
20
  import { PlanPrice } from './PlanPrice';
14
21
  import { getSelectedTier } from '../utils/priceTierUtils';
15
22
 
23
+ const PlanOfferingButtonHeight = '66px';
24
+
16
25
  const PlanOfferingContainer = styled.div<{ $isHighlighted: boolean; $isCurrentPlan: boolean }>`
17
26
  position: relative;
18
27
  background-color: ${({ theme, $isCurrentPlan }) =>
@@ -82,6 +91,7 @@ type PlanOfferingProps = {
82
91
  paywallLocale: PaywallLocalization;
83
92
  locale: string;
84
93
  withStartingAtRow: boolean;
94
+ isCustomerInCustomPlan: boolean;
85
95
  };
86
96
 
87
97
  const NextPlanTagContainer = styled.div`
@@ -132,6 +142,7 @@ export function PlanOffering({
132
142
  paywallLocale,
133
143
  locale,
134
144
  withStartingAtRow,
145
+ isCustomerInCustomPlan,
135
146
  }: PlanOfferingProps) {
136
147
  const isNextPlan = plan.isNextPlan && plan.isNextPlan(billingPeriod);
137
148
  const planPrices = plan.pricePoints.filter((pricePoint) => pricePoint.billingPeriod === billingPeriod);
@@ -139,6 +150,7 @@ export function PlanOffering({
139
150
  (pricePoint) => pricePoint.billingPeriod === billingPeriod,
140
151
  );
141
152
  const showStartingAt = planPrices.length > 1 && !!paywallCalculatedPrice?.additionalChargesMayApply;
153
+ const showCTAButton = !isCustomerInCustomPlan || plan.pricingType === PricingType.Custom;
142
154
 
143
155
  let planBadge = null;
144
156
  if (isNextPlan) {
@@ -159,7 +171,7 @@ export function PlanOffering({
159
171
 
160
172
  useEffect(() => {
161
173
  setSelectedTierByFeature(getSelectedTier(plan, billingPeriod, currentSubscription, selectedTierByFeature));
162
- }, [billingPeriod]);
174
+ }, [billingPeriod, currentSubscription, plan, selectedTierByFeature]);
163
175
 
164
176
  const onPlanButtonClick = (intentionType: SubscribeIntentionType) => {
165
177
  const billableFeatures: BillableFeature[] = Object.keys(selectedTierByFeature).map((featureId) => ({
@@ -205,19 +217,22 @@ export function PlanOffering({
205
217
  hasMonthlyPrice={hasMonthlyPrice}
206
218
  />
207
219
 
208
- <PlanOfferingButton
209
- isNextPlan={isNextPlan}
210
- customer={customer}
211
- plan={plan}
212
- currentSubscription={currentSubscription}
213
- billingPeriod={billingPeriod}
214
- isCustomerOnTrial={isCustomerOnTrial}
215
- onPlanSelected={onPlanButtonClick}
216
- paywallLocale={paywallLocale}
217
- hasTiersRow={withTiersRow}
218
- withTrialLeftRow={withTrialLeftRow}
219
- selectedTierByFeature={selectedTierByFeature}
220
- />
220
+ {showCTAButton ? (
221
+ <PlanOfferingButton
222
+ isNextPlan={isNextPlan}
223
+ customer={customer}
224
+ plan={plan}
225
+ currentSubscription={currentSubscription}
226
+ billingPeriod={billingPeriod}
227
+ isCustomerOnTrial={isCustomerOnTrial}
228
+ onPlanSelected={onPlanButtonClick}
229
+ paywallLocale={paywallLocale}
230
+ withTrialLeftRow={withTrialLeftRow}
231
+ selectedTierByFeature={selectedTierByFeature}
232
+ />
233
+ ) : (
234
+ <div style={{ height: PlanOfferingButtonHeight }} />
235
+ )}
221
236
 
222
237
  <Divider className="stigg-plan-header-divider" />
223
238
  </HeaderWrapper>
@@ -81,7 +81,6 @@ type PlanOfferingButtonProps = {
81
81
  isCustomerOnTrial: boolean;
82
82
  paywallLocale: PaywallLocalization;
83
83
  onPlanSelected: (intentionType: SubscribeIntentionType) => void | Promise<void>;
84
- hasTiersRow: boolean;
85
84
  withTrialLeftRow: boolean;
86
85
  selectedTierByFeature: Record<string, PriceTierFragment>;
87
86
  };
@@ -94,7 +93,6 @@ export function PlanOfferingButton({
94
93
  isCustomerOnTrial,
95
94
  onPlanSelected,
96
95
  paywallLocale,
97
- hasTiersRow,
98
96
  withTrialLeftRow,
99
97
  currentSubscription,
100
98
  selectedTierByFeature,
@@ -208,7 +206,7 @@ export function PlanOfferingButton({
208
206
  {isLoading && <LoadingIndicator color={theme.stigg.palette.text.disabled} loading size={16} />}
209
207
  </OfferingButton>
210
208
 
211
- {hasTiersRow && !withTrialLeftRow ? (
209
+ {!withTrialLeftRow ? (
212
210
  <div style={{ height: '20px' }} />
213
211
  ) : (
214
212
  <TrialDaysLeft className="stigg-trial-days-left-text" variant="h6" color="secondary">