@stigg/react-sdk 4.4.0-beta.5 → 4.4.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/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/progressBar/CheckoutProgressBar.style.d.ts +2 -2
  5. package/dist/components/checkout/steps/plan/BillingPeriodPicker.style.d.ts +1 -0
  6. package/dist/react-sdk.cjs.development.js +264 -126
  7. package/dist/react-sdk.cjs.development.js.map +1 -1
  8. package/dist/react-sdk.cjs.production.min.js +1 -1
  9. package/dist/react-sdk.cjs.production.min.js.map +1 -1
  10. package/dist/react-sdk.esm.js +277 -130
  11. package/dist/react-sdk.esm.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/components/checkout/CheckoutContainer.style.ts +1 -0
  14. package/src/components/checkout/CheckoutContainer.tsx +2 -0
  15. package/src/components/checkout/components/Button.tsx +3 -14
  16. package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
  17. package/src/components/checkout/components/DowngradeToFreeContainer.tsx +27 -7
  18. package/src/components/checkout/components/Skeletons.style.ts +4 -1
  19. package/src/components/checkout/hooks/usePreviewSubscription.ts +2 -1
  20. package/src/components/checkout/planHeader/PlanHeader.style.tsx +1 -1
  21. package/src/components/checkout/planHeader/PlanHeader.tsx +7 -15
  22. package/src/components/checkout/progressBar/CheckoutProgressBar.style.ts +3 -1
  23. package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +5 -2
  24. package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
  25. package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +53 -9
  26. package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +26 -6
  27. package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +28 -7
  28. package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +5 -4
  29. package/src/components/checkout/summary/CheckoutSummary.tsx +7 -7
  30. package/src/components/checkout/summary/components/CheckoutCaptions.tsx +2 -2
  31. package/src/components/checkout/textOverrides.ts +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.4.0-beta.5",
2
+ "version": "4.4.0-beta.7",
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,7 +37,8 @@ export const usePreviewSubscriptionAction = () => {
37
37
  let subscriptionPreview: SubscriptionPreview | null = null;
38
38
  let errorMessage: string | null = null;
39
39
 
40
- const isValid = !subscription.billableFeatures.some(({ quantity }) => quantity === null);
40
+ let isValid = !subscription.billableFeatures.some(({ quantity }) => quantity === null || quantity <= 0);
41
+ isValid = isValid && !estimateAddons.some(({ quantity }) => quantity === null || quantity <= 0);
41
42
  if (!isValid) {
42
43
  return { subscriptionPreview };
43
44
  }
@@ -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,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,16 +97,24 @@ 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()}>
117
+ <Button sx={{ paddingX: '22px', paddingY: '8px' }} onClick={() => handleQuantityChange(1)}>
91
118
  {checkoutLocalization.addAddonText}
92
119
  </Button>
93
120
  )}
@@ -99,13 +126,26 @@ function AddonListItem({
99
126
  export function CheckoutAddonsStep() {
100
127
  const { checkoutLocalization } = useCheckoutModel();
101
128
  const { billingPeriod } = usePlanStepModel();
129
+ const { setIsDisabled } = useProgressBarModel();
102
130
  const { initialAddons, addons, availableAddons, setAddon, removeAddon } = useAddonsStepModel();
131
+ const [addonsValidation, setAddonsValidation] = useState(
132
+ availableAddons?.reduce<Record<string, boolean>>((acc, curr) => {
133
+ acc[curr.id] = true;
134
+ return acc;
135
+ }, {}) || {},
136
+ );
137
+
138
+ useEffect(() => {
139
+ const isDisabled = Object.values(addonsValidation).some((x) => !x);
140
+ setIsDisabled(isDisabled);
141
+ }, [addonsValidation, setIsDisabled]);
103
142
 
104
143
  return (
105
144
  <CheckoutAddonsContainer container>
106
145
  {availableAddons?.map((addon) => {
107
146
  const addonState = addons.find((x) => x.addon.id === addon.id);
108
147
  const initialAddonState = initialAddons?.find((x) => x.addon.id === addon.id);
148
+ const isValid = addonsValidation[addon.id];
109
149
 
110
150
  return (
111
151
  <AddonListItem
@@ -117,6 +157,10 @@ export function CheckoutAddonsStep() {
117
157
  setAddon={setAddon}
118
158
  removeAddon={removeAddon}
119
159
  checkoutLocalization={checkoutLocalization}
160
+ onAddonsValidationChange={({ addonId, isValid }: { addonId: string; isValid: boolean }) =>
161
+ setAddonsValidation({ ...addonsValidation, [addonId]: isValid })
162
+ }
163
+ isValid={isValid}
120
164
  />
121
165
  );
122
166
  })}
@@ -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>
@@ -58,7 +58,7 @@ export function PlanCharge({
58
58
  onValidationChange({ featureId, isValid: false });
59
59
  // Reset the input value to null
60
60
  // @ts-ignore
61
- setBillableFeature(featureId, null);
61
+ setBillableFeature(featureId, value ?? null);
62
62
  return;
63
63
  }
64
64
 
@@ -96,17 +96,18 @@ export function PlanCharge({
96
96
  } else {
97
97
  chargeRow = (
98
98
  <InputField
99
- sx={{ width: 145 }}
99
+ sx={{ width: 120 }}
100
100
  id={`${featureId}-input`}
101
101
  type="number"
102
102
  InputProps={
103
103
  hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
104
104
  }
105
105
  error={!isValid}
106
- helperText={!isValid ? 'Not a valid value' : ''}
106
+ helperText={!isValid ? 'Minimum 1' : undefined}
107
107
  FormHelperTextProps={{ sx: { margin: '4px' } }}
108
- value={billableFeature?.quantity || charge.minUnitQuantity || ''}
108
+ value={billableFeature?.quantity ?? charge.minUnitQuantity ?? ''}
109
109
  onChange={handleQuantityChange}
110
+ onWheel={(e: React.WheelEvent<HTMLInputElement>) => (e.target as HTMLElement).blur()}
110
111
  />
111
112
  );
112
113
  }
@@ -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) => {
@@ -58,11 +58,11 @@ const ScheduledUpdatesCaption = ({
58
58
  };
59
59
 
60
60
  const ChargeDueTodayCaption = ({ subscriptionPreview, plan, isFetchingSubscriptionPreview }: CheckoutCaptionProps) => {
61
- if (!subscriptionPreview?.total) {
61
+ if (!subscriptionPreview?.total || subscriptionPreview.total.amount <= 0) {
62
62
  return null;
63
63
  }
64
64
 
65
- const total = currencyPriceFormatter(subscriptionPreview?.total);
65
+ const total = currencyPriceFormatter(subscriptionPreview.total);
66
66
  const usedCredits = !!subscriptionPreview.credits?.used;
67
67
 
68
68
  return (
@@ -56,7 +56,7 @@ export function getResolvedCheckoutLocalize(
56
56
  },
57
57
  appliedCreditsTitle: 'Applied credits',
58
58
  taxTitle: ({ taxDetails }) => `Tax (${taxDetails?.percentage}%)`,
59
- downgradeToFreeAlertText: () => `We’re sorry to see you downgrade your plan 😞`,
59
+ downgradeToFreeAlertText: () => `We’re sorry to see you cancel your paid subscription 😭`,
60
60
  };
61
61
 
62
62
  return merge(checkoutDefaultLocalization, localizeOverride);