@stigg/react-sdk 4.4.0-beta.4 → 4.4.0-beta.6

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 (34) hide show
  1. package/dist/components/checkout/components/Button.d.ts +0 -1
  2. package/dist/components/checkout/components/ChangePlanButton.d.ts +8 -0
  3. package/dist/components/checkout/components/DowngradeToFreeContainer.d.ts +6 -2
  4. package/dist/components/checkout/hooks/usePreviewSubscription.d.ts +3 -0
  5. package/dist/components/checkout/progressBar/CheckoutProgressBar.style.d.ts +2 -2
  6. package/dist/components/checkout/steps/plan/BillingPeriodPicker.style.d.ts +1 -0
  7. package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +2 -1
  8. package/dist/components/customerPortal/subscriptionOverview/subscriptionView/SubscriptionView.style.d.ts +1 -1
  9. package/dist/react-sdk.cjs.development.js +295 -142
  10. package/dist/react-sdk.cjs.development.js.map +1 -1
  11. package/dist/react-sdk.cjs.production.min.js +1 -1
  12. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  13. package/dist/react-sdk.esm.js +308 -146
  14. package/dist/react-sdk.esm.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/checkout/CheckoutContainer.style.ts +1 -0
  17. package/src/components/checkout/CheckoutContainer.tsx +2 -0
  18. package/src/components/checkout/components/Button.tsx +3 -14
  19. package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
  20. package/src/components/checkout/components/DowngradeToFreeContainer.tsx +27 -7
  21. package/src/components/checkout/components/Skeletons.style.ts +4 -1
  22. package/src/components/checkout/hooks/usePreviewSubscription.ts +9 -1
  23. package/src/components/checkout/planHeader/PlanHeader.style.tsx +1 -1
  24. package/src/components/checkout/planHeader/PlanHeader.tsx +7 -15
  25. package/src/components/checkout/progressBar/CheckoutProgressBar.style.ts +3 -1
  26. package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +5 -2
  27. package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
  28. package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +47 -8
  29. package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +26 -6
  30. package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +28 -7
  31. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +16 -16
  32. package/src/components/checkout/summary/CheckoutSummary.tsx +7 -7
  33. package/src/components/checkout/summary/components/CheckoutCaptions.tsx +2 -2
  34. package/src/components/checkout/textOverrides.ts +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.4.0-beta.4",
2
+ "version": "4.4.0-beta.6",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -4,6 +4,7 @@ import Box from '@mui/material/Box';
4
4
  export const CheckoutLayout = styled.div`
5
5
  margin: auto;
6
6
  width: 100%;
7
+ min-height: 760px;
7
8
  max-width: 920px;
8
9
  display: flex;
9
10
  position: relative;
@@ -79,6 +79,8 @@ export function CheckoutContainer({
79
79
  checkoutLocalization={checkoutLocalization}
80
80
  freePlan={plan!}
81
81
  activeSubscription={activeSubscription!}
82
+ allowChangePlan={allowChangePlan}
83
+ onChangePlan={onChangePlan}
82
84
  />
83
85
  ) : (
84
86
  <>
@@ -3,24 +3,13 @@ import React from 'react';
3
3
  import styled from '@emotion/styled/macro';
4
4
  import { Button as MuiButton, ButtonProps, css } from '@mui/material';
5
5
 
6
- export type StyledButtonProps = { $isLoading?: boolean; $success?: boolean; $error?: boolean };
6
+ export type StyledButtonProps = { $success?: boolean; $error?: boolean };
7
7
 
8
- const StyledButton = styled(MuiButton)<StyledButtonProps>`
8
+ const StyledButton = styled(MuiButton, { shouldForwardProp: (prop) => !prop.startsWith('$') })<StyledButtonProps>`
9
9
  border-radius: 10px;
10
10
  text-transform: none;
11
11
 
12
- ${({ theme, $isLoading, $success, $error }) => {
13
- if ($isLoading) {
14
- return css`
15
- background-color: ${theme.stigg.palette.primaryDark};
16
- cursor: no-drop;
17
-
18
- &:hover {
19
- background-color: ${theme.stigg.palette.primaryDark};
20
- }
21
- `;
22
- }
23
-
12
+ ${({ theme, $success, $error }) => {
24
13
  if ($success) {
25
14
  return css`
26
15
  background-color: ${theme.stigg.palette.success};
