@stigg/react-sdk 4.4.0-beta.1 → 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 (91) 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/customerPortal/subscriptionOverview/subscriptionView/SubscriptionView.style.d.ts +1 -1
  29. package/dist/components/paywall/paywallTextOverrides.d.ts +4 -0
  30. package/dist/components/utils/getPaidPriceText.d.ts +3 -1
  31. package/dist/react-sdk.cjs.development.js +1147 -524
  32. package/dist/react-sdk.cjs.development.js.map +1 -1
  33. package/dist/react-sdk.cjs.production.min.js +1 -1
  34. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  35. package/dist/react-sdk.esm.js +1181 -532
  36. package/dist/react-sdk.esm.js.map +1 -1
  37. package/dist/theme/getResolvedTheme.d.ts +1 -0
  38. package/dist/theme/types.d.ts +1 -0
  39. package/package.json +2 -2
  40. package/src/assets/payment-method.svg +3 -10
  41. package/src/components/checkout/Checkout.tsx +2 -1
  42. package/src/components/checkout/CheckoutContainer.style.ts +1 -0
  43. package/src/components/checkout/CheckoutContainer.tsx +59 -28
  44. package/src/components/checkout/CheckoutProvider.tsx +18 -18
  45. package/src/components/checkout/components/Button.tsx +19 -35
  46. package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
  47. package/src/components/checkout/components/DowngradeToFreeContainer.tsx +118 -0
  48. package/src/components/checkout/components/Skeletons.style.ts +4 -1
  49. package/src/components/checkout/hooks/useCheckoutModel.ts +12 -2
  50. package/src/components/checkout/hooks/usePaymentStepModel.ts +22 -3
  51. package/src/components/checkout/hooks/usePlanStepModel.ts +25 -10
  52. package/src/components/checkout/hooks/usePreviewSubscription.ts +112 -40
  53. package/src/components/checkout/hooks/useProgressBarModel.ts +18 -0
  54. package/src/components/checkout/hooks/useSubscriptionModel.ts +8 -2
  55. package/src/components/checkout/hooks/useSubscriptionState.ts +2 -1
  56. package/src/components/checkout/index.ts +2 -0
  57. package/src/components/checkout/planHeader/PlanHeader.style.tsx +1 -1
  58. package/src/components/checkout/planHeader/PlanHeader.tsx +7 -15
  59. package/src/components/checkout/progressBar/CheckoutProgressBar.style.ts +6 -3
  60. package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +13 -9
  61. package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
  62. package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +58 -11
  63. package/src/components/checkout/steps/payment/PaymentMethods.style.ts +1 -0
  64. package/src/components/checkout/steps/payment/PaymentMethods.tsx +13 -6
  65. package/src/components/checkout/steps/payment/PaymentStep.tsx +3 -1
  66. package/src/components/checkout/steps/payment/stripe/StripePaymentForm.tsx +35 -4
  67. package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +4 -3
  68. package/src/components/checkout/steps/payment/stripe/useSubmit.ts +61 -48
  69. package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +27 -6
  70. package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +26 -5
  71. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +62 -12
  72. package/src/components/checkout/summary/CheckoutSuccess.tsx +52 -6
  73. package/src/components/checkout/summary/CheckoutSummary.tsx +48 -33
  74. package/src/components/checkout/summary/components/CheckoutCaptions.tsx +30 -29
  75. package/src/components/checkout/summary/components/LineItems.tsx +8 -16
  76. package/src/components/checkout/textOverrides.ts +15 -12
  77. package/src/components/checkout/theme.ts +0 -4
  78. package/src/components/checkout/types.ts +9 -0
  79. package/src/components/common/Icon.tsx +4 -6
  80. package/src/components/common/mapExternalTheme.ts +1 -2
  81. package/src/components/paywall/PlanPrice.tsx +10 -2
  82. package/src/components/paywall/paywallTextOverrides.ts +3 -0
  83. package/src/components/utils/getPaidPriceText.ts +8 -2
  84. package/src/components/utils/getPlanPrice.ts +1 -1
  85. package/src/stories/Checkout.stories.tsx +6 -5
  86. package/src/theme/Theme.tsx +10 -1
  87. package/src/theme/getResolvedTheme.ts +1 -0
  88. package/src/theme/types.ts +1 -0
  89. package/dist/components/checkout/steps/surprise/SurpriseStep.d.ts +0 -2
  90. package/src/assets/nyancat.svg +0 -634
  91. package/src/components/checkout/steps/surprise/SurpriseStep.tsx +0 -27
