@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 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
@@ -0,0 +1,7 @@
1
+ # @akinon/pz-google-pay
2
+
3
+ ## 1.116.0-rc.11
4
+
5
+ ### Minor Changes
6
+
7
+ - 726491df: ZERO-3988: Add google pay integration
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;