@@ -0,0 +1,32 @@
1
+ import { Button } from '@mui/material';
2
+ import { Typography } from '../../common/Typography';
3
+ import React from 'react';
4
+ import { CheckoutLocalization } from '../textOverrides';
5
+ import { ButtonProps } from '@mui/material/Button';
6
+
7
+ export const ChangePlanButton = ({
8
+ onClick,
9
+ checkoutLocalization,
10
+ size,
11
+ }: {
12
+ onClick: () => void;
13
+ checkoutLocalization: CheckoutLocalization;
14
+ size: ButtonProps['size'];
15
+ }) => {
16
+ return (
17
+ <Button
18
+ className="stigg-checkout-change-plan-button"
19
+ sx={{ textTransform: 'none' }}
20
+ variant="text"
21
+ size={size}
22
+ onClick={onClick}>
23
+ <Typography
24
+ className="stigg-checkout-change-plan-button-text"
25
+ color="primary.main"
26
+ variant="body1"
27
+ style={{ lineHeight: '24px' }}>
28
+ {checkoutLocalization.changePlan}
29
+ </Typography>
30
+ </Button>
31
+ );
32
+ };
@@ -7,6 +7,8 @@ import { CheckoutStatePlan, Subscription } from '@stigg/js-client-sdk';
7
7
  import { Currency, BillingPeriod } from '@stigg/js-client-sdk';
8
8
  import { currencyPriceFormatter } from '../../utils/currencyUtils';
9
9
  import { CheckoutLocalization } from '../textOverrides';
10
+ import { CheckoutContainerProps } from '../CheckoutContainer';
11
+ import { ChangePlanButton } from './ChangePlanButton';
10
12
 
