@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.
- package/dist/components/checkout/Checkout.d.ts +1 -1
- package/dist/components/checkout/CheckoutContainer.d.ts +7 -2
- package/dist/components/checkout/CheckoutProvider.d.ts +3 -1
- 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 +32 -0
- package/dist/components/checkout/hooks/useCheckoutModel.d.ts +2 -0
- package/dist/components/checkout/hooks/usePaymentStepModel.d.ts +8 -2
- package/dist/components/checkout/hooks/usePreviewSubscription.d.ts +10 -1
- package/dist/components/checkout/hooks/useProgressBarModel.d.ts +3 -0
- package/dist/components/checkout/hooks/useSubscriptionModel.d.ts +2 -1
- package/dist/components/checkout/index.d.ts +2 -0
- package/dist/components/checkout/progressBar/CheckoutProgressBar.style.d.ts +3 -2
- package/dist/components/checkout/steps/payment/PaymentMethods.d.ts +3 -2
- package/dist/components/checkout/steps/payment/PaymentStep.d.ts +2 -1
- package/dist/components/checkout/steps/payment/stripe/StripePaymentForm.d.ts +2 -1
- package/dist/components/checkout/steps/payment/stripe/stripe.utils.d.ts +4 -0
- package/dist/components/checkout/steps/payment/stripe/useSubmit.d.ts +4 -3
- package/dist/components/checkout/steps/plan/BillingPeriodPicker.style.d.ts +1 -0
- package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +6 -1
- package/dist/components/checkout/summary/CheckoutSuccess.d.ts +4 -1
- package/dist/components/checkout/summary/CheckoutSummary.d.ts +3 -1
- package/dist/components/checkout/summary/components/CheckoutCaptions.d.ts +2 -1
- package/dist/components/checkout/summary/components/LineItems.d.ts +2 -2
- package/dist/components/checkout/textOverrides.d.ts +7 -3
- package/dist/components/checkout/theme.d.ts +0 -1
- package/dist/components/checkout/types.d.ts +7 -0
- package/dist/components/customerPortal/subscriptionOverview/subscriptionView/SubscriptionView.style.d.ts +1 -1
- package/dist/components/paywall/paywallTextOverrides.d.ts +4 -0
- package/dist/components/utils/getPaidPriceText.d.ts +3 -1
- package/dist/react-sdk.cjs.development.js +1147 -524
- 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 +1181 -532
- package/dist/react-sdk.esm.js.map +1 -1
- package/dist/theme/getResolvedTheme.d.ts +1 -0
- package/dist/theme/types.d.ts +1 -0
- package/package.json +2 -2
- package/src/assets/payment-method.svg +3 -10
- package/src/components/checkout/Checkout.tsx +2 -1
- package/src/components/checkout/CheckoutContainer.style.ts +1 -0
- package/src/components/checkout/CheckoutContainer.tsx +59 -28
- package/src/components/checkout/CheckoutProvider.tsx +18 -18
- package/src/components/checkout/components/Button.tsx +19 -35
- package/src/components/checkout/components/ChangePlanButton.tsx +32 -0
- package/src/components/checkout/components/DowngradeToFreeContainer.tsx +118 -0
- package/src/components/checkout/components/Skeletons.style.ts +4 -1
- package/src/components/checkout/hooks/useCheckoutModel.ts +12 -2
- package/src/components/checkout/hooks/usePaymentStepModel.ts +22 -3
- package/src/components/checkout/hooks/usePlanStepModel.ts +25 -10
- package/src/components/checkout/hooks/usePreviewSubscription.ts +112 -40
- package/src/components/checkout/hooks/useProgressBarModel.ts +18 -0
- package/src/components/checkout/hooks/useSubscriptionModel.ts +8 -2
- package/src/components/checkout/hooks/useSubscriptionState.ts +2 -1
- package/src/components/checkout/index.ts +2 -0
- 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 +6 -3
- package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +13 -9
- package/src/components/checkout/promotionCode/AddPromotionCode.tsx +6 -7
- package/src/components/checkout/steps/addons/CheckoutAddonsStep.tsx +58 -11
- package/src/components/checkout/steps/payment/PaymentMethods.style.ts +1 -0
- package/src/components/checkout/steps/payment/PaymentMethods.tsx +13 -6
- package/src/components/checkout/steps/payment/PaymentStep.tsx +3 -1
- package/src/components/checkout/steps/payment/stripe/StripePaymentForm.tsx +35 -4
- package/src/components/checkout/steps/payment/stripe/stripe.utils.ts +4 -3
- package/src/components/checkout/steps/payment/stripe/useSubmit.ts +61 -48
- package/src/components/checkout/steps/plan/BillingPeriodPicker.style.tsx +27 -6
- package/src/components/checkout/steps/plan/BillingPeriodPicker.tsx +26 -5
- package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +62 -12
- package/src/components/checkout/summary/CheckoutSuccess.tsx +52 -6
- package/src/components/checkout/summary/CheckoutSummary.tsx +48 -33
- package/src/components/checkout/summary/components/CheckoutCaptions.tsx +30 -29
- package/src/components/checkout/summary/components/LineItems.tsx +8 -16
- package/src/components/checkout/textOverrides.ts +15 -12
- package/src/components/checkout/theme.ts +0 -4
- package/src/components/checkout/types.ts +9 -0
- package/src/components/common/Icon.tsx +4 -6
- package/src/components/common/mapExternalTheme.ts +1 -2
- package/src/components/paywall/PlanPrice.tsx +10 -2
- package/src/components/paywall/paywallTextOverrides.ts +3 -0
- package/src/components/utils/getPaidPriceText.ts +8 -2
- package/src/components/utils/getPlanPrice.ts +1 -1
- package/src/stories/Checkout.stories.tsx +6 -5
- package/src/theme/Theme.tsx +10 -1
- package/src/theme/getResolvedTheme.ts +1 -0
- package/src/theme/types.ts +1 -0
- package/dist/components/checkout/steps/surprise/SurpriseStep.d.ts +0 -2
- package/src/assets/nyancat.svg +0 -634
- 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: '
|
|
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">{`
|
|
59
|
+
text={<Typography variant="h6">{`Card ending in ${last4Digits}`}</Typography>}
|
|
59
60
|
subtitle={
|
|
60
61
|
!!expirationMonth &&
|
|
61
62
|
!!expirationYear && (
|
|
62
|
-
<Typography variant="body1">{`
|
|
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({
|
|
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:
|
|
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
|
-
|
|
12
|
-
|
|
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 }
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
const { success } = await handleStripeFormValidations({ elements });
|
|
43
|
-
if (!success) {
|
|
44
|
-
setWidgetReadOnly(false);
|
|
45
|
-
return;
|
|
55
|
+
paymentMethodId = paymentMethodResults.paymentMethodId;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
errorMessage = paymentMethodResults.errorMessage;
|
|
58
|
+
if (errorMessage) {
|
|
59
|
+
return { success: false, errorMessage };
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
}
|
|
62
|
+
checkoutParams = { ...checkoutParams, paymentMethodId };
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
errorMessage = externalCheckoutResults.errorMessage;
|
|
71
|
+
checkoutResults = nextActionResults;
|
|
72
|
+
if (nextActionResults.errorMessage) {
|
|
73
|
+
errorMessage = nextActionResults.errorMessage;
|
|
84
74
|
}
|
|
85
|
-
|
|
86
|
-
|
|
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<{
|
|
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
|
+
}
|
|
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 = ({
|
|
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}>
|
|
@@ -52,10 +65,18 @@ export const BillingPeriodPicker = ({ plan, checkoutLocalization }: BillingPerio
|
|
|
52
65
|
|
|
53
66
|
<BillingPeriodOptions>
|
|
54
67
|
{!!monthlyPrices?.length && (
|
|
55
|
-
<BillingPeriodOption
|
|
68
|
+
<BillingPeriodOption
|
|
69
|
+
key={BillingPeriod.Monthly}
|
|
70
|
+
billingPeriod={BillingPeriod.Monthly}
|
|
71
|
+
isOnlyBillingPeriod={!hasBothBillingPeriods}
|
|
72
|
+
/>
|
|
56
73
|
)}
|
|
57
74
|
{!!annualPrices?.length && (
|
|
58
|
-
<BillingPeriodOption
|
|
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
|
|
53
|
-
const
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
value={billableFeature?.quantity
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|