@@ -13,6 +13,7 @@ import {
13
13
  } from './PaymentMethods.style';
14
14
  import { StripePaymentForm } from './stripe';
15
15
  import { CheckoutLocalization } from '../../textOverrides';
16
+ import { CheckoutContainerProps } from '../../CheckoutContainer';
16
17
 
17
18
  export type PaymentMethodLayoutProps = {
18
19
  checked: boolean;
@@ -28,13 +29,13 @@ export type PaymentMethodProps = Pick<Customer, 'paymentMethodDetails'> &
28
29
  export type NewPaymentMethodProps = Pick<PaymentMethodLayoutProps, 'checked' | 'readOnly'> & {
29
30
  onSelect: () => void;
30
31
  checkoutLocalization: CheckoutLocalization;
31
- };
32
+ } & Pick<CheckoutContainerProps, 'onBillingAddressChange'>;
32
33
 
33
34
  function PaymentMethodLayout({ checked, icon, text, subtitle, readOnly }: PaymentMethodLayoutProps) {
34
35
  return (
35
36
  <PaymentMethodLayoutContainer>
36
37
  <Radio checked={checked} disabled={readOnly} />
37
- <Icon icon={icon} style={{ display: 'contents' }} />
38
+ <Icon icon={icon} style={{ display: 'flex' }} />
38
39
  <PaymentMethodTextContainer container>
39
40
  <Grid item>{text}</Grid>
40
41
  {subtitle && <Grid item>{subtitle}</Grid>}
@@ -55,11 +56,11 @@ export function ExistingPaymentMethod({ checked, paymentMethodDetails, readOnly,
55
56
  checked={checked}
56
57
  readOnly={readOnly}
57
58
  icon="PaymentMethod"
58
- text={<Typography variant="h6">{`Ending in ${last4Digits}`}</Typography>}
59
+ text={<Typography variant="h6">{`Card ending in ${last4Digits}`}</Typography>}
59
60
  subtitle={
60
61
  !!expirationMonth &&
61
62
  !!expirationYear && (
62
- <Typography variant="body1">{`Exp. ${expirationMonth
63
+ <Typography variant="body1">{`Expires ${expirationMonth
63
64
  .toString()
64
65
  .padStart(2, '0')}/${expirationYear}`}</Typography>
65
66
  )
@@ -69,7 +70,13 @@ export function ExistingPaymentMethod({ checked, paymentMethodDetails, readOnly,
69
70
  );
70
71
  }
71
72
 
72
- export function NewPaymentMethod({ checked, onSelect, readOnly, checkoutLocalization }: NewPaymentMethodProps) {
73
+ export function NewPaymentMethod({
74
+ checked,
75
+ onSelect,
76
+ readOnly,
77
+ checkoutLocalization,
78
+ onBillingAddressChange,
79
+ }: NewPaymentMethodProps) {
73
80
  return (
74
81
  <NewPaymentMethodContainer item onClick={onSelect} $disabled={readOnly}>
75
82
  <PaymentMethodLayout
@@ -79,7 +86,7 @@ export function NewPaymentMethod({ checked, onSelect, readOnly, checkoutLocaliza
79
86
  text={<Typography variant="h6">{checkoutLocalization.newPaymentMethodText}</Typography>}
80
87
  />
81
88
  <Collapse in={checked}>
82
- <StripePaymentForm />
89
+ <StripePaymentForm onBillingAddressChange={onBillingAddressChange} />
83
90
  </Collapse>
84
91
  </NewPaymentMethodContainer>
85
92
  );
@@ -4,6 +4,7 @@ import { Alert, Grid } from '@mui/material';
4
4
  import { useCheckoutModel, usePaymentStepModel } from '../../hooks';
5
5
  import { ExistingPaymentMethod, NewPaymentMethod } from './PaymentMethods';
6
6
  import { Typography } from '../../../common/Typography';
7
+ import { CheckoutContainerProps } from '../../CheckoutContainer';
7
8
 
8
9
  const PaymentContainer = styled(Grid)`
9
10
  display: flex;
@@ -12,7 +13,7 @@ const PaymentContainer = styled(Grid)`
12
13
  margin: 32px 0;
13
14
  `;
14
15
 
15
- export function PaymentStep() {
16
+ export function PaymentStep({ onBillingAddressChange }: Pick<CheckoutContainerProps, 'onBillingAddressChange'>) {
16
17
  const { checkoutState, checkoutLocalization, widgetState } = useCheckoutModel();
17
18
  const { customer } = checkoutState || {};
18
19
  const { errorMessage, useNewPaymentMethod, setUseNewPaymentMethod } = usePaymentStepModel();
@@ -44,6 +45,7 @@ export function PaymentStep() {
44
45
  checked={useNewPaymentMethod}
45
46
  checkoutLocalization={checkoutLocalization}
46
47
  onSelect={() => handleOnSelect(true)}
48
+ onBillingAddressChange={onBillingAddressChange}
47
49
  />
48
50
  </PaymentContainer>
49
51
  );
@@ -1,22 +1,53 @@
1
1
  import React from 'react';
2
+ import { StripeAddressElementChangeEvent } from '@stripe/stripe-js';
2
3
  import { Grid } from '@mui/material';
4
+ import { BillingAddress } from '@stigg/js-client-sdk';
3
5
  import { AddressElement, PaymentElement } from '@stripe/react-stripe-js';
4
6
  import { Typography } from '../../../../common/Typography';
5
- import { useCheckoutModel } from '../../../hooks';
7
+ import { useCheckoutModel, usePaymentStepModel } from '../../../hooks';
8
+ import { CheckoutContainerProps } from '../../../CheckoutContainer';
6
9
 
7
- export function StripePaymentForm() {
8
- const { checkoutState, checkoutLocalization, widgetState } = useCheckoutModel();
10
+ export function StripePaymentForm({ onBillingAddressChange }: Pick<CheckoutContainerProps, 'onBillingAddressChange'>) {
11
+ const { checkoutState, checkoutLocalization, widgetState, setWidgetReadOnly } = useCheckoutModel();
12
+ const { setBillingAddress } = usePaymentStepModel();
9
13
  const { customer, configuration } = checkoutState || {};
10
14
  const { readOnly } = widgetState;
11
15
 
16
+ const handleAddressChange = (args: StripeAddressElementChangeEvent) => {
17
+ if (!args.complete) {
18
+ return;
19
+ }
20
+
21
+ const { postal_code: postalCode, ...addressFields } = args.value.address;
22
+ const billingAddress: BillingAddress = {
23
+ postalCode,
24
+ ...addressFields,
25
+ };
26
+
27
+ setWidgetReadOnly(true);
28
+ setBillingAddress(billingAddress);
29
+
30
+ if (onBillingAddressChange) {
31
+ const callExternalBillingAddressChanged = async () => {
32
+ await onBillingAddressChange({ billingAddress });
33
+ setWidgetReadOnly(false);
34
+ };
35
+
36
+ void callExternalBillingAddressChanged();
37
+ } else {
38
+ setWidgetReadOnly(false);
39
+ }
40
+ };
41
+
12
42
  return (
13
43
  <Grid flexDirection="column" container gap={3} padding="16px" sx={{ pointerEvents: readOnly ? 'none' : undefined }}>
14
44
  <Grid flexDirection="column" container gap={2}>
15
45
  <Typography variant="h6">{checkoutLocalization.newPaymentMethodBillingAddressTitle}</Typography>
16
46
  <AddressElement
47
+ onChange={handleAddressChange}
17
48
  options={{
18
49
  mode: 'billing',
19
- fields: { phone: !!configuration?.content?.collectPhoneNumber ? 'always' : 'auto' },
50
+ fields: { phone: configuration?.content?.collectPhoneNumber ? 'always' : 'auto' },
20
51
  defaultValues: {
21
52
  ...(customer?.name && { name: customer.name }),
22
53
  },
@@ -8,15 +8,16 @@ type StripeElementsProps = {
8
8
 
9
9
  export async function handleStripeFormValidations({ elements }: Pick<StripeElementsProps, 'elements'>) {
10
10
  if (!elements) {
11
- console.error('Stripe elements not initialized');
12
- return { success: false };
11
+ const errorMessage = 'Stripe elements not initialized';
12
+ console.error(errorMessage);
13
+ return { success: false, errorMessage };
13
14
  }
14
15
 
15
16
  const { error: elementsError } = await elements.submit();
16
17
 
17
18
  if (elementsError) {
18
19
  console.log(elementsError.message);
19
- return { success: false };
20
+ return { success: false, errorMessage: elementsError.message || '' };
20
21
  }
21
22
 
22
23
  return { success: true };
@@ -8,15 +8,15 @@ import { CheckoutContainerProps, CheckoutResult } from '../../../CheckoutContain
8
8
  import { handleNewPaymentMethod, handleStripeFormValidations, handleStripeNextAction } from './stripe.utils';
9
9
  import { ANIMATION_DURATION } from '../../../summary/CheckoutSuccess';
10
10
 
11
- const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
11
+ const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
12
12
 
13
- export type HandleSubmitResult = { results?: ApplySubscriptionResults; errorMessage?: string } | undefined;
13
+ export type HandleSubmitResult = { results?: ApplySubscriptionResults; success: boolean; errorMessage?: string };
14
14
 
15
15
  export type UseSubmitProps = {
16
16
  onSuccess?: () => void;
17
- } & Pick<CheckoutContainerProps, 'onCheckout' | 'onCheckoutCompleted'>;
17
+ } & Pick<CheckoutContainerProps, 'onCheckout' | 'onCheckoutCompleted' | 'disableSuccessAnimation'>;
18
18
 
19
- export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess }: UseSubmitProps) {
19
+ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess, disableSuccessAnimation }: UseSubmitProps) {
20
20
  const { stigg } = useStiggContext();
21
21
  const { useNewPaymentMethod } = usePaymentStepModel();
22
22
  const subscriptionState = useSubscriptionState();
@@ -29,75 +29,88 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess }: UseSub
29
29
  e.preventDefault();
30
30
 
31
31
  if (!subscriptionState) {
32
- return;
32
+ return { success: false, errorMessage: 'Unexpected error, please contact support.' };
33
33
  }
34
34
 
35
+ let checkoutParams: ApplySubscription = { ...subscriptionState };
35
36
  let checkoutResults: ApplySubscriptionResults | undefined;
36
37
  let errorMessage: string | undefined;
37
38
  let paymentMethodId: string | undefined;
38
39
 
39
- setWidgetReadOnly(true);
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
+ }
40
54
 
41
- if (useNewPaymentMethod) {
42
- const { success } = await handleStripeFormValidations({ elements });
43
- if (!success) {
44
- setWidgetReadOnly(false);
45
- return;
55
+ paymentMethodId = paymentMethodResults.paymentMethodId;
46
56
  }
47
57
 
48
- const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
49
- if (!paymentMethodResults.success) {
50
- errorMessage = paymentMethodResults.errorMessage;
58
+ if (errorMessage) {
59
+ return { success: false, errorMessage };
51
60
  }
52
61
 
53
- paymentMethodId = paymentMethodResults.paymentMethodId;
54
- }
62
+ checkoutParams = { ...checkoutParams, paymentMethodId };
55
63
 
56
- if (!errorMessage) {
57
- const checkoutParams: ApplySubscription = { ...subscriptionState, paymentMethodId };
58
-
59
- const checkoutAction = async (): Promise<CheckoutResult> => {
60
- try {
61
- const applySubscriptionResults = await stigg.applySubscription(checkoutParams);
62
- const nextActionResults = await handleStripeNextAction({
63
- applySubscriptionResults,
64
- stripe,
65
- });
66
-
67
- checkoutResults = nextActionResults;
68
- if (nextActionResults.errorMessage) {
69
- errorMessage = nextActionResults.errorMessage;
70
- }
71
-
72
- return { success: !nextActionResults.errorMessage, errorMessage: nextActionResults.errorMessage };
73
- } catch (e) {
74
- console.error(e);
75
- errorMessage = (e as any)?.message;
76
- return { success: false, errorMessage };
77
- }
78
- };
64
+ try {
65
+ const applySubscriptionResults = await stigg.applySubscription(checkoutParams);
66
+ const nextActionResults = await handleStripeNextAction({
67
+ applySubscriptionResults,
68
+ stripe,
69
+ });
79
70
 
80
- if (onCheckout) {
81
- const externalCheckoutResults = await onCheckout({ checkoutParams, checkoutAction });
82
- if (!externalCheckoutResults.success && externalCheckoutResults.errorMessage) {
83
- errorMessage = externalCheckoutResults.errorMessage;
71
+ checkoutResults = nextActionResults;
72
+ if (nextActionResults.errorMessage) {
73
+ errorMessage = nextActionResults.errorMessage;
84
74
  }
85
- } else {
86
- await checkoutAction();
75
+
76
+ return { success: !nextActionResults.errorMessage, errorMessage: nextActionResults.errorMessage };
77
+ } catch (e) {
78
+ console.error(e);
79
+ errorMessage = (e as any)?.message;
80
+ return { success: false, errorMessage };
87
81
  }
82
+ };
83
+
84
+ setWidgetReadOnly(true);
85
+
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;
97
+ }
98
+ success = checkoutActionResults.success && !errorMessage;
88
99
  }
89
100
 
90
101
  setWidgetReadOnly(false);
91
102
 
92
- const success = !errorMessage && !!checkoutResults?.subscription;
93
103
  if (success && onSuccess) {
94
104
  onSuccess();
105
+
106
+ if (!disableSuccessAnimation) {
107
+ await delay(ANIMATION_DURATION); // Wait for animation to finish
108
+ }
95
109
  }
96
110
 
97
- await delay(ANIMATION_DURATION); // Wait for animation to finish
98
111
  await onCheckoutCompleted({ success, error: errorMessage });
99
112
 
100
- return { results: checkoutResults, errorMessage };
113
+ return { results: checkoutResults, success: !errorMessage, errorMessage };
101
114
  };
102
115
 
103
116
  return { handleSubmit, isLoading: !!widgetState?.readOnly };
@@ -5,19 +5,40 @@ export const BillingPeriodPickerContainer = styled(Box)`
5
5
  margin: 16px 0;
6
6
  `;
7
7
 
8
- export const BillingPeriodButton = styled.button<{ $isActive?: boolean; $disabled?: boolean }>`
9
- cursor: ${({ $disabled }) => ($disabled ? 'default' : 'pointer')};
8
+ export const BillingPeriodButton = styled.button<{
9
+ $isActive?: boolean;
10
+ $disabled?: boolean;
11
+ $isOnlyBillingPeriod?: boolean;
12
+ }>`
13
+ cursor: ${({ $disabled, $isOnlyBillingPeriod }) =>
14
+ $disabled ? 'default' : $isOnlyBillingPeriod ? 'default' : 'pointer'};
10
15
  flex: 1;
11
16
  display: flex;
12
17
  align-items: center;
13
18
  justify-content: flex-start;
14
- padding: 8px 8px 8px 4px;
19
+ padding: 2px 8px;
15
20
  border-radius: 10px;
16
- border: ${({ theme, $isActive }) =>
17
- `1px solid ${$isActive ? theme.stigg.palette.outlinedRestingBorder : theme.stigg.palette.outlinedBorder}`};
18
- background: ${({ theme, $isActive }) => ($isActive ? theme.stigg.palette.backgroundSection : 'transparent')};
21
+ border: ${({ theme, $isActive, $isOnlyBillingPeriod }) => {
22
+ let borderColor = theme.stigg.palette.outlinedBorder;
23
+ if ($isOnlyBillingPeriod) {
24
+ borderColor = 'transparent';
25
+ } else if ($isActive) {
26
+ borderColor = theme.stigg.palette.outlinedRestingBorder;
27
+ }
28
+ return `1px solid ${borderColor}`;
29
+ }};
30
+ background: ${({ theme, $isActive, $isOnlyBillingPeriod }) => {
31
+ if ($isOnlyBillingPeriod) {
32
+ return 'transparent';
33
+ }
34
+ if ($isActive) {
35
+ return theme.stigg.palette.primaryLight;
36
+ }
37
+ return 'transparent';
38
+ }};
19
39
  text-transform: none;
20
40
  text-align: start;
41
+ height: 36px;
21
42
 
22
43
  &.MuiButton-root {
23
44
  padding: 0 16px 0 8px;
@@ -11,21 +11,32 @@ import { usePlanStepModel } from '../../hooks/usePlanStepModel';
11
11
  import { BillingPeriodButton, BillingPeriodOptions, BillingPeriodPickerContainer } from './BillingPeriodPicker.style';
12
12
  import { CheckoutLocalization } from '../../textOverrides';
13
13
 
14
- const BillingPeriodOption = ({ billingPeriod }: { billingPeriod: BillingPeriod }) => {
14
+ const BillingPeriodOption = ({
15
+ billingPeriod,
16
+ isOnlyBillingPeriod,
17
+ }: {
18
+ billingPeriod: BillingPeriod;
19
+ isOnlyBillingPeriod: boolean;
20
+ }) => {
15
21
  const { billingPeriod: selectedBillingPeriod, setBillingPeriod } = usePlanStepModel();
16
22
  const isActive = selectedBillingPeriod === billingPeriod;
17
23
 
18
24
  return (
19
- <BillingPeriodButton onClick={() => setBillingPeriod(billingPeriod)} $isActive={isActive}>
25
+ <BillingPeriodButton
26
+ onClick={() => setBillingPeriod(billingPeriod)}
27
+ $isActive={isActive}
28
+ $isOnlyBillingPeriod={isOnlyBillingPeriod}>
20
29
  <Radio
21
30
  checked={isActive}
22
31
  onChange={() => setBillingPeriod(billingPeriod)}
23
32
  value={billingPeriod}
33
+ disabled={isOnlyBillingPeriod}
24
34
  inputProps={{ 'aria-label': formatBillingPeriod(billingPeriod) }}
35
+ sx={{ padding: 0, marginRight: '8px' }}
25
36
  />
26
37
 
27
38
  <Box>
28
- <Typography variant="h6" color="primary" fontWeight={FontWeight.Medium}>
39
+ <Typography variant="body1" color="primary">
29
40
  {formatBillingPeriod(billingPeriod)}
30
41
  </Typography>
31
42
  </Box>
@@ -44,6 +55,8 @@ export const BillingPeriodPicker = ({ plan, checkoutLocalization }: BillingPerio
44
55
  (price) => price.billingPeriod === BillingPeriod.Monthly,
45
56
  );
46
57
 
58
+ const hasBothBillingPeriods = !!monthlyPrices?.length && !!annualPrices?.length;
59
+
47
60
  return (
48
61
  <BillingPeriodPickerContainer>
49
62
  <Typography variant="h6" color="primary" fontWeight={FontWeight.Medium}>
@@ -52,10 +65,18 @@ export const BillingPeriodPicker = ({ plan, checkoutLocalization }: BillingPerio
52
65
 
53
66
  <BillingPeriodOptions>
54
67
  {!!monthlyPrices?.length && (
55
- <BillingPeriodOption key={BillingPeriod.Monthly} billingPeriod={BillingPeriod.Monthly} />
68
+ <BillingPeriodOption
69
+ key={BillingPeriod.Monthly}
70
+ billingPeriod={BillingPeriod.Monthly}
71
+ isOnlyBillingPeriod={!hasBothBillingPeriods}
72
+ />
56
73
  )}
57
74
  {!!annualPrices?.length && (
58
- <BillingPeriodOption key={BillingPeriod.Annually} billingPeriod={BillingPeriod.Annually} />
75
+ <BillingPeriodOption
76
+ key={BillingPeriod.Annually}
77
+ billingPeriod={BillingPeriod.Annually}
78
+ isOnlyBillingPeriod={!hasBothBillingPeriods}
79
+ />
59
80
  )}
60
81
  </BillingPeriodOptions>
61
82
  </BillingPeriodPickerContainer>
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
 
3
3
  import styled from '@emotion/styled';
4
4
  import { Box } from '@mui/material';
@@ -10,7 +10,7 @@ import { useChargesSort } from '../../../hooks/useChargeSort';
10
10
  import { calculateUnitQuantityText } from '../../../paywall/utils/calculateUnitQuantityText';
11
11
  import { currencyPriceFormatter } from '../../../utils/currencyUtils';
12
12
  import { InputField } from '../../components';
13
- import { usePlanStepModel } from '../../hooks';
13
+ import { useCheckoutModel, usePlanStepModel, useProgressBarModel } from '../../hooks';
14
14
  import { TiersSelectContainer } from '../../../common/TiersSelectContainer';
15
15
  import { getPriceFeatureUnit, getTierByQuantity } from '../../../utils/priceTierUtils';
16
16
  import { getValidPriceQuantity } from '../../../utils/priceUtils';
@@ -31,14 +31,31 @@ const StyledPlanCharge = styled.div`
31
31
  margin-top: 16px;
32
32
  `;
33
33
 
34
+ const getValidationText = (charge: Price, quantity?: number) => {
35
+ const { minUnitQuantity, maxUnitQuantity } = charge;
36
+ if (!quantity || quantity < (minUnitQuantity || 1)) {
37
+ return `Minimum ${minUnitQuantity || 1}`;
38
+ }
39
+
40
+ if (maxUnitQuantity && quantity > maxUnitQuantity) {
41
+ return `Maximum ${maxUnitQuantity}`;
42
+ }
43
+
44
+ return '';
45
+ };
46
+
34
47
  export function PlanCharge({
35
48
  charge,
49
+ isValid,
36
50
  setBillableFeature,
37
51
  billableFeature,
52
+ onValidationChange,
38
53
  }: {
39
54
  charge: Price;
55
+ isValid: boolean;
40
56
  billableFeature?: BillableFeatureInput;
41
57
  setBillableFeature: UsePlanStepModel['setBillableFeature'];
58
+ onValidationChange: ({ featureId, isValid }: { featureId: string; isValid: boolean }) => void;
42
59
  }) {
43
60
  const featureId = charge.feature?.featureId;
44
61
  const isBaseCharge = !featureId;
@@ -49,9 +66,23 @@ export function PlanCharge({
49
66
  const handleQuantityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
50
67
  if (isBaseCharge || !featureId) return;
51
68
 
52
- const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity || 1;
53
- const quantity = getValidPriceQuantity(charge, value || 1);
69
+ const { minUnitQuantity, maxUnitQuantity } = charge;
70
+ const value = event?.target?.value ? Number(event?.target?.value) : null;
71
+ if (
72
+ !value ||
73
+ value <= 0 ||
74
+ (minUnitQuantity && value < minUnitQuantity) ||
75
+ (maxUnitQuantity && value > maxUnitQuantity)
76
+ ) {
77
+ onValidationChange({ featureId, isValid: false });
78
+ // Reset the input value to null
79
+ // @ts-ignore
80
+ setBillableFeature(featureId, value ?? null);
81
+ return;
82
+ }
54
83
 
84
+ onValidationChange({ featureId, isValid: true });
85
+ const quantity = getValidPriceQuantity(charge, value);
55
86
  setBillableFeature(featureId, quantity);
56
87
  };
57
88
 
@@ -77,7 +108,7 @@ export function PlanCharge({
77
108
  tierUnits={getPriceFeatureUnit(charge)}
78
109
  selectedTier={tier}
79
110
  handleTierChange={(tier: PriceTierFragment) => {
80
- setBillableFeature(featureId!, tier!.upTo);
111
+ setBillableFeature(featureId!, tier.upTo);
81
112
  }}
82
113
  />
83
114
  );
@@ -87,11 +118,12 @@ export function PlanCharge({
87
118
  sx={{ width: 120 }}
88
119
  id={`${featureId}-input`}
89
120
  type="number"
90
- InputProps={
91
- hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
92
- }
93
- value={billableFeature?.quantity || charge.minUnitQuantity || 1}
121
+ error={!isValid}
122
+ helperText={!isValid ? getValidationText(charge, billableFeature?.quantity) : undefined}
123
+ FormHelperTextProps={{ sx: { margin: '4px' } }}
124
+ value={billableFeature?.quantity ?? ''}
94
125
  onChange={handleQuantityChange}
126
+ onWheel={(e: React.WheelEvent<HTMLInputElement>) => (e.target as HTMLElement).blur()}
95
127
  />
96
128
  );
97
129
  }
@@ -118,18 +150,36 @@ export function PlanCharge({
118
150
 
119
151
  export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListProps) {
120
152
  const { billableFeatures, setBillableFeature } = usePlanStepModel();
121
- const planCharges = useChargesSort(plan?.pricePoints?.filter(p => p.billingPeriod === billingPeriod) || []);
153
+ const { setIsDisabled } = useProgressBarModel();
154
+ const { setIsValid } = useCheckoutModel();
155
+ const planCharges = useChargesSort(plan?.pricePoints?.filter((p) => p.billingPeriod === billingPeriod) || []);
156
+ const [chargesValidation, setChargesValidation] = useState(
157
+ planCharges?.reduce<Record<string, boolean>>((acc, curr) => {
158
+ acc[curr.feature?.featureId || 'base-charge'] = true;
159
+ return acc;
160
+ }, {}),
161
+ );
162
+
163
+ useEffect(() => {
164
+ const isDisabled = Object.values(chargesValidation).some((x) => !x);
165
+ setIsDisabled(isDisabled);
166
+ setIsValid(!isDisabled);
167
+ }, [chargesValidation, setIsDisabled, setIsValid]);
122
168
 
123
169
  return (
124
170
  <div>
125
- {planCharges?.map(charge => {
126
- const billableFeature = billableFeatures.find(x => x.featureId === charge.feature?.featureId);
171
+ {planCharges?.map((charge) => {
172
+ const billableFeature = billableFeatures.find((x) => x.featureId === charge.feature?.featureId);
127
173
  return (
128
174
  <PlanCharge
129
175
  key={charge.feature?.featureId || 'base-charge'}
130
176
  charge={charge}
131
177
  setBillableFeature={setBillableFeature}
132
178
  billableFeature={billableFeature}
179
+ isValid={chargesValidation[charge.feature?.featureId || 'base-charge']}
180
+ onValidationChange={({ featureId, isValid }: { featureId: string; isValid: boolean }) =>
181
+ setChargesValidation((prev) => ({ ...prev, [featureId]: isValid }))
182
+ }
133
183
  />
134
184
  );
135
185
  })}
@@ -4,27 +4,73 @@ import Color from 'color';
4
4
  import React from 'react';
5
5
  import Lottie from 'react-lottie';
6
6
  import animationData from '../../../assets/lottie/checkout-success.json';
7
+ import { Typography } from '../../common/Typography';
8
+ import { CheckoutLocalization } from '../textOverrides';
7
9
 
8
10
  export const ANIMATION_DURATION = 5000;
9
11
 
10
- const BACKGROUND_COLOR = Color('#0b2f7a').alpha(0.3).toString();
11
-
12
12
  const CheckoutSuccessContainer = styled(Box)`
13
+ @keyframes blurFade {
14
+ 0% {
15
+ background-color: ${({ theme }) => Color(theme.stigg.palette.backgroundPaper).alpha(0).toString()};
16
+ backdrop-filter: blur(0px);
17
+ }
18
+ 100% {
19
+ background-color: ${({ theme }) => Color(theme.stigg.palette.backgroundPaper).alpha(0.9).toString()};
20
+ backdrop-filter: blur(6.5px);
21
+ }
22
+ }
23
+
13
24
  position: absolute;
14
25
  top: 0;
15
26
  left: 0;
16
27
  bottom: 0;
17
28
  right: 0;
18
- background-color: ${BACKGROUND_COLOR};
29
+ z-index: 5;
30
+ background-color: ${({ theme }) => Color(theme.stigg.palette.backgroundPaper).alpha(0.9).toString()};
31
+ animation: blurFade 2s ease-in forwards;
32
+ display: flex;
33
+ flex-direction: column;
34
+ justify-content: center;
35
+
19
36
  * rect {
20
37
  fill: transparent;
21
38
  }
39
+
40
+ & path {
41
+ stroke: ${({ theme }) => theme.stigg.palette.primary};
42
+ }
43
+ `;
44
+
45
+ const CheckoutSuccessText = styled(Typography)`
46
+ @keyframes fadeIn {
47
+ 0% {
48
+ opacity: 0;
49
+ }
50
+ 75% {
51
+ opacity: 0;
52
+ }
53
+ 100% {
54
+ opacity: 1;
55
+ }
56
+ }
57
+
58
+ align-self: center;
59
+ animation: fadeIn 5s ease-in forwards;
22
60
  `;
23
61
 
24
- export function CheckoutSuccess() {
62
+ export function CheckoutSuccess({ checkoutLocalization }: { checkoutLocalization: CheckoutLocalization }) {
25
63
  return (
26
- <CheckoutSuccessContainer>
27
- <Lottie width={350} options={{ loop: false, autoplay: true, animationData }} />
64
+ <CheckoutSuccessContainer className="stigg-checkout-success-container">
65
+ <Lottie
66
+ width={350}
67
+ height="auto"
68
+ isClickToPauseDisabled
69
+ options={{ loop: false, autoplay: true, animationData }}
70
+ />
71
+ <CheckoutSuccessText variant="h1" color="primary.main">
72
+ {checkoutLocalization.checkoutSuccessText}
73
+ </CheckoutSuccessText>
28
74
  </CheckoutSuccessContainer>
29
75
  );
30
76
  }