11
13
  const DowngradeToFreePlansContainer = styled(Box)`
12
14
  display: flex;
@@ -62,19 +64,37 @@ export const DowngradeToFreeContent = ({
62
64
  );
63
65
  };
64
66
 
67
+ type DowngradeToFreePlanProps = {
68
+ checkoutLocalization: CheckoutLocalization;
69
+ activeSubscription: Subscription;
70
+ freePlan: CheckoutStatePlan;
71
+ allowChangePlan?: boolean;
72
+ } & Pick<CheckoutContainerProps, 'onChangePlan'>;
73
+
65
74
  export const DowngradeToFreePlan = ({
66
75
  checkoutLocalization,
67
76
  activeSubscription,
68
77
  freePlan,
69
- }: {
70
- checkoutLocalization: CheckoutLocalization;
71
- activeSubscription: Subscription;
72
- freePlan: CheckoutStatePlan;
73
- }) => {
78
+ allowChangePlan = false,
79
+ onChangePlan,
80
+ }: DowngradeToFreePlanProps) => {
81
+ let alertAction;
82
+ if (allowChangePlan && onChangePlan) {
83
+ alertAction = (
84
+ <ChangePlanButton
85
+ onClick={() => {
86
+ onChangePlan({ currentPlan: freePlan });
87
+ }}
88
+ checkoutLocalization={checkoutLocalization}
89
+ size="small"
90
+ />
91
+ );
92
+ }
93
+
74
94
  return (
75
95
  <>
76
- <DowngradeToFreeAlert className="stigg-checkout-downgrade-to-free-alert" severity="info">
77
- <Typography color="secondary">
96
+ <DowngradeToFreeAlert action={alertAction} className="stigg-checkout-downgrade-to-free-alert" severity="info">
97
+ <Typography span color="secondary">
78
98
  {checkoutLocalization.downgradeToFreeAlertText({ plan: activeSubscription.plan })}
79
99
  </Typography>
80
100
  </DowngradeToFreeAlert>
@@ -2,7 +2,10 @@ import styled from '@emotion/styled/macro';
2
2
  import { Grid } from '@mui/material';
3
3
  import ReactSkeleton from 'react-loading-skeleton';
4
4
 
5
- export const SkeletonsContainer = styled(Grid)<{ $gap: number; $flexDirection?: 'row' | 'column' }>`
5
+ export const SkeletonsContainer = styled(Grid, { shouldForwardProp: (prop) => !prop.startsWith('$') })<{
6
+ $gap: number;
7
+ $flexDirection?: 'row' | 'column';
8
+ }>`
6
9
  display: flex;
7
10
  flex-direction: ${({ $flexDirection }) => $flexDirection || 'row'};
8
11
  gap: ${({ $gap }) => $gap}px;
@@ -37,6 +37,12 @@ export const usePreviewSubscriptionAction = () => {
37
37
  let subscriptionPreview: SubscriptionPreview | null = null;
38
38
  let errorMessage: string | null = null;
39
39
 
40
+ let isValid = !subscription.billableFeatures.some(({ quantity }) => quantity === null || quantity <= 0);
41
+ isValid = isValid && !estimateAddons.some(({ quantity }) => quantity === null || quantity <= 0);
42
+ if (!isValid) {
43
+ return { subscriptionPreview };
44
+ }
45
+
40
46
  try {
41
47
  if (customer?.id && plan?.id) {
42
48
  const previewSubscriptionProps: PreviewSubscription = {
@@ -92,7 +98,9 @@ export const usePreviewSubscription = () => {
92
98
  setIsFetchingSubscriptionPreview(true);
93
99
 
94
100
  const { subscriptionPreview } = await previewSubscriptionAction();
95
- setSubscriptionPreview(subscriptionPreview);
101
+ if (subscriptionPreview) {
102
+ setSubscriptionPreview(subscriptionPreview);
103
+ }
96
104
 
97
105
  setIsFetchingSubscriptionPreview(false);
98
106
  };
@@ -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,
@@ -14,12 +14,15 @@ 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 (
@@ -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,26 @@ 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
+ onAddonsValidationChange({ addonId: addon.id, isValid: true });
67
+ }
50
68
  };
51
69
 
52
70
  return (
@@ -78,8 +96,11 @@ function AddonListItem({
78
96
  id={`${addon.id}-input`}
79
97
  type="number"
80
98
  sx={{ width: 120, marginX: 2 }}
81
- value={addonState?.quantity || 1}
82
- onChange={(event) => handleQuantityChange(event?.target?.value ? Number(event?.target?.value) : 1)}
99
+ value={addonState?.quantity ?? ''}
100
+ error={!isValid}
101
+ helperText={!isValid ? 'Minimum 1' : undefined}
102
+ FormHelperTextProps={{ sx: { margin: '4px' } }}
103
+ onChange={(event) => handleQuantityChange(event?.target?.value ? Number(event?.target?.value) : null)}
83
104
  />
84
105
  <TrashButton color="error" onClick={() => removeAddon(addon.id)}>
85
106
  <Icon icon="Trash" style={{ display: 'flex' }} />
@@ -87,7 +108,7 @@ function AddonListItem({
87
108
  </>
88
109
  )}
89
110
  {!isAdded && (
90
- <Button sx={{ paddingX: '22px', paddingY: '8px' }} onClick={() => handleQuantityChange()}>
111
+ <Button sx={{ paddingX: '22px', paddingY: '8px' }} onClick={() => handleQuantityChange(1)}>
91
112
  {checkoutLocalization.addAddonText}
92
113
  </Button>
93
114
  )}
@@ -99,13 +120,27 @@ function AddonListItem({
99
120
  export function CheckoutAddonsStep() {
100
121
  const { checkoutLocalization } = useCheckoutModel();
101
122
  const { billingPeriod } = usePlanStepModel();
123
+ const { setIsDisabled } = useProgressBarModel();
102
124
  const { initialAddons, addons, availableAddons, setAddon, removeAddon } = useAddonsStepModel();
125
+ const [addonsValidation, setAddonsValidation] = useState(
126
+ availableAddons?.reduce<Record<string, boolean>>((acc, curr) => {
127
+ const addonState = addons.find((x) => x.addon.id === curr.id);
128
+ acc[curr.id] = !!addonState && addonState.quantity > 0;
129
+ return acc;
130
+ }, {}) || {},
131
+ );
132
+
133
+ useEffect(() => {
134
+ const isDisabled = Object.values(addonsValidation).some((x) => !x);
135
+ setIsDisabled(isDisabled);
136
+ }, [addonsValidation, setIsDisabled]);
103
137
 
104
138
  return (
105
139
  <CheckoutAddonsContainer container>
106
140
  {availableAddons?.map((addon) => {
107
141
  const addonState = addons.find((x) => x.addon.id === addon.id);
108
142
  const initialAddonState = initialAddons?.find((x) => x.addon.id === addon.id);
143
+ const isValid = addonsValidation[addon.id];
109
144
 
110
145
  return (
111
146
  <AddonListItem
@@ -117,6 +152,10 @@ export function CheckoutAddonsStep() {
117
152
  setAddon={setAddon}
118
153
  removeAddon={removeAddon}
119
154
  checkoutLocalization={checkoutLocalization}
155
+ onAddonsValidationChange={({ addonId, isValid }: { addonId: string; isValid: boolean }) =>
156
+ setAddonsValidation({ ...addonsValidation, [addonId]: isValid })
157
+ }
158
+ isValid={isValid}
120
159
  />
121
160
  );
122
161
  })}
@@ -5,19 +5,39 @@ 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
+ } else if ($isActive) {
34
+ return theme.stigg.palette.backgroundSection;
35
+ }
36
+ return 'transparent';
37
+ }};
19
38
  text-transform: none;
20
39
  text-align: start;
40
+ height: 36px;
21
41
 
22
42
  &.MuiButton-root {
23
43
  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}>
@@ -51,11 +64,19 @@ export const BillingPeriodPicker = ({ plan, checkoutLocalization }: BillingPerio
51
64
  </Typography>
52
65
 
53
66
  <BillingPeriodOptions>
54
- {!!monthlyPrices?.length && (
55
- <BillingPeriodOption key={BillingPeriod.Monthly} billingPeriod={BillingPeriod.Monthly} />
56
- )}
57
67
  {!!annualPrices?.length && (
58
- <BillingPeriodOption key={BillingPeriod.Annually} billingPeriod={BillingPeriod.Annually} />
68
+ <BillingPeriodOption
69
+ key={BillingPeriod.Annually}
70
+ billingPeriod={BillingPeriod.Annually}
71
+ isOnlyBillingPeriod={!hasBothBillingPeriods}
72
+ />
73
+ )}
74
+ {!!monthlyPrices?.length && (
75
+ <BillingPeriodOption
76
+ key={BillingPeriod.Monthly}
77
+ billingPeriod={BillingPeriod.Monthly}
78
+ isOnlyBillingPeriod={!hasBothBillingPeriods}
79
+ />
59
80
  )}
60
81
  </BillingPeriodOptions>
61
82
  </BillingPeriodPickerContainer>
@@ -33,43 +33,36 @@ const StyledPlanCharge = styled.div`
33
33
 
