|
| 1 | +/** |
| 2 | + * Copyright 2024 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +//============================================================================= |
| 18 | +// Configuration |
| 19 | +//============================================================================= |
| 20 | + |
| 21 | +// The DOM element that the Google Pay button will be rendered into |
| 22 | +const GPAY_BUTTON_CONTAINER_ID = 'gpay-container'; |
| 23 | + |
| 24 | +// Update the `merchantId` and `merchantName` properties with your own values. |
| 25 | +// These fields are optional when the environment is `TEST`. |
| 26 | +// Get your merchant Id at https://goo.gle/3Cmv497 |
| 27 | +const merchantInfo = { |
| 28 | + merchantId: '12345678901234567890', |
| 29 | + merchantName: 'Example Merchant', |
| 30 | +}; |
| 31 | + |
| 32 | +/** |
| 33 | + * This is the base configuration for all Google Pay requests. This |
| 34 | + * configuration will be cloned, modified, and used for all Google Pay requests. |
| 35 | + * |
| 36 | + * @see {@link https://developers.google.com/pay/api/web/guides/test-and-deploy/integration-checklist} |
| 37 | + * @see {@link https://developers.google.com/pay/api/web/reference/request-objects} |
| 38 | + * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#gateway} |
| 39 | + * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#MerchantInfo} |
| 40 | + */ |
| 41 | +const baseGooglePayRequest = { |
| 42 | + apiVersion: 2, |
| 43 | + apiVersionMinor: 0, |
| 44 | + allowedPaymentMethods: [ |
| 45 | + { |
| 46 | + type: 'CARD', |
| 47 | + parameters: { |
| 48 | + allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], |
| 49 | + allowedCardNetworks: ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCARD', 'VISA'], |
| 50 | + }, |
| 51 | + tokenizationSpecification: { |
| 52 | + type: 'PAYMENT_GATEWAY', |
| 53 | + parameters: { |
| 54 | + gateway: 'example', |
| 55 | + gatewayMerchantId: 'exampleGatewayMerchantId', |
| 56 | + }, |
| 57 | + }, |
| 58 | + }, |
| 59 | + ], |
| 60 | + merchantInfo, |
| 61 | +}; |
| 62 | + |
| 63 | +// Prevent accidental edits to the base configuration. Mutations will be |
| 64 | +// handled by cloning the config using deepCopy() and modifying the copy. |
| 65 | +Object.freeze(baseGooglePayRequest); |
| 66 | + |
| 67 | +//============================================================================= |
| 68 | +// Google payments client singleton |
| 69 | +//============================================================================= |
| 70 | + |
| 71 | +/** |
| 72 | + * A variable to store the Google Payments Client instance. |
| 73 | + * Initialized to null to indicate it hasn't been created yet. |
| 74 | + */ |
| 75 | +let paymentsClient = null; |
| 76 | + |
| 77 | +/** |
| 78 | + * Gets an instance of the Google Payments Client. |
| 79 | + * |
| 80 | + * This function ensures that only one instance of the Google Payments Client |
| 81 | + * is created and reused throughout the application. It lazily initializes |
| 82 | + * the client if it hasn't been created yet. |
| 83 | + * |
| 84 | + * @see {@link https://developers.google.com/pay/api/web/reference/client#PaymentsClient} |
| 85 | + * @return {google.payments.api.PaymentsClient} Google Payments Client instance. |
| 86 | + */ |
| 87 | +function getGooglePaymentsClient() { |
| 88 | + // Check if the paymentsClient has already been initialized. |
| 89 | + if (paymentsClient === null) { |
| 90 | + // If not, create a new instance of the Google Payments Client. |
| 91 | + paymentsClient = new google.payments.api.PaymentsClient({ |
| 92 | + // Set the environment for the client ('TEST' or 'PRODUCTION'). |
| 93 | + // `TEST` is default. |
| 94 | + environment: 'TEST', |
| 95 | + // Add the merchant information (optional) |
| 96 | + merchantInfo, |
| 97 | + paymentDataCallbacks: { |
| 98 | + onPaymentAuthorized: onPaymentAuthorized, |
| 99 | + onPaymentDataChanged: onPaymentDataChanged, |
| 100 | + }, |
| 101 | + }); |
| 102 | + } |
| 103 | + |
| 104 | + return paymentsClient; |
| 105 | +} |
| 106 | + |
| 107 | +//============================================================================= |
| 108 | +// Helpers |
| 109 | +//============================================================================= |
| 110 | + |
| 111 | +/** |
| 112 | + * Creates a deep copy of an object. |
| 113 | + * |
| 114 | + * This function uses JSON serialization and deserialization to create a deep |
| 115 | + * copy of the provided object. It's a convenient way to clone objects without |
| 116 | + * worrying about shared references. |
| 117 | + * |
| 118 | + * @param {Object} obj - The object to be copied. |
| 119 | + * @returns {Object} A deep copy of the original object. |
| 120 | + */ |
| 121 | +const deepCopy = obj => JSON.parse(JSON.stringify(obj)); |
| 122 | + |
| 123 | +/** |
| 124 | + * Renders the Google Pay button to the DOM. |
| 125 | + * |
| 126 | + * This function creates a Google Pay button using the Google Pay API and adds |
| 127 | + * it to the container element specified by `GPAY_BUTTON_CONTAINER_ID`. |
| 128 | + * When clicked, button triggers the `onGooglePaymentButtonClicked` handler. |
| 129 | + * |
| 130 | + * @see {@link https://developers.google.com/pay/api/web/reference/client#createButton} |
| 131 | + * @returns {void} |
| 132 | + */ |
| 133 | +function renderGooglePayButton() { |
| 134 | + // Create a Google Pay button using the PaymentsClient. |
| 135 | + const button = getGooglePaymentsClient().createButton({ |
| 136 | + // Set the click handler for the button to the onGooglePaymentButtonClicked |
| 137 | + onClick: onGooglePaymentButtonClicked, |
| 138 | + // Set the allowed payment methods for the button. |
| 139 | + allowedPaymentMethods: baseGooglePayRequest.allowedPaymentMethods, |
| 140 | + }); |
| 141 | + // Add the Google Pay button to the container element on the page. |
| 142 | + document.getElementById(GPAY_BUTTON_CONTAINER_ID).appendChild(button); |
| 143 | +} |
| 144 | + |
| 145 | +/** |
| 146 | + * Prefetches Google Pay payment data to improve the payment flow performance. |
| 147 | + * |
| 148 | + * This function creates a payment data request object and uses the Google Pay client |
| 149 | + * to prefetch payment data. This can help speed up the payment process when the user |
| 150 | + * actually initiates a transaction. The prefetched data is cached and can be used |
| 151 | + * for subsequent payment requests. |
| 152 | + * |
| 153 | + * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo} |
| 154 | + * @returns {void} |
| 155 | + */ |
| 156 | +function prefetchGooglePaymentData() { |
| 157 | + // Create a deep copy of the base Google Pay request object. |
| 158 | + // This ensures that the original request object is not modified. |
| 159 | + const req = deepCopy(baseGooglePayRequest); |
| 160 | + |
| 161 | + // Set the transactionInfo property on the request object. |
| 162 | + // This is required for prefetching, even though the values |
| 163 | + // are not used for caching. |
| 164 | + req.transactionInfo = { |
| 165 | + totalPriceStatus: 'NOT_CURRENTLY_KNOWN', |
| 166 | + currencyCode: 'USD', |
| 167 | + }; |
| 168 | + |
| 169 | + // Get an instance of the Google Pay client and use it to |
| 170 | + // prefetch the payment data. |
| 171 | + getGooglePaymentsClient().prefetchPaymentData(req); |
| 172 | +} |
| 173 | + |
| 174 | +//============================================================================= |
| 175 | +// Event Handlers |
| 176 | +//============================================================================= |
| 177 | + |
| 178 | +/** |
| 179 | + * Google Pay API loaded handler |
| 180 | + * |
| 181 | + * This function will be called by the script tag in index.html when the pay.js |
| 182 | + * script has finished loading. Once the script is loaded, it will first check |
| 183 | + * to see if the consumer is ready to pay with Google Pay. If they are ready, |
| 184 | + * the next thing it does is add the Google Pay button to the page. Otherwise, |
| 185 | + * it logs an error to the console. |
| 186 | + * |
| 187 | + * @see {@link https://developers.google.com/pay/api/web/reference/client#isReadyToPay} |
| 188 | + * @returns {void} |
| 189 | + */ |
| 190 | +function onGooglePayLoaded() { |
| 191 | + // Create a deep copy of the base Google Pay request object. |
| 192 | + // This ensures that any modifications made to the request object |
| 193 | + // do not affect the original base request. |
| 194 | + const req = deepCopy(baseGooglePayRequest); |
| 195 | + |
| 196 | + // Get an instance of the Google Payments Client. |
| 197 | + getGooglePaymentsClient() |
| 198 | + // Check if the user is ready to pay with Google Pay. |
| 199 | + .isReadyToPay(req) |
| 200 | + // Handle the response from the isReadyToPay() method. |
| 201 | + .then(function (res) { |
| 202 | + // If the user is ready to pay with Google Pay... |
| 203 | + if (res.result) { |
| 204 | + // Render the Google Pay button to the page. |
| 205 | + renderGooglePayButton(); |
| 206 | + // Prefetch the payment data to improve performance. |
| 207 | + prefetchGooglePaymentData(); |
| 208 | + } else { |
| 209 | + // If the user is not ready to pay with Google Pay, log |
| 210 | + // an error to the console. |
| 211 | + console.log('Google Pay is not ready for this user.'); |
| 212 | + } |
| 213 | + }) |
| 214 | + // Handle any errors that occur during the process. |
| 215 | + .catch(console.error); |
| 216 | +} |
| 217 | + |
| 218 | +/** |
| 219 | + * Google Pay button click handler |
| 220 | + * |
| 221 | + * @see {@link https://developers.google.com/pay/api/web/reference/client#loadPaymentData} |
| 222 | + * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentMethodTokenizationData} |
| 223 | + * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo} |
| 224 | + * @returns {void} |
| 225 | + */ |
| 226 | +function onGooglePaymentButtonClicked() { |
| 227 | + // Create a new request data object for this request |
| 228 | + const req = { |
| 229 | + ...deepCopy(baseGooglePayRequest), |
| 230 | + transactionInfo: { |
| 231 | + countryCode: 'US', |
| 232 | + currencyCode: 'USD', |
| 233 | + totalPriceStatus: 'FINAL', |
| 234 | + totalPrice: (Math.random() * 999 + 1).toFixed(2), |
| 235 | + }, |
| 236 | + callbackIntents: ['PAYMENT_AUTHORIZATION', 'SHIPPING_ADDRESS', 'SHIPPING_OPTION', 'OFFER'], |
| 237 | + shippingAddressRequired: true, |
| 238 | + shippingOptionRequired: true, |
| 239 | + shippingOptionParameters: { |
| 240 | + defaultSelectedOptionId: 'shipping-001', |
| 241 | + shippingOptions: [ |
| 242 | + { |
| 243 | + id: 'shipping-001', |
| 244 | + label: '$0.00: Free shipping', |
| 245 | + description: 'Free Shipping delivered in 5 business days.', |
| 246 | + }, |
| 247 | + { |
| 248 | + id: 'shipping-002', |
| 249 | + label: '$1.99: Standard shipping', |
| 250 | + description: 'Standard shipping delivered in 3 business days.', |
| 251 | + }, |
| 252 | + { |
| 253 | + id: 'shipping-003', |
| 254 | + label: '$1000: Express shipping', |
| 255 | + description: 'Express shipping delivered in 1 business day.', |
| 256 | + }, |
| 257 | + ], |
| 258 | + }, |
| 259 | + }; |
| 260 | + |
| 261 | + // Write the data to console for debugging |
| 262 | + console.log('onGooglePaymentButtonClicked', req); |
| 263 | + |
| 264 | + // Get an instance of the Google Payments Client. |
| 265 | + getGooglePaymentsClient() |
| 266 | + // Load the payment data in console for the transaction. |
| 267 | + .loadPaymentData(req) |
| 268 | + // If the payment is successful, process the payment |
| 269 | + .then(function (res) { |
| 270 | + // show returned data for debugging |
| 271 | + console.log(res); |
| 272 | + // @todo pass payment token to your gateway to process payment |
| 273 | + // @note DO NOT save the payment credentials for future transactions, |
| 274 | + // unless they're used for merchant-initiated transactions with user |
| 275 | + // consent in place. |
| 276 | + paymentToken = res.paymentMethodData.tokenizationData.token; |
| 277 | + }) |
| 278 | + // If there is an error, log it to the console. |
| 279 | + .catch(console.error); |
| 280 | +} |
| 281 | + |
| 282 | +function onPaymentAuthorized(paymentData) { |
| 283 | + return new Promise(function (resolve, reject) { |
| 284 | + // Write the data to console for debugging |
| 285 | + console.log('onPaymentAuthorized', paymentData); |
| 286 | + |
| 287 | + // Do something here to pass token to your gateway |
| 288 | + |
| 289 | + // To simulate the payment processing, there is a 70% chance of success |
| 290 | + const paymentAuthorizationResult = |
| 291 | + Math.random() > 0.3 |
| 292 | + ? { transactionState: 'SUCCESS' } |
| 293 | + : { |
| 294 | + transactionState: 'ERROR', |
| 295 | + error: { |
| 296 | + intent: 'PAYMENT_AUTHORIZATION', |
| 297 | + message: 'Insufficient funds', |
| 298 | + reason: 'PAYMENT_DATA_INVALID', |
| 299 | + }, |
| 300 | + }; |
| 301 | + |
| 302 | + resolve(paymentAuthorizationResult); |
| 303 | + }); |
| 304 | +} |
| 305 | + |
| 306 | +function onPaymentDataChanged(intermediatePaymentData) { |
| 307 | + return new Promise(function (resolve, reject) { |
| 308 | + let paymentDataRequestUpdate = {}; |
| 309 | + |
| 310 | + // Write the data to console for debugging |
| 311 | + console.log('onPaymentDataChanged', intermediatePaymentData); |
| 312 | + |
| 313 | + switch (intermediatePaymentData.callbackTrigger) { |
| 314 | + case 'INITIALIZE': |
| 315 | + // Handle initialize |
| 316 | + break; |
| 317 | + case 'SHIPPING_ADDRESS': |
| 318 | + // Read intermediatePaymentData.transactionInfo |
| 319 | + // Read intermediatePaymentData.shippingAddress |
| 320 | + // Update paymentDataRequestUpdate.newTransactionInfo |
| 321 | + break; |
| 322 | + case 'SHIPPING_OPTION': |
| 323 | + // Read intermediatePaymentData.transactionInfo |
| 324 | + // Read intermediatePaymentData.shippingOptionData |
| 325 | + // Update paymentDataRequestUpdate.newTransactionInfo |
| 326 | + // Update paymentDataRequestUpdate.newShippingOptionParameters |
| 327 | + break; |
| 328 | + case 'OFFER': |
| 329 | + // Read intermediatePaymentData.offerData |
| 330 | + // Read intermediatePaymentData.transactionInfo |
| 331 | + // Update paymentDataRequestUpdate.newTransactionInfo |
| 332 | + // Update paymentDataRequestUpdate.newOfferInfo |
| 333 | + break; |
| 334 | + default: |
| 335 | + // Update paymentDataRequestUpdate.error |
| 336 | + } |
| 337 | + |
| 338 | + resolve(paymentDataRequestUpdate); |
| 339 | + }); |
| 340 | +} |
0 commit comments