@stigg/react-sdk 4.4.0-beta.0 → 4.4.0-beta.10

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 (90) hide show
  1. package/dist/components/checkout/Checkout.d.ts +1 -1
  2. package/dist/components/checkout/CheckoutContainer.d.ts +7 -2
  3. package/dist/components/checkout/CheckoutProvider.d.ts +3 -1
  4. package/dist/components/checkout/components/Button.d.ts +0 -1
  5. package/dist/components/checkout/components/ChangePlanButton.d.ts +8 -0
  6. package/dist/components/checkout/components/DowngradeToFreeContainer.d.ts +32 -0
  7. package/dist/components/checkout/hooks/useCheckoutModel.d.ts +2 -0
  8. package/dist/components/checkout/hooks/usePaymentStepModel.d.ts +8 -2
  9. package/dist/components/checkout/hooks/usePreviewSubscription.d.ts +10 -1
  10. package/dist/components/checkout/hooks/useProgressBarModel.d.ts +3 -0
  11. package/dist/components/checkout/hooks/useSubscriptionModel.d.ts +2 -1
  12. package/dist/components/checkout/index.d.ts +2 -0
  13. package/dist/components/checkout/progressBar/CheckoutProgressBar.style.d.ts +3 -2
  14. package/dist/components/checkout/steps/payment/PaymentMethods.d.ts +3 -2
  15. package/dist/components/checkout/steps/payment/PaymentStep.d.ts +2 -1
  16. package/dist/components/checkout/steps/payment/stripe/StripePaymentForm.d.ts +2 -1
  17. package/dist/components/checkout/steps/payment/stripe/stripe.utils.d.ts +4 -0
  18. package/dist/components/checkout/steps/payment/stripe/useSubmit.d.ts +4 -3
  19. package/dist/components/checkout/steps/plan/BillingPeriodPicker.style.d.ts +1 -0
  20. package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +6 -1
  21. package/dist/components/checkout/summary/CheckoutSuccess.d.ts +4 -1
  22. package/dist/components/checkout/summary/CheckoutSummary.d.ts +3 -1
  23. package/dist/components/checkout/summary/components/CheckoutCaptions.d.ts +2 -1
  24. package/dist/components/checkout/summary/components/LineItems.d.ts +2 -2
  25. package/dist/components/checkout/textOverrides.d.ts +7 -3
  26. package/dist/components/checkout/theme.d.ts +0 -1
  27. package/dist/components/checkout/types.d.ts +7 -0
  28. package/dist/components/paywall/paywallTextOverrides.d.ts +4 -0
  29. package/dist/components/utils/getPaidPriceText.d.ts +3 -1
  30. package/dist/react-sdk.cjs.development.js +1148 -524
  31. package/dist/react-sdk.cjs.development.js.map +1 -1
  32. package/dist/react-sdk.cjs.production.min.js +1 -1
  33. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  34. package/dist/react-sdk.esm.js +1182 -532
  35. package/dist/react-sdk.esm.js.map +1 -1
  36. package/dist/theme/getResolvedTheme.d.ts +1 -0
  37. package/dist/theme/types.d.ts +1 -0
  38. package/package.json +2 -2
  39. package/src/assets/payment-method.svg +3 -10
  40. package/src/components/checkout/Checkout.tsx +2 -1
  41. package/src/components/checkout/CheckoutContainer.style.ts +1 -0
  42. package/src/components/checkout/CheckoutContainer.tsx +59 -28
  43. package/src/components/checkout/CheckoutProvider.tsx +18 -18
  44. package/src/components/checkout/components/Button.tsx +19 -35
  45. package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
  46. package/src/components/checkout/components/DowngradeToFreeContainer.tsx +118 -0
  47. package/src/components/checkout/components/Skeletons.style.ts +4 -1
  48. package/src/components/checkout/hooks/useCheckoutModel.ts +12 -2
  49. package/src/components/checkout/hooks/usePaymentStepModel.ts +22 -3
  50. package/src/components/checkout/hooks/usePlanStepModel.ts +25 -10
  51. package/src/components/checkout/hooks/usePreviewSubscription.ts +112 -40
  52. package/src/components/checkout/hooks/useProgressBarModel.ts +18 -0
  53. package/src/components/checkout/hooks/useSubscriptionModel.ts +8 -2
  54. package/src/components/checkout/hooks/useSubscriptionState.ts +2 -1
  55. package/src/components/checkout/index.ts +2 -0
  56. package/src/components/checkout/planHeader/PlanHeader.style.tsx +1 -1
  57. package/src/components/checkout/planHeader/PlanHeader.tsx +7 -15
  58. package/src/components/checkout/progressBar/CheckoutProgressBar.style.ts +6 -3
  59. package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +13 -9
  60. package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
  61. package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +58 -11
  62. package/src/components/checkout/steps/payment/PaymentMethods.style.ts +1 -0
  63. package/src/components/checkout/steps/payment/PaymentMethods.tsx +13 -6
  64. package/src/components/checkout/steps/payment/PaymentStep.tsx +3 -1
  65. package/src/components/checkout/steps/payment/stripe/StripePaymentForm.tsx +35 -4
  66. package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +4 -3
  67. package/src/components/checkout/steps/payment/stripe/useSubmit.ts +61 -48
  68. package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +27 -6
  69. package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +26 -5
  70. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +62 -12
  71. package/src/components/checkout/summary/CheckoutSuccess.tsx +52 -8
  72. package/src/components/checkout/summary/CheckoutSummary.tsx +48 -33
  73. package/src/components/checkout/summary/components/CheckoutCaptions.tsx +30 -29
  74. package/src/components/checkout/summary/components/LineItems.tsx +8 -16
  75. package/src/components/checkout/textOverrides.ts +15 -12
  76. package/src/components/checkout/theme.ts +0 -4
  77. package/src/components/checkout/types.ts +9 -0
  78. package/src/components/common/Icon.tsx +4 -6
  79. package/src/components/common/mapExternalTheme.ts +1 -2
  80. package/src/components/paywall/PlanPrice.tsx +10 -2
  81. package/src/components/paywall/paywallTextOverrides.ts +3 -0
  82. package/src/components/utils/getPaidPriceText.ts +8 -2
  83. package/src/components/utils/getPlanPrice.ts +1 -1
  84. package/src/stories/Checkout.stories.tsx +2 -0
  85. package/src/theme/Theme.tsx +10 -1
  86. package/src/theme/getResolvedTheme.ts +1 -0
  87. package/src/theme/types.ts +1 -0
  88. package/dist/components/checkout/steps/surprise/SurpriseStep.d.ts +0 -2
  89. package/src/assets/nyancat.svg +0 -634
  90. package/src/components/checkout/steps/surprise/SurpriseStep.tsx +0 -27
