@akinon/pz-google-pay 1.116.0-rc.11
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/.prettierrc +13 -0
- package/CHANGELOG.md +7 -0
- package/README.md +81 -0
- package/package.json +19 -0
- package/src/hooks/use-google-pay.tsx +110 -0
- package/src/index.tsx +1 -0
- package/src/views/index.tsx +167 -0
package/.prettierrc
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bracketSameLine": false,
|
|
3
|
+
"tabWidth": 2,
|
|
4
|
+
"singleQuote": true,
|
|
5
|
+
"jsxSingleQuote": false,
|
|
6
|
+
"bracketSpacing": true,
|
|
7
|
+
"semi": true,
|
|
8
|
+
"useTabs": false,
|
|
9
|
+
"arrowParens": "always",
|
|
10
|
+
"endOfLine": "lf",
|
|
11
|
+
"proseWrap": "never",
|
|
12
|
+
"trailingComma": "none"
|
|
13
|
+
}
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Project Zero - Google Pay Plugin
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
You can use the following command to install the extension with the latest plugins:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @akinon/projectzero@latest --plugins
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
# Adding Google Pay Payment Method to Checkout
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Create Google Pay File
|
|
16
|
+
|
|
17
|
+
**views/checkout/steps/payment/options/google-pay.tsx**
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
21
|
+
|
|
22
|
+
export default function GooglePay() {
|
|
23
|
+
return <PluginModule component={Component.GooglePay} />;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 2. Update Payment Step
|
|
28
|
+
|
|
29
|
+
**views/checkout/steps/payment/index.tsx**
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import GooglePay from './options/google-pay';
|
|
33
|
+
|
|
34
|
+
export const PaymentOptionViews: Array<CheckoutPaymentOption> = [
|
|
35
|
+
{
|
|
36
|
+
slug: 'payment-option-slug',
|
|
37
|
+
view: GooglePay
|
|
38
|
+
}
|
|
39
|
+
// Other payment methods can be added here
|
|
40
|
+
];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
### customUIRender
|
|
46
|
+
|
|
47
|
+
A function to fully customize the Google Pay component's appearance. If provided, the default UI will not be shown; instead, the JSX returned by this function will be rendered.
|
|
48
|
+
|
|
49
|
+
| Property | Type | Description |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `customUIRender` | `React.ReactNode` | Optional function to fully customize the Google Pay component. |
|
|
52
|
+
|
|
53
|
+
#### Example
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<PluginModule
|
|
57
|
+
component={Component.GooglePay}
|
|
58
|
+
props={{
|
|
59
|
+
customUIRender: ({ handlePaymentRequest, paymentErrors }) => {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex flex-col gap-4">
|
|
62
|
+
<button
|
|
63
|
+
onClick={handlePaymentRequest}
|
|
64
|
+
className="group relative w-full flex justify-center items-center gap-3 bg-black hover:bg-gray-800 text-white font-medium py-4 px-6 rounded-lg transition-all duration-200 active:scale-[0.98]"
|
|
65
|
+
>
|
|
66
|
+
<span className="text-lg">Pay with Google Pay</span>
|
|
67
|
+
|
|
68
|
+
<span className="absolute right-4 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
69
|
+
→
|
|
70
|
+
</span>
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
{paymentErrors && (
|
|
74
|
+
<p className="text-sm text-red-700 mt-1">{paymentErrors}</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akinon/pz-google-pay",
|
|
3
|
+
"version": "1.116.0-rc.11",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^18.0.0",
|
|
8
|
+
"react-dom": "^18.0.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/node": "^18.7.8",
|
|
12
|
+
"@types/react": "^18.0.17",
|
|
13
|
+
"@types/react-dom": "^18.0.6",
|
|
14
|
+
"prettier": "^3.0.3",
|
|
15
|
+
"react": "^18.2.0",
|
|
16
|
+
"react-dom": "^18.2.0",
|
|
17
|
+
"typescript": "^5.2.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { checkoutApi } from '@akinon/next/data/client/checkout';
|
|
2
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
3
|
+
import { getCookie } from '@akinon/next/utils';
|
|
4
|
+
import { getUrlPathWithLocale } from '@akinon/next/utils/localization';
|
|
5
|
+
import { RootState } from '@theme/redux/store';
|
|
6
|
+
import { useState, useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
export default function useGooglePay() {
|
|
9
|
+
const dispatch = useAppDispatch();
|
|
10
|
+
const { preOrder } = useAppSelector((state: RootState) => state.checkout);
|
|
11
|
+
const [errors, setErrors] = useState<any>(null);
|
|
12
|
+
|
|
13
|
+
const paymentErrors = useMemo(() => {
|
|
14
|
+
if (typeof errors === 'string') return errors;
|
|
15
|
+
if (Array.isArray(errors)) return errors.join(', ');
|
|
16
|
+
if (typeof errors === 'object')
|
|
17
|
+
return Object.values(errors ?? {}).join(', ');
|
|
18
|
+
return null;
|
|
19
|
+
}, [errors]);
|
|
20
|
+
|
|
21
|
+
const redirectToThankYouPage = (contextList: any[]) => {
|
|
22
|
+
const thankYouPageContext = contextList.find(
|
|
23
|
+
(c: any) => c.page_name === 'ThankYouPage'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (thankYouPageContext) {
|
|
27
|
+
const redirectUrl =
|
|
28
|
+
thankYouPageContext.page_context.context_data.redirect_url;
|
|
29
|
+
const redirectUrlWithLocale = `${window.location.origin}${getUrlPathWithLocale(
|
|
30
|
+
redirectUrl,
|
|
31
|
+
getCookie('pz-locale')
|
|
32
|
+
)}`;
|
|
33
|
+
window.location.href = redirectUrlWithLocale;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleGooglePaySuccess = async (paymentData: any) => {
|
|
38
|
+
setErrors(null);
|
|
39
|
+
|
|
40
|
+
const rawTokenString = paymentData.paymentMethodData.tokenizationData.token;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const walletSelectionPageResponse = await dispatch(
|
|
44
|
+
checkoutApi.endpoints.setWalletSelectionPage.initiate({
|
|
45
|
+
payment_option: preOrder.payment_option?.pk
|
|
46
|
+
})
|
|
47
|
+
).unwrap();
|
|
48
|
+
|
|
49
|
+
const walletPaymentPageContext =
|
|
50
|
+
walletSelectionPageResponse.context_list.find(
|
|
51
|
+
(c: any) => c.page_name === 'WalletPaymentPage'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!walletPaymentPageContext) {
|
|
55
|
+
setErrors('Error: Could not proceed to payment step (Context error).');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const walletPaymentPageResponse = await dispatch(
|
|
60
|
+
checkoutApi.endpoints.setWalletPaymentPage.initiate({
|
|
61
|
+
payment_token: JSON.stringify(rawTokenString)
|
|
62
|
+
})
|
|
63
|
+
).unwrap();
|
|
64
|
+
|
|
65
|
+
if (walletPaymentPageResponse.errors) {
|
|
66
|
+
setErrors(walletPaymentPageResponse.errors);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const redirectPageContext = walletPaymentPageResponse.context_list.find(
|
|
71
|
+
(c: any) => c.page_name === 'WalletRedirectCompletePage'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (redirectPageContext) {
|
|
75
|
+
window.location.href = redirectPageContext.page_context.redirect_url;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const completePageContext = walletPaymentPageResponse.context_list.find(
|
|
80
|
+
(c: any) => c.page_name === 'WalletCompletePage'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (completePageContext) {
|
|
84
|
+
const paymentCompleteResponse = await dispatch(
|
|
85
|
+
checkoutApi.endpoints.setWalletCompletePage.initiate({
|
|
86
|
+
success: true
|
|
87
|
+
})
|
|
88
|
+
).unwrap();
|
|
89
|
+
|
|
90
|
+
if (paymentCompleteResponse.errors) {
|
|
91
|
+
setErrors(paymentCompleteResponse.errors);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
redirectToThankYouPage(paymentCompleteResponse.context_list);
|
|
96
|
+
} else {
|
|
97
|
+
setErrors('Payment could not be completed.');
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Google Pay Flow Error:', error);
|
|
101
|
+
setErrors('An unexpected error occurred during the process.');
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
paymentErrors,
|
|
107
|
+
setPaymentErrors: setErrors,
|
|
108
|
+
handleGooglePaySuccess
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './views';
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
+
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
5
|
+
import { RootState } from '@theme/redux/store';
|
|
6
|
+
import Script from 'next/script';
|
|
7
|
+
import useGooglePay from '../hooks/use-google-pay';
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
google: any;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GooglePayProps {
|
|
16
|
+
customUIRender?: (props: {
|
|
17
|
+
handlePaymentRequest: () => void;
|
|
18
|
+
paymentErrors?: string | null;
|
|
19
|
+
}) => JSX.Element | null;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const GooglePay = ({ customUIRender, className }: GooglePayProps) => {
|
|
24
|
+
const { walletPaymentData } = useAppSelector(
|
|
25
|
+
(state: RootState) => state.checkout
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const { handleGooglePaySuccess, paymentErrors } = useGooglePay();
|
|
29
|
+
|
|
30
|
+
const buttonContainerRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const [isScriptLoaded, setIsScriptLoaded] = useState(false);
|
|
32
|
+
const [canMakePayment, setCanMakePayment] = useState(false);
|
|
33
|
+
const paymentsClientRef = useRef<any>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (window.google?.payments) {
|
|
37
|
+
setIsScriptLoaded(true);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const getPaymentsClient = useCallback(() => {
|
|
42
|
+
if (!walletPaymentData?.data || !window.google?.payments) return null;
|
|
43
|
+
|
|
44
|
+
if (!paymentsClientRef.current) {
|
|
45
|
+
paymentsClientRef.current = new window.google.payments.api.PaymentsClient(
|
|
46
|
+
{
|
|
47
|
+
environment: walletPaymentData.data.environment
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return paymentsClientRef.current;
|
|
52
|
+
}, [walletPaymentData]);
|
|
53
|
+
|
|
54
|
+
const handlePaymentRequest = useCallback(() => {
|
|
55
|
+
if (!walletPaymentData?.data) return;
|
|
56
|
+
|
|
57
|
+
const client = getPaymentsClient();
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
apiVersion,
|
|
61
|
+
apiVersionMinor,
|
|
62
|
+
allowedPaymentMethods,
|
|
63
|
+
transactionInfo,
|
|
64
|
+
merchantInfo
|
|
65
|
+
} = walletPaymentData.data;
|
|
66
|
+
|
|
67
|
+
const paymentDataRequest = {
|
|
68
|
+
apiVersion,
|
|
69
|
+
apiVersionMinor,
|
|
70
|
+
allowedPaymentMethods,
|
|
71
|
+
transactionInfo,
|
|
72
|
+
...(merchantInfo && { merchantInfo })
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
client
|
|
76
|
+
.loadPaymentData(paymentDataRequest)
|
|
77
|
+
.then((paymentData: any) => {
|
|
78
|
+
handleGooglePaySuccess(paymentData);
|
|
79
|
+
})
|
|
80
|
+
.catch((err: any) => {
|
|
81
|
+
console.error('Err', err);
|
|
82
|
+
});
|
|
83
|
+
}, [walletPaymentData, getPaymentsClient, handleGooglePaySuccess]);
|
|
84
|
+
|
|
85
|
+
const addGooglePayButton = () => {
|
|
86
|
+
if (customUIRender) return;
|
|
87
|
+
|
|
88
|
+
const client = getPaymentsClient();
|
|
89
|
+
const button = client.createButton({
|
|
90
|
+
onClick: handlePaymentRequest,
|
|
91
|
+
buttonColor: 'black',
|
|
92
|
+
buttonType: 'buy'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (buttonContainerRef.current) {
|
|
96
|
+
buttonContainerRef.current.innerHTML = '';
|
|
97
|
+
buttonContainerRef.current.appendChild(button);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (isScriptLoaded && walletPaymentData?.data) {
|
|
103
|
+
const client = getPaymentsClient();
|
|
104
|
+
|
|
105
|
+
if (!client) return;
|
|
106
|
+
|
|
107
|
+
const { apiVersion, apiVersionMinor, allowedPaymentMethods } =
|
|
108
|
+
walletPaymentData.data;
|
|
109
|
+
|
|
110
|
+
client
|
|
111
|
+
.isReadyToPay({
|
|
112
|
+
apiVersion,
|
|
113
|
+
apiVersionMinor,
|
|
114
|
+
allowedPaymentMethods
|
|
115
|
+
})
|
|
116
|
+
.then((response: any) => {
|
|
117
|
+
if (response.result) setCanMakePayment(true);
|
|
118
|
+
})
|
|
119
|
+
.catch((err: any) => console.error('Error:', err));
|
|
120
|
+
}
|
|
121
|
+
}, [isScriptLoaded, walletPaymentData, getPaymentsClient]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (
|
|
125
|
+
canMakePayment &&
|
|
126
|
+
!customUIRender &&
|
|
127
|
+
buttonContainerRef.current &&
|
|
128
|
+
paymentsClientRef.current
|
|
129
|
+
) {
|
|
130
|
+
addGooglePayButton();
|
|
131
|
+
}
|
|
132
|
+
}, [canMakePayment, customUIRender, handlePaymentRequest]);
|
|
133
|
+
|
|
134
|
+
if (!walletPaymentData) return null;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className={className}>
|
|
138
|
+
<Script
|
|
139
|
+
src="https://pay.google.com/gp/p/js/pay.js"
|
|
140
|
+
onLoad={() => setIsScriptLoaded(true)}
|
|
141
|
+
strategy="afterInteractive"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
{canMakePayment && (
|
|
145
|
+
<>
|
|
146
|
+
{customUIRender ? (
|
|
147
|
+
customUIRender({
|
|
148
|
+
handlePaymentRequest,
|
|
149
|
+
paymentErrors
|
|
150
|
+
})
|
|
151
|
+
) : (
|
|
152
|
+
<div className="w-full">
|
|
153
|
+
{paymentErrors && (
|
|
154
|
+
<div className="text-red-600 mb-2 text-sm font-medium">
|
|
155
|
+
{paymentErrors}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
<div ref={buttonContainerRef} style={{ width: '100%' }} />
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export default GooglePay;
|