34
34
  export function PlanCharge({
35
35
  charge,
36
+ isValid,
36
37
  setBillableFeature,
37
38
  billableFeature,
38
39
  onValidationChange,
39
40
  }: {
40
41
  charge: Price;
42
+ isValid: boolean;
41
43
  billableFeature?: BillableFeatureInput;
42
44
  setBillableFeature: UsePlanStepModel['setBillableFeature'];
43
45
  onValidationChange: ({ featureId, isValid }: { featureId: string; isValid: boolean }) => void;
44
46
  }) {
45
- const [isValid, setIsValid] = React.useState(true);
46
47
  const featureId = charge.feature?.featureId;
47
48
  const isBaseCharge = !featureId;
48
49
  const isPayAsYouGo = charge.pricingModel === BillingModel.UsageBased;
49
50
  const displayName = isBaseCharge ? 'Base charge' : charge.feature?.displayName;
50
51
  const hasQuantityRestrictions = !!(charge?.minUnitQuantity || charge?.maxUnitQuantity);
51
52
 
52
- useEffect(() => {
53
- if (!featureId) {
54
- return;
55
- }
56
-
57
- onValidationChange({ featureId, isValid });
58
- }, [featureId, isValid, onValidationChange]);
59
-
60
53
  const handleQuantityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
61
54
  if (isBaseCharge || !featureId) return;
62
55
 
63
56
  const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity;
64
57
  if (!value || value <= 0) {
65
- setIsValid(false);
58
+ onValidationChange({ featureId, isValid: false });
66
59
  // Reset the input value to null
67
60
  // @ts-ignore
68
- setBillableFeature(featureId, null);
61
+ setBillableFeature(featureId, value ?? null);
69
62
  return;
70
63
  }
71
64
 
72
- setIsValid(true);
65
+ onValidationChange({ featureId, isValid: true });
73
66
  const quantity = getValidPriceQuantity(charge, value);
74
67
  setBillableFeature(featureId, quantity);
75
68
  };
