@stigg/react-sdk 6.9.1 → 6.11.0
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/CheckoutProvider.d.ts +1 -0
- package/dist/components/checkout/hooks/useCheckoutModel.d.ts +2 -0
- package/dist/components/checkout/steps/payment/PaymentMethods.d.ts +2 -2
- package/dist/components/checkout/steps/payment/PaymentStep.d.ts +1 -1
- package/dist/components/checkout/steps/payment/zuora/ZuoraPaymentForm.d.ts +3 -0
- package/dist/components/checkout/steps/payment/zuora/index.d.ts +3 -0
- package/dist/components/checkout/steps/payment/zuora/useZuoraIntegration.d.ts +5 -0
- package/dist/components/checkout/steps/payment/zuora/useZuoraSubmit.d.ts +11 -0
- package/dist/components/checkout/steps/payment/zuora/zuora.utils.d.ts +34 -0
- package/dist/react-sdk.cjs.development.js +583 -48
- 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 +583 -48
- package/dist/react-sdk.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/components/checkout/CheckoutContainer.tsx +19 -3
- package/src/components/checkout/CheckoutProvider.tsx +2 -0
- package/src/components/checkout/hooks/useCheckoutModel.ts +14 -3
- package/src/components/checkout/steps/payment/PaymentMethods.tsx +15 -2
- package/src/components/checkout/steps/payment/PaymentStep.tsx +8 -1
- package/src/components/checkout/steps/payment/zuora/ZuoraPaymentForm.tsx +128 -0
- package/src/components/checkout/steps/payment/zuora/index.ts +11 -0
- package/src/components/checkout/steps/payment/zuora/useZuoraIntegration.ts +56 -0
- package/src/components/checkout/steps/payment/zuora/useZuoraSubmit.ts +125 -0
- package/src/components/checkout/steps/payment/zuora/zuora.utils.ts +72 -0
- package/src/components/checkout/summary/CheckoutSummary.tsx +71 -44
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "6.
|
|
2
|
+
"version": "6.11.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"typings": "dist/index.d.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"@emotion/react": "^11.10.5",
|
|
115
115
|
"@emotion/styled": "^11.10.5",
|
|
116
116
|
"@mui/material": "^5.12.0",
|
|
117
|
-
"@stigg/js-client-sdk": "3.60.
|
|
117
|
+
"@stigg/js-client-sdk": "3.60.2",
|
|
118
118
|
"@stripe/react-stripe-js": "^2.1.1",
|
|
119
119
|
"@stripe/stripe-js": "^1.54.1",
|
|
120
120
|
"@types/styled-components": "^5.1.26",
|
|
@@ -33,7 +33,12 @@ const getStepProps = (
|
|
|
33
33
|
onBillingAddressChange,
|
|
34
34
|
onChangePlan,
|
|
35
35
|
collectPhoneNumber,
|
|
36
|
-
|
|
36
|
+
onCheckout,
|
|
37
|
+
onCheckoutCompleted,
|
|
38
|
+
}: Pick<
|
|
39
|
+
CheckoutContainerProps,
|
|
40
|
+
'onBillingAddressChange' | 'onChangePlan' | 'collectPhoneNumber' | 'onCheckout' | 'onCheckoutCompleted'
|
|
41
|
+
>,
|
|
37
42
|
): StepProps => {
|
|
38
43
|
switch (currentStep.key) {
|
|
39
44
|
case CheckoutStepKey.PLAN:
|
|
@@ -43,7 +48,12 @@ const getStepProps = (
|
|
|
43
48
|
case CheckoutStepKey.PAYMENT:
|
|
44
49
|
return {
|
|
45
50
|
content: (
|
|
46
|
-
<PaymentStep
|
|
51
|
+
<PaymentStep
|
|
52
|
+
onBillingAddressChange={onBillingAddressChange}
|
|
53
|
+
collectPhoneNumber={collectPhoneNumber}
|
|
54
|
+
onCheckout={onCheckout}
|
|
55
|
+
onCheckoutCompleted={onCheckoutCompleted}
|
|
56
|
+
/>
|
|
47
57
|
),
|
|
48
58
|
};
|
|
49
59
|
default:
|
|
@@ -96,7 +106,13 @@ export function CheckoutContainer({
|
|
|
96
106
|
!!activeSubscription &&
|
|
97
107
|
activeSubscription.pricingType !== PricingType.Free;
|
|
98
108
|
|
|
99
|
-
const { content } = getStepProps(currentStep, {
|
|
109
|
+
const { content } = getStepProps(currentStep, {
|
|
110
|
+
onBillingAddressChange,
|
|
111
|
+
onChangePlan,
|
|
112
|
+
collectPhoneNumber,
|
|
113
|
+
onCheckout,
|
|
114
|
+
onCheckoutCompleted,
|
|
115
|
+
});
|
|
100
116
|
|
|
101
117
|
const checkoutContent = (
|
|
102
118
|
<>
|
|
@@ -36,6 +36,7 @@ export interface CheckoutContextState {
|
|
|
36
36
|
paymentStep: PaymentStepState;
|
|
37
37
|
widgetState: WidgetState;
|
|
38
38
|
isWidgetWatermarkEnabled: boolean;
|
|
39
|
+
checkoutSuccess: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export const CheckoutContext = React.createContext<
|
|
@@ -154,6 +155,7 @@ export function CheckoutProvider({
|
|
|
154
155
|
resourceId: checkout?.resource?.id,
|
|
155
156
|
widgetState: { readOnly: false, isValid: true, isLoadingCheckoutData: isLoading },
|
|
156
157
|
isWidgetWatermarkEnabled,
|
|
158
|
+
checkoutSuccess: false,
|
|
157
159
|
};
|
|
158
160
|
|
|
159
161
|
return initialState;
|
|
@@ -7,8 +7,8 @@ export type WidgetState = {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
function useCheckoutState() {
|
|
10
|
-
const [{ checkout, widgetState, checkoutLocalization }] = useCheckoutContext();
|
|
11
|
-
return { checkoutState: checkout, widgetState, checkoutLocalization };
|
|
10
|
+
const [{ checkout, widgetState, checkoutLocalization, checkoutSuccess }] = useCheckoutContext();
|
|
11
|
+
return { checkoutState: checkout, widgetState, checkoutLocalization, checkoutSuccess };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function useSetWidgetReadonly() {
|
|
@@ -29,14 +29,25 @@ function useSetIsValid() {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function useSetCheckoutSuccess() {
|
|
33
|
+
const [, setState] = useCheckoutContext();
|
|
34
|
+
|
|
35
|
+
return (checkoutSuccess: boolean) =>
|
|
36
|
+
setState((draft) => {
|
|
37
|
+
draft.checkoutSuccess = checkoutSuccess;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export function useCheckoutModel() {
|
|
33
|
-
const { checkoutState, widgetState, checkoutLocalization } = useCheckoutState();
|
|
42
|
+
const { checkoutState, widgetState, checkoutLocalization, checkoutSuccess } = useCheckoutState();
|
|
34
43
|
|
|
35
44
|
return {
|
|
36
45
|
checkoutState,
|
|
37
46
|
widgetState,
|
|
38
47
|
checkoutLocalization,
|
|
48
|
+
checkoutSuccess,
|
|
39
49
|
setWidgetReadOnly: useSetWidgetReadonly(),
|
|
40
50
|
setIsValid: useSetIsValid(),
|
|
51
|
+
setCheckoutSuccess: useSetCheckoutSuccess(),
|
|
41
52
|
};
|
|
42
53
|
}
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
PaymentMethodTextContainer,
|
|
14
14
|
} from './PaymentMethods.style';
|
|
15
15
|
import { StripePaymentForm } from './stripe';
|
|
16
|
+
import { ZuoraPaymentForm } from './zuora/ZuoraPaymentForm';
|
|
17
|
+
import { useZuoraIntegration } from './zuora/useZuoraIntegration';
|
|
16
18
|
import { CheckoutLocalization } from '../../configurations/textOverrides';
|
|
17
19
|
import { CheckoutContainerProps } from '../../CheckoutContainer';
|
|
18
20
|
|
|
@@ -30,7 +32,10 @@ export type NewPaymentMethodProps = Pick<PaymentMethodLayoutProps, 'checked' | '
|
|
|
30
32
|
onSelect: () => void;
|
|
31
33
|
checkoutLocalization: CheckoutLocalization;
|
|
32
34
|
hasExistingPaymentMethod: boolean;
|
|
33
|
-
} & Pick<
|
|
35
|
+
} & Pick<
|
|
36
|
+
CheckoutContainerProps,
|
|
37
|
+
'onBillingAddressChange' | 'collectPhoneNumber' | 'onCheckout' | 'onCheckoutCompleted'
|
|
38
|
+
>;
|
|
34
39
|
|
|
35
40
|
function PaymentMethodLayout({ checked, text, subtitle, readOnly }: PaymentMethodLayoutProps) {
|
|
36
41
|
return (
|
|
@@ -77,7 +82,11 @@ export function NewPaymentMethod({
|
|
|
77
82
|
checkoutLocalization,
|
|
78
83
|
onBillingAddressChange,
|
|
79
84
|
collectPhoneNumber,
|
|
85
|
+
onCheckout,
|
|
86
|
+
onCheckoutCompleted,
|
|
80
87
|
}: NewPaymentMethodProps) {
|
|
88
|
+
const { isZuoraIntegration } = useZuoraIntegration();
|
|
89
|
+
|
|
81
90
|
return (
|
|
82
91
|
<NewPaymentMethodContainer item $hideBorders={!hasExistingPaymentMethod} onClick={onSelect} $disabled={readOnly}>
|
|
83
92
|
{hasExistingPaymentMethod && (
|
|
@@ -88,7 +97,11 @@ export function NewPaymentMethod({
|
|
|
88
97
|
/>
|
|
89
98
|
)}
|
|
90
99
|
<Collapse in={checked}>
|
|
91
|
-
|
|
100
|
+
{isZuoraIntegration ? (
|
|
101
|
+
<ZuoraPaymentForm onCheckout={onCheckout} onCheckoutCompleted={onCheckoutCompleted} />
|
|
102
|
+
) : (
|
|
103
|
+
<StripePaymentForm onBillingAddressChange={onBillingAddressChange} collectPhoneNumber={collectPhoneNumber} />
|
|
104
|
+
)}
|
|
92
105
|
</Collapse>
|
|
93
106
|
</NewPaymentMethodContainer>
|
|
94
107
|
);
|
|
@@ -16,7 +16,12 @@ const PaymentContainer = styled(Grid)`
|
|
|
16
16
|
export function PaymentStep({
|
|
17
17
|
onBillingAddressChange,
|
|
18
18
|
collectPhoneNumber,
|
|
19
|
-
|
|
19
|
+
onCheckout,
|
|
20
|
+
onCheckoutCompleted,
|
|
21
|
+
}: Pick<
|
|
22
|
+
CheckoutContainerProps,
|
|
23
|
+
'onBillingAddressChange' | 'collectPhoneNumber' | 'onCheckout' | 'onCheckoutCompleted'
|
|
24
|
+
>) {
|
|
20
25
|
const { checkoutState, checkoutLocalization, widgetState } = useCheckoutModel();
|
|
21
26
|
const { customer } = checkoutState || {};
|
|
22
27
|
const { errorMessage, useNewPaymentMethod, setUseNewPaymentMethod } = usePaymentStepModel();
|
|
@@ -51,6 +56,8 @@ export function PaymentStep({
|
|
|
51
56
|
onSelect={() => handleOnSelect(true)}
|
|
52
57
|
onBillingAddressChange={onBillingAddressChange}
|
|
53
58
|
collectPhoneNumber={collectPhoneNumber}
|
|
59
|
+
onCheckout={onCheckout}
|
|
60
|
+
onCheckoutCompleted={onCheckoutCompleted}
|
|
54
61
|
/>
|
|
55
62
|
</PaymentContainer>
|
|
56
63
|
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import styled from '@emotion/styled';
|
|
3
|
+
import Box from '@mui/material/Box';
|
|
4
|
+
import CircularProgress from '@mui/material/CircularProgress';
|
|
5
|
+
import { Currency, ZuoraCheckoutCredentialsFragment } from '@stigg/js-client-sdk';
|
|
6
|
+
import { useZuoraIntegration } from './useZuoraIntegration';
|
|
7
|
+
import { createZuoraPaymentFormConfig, ZuoraPaymentResult } from './zuora.utils';
|
|
8
|
+
import { useZuoraSubmit } from './useZuoraSubmit';
|
|
9
|
+
import { useCheckoutModel, usePaymentStepModel, usePlanStepModel } from '../../../hooks';
|
|
10
|
+
import { CheckoutContainerProps } from '../../../CheckoutContainer';
|
|
11
|
+
|
|
12
|
+
const Container = styled(Box)`
|
|
13
|
+
position: relative;
|
|
14
|
+
max-width: 500px;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const PaymentContainer = styled(Box)<{ $showLoading: boolean }>`
|
|
18
|
+
opacity: ${({ $showLoading }) => ($showLoading ? 0.6 : 1)};
|
|
19
|
+
pointer-events: ${({ $showLoading }) => ($showLoading ? 'none' : 'auto')};
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const LoadingOverlay = styled(Box)`
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: 40px;
|
|
25
|
+
left: 0;
|
|
26
|
+
right: 0;
|
|
27
|
+
bottom: 0;
|
|
28
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
z-index: 10;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const LoadingContent = styled(Box)`
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 12px;
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
export function ZuoraPaymentForm({
|
|
44
|
+
onCheckout,
|
|
45
|
+
onCheckoutCompleted,
|
|
46
|
+
}: Pick<CheckoutContainerProps, 'onCheckout' | 'onCheckoutCompleted'>) {
|
|
47
|
+
const { isZuoraIntegration, isInitialized, createPaymentSession } = useZuoraIntegration();
|
|
48
|
+
const { setErrorMessage } = usePaymentStepModel();
|
|
49
|
+
const { billingCountryCode } = usePlanStepModel();
|
|
50
|
+
const { checkoutState, widgetState } = useCheckoutModel();
|
|
51
|
+
const { handleZuoraSubmit } = useZuoraSubmit({ onCheckout, onCheckoutCompleted });
|
|
52
|
+
const hasRenderedRef = useRef(false);
|
|
53
|
+
const [paymentFormLoading, setPaymentFormLoading] = React.useState(false);
|
|
54
|
+
|
|
55
|
+
const currency = checkoutState?.plan?.pricePoints?.[0]?.currency || Currency.Usd;
|
|
56
|
+
const { publishableKey } = checkoutState?.billingIntegration.billingCredentials as ZuoraCheckoutCredentialsFragment;
|
|
57
|
+
|
|
58
|
+
const handleZuoraComplete = useCallback(
|
|
59
|
+
async (result: ZuoraPaymentResult) => {
|
|
60
|
+
if (!result.success || !result.paymentMethodId) {
|
|
61
|
+
const errorMessage = result.error?.message || 'Payment failed';
|
|
62
|
+
setErrorMessage(errorMessage);
|
|
63
|
+
await onCheckoutCompleted({ success: false, error: errorMessage });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await handleZuoraSubmit(result.paymentMethodId);
|
|
68
|
+
},
|
|
69
|
+
[handleZuoraSubmit, setErrorMessage, onCheckoutCompleted],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const renderZuoraForm = async () => {
|
|
74
|
+
if (!isInitialized || hasRenderedRef.current) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setPaymentFormLoading(true);
|
|
79
|
+
setErrorMessage(undefined);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
hasRenderedRef.current = true;
|
|
83
|
+
|
|
84
|
+
const zuora = window.Zuora(publishableKey);
|
|
85
|
+
const config = createZuoraPaymentFormConfig(
|
|
86
|
+
createPaymentSession,
|
|
87
|
+
handleZuoraComplete,
|
|
88
|
+
currency,
|
|
89
|
+
billingCountryCode,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const paymentForm = await zuora.createPaymentForm(config);
|
|
93
|
+
paymentForm.mount('#zuora_payment');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
hasRenderedRef.current = false;
|
|
96
|
+
setErrorMessage('Failed to render Zuora payment form');
|
|
97
|
+
} finally {
|
|
98
|
+
setPaymentFormLoading(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
void renderZuoraForm();
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
hasRenderedRef.current = false;
|
|
106
|
+
};
|
|
107
|
+
// Only depend on isInitialized to prevent multiple form renders
|
|
108
|
+
// createPaymentSession and handleZuoraComplete are intentionally omitted
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, [isInitialized]);
|
|
111
|
+
|
|
112
|
+
if (!isZuoraIntegration) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Container>
|
|
118
|
+
<PaymentContainer id="zuora_payment" $showLoading={widgetState.readOnly || paymentFormLoading} />
|
|
119
|
+
{(widgetState.readOnly || paymentFormLoading) && (
|
|
120
|
+
<LoadingOverlay>
|
|
121
|
+
<LoadingContent>
|
|
122
|
+
<CircularProgress size={28} />
|
|
123
|
+
</LoadingContent>
|
|
124
|
+
</LoadingOverlay>
|
|
125
|
+
)}
|
|
126
|
+
</Container>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ZuoraPaymentForm } from './ZuoraPaymentForm';
|
|
2
|
+
export { useZuoraIntegration } from './useZuoraIntegration';
|
|
3
|
+
export {
|
|
4
|
+
isZuoraLibraryAvailable,
|
|
5
|
+
loadZuoraLibrary,
|
|
6
|
+
createZuoraPaymentFormConfig,
|
|
7
|
+
ZuoraInstance,
|
|
8
|
+
ZuoraPaymentFormConfig,
|
|
9
|
+
ZuoraPaymentForm as ZuoraPaymentFormType,
|
|
10
|
+
ZuoraPaymentResult,
|
|
11
|
+
} from './zuora.utils';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { useCheckoutModel, usePaymentStepModel, usePlanStepModel } from '../../../hooks';
|
|
3
|
+
import { isZuoraLibraryAvailable, loadZuoraLibrary } from './zuora.utils';
|
|
4
|
+
import { BillingVendorIdentifier, useStiggContext } from '../../../../..';
|
|
5
|
+
|
|
6
|
+
export function useZuoraIntegration() {
|
|
7
|
+
const { stigg } = useStiggContext();
|
|
8
|
+
const { setErrorMessage } = usePaymentStepModel();
|
|
9
|
+
const { billingCountryCode } = usePlanStepModel();
|
|
10
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
11
|
+
const { checkoutState } = useCheckoutModel();
|
|
12
|
+
const { billingIntegration } = checkoutState || {};
|
|
13
|
+
|
|
14
|
+
const createPaymentSession = useCallback(async () => {
|
|
15
|
+
if (!checkoutState?.customer.id || !checkoutState?.plan.id) {
|
|
16
|
+
throw new Error('Customer and plan are required');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { token } = await stigg.createPaymentSession({
|
|
20
|
+
planId: checkoutState.plan.id,
|
|
21
|
+
billingCountryCode,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return token;
|
|
25
|
+
}, [checkoutState, stigg, billingCountryCode]);
|
|
26
|
+
|
|
27
|
+
const initializeZuora = useCallback(() => {
|
|
28
|
+
try {
|
|
29
|
+
if (!isZuoraLibraryAvailable()) {
|
|
30
|
+
loadZuoraLibrary();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setIsInitialized(true);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setErrorMessage('Failed to initialize Zuora');
|
|
36
|
+
}
|
|
37
|
+
}, [setIsInitialized, setErrorMessage]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (billingIntegration) {
|
|
41
|
+
const { billingIdentifier } = billingIntegration;
|
|
42
|
+
|
|
43
|
+
if (billingIdentifier === BillingVendorIdentifier.Zuora) {
|
|
44
|
+
void initializeZuora();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}, [billingIntegration, initializeZuora]);
|
|
48
|
+
|
|
49
|
+
const isZuoraIntegration = billingIntegration?.billingIdentifier === BillingVendorIdentifier.Zuora;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isZuoraIntegration,
|
|
53
|
+
isInitialized,
|
|
54
|
+
createPaymentSession,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { ApplySubscription } from '@stigg/js-client-sdk';
|
|
3
|
+
import { useStiggContext } from '../../../../..';
|
|
4
|
+
import { useSubscriptionState } from '../../../hooks/useSubscriptionState';
|
|
5
|
+
import { useCheckoutModel, usePaymentStepModel } from '../../../hooks';
|
|
6
|
+
import { CheckoutContainerProps } from '../../../CheckoutContainer';
|
|
7
|
+
import { ANIMATION_DURATION } from '../../../summary/CheckoutSuccess';
|
|
8
|
+
|
|
9
|
+
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
|
10
|
+
|
|
11
|
+
export type ZuoraSubmitResult = {
|
|
12
|
+
success: boolean;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type UseZuoraSubmitProps = {
|
|
17
|
+
onSuccess?: () => void;
|
|
18
|
+
} & Pick<CheckoutContainerProps, 'onCheckout' | 'onCheckoutCompleted' | 'disableSuccessAnimation'>;
|
|
19
|
+
|
|
20
|
+
export function useZuoraSubmit({
|
|
21
|
+
onCheckout,
|
|
22
|
+
onCheckoutCompleted,
|
|
23
|
+
onSuccess,
|
|
24
|
+
disableSuccessAnimation,
|
|
25
|
+
}: UseZuoraSubmitProps) {
|
|
26
|
+
const { stigg } = useStiggContext();
|
|
27
|
+
const subscriptionState = useSubscriptionState();
|
|
28
|
+
const { checkoutState, setWidgetReadOnly, setCheckoutSuccess } = useCheckoutModel();
|
|
29
|
+
const { setErrorMessage } = usePaymentStepModel();
|
|
30
|
+
|
|
31
|
+
const handleZuoraSubmit = useCallback(
|
|
32
|
+
async (paymentMethodId?: string): Promise<ZuoraSubmitResult> => {
|
|
33
|
+
setWidgetReadOnly(true);
|
|
34
|
+
|
|
35
|
+
let success = true;
|
|
36
|
+
let errorMessage: string | undefined;
|
|
37
|
+
|
|
38
|
+
const currentSubscriptionState = subscriptionState;
|
|
39
|
+
if (!currentSubscriptionState) {
|
|
40
|
+
success = false;
|
|
41
|
+
errorMessage = 'Unexpected error, please contact support.';
|
|
42
|
+
} else {
|
|
43
|
+
const checkoutParams: ApplySubscription = {
|
|
44
|
+
...currentSubscriptionState,
|
|
45
|
+
paymentMethodId,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const checkoutAction = async (params: ApplySubscription) => {
|
|
49
|
+
try {
|
|
50
|
+
const applySubscriptionResults = await stigg.applySubscription(params);
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
results: applySubscriptionResults,
|
|
54
|
+
};
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(e);
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
errorMessage: (e as any)?.message || 'Failed to apply subscription',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (onCheckout) {
|
|
66
|
+
const externalCheckoutResults = await onCheckout({
|
|
67
|
+
customerId: checkoutState!.customer.id,
|
|
68
|
+
checkoutParams,
|
|
69
|
+
checkoutAction,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (externalCheckoutResults.errorMessage) {
|
|
73
|
+
errorMessage = externalCheckoutResults.errorMessage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
success = success && externalCheckoutResults.success && !errorMessage;
|
|
77
|
+
} else {
|
|
78
|
+
const checkoutActionResults = await checkoutAction(checkoutParams);
|
|
79
|
+
if (!checkoutActionResults.success && checkoutActionResults.errorMessage) {
|
|
80
|
+
errorMessage = checkoutActionResults.errorMessage;
|
|
81
|
+
}
|
|
82
|
+
success = checkoutActionResults.success && !errorMessage;
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
success = false;
|
|
86
|
+
errorMessage = (err as Error).message || 'Failed to process checkout';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (success) {
|
|
91
|
+
setCheckoutSuccess(true);
|
|
92
|
+
|
|
93
|
+
if (onSuccess) {
|
|
94
|
+
onSuccess();
|
|
95
|
+
|
|
96
|
+
if (!disableSuccessAnimation) {
|
|
97
|
+
await delay(ANIMATION_DURATION);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
setErrorMessage(errorMessage);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setWidgetReadOnly(false);
|
|
105
|
+
|
|
106
|
+
await onCheckoutCompleted({ success, error: errorMessage });
|
|
107
|
+
|
|
108
|
+
return { success, errorMessage };
|
|
109
|
+
},
|
|
110
|
+
[
|
|
111
|
+
stigg,
|
|
112
|
+
subscriptionState,
|
|
113
|
+
checkoutState,
|
|
114
|
+
onCheckout,
|
|
115
|
+
onCheckoutCompleted,
|
|
116
|
+
onSuccess,
|
|
117
|
+
disableSuccessAnimation,
|
|
118
|
+
setWidgetReadOnly,
|
|
119
|
+
setCheckoutSuccess,
|
|
120
|
+
setErrorMessage,
|
|
121
|
+
],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return { handleZuoraSubmit };
|
|
125
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Currency } from '@stigg/js-client-sdk';
|
|
2
|
+
|
|
3
|
+
// Zuora Payment Form v3 API types
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
Zuora: (publishableKey: string) => ZuoraInstance;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ZuoraInstance = {
|
|
11
|
+
createPaymentForm: (config: ZuoraPaymentFormConfig) => Promise<ZuoraPaymentForm>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ZuoraPaymentFormConfig = {
|
|
15
|
+
locale?: string;
|
|
16
|
+
region?: string;
|
|
17
|
+
currency?: string;
|
|
18
|
+
amount?: string;
|
|
19
|
+
profile?: string;
|
|
20
|
+
createPaymentSession: () => Promise<string>;
|
|
21
|
+
onComplete: (result: ZuoraPaymentResult) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ZuoraPaymentForm = {
|
|
25
|
+
mount: (containerId: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ZuoraPaymentResult = {
|
|
29
|
+
success: boolean;
|
|
30
|
+
paymentMethodId?: string;
|
|
31
|
+
paymentId?: string;
|
|
32
|
+
error?: {
|
|
33
|
+
type: string;
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function isZuoraLibraryAvailable(): boolean {
|
|
40
|
+
return !!window.Zuora;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadZuoraLibrary() {
|
|
44
|
+
const script = document.createElement('script');
|
|
45
|
+
script.type = 'text/javascript';
|
|
46
|
+
script.src = 'https://js.zuora.com/payment/v3/zuora.js';
|
|
47
|
+
script.async = true;
|
|
48
|
+
|
|
49
|
+
script.onerror = () => {
|
|
50
|
+
throw new Error('Failed to load Zuora library');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
document.head.appendChild(script);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createZuoraPaymentFormConfig(
|
|
57
|
+
createPaymentSession: () => Promise<string>,
|
|
58
|
+
onComplete: (result: ZuoraPaymentResult) => Promise<void>,
|
|
59
|
+
currency: Currency,
|
|
60
|
+
billingCountryCode?: string,
|
|
61
|
+
): ZuoraPaymentFormConfig {
|
|
62
|
+
const AUTH_AMOUNT = '1';
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
locale: billingCountryCode || 'en',
|
|
66
|
+
region: billingCountryCode?.toUpperCase() || 'US',
|
|
67
|
+
currency,
|
|
68
|
+
amount: AUTH_AMOUNT,
|
|
69
|
+
createPaymentSession,
|
|
70
|
+
onComplete,
|
|
71
|
+
};
|
|
72
|
+
}
|