@stigg/react-sdk 4.4.0-beta.3 β 4.4.0-beta.5
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 +6 -2
- package/dist/components/checkout/CheckoutProvider.d.ts +3 -1
- package/dist/components/checkout/components/DowngradeToFreeContainer.d.ts +28 -0
- package/dist/components/checkout/hooks/usePaymentStepModel.d.ts +8 -2
- package/dist/components/checkout/hooks/usePreviewSubscription.d.ts +3 -0
- package/dist/components/checkout/hooks/useProgressBarModel.d.ts +2 -0
- package/dist/components/checkout/hooks/useSubscriptionModel.d.ts +2 -1
- package/dist/components/checkout/index.d.ts +1 -0
- 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 +2 -1
- package/dist/components/checkout/steps/plan/CheckoutChargeList.d.ts +6 -1
- package/dist/components/checkout/summary/CheckoutSummary.d.ts +3 -1
- package/dist/components/checkout/summary/components/LineItems.d.ts +2 -2
- package/dist/components/checkout/textOverrides.d.ts +4 -1
- package/dist/components/checkout/types.d.ts +7 -0
- 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 +607 -271
- 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 +619 -271
- package/dist/react-sdk.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/assets/payment-method.svg +3 -10
- package/src/components/checkout/Checkout.tsx +3 -1
- package/src/components/checkout/CheckoutContainer.tsx +48 -22
- package/src/components/checkout/CheckoutProvider.tsx +8 -4
- package/src/components/checkout/components/DowngradeToFreeContainer.tsx +98 -0
- package/src/components/checkout/hooks/usePaymentStepModel.ts +22 -3
- package/src/components/checkout/hooks/usePlanStepModel.ts +5 -5
- package/src/components/checkout/hooks/usePreviewSubscription.ts +34 -4
- package/src/components/checkout/hooks/useProgressBarModel.ts +15 -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 +1 -0
- package/src/components/checkout/progressBar/CheckoutProgressBar.tsx +3 -2
- 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 +54 -45
- package/src/components/checkout/steps/plan/CheckoutChargeList.tsx +41 -10
- package/src/components/checkout/summary/CheckoutSuccess.tsx +1 -1
- package/src/components/checkout/summary/CheckoutSummary.tsx +24 -19
- package/src/components/checkout/summary/components/LineItems.tsx +8 -16
- package/src/components/checkout/textOverrides.ts +5 -4
- package/src/components/checkout/types.ts +9 -0
- 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/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
|
@@ -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,9 +8,9 @@ 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;
|
|
@@ -29,61 +29,69 @@ 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
|
+
}
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const paymentMethodResults = await handleNewPaymentMethod({ elements, stripe, setupIntentClientSecret });
|
|
51
|
+
if (!paymentMethodResults.success) {
|
|
52
|
+
errorMessage = paymentMethodResults.errorMessage;
|
|
53
|
+
}
|
|
54
|
+
|
|
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 };
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
setWidgetReadOnly(true);
|
|
85
|
+
|
|
86
|
+
if (onCheckout) {
|
|
87
|
+
const externalCheckoutResults = await onCheckout({ checkoutParams, checkoutAction });
|
|
88
|
+
if (!externalCheckoutResults.success && externalCheckoutResults.errorMessage) {
|
|
89
|
+
errorMessage = externalCheckoutResults.errorMessage;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
const checkoutActionResults = await checkoutAction();
|
|
93
|
+
if (!checkoutActionResults.success && checkoutActionResults.errorMessage) {
|
|
94
|
+
errorMessage = checkoutActionResults.errorMessage;
|
|
87
95
|
}
|
|
88
96
|
}
|
|
89
97
|
|
|
@@ -92,12 +100,13 @@ export function useSubmit({ onCheckout, onCheckoutCompleted, onSuccess }: UseSub
|
|
|
92
100
|
const success = !errorMessage && !!checkoutResults?.subscription;
|
|
93
101
|
if (success && onSuccess) {
|
|
94
102
|
onSuccess();
|
|
103
|
+
|
|
104
|
+
await delay(ANIMATION_DURATION); // Wait for animation to finish
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
await delay(ANIMATION_DURATION); // Wait for animation to finish
|
|
98
107
|
await onCheckoutCompleted({ success, error: errorMessage });
|
|
99
108
|
|
|
100
|
-
return { results: checkoutResults, errorMessage };
|
|
109
|
+
return { results: checkoutResults, success: !errorMessage, errorMessage };
|
|
101
110
|
};
|
|
102
111
|
|
|
103
112
|
return { handleSubmit, isLoading: !!widgetState?.readOnly };
|
|
@@ -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 { 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';
|
|
@@ -33,12 +33,16 @@ const StyledPlanCharge = styled.div`
|
|
|
33
33
|
|
|
34
34
|
export function PlanCharge({
|
|
35
35
|
charge,
|
|
36
|
+
isValid,
|
|
36
37
|
setBillableFeature,
|
|
37
38
|
billableFeature,
|
|
39
|
+
onValidationChange,
|
|
38
40
|
}: {
|
|
39
41
|
charge: Price;
|
|
42
|
+
isValid: boolean;
|
|
40
43
|
billableFeature?: BillableFeatureInput;
|
|
41
44
|
setBillableFeature: UsePlanStepModel['setBillableFeature'];
|
|
45
|
+
onValidationChange: ({ featureId, isValid }: { featureId: string; isValid: boolean }) => void;
|
|
42
46
|
}) {
|
|
43
47
|
const featureId = charge.feature?.featureId;
|
|
44
48
|
const isBaseCharge = !featureId;
|
|
@@ -49,9 +53,17 @@ export function PlanCharge({
|
|
|
49
53
|
const handleQuantityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
54
|
if (isBaseCharge || !featureId) return;
|
|
51
55
|
|
|
52
|
-
const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity
|
|
53
|
-
|
|
56
|
+
const value = event?.target?.value ? Number(event?.target?.value) : charge.minUnitQuantity;
|
|
57
|
+
if (!value || value <= 0) {
|
|
58
|
+
onValidationChange({ featureId, isValid: false });
|
|
59
|
+
// Reset the input value to null
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
setBillableFeature(featureId, null);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
54
64
|
|
|
65
|
+
onValidationChange({ featureId, isValid: true });
|
|
66
|
+
const quantity = getValidPriceQuantity(charge, value);
|
|
55
67
|
setBillableFeature(featureId, quantity);
|
|
56
68
|
};
|
|
57
69
|
|
|
@@ -77,20 +89,23 @@ export function PlanCharge({
|
|
|
77
89
|
tierUnits={getPriceFeatureUnit(charge)}
|
|
78
90
|
selectedTier={tier}
|
|
79
91
|
handleTierChange={(tier: PriceTierFragment) => {
|
|
80
|
-
setBillableFeature(featureId!, tier
|
|
92
|
+
setBillableFeature(featureId!, tier.upTo);
|
|
81
93
|
}}
|
|
82
94
|
/>
|
|
83
95
|
);
|
|
84
96
|
} else {
|
|
85
97
|
chargeRow = (
|
|
86
98
|
<InputField
|
|
87
|
-
sx={{ width:
|
|
99
|
+
sx={{ width: 145 }}
|
|
88
100
|
id={`${featureId}-input`}
|
|
89
101
|
type="number"
|
|
90
102
|
InputProps={
|
|
91
103
|
hasQuantityRestrictions ? { inputProps: { min: charge.minUnitQuantity, max: charge.maxUnitQuantity } } : {}
|
|
92
104
|
}
|
|
93
|
-
|
|
105
|
+
error={!isValid}
|
|
106
|
+
helperText={!isValid ? 'Not a valid value' : ''}
|
|
107
|
+
FormHelperTextProps={{ sx: { margin: '4px' } }}
|
|
108
|
+
value={billableFeature?.quantity || charge.minUnitQuantity || ''}
|
|
94
109
|
onChange={handleQuantityChange}
|
|
95
110
|
/>
|
|
96
111
|
);
|
|
@@ -118,18 +133,34 @@ export function PlanCharge({
|
|
|
118
133
|
|
|
119
134
|
export function CheckoutChargeList({ plan, billingPeriod }: CheckoutChargeListProps) {
|
|
120
135
|
const { billableFeatures, setBillableFeature } = usePlanStepModel();
|
|
121
|
-
const
|
|
136
|
+
const { setIsDisabled } = useProgressBarModel();
|
|
137
|
+
const planCharges = useChargesSort(plan?.pricePoints?.filter((p) => p.billingPeriod === billingPeriod) || []);
|
|
138
|
+
const [chargesValidation, setChargesValidation] = useState(
|
|
139
|
+
planCharges?.reduce<Record<string, boolean>>((acc, curr) => {
|
|
140
|
+
acc[curr.feature?.featureId || 'base-charge'] = true;
|
|
141
|
+
return acc;
|
|
142
|
+
}, {}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const isDisabled = Object.values(chargesValidation).some((x) => !x);
|
|
147
|
+
setIsDisabled(isDisabled);
|
|
148
|
+
}, [chargesValidation, setIsDisabled]);
|
|
122
149
|
|
|
123
150
|
return (
|
|
124
151
|
<div>
|
|
125
|
-
{planCharges?.map(charge => {
|
|
126
|
-
const billableFeature = billableFeatures.find(x => x.featureId === charge.feature?.featureId);
|
|
152
|
+
{planCharges?.map((charge) => {
|
|
153
|
+
const billableFeature = billableFeatures.find((x) => x.featureId === charge.feature?.featureId);
|
|
127
154
|
return (
|
|
128
155
|
<PlanCharge
|
|
129
156
|
key={charge.feature?.featureId || 'base-charge'}
|
|
130
157
|
charge={charge}
|
|
131
158
|
setBillableFeature={setBillableFeature}
|
|
132
159
|
billableFeature={billableFeature}
|
|
160
|
+
isValid={chargesValidation[charge.feature?.featureId || 'base-charge']}
|
|
161
|
+
onValidationChange={({ featureId, isValid }: { featureId: string; isValid: boolean }) =>
|
|
162
|
+
setChargesValidation((prev) => ({ ...prev, [featureId]: isValid }))
|
|
163
|
+
}
|
|
133
164
|
/>
|
|
134
165
|
);
|
|
135
166
|
})}
|
|
@@ -24,7 +24,7 @@ const CheckoutSuccessContainer = styled(Box)`
|
|
|
24
24
|
export function CheckoutSuccess() {
|
|
25
25
|
return (
|
|
26
26
|
<CheckoutSuccessContainer>
|
|
27
|
-
<Lottie width={350} options={{ loop: false, autoplay: true, animationData }} />
|
|
27
|
+
<Lottie width={350} isClickToPauseDisabled options={{ loop: false, autoplay: true, animationData }} />
|
|
28
28
|
</CheckoutSuccessContainer>
|
|
29
29
|
);
|
|
30
30
|
}
|
|
@@ -84,7 +84,12 @@ function resolveCheckoutButtonText({
|
|
|
84
84
|
return checkoutLocalization.checkoutButton.purchaseText;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export const CheckoutSummary = ({
|
|
87
|
+
export const CheckoutSummary = ({
|
|
88
|
+
onCheckout,
|
|
89
|
+
onCheckoutCompleted,
|
|
90
|
+
disablePromotionCode,
|
|
91
|
+
isFreeDowngrade,
|
|
92
|
+
}: CheckoutContainerProps & { isFreeDowngrade: boolean }) => {
|
|
88
93
|
const [isCheckoutCompletedSuccessfully, setIsCheckoutCompletedSuccessfully] = useState(false);
|
|
89
94
|
const { setErrorMessage } = usePaymentStepModel();
|
|
90
95
|
const progressBar = useProgressBarModel();
|
|
@@ -96,8 +101,7 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
96
101
|
);
|
|
97
102
|
const [baseCharges, usageCharges] = partition(planPrices, (price) => price.pricingModel === BillingModel.FlatFee);
|
|
98
103
|
const [baseCharge] = baseCharges || [];
|
|
99
|
-
const
|
|
100
|
-
const isLastStep = progressBar.isCheckoutComplete && progressBar.isLastStep;
|
|
104
|
+
const isLastStep = isFreeDowngrade || (progressBar.isCheckoutComplete && progressBar.isLastStep);
|
|
101
105
|
|
|
102
106
|
const { subscriptionPreview, isFetchingSubscriptionPreview } = usePreviewSubscription();
|
|
103
107
|
|
|
@@ -119,11 +123,9 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
119
123
|
if (errorMessage) {
|
|
120
124
|
setErrorMessage(errorMessage);
|
|
121
125
|
setIsCheckoutCompletedSuccessfully(false);
|
|
122
|
-
|
|
126
|
+
} else {
|
|
127
|
+
setErrorMessage(undefined);
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
setErrorMessage(undefined);
|
|
126
|
-
setIsCheckoutCompletedSuccessfully(true);
|
|
127
129
|
};
|
|
128
130
|
|
|
129
131
|
const onCheckoutClick = async (e: any): Promise<void> => {
|
|
@@ -211,7 +213,7 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
211
213
|
|
|
212
214
|
<StyledDivider className="stigg-checkout-summary-divider" />
|
|
213
215
|
|
|
214
|
-
{
|
|
216
|
+
{!disablePromotionCode && (
|
|
215
217
|
<>
|
|
216
218
|
<PromotionCodeSection checkoutLocalization={checkoutLocalization} />
|
|
217
219
|
|
|
@@ -219,18 +221,18 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
219
221
|
</>
|
|
220
222
|
)}
|
|
221
223
|
|
|
222
|
-
<
|
|
224
|
+
<DiscountLineItem
|
|
223
225
|
subscriptionPreview={subscriptionPreview}
|
|
224
226
|
isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
|
|
225
|
-
checkoutLocalization={checkoutLocalization}
|
|
226
227
|
/>
|
|
227
228
|
|
|
228
|
-
<
|
|
229
|
+
<TaxLineItem
|
|
229
230
|
subscriptionPreview={subscriptionPreview}
|
|
230
231
|
isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
|
|
232
|
+
checkoutLocalization={checkoutLocalization}
|
|
231
233
|
/>
|
|
232
234
|
|
|
233
|
-
<
|
|
235
|
+
<AppliedCreditsLineItem
|
|
234
236
|
subscriptionPreview={subscriptionPreview}
|
|
235
237
|
isFetchingSubscriptionPreview={isFetchingSubscriptionPreview}
|
|
236
238
|
checkoutLocalization={checkoutLocalization}
|
|
@@ -256,10 +258,11 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
256
258
|
/>
|
|
257
259
|
|
|
258
260
|
<Button
|
|
259
|
-
disableRipple={isLoading}
|
|
260
|
-
$isLoading={isLoading}
|
|
261
|
+
disableRipple={isLoading || progressBar.progressBarState.isDisabled}
|
|
262
|
+
$isLoading={isLoading || progressBar.progressBarState.isDisabled}
|
|
261
263
|
$success={isCheckoutCompletedSuccessfully}
|
|
262
264
|
$error={isLastStep && subscriptionPreview?.isPlanDowngrade}
|
|
265
|
+
disabled={isLoading || isFetchingSubscriptionPreview}
|
|
263
266
|
className="stigg-checkout-summary-cta-button"
|
|
264
267
|
sx={{ textTransform: 'none', borderRadius: '10px', marginTop: '24px' }}
|
|
265
268
|
variant="contained"
|
|
@@ -269,11 +272,13 @@ export const CheckoutSummary = ({ onCheckout, onCheckoutCompleted }: CheckoutCon
|
|
|
269
272
|
}}
|
|
270
273
|
fullWidth>
|
|
271
274
|
<Typography className="stigg-checkout-summary-cta-button-text" color="white" style={{ display: 'flex' }}>
|
|
272
|
-
{isCheckoutCompletedSuccessfully
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
{isCheckoutCompletedSuccessfully ? (
|
|
276
|
+
<Icon icon="Check" style={{ display: 'contents' }} />
|
|
277
|
+
) : isLoading || isFetchingSubscriptionPreview ? (
|
|
278
|
+
<CircularProgress size={20} sx={{ color: 'white' }} />
|
|
279
|
+
) : (
|
|
280
|
+
resolveCheckoutButtonText({ isLastStep, subscriptionPreview, checkoutLocalization })
|
|
281
|
+
)}
|
|
277
282
|
</Typography>
|
|
278
283
|
</Button>
|
|
279
284
|
</SummaryCard>
|
|
@@ -21,24 +21,16 @@ export const LineItemRow = styled.div`
|
|
|
21
21
|
justify-content: space-between;
|
|
22
22
|
`;
|
|
23
23
|
|
|
24
|
-
export const getPriceString = ({
|
|
25
|
-
amountPerMonth,
|
|
26
|
-
price,
|
|
27
|
-
quantity,
|
|
28
|
-
}: {
|
|
29
|
-
amountPerMonth: number;
|
|
30
|
-
price: Price;
|
|
31
|
-
quantity: number;
|
|
32
|
-
}) => {
|
|
24
|
+
export const getPriceString = ({ amount, price, quantity }: { amount: number; price: Price; quantity: number }) => {
|
|
33
25
|
const { billingPeriod } = price;
|
|
34
26
|
let billingPeriodString = null;
|
|
35
27
|
|
|
36
28
|
if (billingPeriod === BillingPeriod.Annually) {
|
|
37
|
-
|
|
29
|
+
amount /= 12;
|
|
38
30
|
billingPeriodString = '12 months';
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
const addonPriceFormat = currencyPriceFormatter({ amount
|
|
33
|
+
const addonPriceFormat = currencyPriceFormatter({ amount, currency: price.currency });
|
|
42
34
|
|
|
43
35
|
return `${quantity > 1 ? `${quantity} x ${addonPriceFormat} each` : addonPriceFormat}${
|
|
44
36
|
billingPeriodString ? ` x ${billingPeriodString}` : ''
|
|
@@ -48,12 +40,12 @@ export const getPriceString = ({
|
|
|
48
40
|
export const BilledPriceLineItem = ({ label, quantity, price }: { label: string; quantity: number; price: Price }) => {
|
|
49
41
|
const { billingPeriod } = price;
|
|
50
42
|
|
|
51
|
-
let
|
|
43
|
+
let amount;
|
|
52
44
|
if (price.isTieredPrice) {
|
|
53
45
|
const tier = getTierByQuantity(price.tiers!, quantity);
|
|
54
|
-
|
|
46
|
+
amount = tier!.unitPrice.amount!;
|
|
55
47
|
} else {
|
|
56
|
-
|
|
48
|
+
amount = price.amount!;
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
return (
|
|
@@ -65,13 +57,13 @@ export const BilledPriceLineItem = ({ label, quantity, price }: { label: string;
|
|
|
65
57
|
</Typography>
|
|
66
58
|
{(quantity > 1 || billingPeriod === BillingPeriod.Annually) && (
|
|
67
59
|
<Typography variant="body1" color="secondary">
|
|
68
|
-
{getPriceString({
|
|
60
|
+
{getPriceString({ amount, price, quantity })}
|
|
69
61
|
</Typography>
|
|
70
62
|
)}
|
|
71
63
|
</Grid>
|
|
72
64
|
<Grid item>
|
|
73
65
|
<Typography variant="body1" color="secondary" style={{ wordBreak: 'break-word' }}>
|
|
74
|
-
{currencyPriceFormatter({ amount: quantity *
|
|
66
|
+
{currencyPriceFormatter({ amount: quantity * amount, currency: price.currency })}
|
|
75
67
|
</Typography>
|
|
76
68
|
</Grid>
|
|
77
69
|
</LineItemRow>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { BillingPeriod, SubscriptionPreviewTaxDetails } from '@stigg/js-client-sdk';
|
|
1
|
+
import { BillingPeriod, Plan, SubscriptionPreviewTaxDetails } from '@stigg/js-client-sdk';
|
|
2
2
|
import moment from 'moment';
|
|
3
3
|
import merge from 'lodash/merge';
|
|
4
4
|
import { DeepPartial } from '../../types';
|
|
5
|
-
import { formatBillingPeriod } from './formatting';
|
|
6
5
|
|
|
7
6
|
export type CheckoutLocalization = {
|
|
8
7
|
changePlan: string;
|
|
@@ -26,6 +25,7 @@ export type CheckoutLocalization = {
|
|
|
26
25
|
};
|
|
27
26
|
appliedCreditsTitle: string;
|
|
28
27
|
taxTitle: (params: { taxDetails: SubscriptionPreviewTaxDetails }) => string;
|
|
28
|
+
downgradeToFreeAlertText: (params: { plan: Plan }) => string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export function getResolvedCheckoutLocalize(
|
|
@@ -38,7 +38,7 @@ export function getResolvedCheckoutLocalize(
|
|
|
38
38
|
newPaymentMethodText: 'New payment method',
|
|
39
39
|
newPaymentMethodBillingAddressTitle: 'Billing address',
|
|
40
40
|
newPaymentMethodCardTitle: 'Payment method',
|
|
41
|
-
baseChargeText: (
|
|
41
|
+
baseChargeText: () => 'Base charge',
|
|
42
42
|
totalText: 'Total due today',
|
|
43
43
|
subTotalText: 'Subtotal',
|
|
44
44
|
addCouponCodeText: 'Add coupon code',
|
|
@@ -55,7 +55,8 @@ export function getResolvedCheckoutLocalize(
|
|
|
55
55
|
purchaseText: 'Purchase',
|
|
56
56
|
},
|
|
57
57
|
appliedCreditsTitle: 'Applied credits',
|
|
58
|
-
taxTitle: ({ taxDetails }) =>
|
|
58
|
+
taxTitle: ({ taxDetails }) => `Tax (${taxDetails?.percentage}%)`,
|
|
59
|
+
downgradeToFreeAlertText: () => `Weβre sorry to see you downgrade your plan π`,
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
return merge(checkoutDefaultLocalization, localizeOverride);
|
|
@@ -28,14 +28,21 @@ type PriceBillingPeriodProps = {
|
|
|
28
28
|
billingPeriod: BillingPeriod;
|
|
29
29
|
hasMonthlyPrice: boolean;
|
|
30
30
|
hasAnnuallyPrice: boolean;
|
|
31
|
+
paywallLocale: PaywallLocalization;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
function PriceBillingPeriod({
|
|
34
|
+
function PriceBillingPeriod({
|
|
35
|
+
plan,
|
|
36
|
+
billingPeriod,
|
|
37
|
+
hasMonthlyPrice,
|
|
38
|
+
hasAnnuallyPrice,
|
|
39
|
+
paywallLocale,
|
|
40
|
+
}: PriceBillingPeriodProps) {
|
|
34
41
|
const hasPrice = plan.pricePoints.find(pricePoint => pricePoint.billingPeriod === billingPeriod);
|
|
35
42
|
|
|
36
43
|
let content = EMPTY_CHAR;
|
|
37
44
|
if (hasPrice && hasMonthlyPrice && hasAnnuallyPrice) {
|
|
38
|
-
content = `, billed ${billingPeriod.toLowerCase()}`;
|
|
45
|
+
content = paywallLocale.price.billingPeriod?.(billingPeriod) || `, billed ${billingPeriod.toLowerCase()}`;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
return (
|
|
@@ -133,6 +140,7 @@ export const PlanPrice = ({
|
|
|
133
140
|
billingPeriod={billingPeriod}
|
|
134
141
|
hasAnnuallyPrice={hasAnnuallyPrice}
|
|
135
142
|
hasMonthlyPrice={hasMonthlyPrice}
|
|
143
|
+
paywallLocale={paywallLocale}
|
|
136
144
|
/>
|
|
137
145
|
</Typography>
|
|
138
146
|
)}
|
|
@@ -26,6 +26,8 @@ export type PaywallLocalization = {
|
|
|
26
26
|
};
|
|
27
27
|
price: {
|
|
28
28
|
startingAtCaption: string;
|
|
29
|
+
billingPeriod?: (billingPeriod: BillingPeriod) => string;
|
|
30
|
+
pricePeriod: (billingPeriod: BillingPeriod) => string;
|
|
29
31
|
custom: string;
|
|
30
32
|
priceNotSet: string;
|
|
31
33
|
free: PlanPriceText | ((currency?: PaywallCurrency) => PlanPriceText);
|
|
@@ -56,6 +58,7 @@ export function getResolvedPaywallLocalize(localizeOverride?: DeepPartial<Paywal
|
|
|
56
58
|
},
|
|
57
59
|
price: {
|
|
58
60
|
startingAtCaption: 'Starts at',
|
|
61
|
+
pricePeriod: (billingPeriod: BillingPeriod) => (billingPeriod === BillingPeriod.Monthly ? '/ month' : '/ year'),
|
|
59
62
|
free: currency => ({
|
|
60
63
|
price: `${currency?.symbol}0`,
|
|
61
64
|
}),
|