@@ -103,17 +96,18 @@ export function PlanCharge({
103
96
  } else {
104
97
  chargeRow = (
105
98
  <InputField
106
- sx={{ width: 145 }}
99
+ sx={{ width: 120 }}
107
100
  id={`${featureId}-input`}
108
101
  type="number"
109
102
  InputProps={
110
103
  hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
111
104
  }
112
105
  error={!isValid}
113
- helperText={!isValid ? 'Not a valid value' : ''}
106
+ helperText={!isValid ? 'Minimum 1' : undefined}
114
107
  FormHelperTextProps={{ sx: { margin: '4px' } }}
115
- value={billableFeature?.quantity || charge.minUnitQuantity}
108
+ value={billableFeature?.quantity ?? charge.minUnitQuantity ?? ''}
116
109
  onChange={handleQuantityChange}
110
+ onWheel={(e: React.WheelEvent<HTMLInputElement>) => (e.target as HTMLElement).blur()}
117
111
  />
118
112
  );
119
113
  }
@@ -142,7 +136,12 @@ export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListPr
142
136
  const { billableFeatures, setBillableFeature } = usePlanStepModel();
143
137
  const { setIsDisabled } = useProgressBarModel();
144
138
  const planCharges = useChargesSort(plan?.pricePoints?.filter((p) => p.billingPeriod === billingPeriod) || []);
145
- const [chargesValidation, setChargesValidation] = useState({});
139
+ const [chargesValidation, setChargesValidation] = useState(
140
+ planCharges?.reduce<Record<string, boolean>>((acc, curr) => {
141
+ acc[curr.feature?.featureId || 'base-charge'] = true;
142
+ return acc;
143
+ }, {}),
144
+ );
146
145
 
147
146
  useEffect(() => {
148
147
  const isDisabled = Object.values(chargesValidation).some((x) => !x);
@@ -159,6 +158,7 @@ export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListPr
159
158
  charge={charge}
160
159
  setBillableFeature={setBillableFeature}
161
160
  billableFeature={billableFeature}
161
+ isValid={chargesValidation[charge.feature?.featureId || 'base-charge']}
162
162
  onValidationChange={({ featureId, isValid }: { featureId: string; isValid: boolean }) =>
163
163
  setChargesValidation((prev) => ({ ...prev, [featureId]: isValid }))
164
164
  }
@@ -190,11 +190,13 @@ export const CheckoutSummary = ({
190
190
 
191
191
  if (!addonPrice) return null;
192
192
 
193
+ const addonQuantity = addon.quantity && addon.quantity > 0 ? addon.quantity : 1;
194
+
193
195
  return (
194
196
  <BilledPriceLineItem
195
197
  key={addon?.addon?.id}
196
198
  label={addon.addon.displayName}
197
- quantity={addon.quantity}
199
+ quantity={addonQuantity}
198
200
  price={addonPrice}
199
201
  />
200
202
  );
@@ -213,7 +215,7 @@ export const CheckoutSummary = ({
213
215
 
214
216
  <StyledDivider className="stigg-checkout-summary-divider" />
215
217
 
216
- {!disablePromotionCode && (
218
+ {!disablePromotionCode && !isFreeDowngrade && (
217
219
  <>
218
220
  <PromotionCodeSection checkoutLocalization={checkoutLocalization} />
219
221
 
@@ -258,13 +260,11 @@ export const CheckoutSummary = ({
258
260
  />
259
261
 
260
262
  <Button
261
- disableRipple={isLoading || progressBar.progressBarState.isDisabled}
262
- $isLoading={isLoading || progressBar.progressBarState.isDisabled}
263
263
  $success={isCheckoutCompletedSuccessfully}
264
- $error={isLastStep && subscriptionPreview?.isPlanDowngrade}
265
- disabled={isLoading || isFetchingSubscriptionPreview}
264
+ $error={isLastStep && !!subscriptionPreview?.isPlanDowngrade}
265
+ disabled={isLoading || isFetchingSubscriptionPreview || progressBar.progressBarState.isDisabled}
266
266
  className="stigg-checkout-summary-cta-button"
267
- sx={{ textTransform: 'none', borderRadius: '10px', marginTop: '24px' }}
267
+ sx={{ textTransform: 'none', borderRadius: '10px', marginTop: '24px', height: '36px' }}
268
268
  variant="contained"
269
269
  size="medium"
270
270
  onClick={(e: any) => {