@@ -20,6 +20,16 @@ type GetPlanStepInitialStateProps = {
20
20
  preconfiguredBillableFeatures: BillableFeature[];
21
21
  };
22
22
 
23
+ const getBillingPeriod = (billingPeriod: BillingPeriod, hasMonthlyPrices?: boolean, hasAnnualPrices?: boolean) => {
24
+ if (billingPeriod === BillingPeriod.Monthly && hasMonthlyPrices) {
25
+ return billingPeriod;
26
+ }
27
+ if (billingPeriod === BillingPeriod.Annually && hasAnnualPrices) {
28
+ return billingPeriod;
29
+ }
30
+ return null;
31
+ };
32
+
23
33
  const isInAdvanceCommitmentCharge = ({ pricingModel }: Price) => {
24
34
  return pricingModel === BillingModel.PerUnit;
25
35
  };
@@ -88,22 +98,27 @@ function resolveBillingPeriod({
88
98
  const hasMonthlyPrices = plan?.pricePoints.some((pricePoint) => pricePoint.billingPeriod === BillingPeriod.Monthly);
89
99
  const hasAnnualPrices = plan?.pricePoints.some((pricePoint) => pricePoint.billingPeriod === BillingPeriod.Annually);
90
100
 
101
+ if (preferredBillingPeriod) {
102
+ const billingPeriod = getBillingPeriod(preferredBillingPeriod, hasMonthlyPrices, hasAnnualPrices);
103
+ if (billingPeriod) {
104
+ return billingPeriod;
105
+ }
106
+ }
107
+
91
108
  const isUpdate = activeSubscription?.plan?.id === plan?.id;
92
109
  if (isUpdate) {
93
110
  return activeSubscription?.price?.billingPeriod || BillingPeriod.Monthly;
94
111
  }
95
112
 
96
- if (preferredBillingPeriod) {
97
- if (preferredBillingPeriod === BillingPeriod.Monthly && hasMonthlyPrices) {
98
- return BillingPeriod.Monthly;
113
+ if (activeSubscription?.prices && activeSubscription?.prices.length > 0) {
114
+ const billingPeriod = getBillingPeriod(
115
+ activeSubscription?.prices[0].billingPeriod,
116
+ hasMonthlyPrices,
117
+ hasAnnualPrices,
118
+ );
119
+ if (billingPeriod) {
120
+ return billingPeriod;
99
121
  }
100
- if (preferredBillingPeriod === BillingPeriod.Annually && hasAnnualPrices) {
101
- return BillingPeriod.Annually;
102
- }
103
- }
104
-
105
- if (activeSubscription?.price?.billingPeriod) {
106
- return activeSubscription?.price?.billingPeriod;
107
122
  }
108
123
 
109
124
  return hasAnnualPrices ? BillingPeriod.Annually : BillingPeriod.Monthly;
@@ -1,64 +1,127 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
- import { PreviewSubscription, SubscriptionPreview } from '@stigg/js-client-sdk';
2
+ import isEmpty from 'lodash/isEmpty';
3
+ import { BillingAddress, PreviewSubscription, StiggClient, SubscriptionPreview } from '@stigg/js-client-sdk';
3
4
  import { useStiggContext } from '../../StiggProvider';
4
5
  import { useCheckoutContext } from '../CheckoutProvider';
5
6
  import { useCheckoutModel } from './useCheckoutModel';
6
- import { useSubscriptionModel } from './useSubscriptionModel';
7
+ import { SubscriptionState, useSubscriptionModel } from './useSubscriptionModel';
8
+
9
+ function mapBillingInformation({
10
+ billingAddress,
11
+ taxPercentage,
12
+ }: {
13
+ billingAddress?: BillingAddress;
14
+ taxPercentage?: number;
15
+ }): Pick<PreviewSubscription, 'billingInformation'> {
16
+ if (!billingAddress && !taxPercentage) {
17
+ return {};
18
+ }
19
+
20
+ return {
21
+ billingInformation: {
22
+ ...(billingAddress ? { billingAddress } : {}),
23
+ ...(taxPercentage ? { taxPercentage } : {}),
24
+ },
25
+ };
26
+ }
27
+
28
+ export type PreviewSubscriptionProps = {
29
+ customerId?: string;
30
+ planId?: string;
31
+ resourceId?: string;
32
+ stigg: StiggClient;
33
+ } & SubscriptionState;
34
+
35
+ const previewSubscription = async ({
36
+ stigg,
37
+ customerId,
38
+ planId,
39
+ resourceId,
40
+ promotionCode,
41
+ addons,
42
+ billableFeatures,
43
+ billingCountryCode,
44
+ billingPeriod,
45
+ billingAddress,
46
+ taxPercentage,
47
+ }: PreviewSubscriptionProps) => {
48
+ const estimateAddons = addons.map(({ addon, quantity }) => ({ addonId: addon.id, quantity }));
49
+ let subscriptionPreview: SubscriptionPreview | null = null;
50
+ let errorMessage: string | null = null;
51
+
52
+ try {
53
+ if (customerId && planId) {
54
+ const previewSubscriptionProps: PreviewSubscription = {
55
+ customerId,
56
+ planId,
57
+ resourceId,
58
+ billingCountryCode,
59
+ addons: estimateAddons,
60
+ billingPeriod,
61
+ promotionCode,
62
+ billableFeatures: isEmpty(billableFeatures) ? undefined : billableFeatures,
63
+ ...mapBillingInformation({ billingAddress, taxPercentage }),
64
+ };
65
+
66
+ subscriptionPreview = await stigg.previewSubscription(previewSubscriptionProps);
67
+ }
68
+ } catch (error) {
69
+ const [, errorMsg] = (error as any)?.message?.split('Error:') || [];
70
+
71
+ errorMessage = errorMsg?.trim();
72
+ }
73
+
74
+ return { subscriptionPreview, errorMessage };
75
+ };
7
76
 
8
77
  export const usePreviewSubscriptionAction = () => {
9
78
  const { stigg } = useStiggContext();
10
79
  const subscription = useSubscriptionModel();
11
- const [{ resourceId, planStep }] = useCheckoutContext();
12
- const { checkoutState } = useCheckoutModel();
13
- const { plan, activeSubscription, customer } = checkoutState || {};
80
+ const [{ resourceId }] = useCheckoutContext();
81
+ const { checkoutState, widgetState } = useCheckoutModel();
82
+ const { plan, customer } = checkoutState || {};
14
83
 
15
84
  const previewSubscriptionAction = useCallback(
16
85
  async ({ promotionCode }: { promotionCode?: string | null } = {}) => {
17
- const estimateAddons = subscription.addons.map(({ addon, quantity }) => ({ addonId: addon.id, quantity }));
18
- let subscriptionPreview: SubscriptionPreview | null = null;
19
- let errorMessage: string | null = null;
20
-
21
- try {
22
- if (customer?.id && plan?.id) {
23
- const previewSubscriptionProps: PreviewSubscription = {
24
- customerId: customer.id,
25
- planId: plan?.id,
26
- billableFeatures: subscription.billableFeatures,
27
- addons: estimateAddons,
28
- billingPeriod: subscription.billingPeriod,
29
- promotionCode: promotionCode ?? subscription.promotionCode,
30
- resourceId,
31
- billingCountryCode: planStep.billingCountryCode,
32
- // TODO: Add billing information
33
- // billingInformation, // TaxId / Tax percentage
34
- };
35
- subscriptionPreview = await stigg.previewSubscription(previewSubscriptionProps);
36
- }
37
- } catch (error) {
38
- const [, errorMsg] = (error as any)?.message?.split('Error:') || [];
39
-
40
- errorMessage = errorMsg?.trim();
86
+ if (!widgetState.isValid) {
87
+ return { subscriptionPreview: null };
41
88
  }
42
89
 
43
- return { subscriptionPreview, errorMessage };
90
+ return previewSubscription({
91
+ stigg,
92
+ customerId: customer?.id,
93
+ planId: plan?.id,
94
+ resourceId,
95
+ addons: subscription.addons,
96
+ billableFeatures: subscription.billableFeatures,
97
+ billingCountryCode: subscription.billingCountryCode,
98
+ billingPeriod: subscription.billingPeriod,
99
+ billingAddress: subscription.billingAddress,
100
+ taxPercentage: subscription.taxPercentage,
101
+ promotionCode: promotionCode ?? subscription.promotionCode,
102
+ });
44
103
  },
45
104
  [
46
- activeSubscription,
47
- customer,
48
- plan,
49
- resourceId,
50
105
  stigg,
106
+ customer?.id,
107
+ plan?.id,
108
+ resourceId,
51
109
  subscription.addons,
52
- subscription.billingPeriod,
53
110
  subscription.billableFeatures,
111
+ subscription.billingCountryCode,
112
+ subscription.billingPeriod,
113
+ subscription.billingAddress,
114
+ subscription.taxPercentage,
54
115
  subscription.promotionCode,
55
- planStep.billingCountryCode,
116
+ widgetState.isValid,
56
117
  ],
57
118
  );
58
119
 
59
120
  return { previewSubscriptionAction };
60
121
  };
61
122
 
123
+ const SUBSCRIPTION_PREVIEW_DEBOUNCE_TIME = 500;
124
+
62
125
  export const usePreviewSubscription = () => {
63
126
  const [subscriptionPreview, setSubscriptionPreview] = useState<SubscriptionPreview | null>(null);
64
127
  const [isFetchingSubscriptionPreview, setIsFetchingSubscriptionPreview] = useState(false);
@@ -67,15 +130,24 @@ export const usePreviewSubscription = () => {
67
130
 
68
131
  useEffect(() => {
69
132
  const estimateSubscription = async () => {
70
- setIsFetchingSubscriptionPreview(true);
71
-
72
133
  const { subscriptionPreview } = await previewSubscriptionAction();
73
- setSubscriptionPreview(subscriptionPreview);
134
+ if (subscriptionPreview) {
135
+ setSubscriptionPreview(subscriptionPreview);
136
+ }
74
137
 
75
138
  setIsFetchingSubscriptionPreview(false);
76
139
  };
77
140
 
78
- void estimateSubscription();
141
+ setIsFetchingSubscriptionPreview(true);
142
+
143
+ const timer = setTimeout(() => {
144
+ setIsFetchingSubscriptionPreview(true);
145
+ void estimateSubscription();
146
+ }, SUBSCRIPTION_PREVIEW_DEBOUNCE_TIME);
147
+
148
+ return () => {
149
+ clearTimeout(timer);
150
+ };
79
151
  }, [previewSubscriptionAction]);
80
152
 
81
153
  return { subscriptionPreview, isFetchingSubscriptionPreview };
@@ -22,12 +22,14 @@ export type ProgressBarState = {
22
22
  activeStep: number;
23
23
  completedSteps: number[];
24
24
  steps: CheckoutStep[];
25
+ isDisabled: boolean;
25
26
  };
26
27
 
27
28
  const INITIAL_STATE: ProgressBarState = {
28
29
  activeStep: 0,
29
30
  completedSteps: [],
30
31
  steps: CHECKOUT_STEPS,
32
+ isDisabled: false,
31
33
  };
32
34
 
33
35
  export function getProgressBarInitialState({ availableAddons }: { availableAddons?: Addon[] }) {
@@ -66,6 +68,10 @@ function useGoNext() {
66
68
  const [, setState] = useCheckoutContext();
67
69
  return () =>
68
70
  setState(({ progressBar }) => {
71
+ if (progressBar.isDisabled) {
72
+ return;
73
+ }
74
+
69
75
  if (!progressBar.completedSteps.includes(progressBar.activeStep)) {
70
76
  progressBar.completedSteps.push(progressBar.activeStep);
71
77
  }
@@ -76,14 +82,26 @@ function useGoNext() {
76
82
  });
77
83
  }
78
84
 
85
+ function useSetIsDisabled() {
86
+ const [, setState] = useCheckoutContext();
87
+ return (isDisabled?: boolean) =>
88
+ setState(({ progressBar }) => {
89
+ progressBar.isDisabled = !!isDisabled;
90
+ });
91
+ }
92
+
79
93
  export function useProgressBarModel() {
80
94
  const progressBarState = useProgressBarState();
95
+ const currentStep = progressBarState.steps[progressBarState.activeStep];
96
+
81
97
  return {
98
+ currentStep,
82
99
  progressBarState,
83
100
  isLastStep: progressBarState.activeStep === progressBarState.steps.length - 1,
84
101
  isCheckoutComplete: isCheckoutComplete(progressBarState),
85
102
  setActiveStep: useSetActiveStep(),
86
103
  markStepAsCompleted: useMarkStepAsCompleted(),
87
104
  goNext: useGoNext(),
105
+ setIsDisabled: useSetIsDisabled(),
88
106
  };
89
107
  }
@@ -1,16 +1,22 @@
1
1
  import { useCheckoutContext } from '../CheckoutProvider';
2
2
  import { AddonsStepState } from './useAddonsStepModel';
3
3
  import { PromotionCodeState } from './useCouponModel';
4
+ import { PaymentStepState } from './usePaymentStepModel';
4
5
  import { PlanStepState } from './usePlanStepModel';
5
6
 
6
- export type SubscriptionState = PlanStepState & PromotionCodeState & Pick<AddonsStepState, 'addons'>;
7
+ export type SubscriptionState = PlanStepState &
8
+ PromotionCodeState &
9
+ Pick<AddonsStepState, 'addons'> &
10
+ Pick<PaymentStepState, 'billingAddress' | 'taxPercentage'>;
7
11
 
8
12
  export function useSubscriptionModel(): SubscriptionState {
9
- const [{ planStep, addonsStep, promotionCode }] = useCheckoutContext();
13
+ const [{ planStep, addonsStep, promotionCode, paymentStep }] = useCheckoutContext();
10
14
 
11
15
  return {
12
16
  ...planStep,
13
17
  addons: addonsStep.addons,
14
18
  promotionCode,
19
+ billingAddress: paymentStep.billingAddress,
20
+ taxPercentage: paymentStep.taxPercentage,
15
21
  };
16
22
  }
@@ -11,7 +11,7 @@ export function useSubscriptionState(): ApplySubscription | undefined {
11
11
  const addons = subscription.addons.map(({ addon, quantity }) => ({ addonId: addon.id, quantity }));
12
12
 
13
13
  if (!plan?.id) {
14
- return;
14
+ return undefined;
15
15
  }
16
16
 
17
17
  return {
@@ -22,5 +22,6 @@ 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 } } : {}),
25
26
  };
26
27
  }
@@ -1,3 +1,5 @@
1
1
  export { CheckoutTheme } from './theme';
2
2
  export { Checkout, CheckoutProps } from './Checkout';
3
+ export { OnCheckoutCompletedParams, OnCheckoutParams, CheckoutResult } from './CheckoutContainer';
3
4
  export { CheckoutLocalization } from './textOverrides';
5
+ export * from './types';
@@ -19,5 +19,5 @@ export const PlanHeaderContainer = styled(Box)`
19
19
  display: flex;
20
20
  align-content: center;
21
21
  justify-content: space-between;
22
- margin-bottom: 32px;
22
+ margin-bottom: 16px;
23
23
  `;
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
2
 
3
- import { Button, Divider } from '@mui/material';
3
+ import { Divider } from '@mui/material';
4
4
 
5
5
  import { Typography } from '../../common/Typography';
6
6
  import { useCheckoutModel } from '../hooks/useCheckoutModel';
7
7
  import { PlanHeaderContainer, PlanPathContainer, StyledArrowRightIcon } from './PlanHeader.style';
8
8
  import { CheckoutContainerProps } from '../CheckoutContainer';
9
+ import { ChangePlanButton } from '../components/ChangePlanButton';
9
10
 
10
11
  type PlanHeaderProps = {
11
12
  allowChangePlan?: boolean;
@@ -35,22 +36,13 @@ export function PlanHeader({ allowChangePlan = false, onChangePlan }: PlanHeader
35
36
  </PlanPathContainer>
36
37
 
37
38
  {allowChangePlan && onChangePlan && (
38
- <Button
39
- className="stigg-checkout-change-plan-button"
40
- sx={{ textTransform: 'none' }}
41
- variant="text"
42
- size="medium"
39
+ <ChangePlanButton
43
40
  onClick={() => {
44
41
  onChangePlan({ currentPlan: plan });
45
- }}>
46
- <Typography
47
- className="stigg-checkout-change-plan-button-text"
48
- color="primary.main"
49
- style={{ lineHeight: '24px' }}
50
- variant="body1">
51
- {checkoutLocalization.changePlan}
52
- </Typography>
53
- </Button>
42
+ }}
43
+ checkoutLocalization={checkoutLocalization}
44
+ size="medium"
45
+ />
54
46
  )}
55
47
  </PlanHeaderContainer>
56
48
  <Divider className="stigg-checkout-plan-header-divider" />
@@ -4,7 +4,9 @@ import Color from 'color';
4
4
 
5
5
  import { Icon } from '../../common/Icon';
6
6
 
7
- export const StyledProgress = styled(LinearProgress)<{ $disabled?: boolean }>(({ theme, $disabled }) => ({
7
+ export const StyledProgress = styled(LinearProgress, { shouldForwardProp: (prop) => !prop.startsWith('$') })<{
8
+ $disabled?: boolean;
9
+ }>(({ theme, $disabled }) => ({
8
10
  [`&.${linearProgressClasses.root}`]: {
9
11
  borderRadius: theme.stigg.border.radius,
10
12
  backgroundColor: theme.stigg.palette.outlinedBorder,
@@ -25,10 +27,11 @@ export const StyledStepButton = styled(Button)(() => ({
25
27
  },
26
28
  }));
27
29
 
28
- export const StyledIcon = styled(Icon)<{ $disabled?: boolean; $shouldStroke?: boolean }>(
29
- ({ theme, $disabled, $shouldStroke = true }) => ({
30
+ export const StyledIcon = styled(Icon)<{ $disabled?: boolean; $shouldStroke?: boolean; $shouldFill?: boolean }>(
31
+ ({ theme, $disabled, $shouldStroke = true, $shouldFill }) => ({
30
32
  circle: {
31
33
  stroke: $shouldStroke ? ($disabled ? theme.stigg.palette.text.disabled : theme.stigg.palette.primary) : undefined,
34
+ fill: $shouldFill ? theme.stigg.palette.primary : undefined,
32
35
  },
33
36
  }),
34
37
  );
@@ -14,27 +14,31 @@ export const CheckoutProgressBar = () => {
14
14
  const progress = ((activeStep + 1) * 100) / steps.length;
15
15
 
16
16
  return (
17
- <Box sx={{ width: '100%', my: 3 }}>
17
+ <Box sx={{ width: '100%', mb: 3 }}>
18
18
  <StyledProgress variant="determinate" value={progress} $disabled={readOnly} />
19
19
  <Grid container display="flex">
20
20
  {steps.map(({ key, label }, index) => {
21
21
  const isCompleted = completedSteps.includes(index);
22
- const isDisabled = readOnly || (index > activeStep && !isCompleted && !completedSteps.includes(index - 1));
22
+ const isDisabled =
23
+ readOnly ||
24
+ (index > activeStep && !isCompleted && !completedSteps.includes(index - 1)) ||
25
+ (activeStep !== index && progressBarState.isDisabled);
23
26
  const checkedIcon: Icons = isDisabled ? 'OutlinedCheckedCircleDisabled' : 'OutlinedCheckedCircle';
24
27
 
25
28
  return (
26
29
  <Grid key={key} item display="flex" flexDirection="row" flex={1} justifyContent="flex-start">
27
- {isLoadingCheckoutData && <Skeleton width={120} height={20} style={{ marginTop: 8 }} />}
28
- {!isLoadingCheckoutData && (
30
+ {isLoadingCheckoutData ? (
31
+ <Skeleton width={120} height={20} style={{ marginTop: 8 }} />
32
+ ) : (
29
33
  <StyledStepButton onClick={() => setActiveStep(index)} fullWidth disabled={isDisabled}>
30
34
  <Grid item display="flex" flexDirection="row" alignItems="center" gap={1}>
31
35
  <StepIcon
32
36
  icon={
33
- isCompleted ? (
34
- <StyledIcon icon={checkedIcon} $shouldStroke={!isDisabled} />
35
- ) : (
36
- <StyledIcon icon="OutlinedCircle" $disabled={isDisabled} />
37
- )
37
+ <StyledIcon
38
+ icon={isCompleted ? checkedIcon : 'OutlinedCircle'}
39
+ $disabled={isDisabled}
40
+ $shouldFill={isCompleted}
41
+ />
38
42
  }
39
43
  />
40
44
 
@@ -64,13 +64,12 @@ export const AddPromotionCode = ({ checkoutLocalization }: { checkoutLocalizatio
64
64
  inputProps={{ maxLength: 20 }}
65
65
  InputProps={{
66
66
  endAdornment: (
67
- <CouponCodeAddButton
68
- variant="contained"
69
- disableRipple={isLoading}
70
- $isLoading={isLoading}
71
- onClick={handlePromotionCode}
72
- >
73
- {isLoading ? <CircularProgress size={18} /> : <Icon style={{ display: 'flex' }} icon="ArrowForward" />}
67
+ <CouponCodeAddButton variant="contained" disabled={isLoading} onClick={handlePromotionCode}>
68
+ {isLoading ? (
69
+ <CircularProgress size={18} sx={{ color: 'white' }} />
70
+ ) : (
71
+ <Icon style={{ display: 'flex' }} icon="ArrowForward" />
72
+ )}
74
73
  </CouponCodeAddButton>
75
74
  ),
76
75
  }}
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
 
3
3
  import { Grid } from '@mui/material';
4
4
  import { Addon, BillingPeriod, SubscriptionAddon } from '@stigg/js-client-sdk';
@@ -11,7 +11,7 @@ import { useAddonsStepModel } from '../../hooks/useAddonsStepModel';
11
11
  import { usePlanStepModel } from '../../hooks/usePlanStepModel';
12
12
  import { AddonListItemContainer, CheckoutAddonsContainer, TrashButton } from './CheckoutAddonsStep.style';
13
13
  import { CheckoutLocalization } from '../../textOverrides';
14
- import { useCheckoutModel } from '../../hooks';
14
+ import { useCheckoutModel, useProgressBarModel } from '../../hooks';
15
15
 
16
16
  type UseAddonsStepModel = ReturnType<typeof useAddonsStepModel>;
17
17
 
@@ -23,6 +23,8 @@ type AddonListItemProps = {
23
23
  setAddon: UseAddonsStepModel['setAddon'];
24
24
  removeAddon: UseAddonsStepModel['removeAddon'];
25
25
  checkoutLocalization: CheckoutLocalization;
26
+ onAddonsValidationChange: (params: { addonId: string; isValid: boolean }) => void;
27
+ isValid: boolean;
26
28
  };
27
29
 
28
30
  function AddonListItem({
@@ -33,6 +35,8 @@ function AddonListItem({
33
35
  setAddon,
34
36
  removeAddon,
35
37
  checkoutLocalization,
38
+ onAddonsValidationChange,
39
+ isValid,
36
40
  }: AddonListItemProps) {
37
41
  const addonPrice = addon.pricePoints.find((pricePoint) => pricePoint.billingPeriod === billingPeriod);
38
42
  const isAdded = !!addonState;
@@ -41,12 +45,27 @@ function AddonListItem({
41
45
  (!initialAddonState && !!addonState) ||
42
46
  (!!initialAddonState && !addonState);
43
47
 
44
- const handleQuantityChange = (quantity?: number) => {
45
- setAddon(addon, quantity || 1);
48
+ const handleQuantityChange = (quantity: number | null) => {
49
+ if (!quantity || quantity <= 0) {
50
+ onAddonsValidationChange({ addonId: addon.id, isValid: false });
51
+ // Reset the input value to null
52
+ // @ts-ignore
53
+ setAddon(addon, quantity ?? null);
54
+ return;
55
+ }
56
+
57
+ onAddonsValidationChange({ addonId: addon.id, isValid: true });
58
+ setAddon(addon, quantity);
46
59
  };
47
60
 
48
61
  const handleUndo = () => {
49
- initialAddonState ? setAddon(addon, initialAddonState.quantity) : removeAddon(addon.id);
62
+ if (initialAddonState) {
63
+ setAddon(addon, initialAddonState.quantity);
64
+ } else {
65
+ removeAddon(addon.id);
66
+ }
67
+
68
+ onAddonsValidationChange({ addonId: addon.id, isValid: true });
50
69
  };
51
70
 
52
71
  return (
@@ -78,17 +97,27 @@ function AddonListItem({
78
97
  id={`${addon.id}-input`}
79
98
  type="number"
80
99
  sx={{ width: 120, marginX: 2 }}
81
- value={addonState?.quantity || 1}
82
- onChange={(event) => handleQuantityChange(event?.target?.value ? Number(event?.target?.value) : 1)}
100
+ value={addonState?.quantity ?? ''}
101
+ error={!isValid}
102
+ helperText={!isValid ? 'Minimum 1' : undefined}
103
+ FormHelperTextProps={{ sx: { margin: '4px' } }}
104
+ onChange={(event) => handleQuantityChange(event?.target?.value ? Number(event?.target?.value) : null)}
83
105
  />
84
- <TrashButton color="error" onClick={() => removeAddon(addon.id)}>
106
+ <TrashButton
107
+ color="error"
108
+ onClick={() => {
109
+ removeAddon(addon.id);
110
+ onAddonsValidationChange({ addonId: addon.id, isValid: true });
111
+ }}>
85
112
  <Icon icon="Trash" style={{ display: 'flex' }} />
86
113
  </TrashButton>
87
114
  </>
88
115
  )}
89
116
  {!isAdded && (
90
- <Button sx={{ paddingX: '22px', paddingY: '8px' }} onClick={() => handleQuantityChange()}>
91
- {checkoutLocalization.addAddonText}
117
+ <Button sx={{ paddingX: '22px', paddingY: '8px' }} onClick={() => handleQuantityChange(1)}>
118
+ <Typography color="primary.main" variant="body1">
119
+ {checkoutLocalization.addAddonText}
120
+ </Typography>
92
121
  </Button>
93
122
  )}
94
123
  </Grid>
@@ -97,15 +126,29 @@ function AddonListItem({
97
126
  }
98
127
 
99
128
  export function CheckoutAddonsStep() {
100
- const { checkoutLocalization } = useCheckoutModel();
129
+ const { checkoutLocalization, setIsValid } = useCheckoutModel();
101
130
  const { billingPeriod } = usePlanStepModel();
131
+ const { setIsDisabled } = useProgressBarModel();
102
132
  const { initialAddons, addons, availableAddons, setAddon, removeAddon } = useAddonsStepModel();
133
+ const [addonsValidation, setAddonsValidation] = useState(
134
+ availableAddons?.reduce<Record<string, boolean>>((acc, curr) => {
135
+ acc[curr.id] = true;
136
+ return acc;
137
+ }, {}) || {},
138
+ );
139
+
140
+ useEffect(() => {
141
+ const isDisabled = Object.values(addonsValidation).some((x) => !x);
142
+ setIsDisabled(isDisabled);
143
+ setIsValid(!isDisabled);
144
+ }, [addonsValidation, setIsDisabled, setIsValid]);
103
145
 
104
146
  return (
105
147
  <CheckoutAddonsContainer container>
106
148
  {availableAddons?.map((addon) => {
107
149
  const addonState = addons.find((x) => x.addon.id === addon.id);
108
150
  const initialAddonState = initialAddons?.find((x) => x.addon.id === addon.id);
151
+ const isValid = addonsValidation[addon.id];
109
152
 
110
153
  return (
111
154
  <AddonListItem
@@ -117,6 +160,10 @@ export function CheckoutAddonsStep() {
117
160
  setAddon={setAddon}
118
161
  removeAddon={removeAddon}
119
162
  checkoutLocalization={checkoutLocalization}
163
+ onAddonsValidationChange={({ addonId, isValid }: { addonId: string; isValid: boolean }) =>
164
+ setAddonsValidation({ ...addonsValidation, [addonId]: isValid })
165
+ }
166
+ isValid={isValid}
120
167
  />
121
168
  );
122
169
  })}
@@ -6,6 +6,7 @@ export const PaymentMethodContainer = styled(Grid)<{ $disabled?: boolean }>`
6
6
  border-radius: 10px;
7
7
  border: 1px solid ${({ theme }) => theme.stigg.palette.outlinedBorder};
8
8
  cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
9
+ opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
9
10
  `;
10
11
 
11
12
  export const NewPaymentMethodContainer = styled(PaymentMethodContainer)`