@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.
- package/dist/components/checkout/components/Button.d.ts +0 -1
- package/dist/components/checkout/components/ChangePlanButton.d.ts +8 -0
- package/dist/components/checkout/components/DowngradeToFreeContainer.d.ts +6 -2
- package/dist/components/checkout/hooks/usePreviewSubscription.d.ts +3 -0
- package/dist/components/checkout/progressBar/CheckoutProgressBar.style.d.ts +2 -2
- package/dist/components/checkout/steps/plan/BillingPeriodPicker.style.d.ts +1 -0
- package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +2 -1
- package/dist/components/customerPortal/subscriptionOverview/subscriptionView/SubscriptionView.style.d.ts +1 -1
- package/dist/react-sdk.cjs.development.js +295 -142
- package/dist/react-sdk.cjs.development.js.map +1 -1
- package/dist/react-sdk.cjs.production.min.js +1 -1
- package/dist/react-sdk.cjs.production.min.js.map +1 -1
- package/dist/react-sdk.esm.js +308 -146
- package/dist/react-sdk.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/checkout/CheckoutContainer.style.ts +1 -0
- package/src/components/checkout/CheckoutContainer.tsx +2 -0
- package/src/components/checkout/components/Button.tsx +3 -14
- package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
- package/src/components/checkout/components/DowngradeToFreeContainer.tsx +27 -7
- package/src/components/checkout/components/Skeletons.style.ts +4 -1
- package/src/components/checkout/hooks/usePreviewSubscription.ts +9 -1
- package/src/components/checkout/planHeader/PlanHeader.style.tsx +1 -1
- package/src/components/checkout/planHeader/PlanHeader.tsx +7 -15
- package/src/components/checkout/progressBar/CheckoutProgressBar.style.ts +3 -1
- package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +5 -2
- package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
- package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +47 -8
- package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +26 -6
- package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +28 -7
- package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +16 -16
- package/src/components/checkout/summary/CheckoutSummary.tsx +7 -7
- package/src/components/checkout/summary/components/CheckoutCaptions.tsx +2 -2
- package/src/components/checkout/textOverrides.ts +1 -1
package/package.json
CHANGED
|
@@ -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 = { $
|
|
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, $
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
101
|
+
if (subscriptionPreview) {
|
|
102
|
+
setSubscriptionPreview(subscriptionPreview);
|
|
103
|
+
}
|
|
96
104
|
|
|
97
105
|
setIsFetchingSubscriptionPreview(false);
|
|
98
106
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
<
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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%',
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
|
|
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<{
|
|
9
|
-
|
|
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:
|
|
19
|
+
padding: 2px 8px;
|
|
15
20
|
border-radius: 10px;
|
|
16
|
-
border: ${({ theme, $isActive }) =>
|
|
17
|
-
|
|
18
|
-
|
|
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 = ({
|
|
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
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 ? '
|
|
106
|
+
helperText={!isValid ? 'Minimum 1' : undefined}
|
|
114
107
|
FormHelperTextProps={{ sx: { margin: '4px' } }}
|
|
115
|
-
value={billableFeature?.quantity
|
|
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={
|
|
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) => {
|