Source: transpayrent-google-pay-helper.js

/**
 * @copyright Transpayrent ApS 2022
 * @license Common Transpayrent API license
 * @version 1.0.1
 * 
 * @class
 * @classdesc   The Transpayrent Helper SDK for Google Pay<sup>TM</sup> abstracts the [Google Pay Javascript Client]{@link https://developers.google.com/pay/api/web/reference/client}
 * into 2 simple methods:
 *  * [Display the "Buy with Google Pay" button]{@link TranspayrentGooglePayHelper#displayPaymentButton}
 *  * [Display the payment sheet for Google Pay]{@link TranspayrentGooglePayHelper#displayPaymentSheet} allowing the consumer to select a payment card from Google Pay
 * 
 * The Transpayrent SDK handles the secure communication with Transpayrent's Payment Gateway and transmits the Google encrypted payment data and transaction data provided by the Transpayrent Helper SDK for Google Pay.  
 * Generally direct interaction with the helper SDK is unnecessary and the equivalent methods from the [Transpayrent SDK]{@link Transpayrent} called be instead.  
 * Please refer to the following resources for additional details in regard to implementing Google Pay:
 *  * [Google Pay Web developer documentation]{@link https://developers.google.com/pay/api/web/}
 *  * [Google Pay Web integration checklist]{@link https://developers.google.com/pay/api/web/guides/test-and-deploy/integration-checklist}
 *  * [Google Pay Web Brand Guidelines]{@link https://developers.google.com/pay/api/web/guides/brand-guidelines}
 * 
 * Configuration of Google Pay is done dynamically using data provided by the Transpayrent Payment Gateway to automatically handle the following scenarios:
 *  * Strong consumer authentication *(SCA)* using 3D Secure for [PAN_ONLY]{@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters} authorizations if required
 *  * Passing the correct `gateway` and `gatewayMerchantId` to Google Pay
 *  * Dynamically enable supported card networks *(MasterCard, VISA etc.)* for Google Pay based on the configured country
 * 
 * @see [Transpayrent]{@link Transpayrent}
 * @see {@link Transpayrent#displayPaymentButton}
 * @see {@link Transpayrent#displayPaymentSheet}
 * 
 * @constructor
 * @description Creates a new instance of the Transpayrent SDK, which uses the Transpayrent Helper SDK for Google Pay internally to abstract the Google Pay Javascript Client.
 * @example <caption>Instantiate the Transpayrent SDK and display the "Buy with Google Pay" button</caption>
 *  <script src="https://storage.googleapis.com/static.[ENVIRONMENT].transpayrent.cloud/v1/swagger-client.js"></script>
 *  <script src="https://storage.googleapis.com/static.[ENVIRONMENT].transpayrent.cloud/v1/transpayrent.js"></script>
 *  <script async src="https://pay.google.com/gp/p/js/pay.js" onload="onGooglePayLoaded()"></script>
 *  <script>
 *      // SDK configuration
 *      var transpayrentConfig = {
 *          merchantId: [UNIQUE MERCHANT ID ASSIGNED BY TRANSPAYRENT],
 *          sessionId: [ID IN THE RESPONSE FROM "Create Payment Session"],
 *          accessToken: '[x-transpayrent-access-token HTTP HEADER IN THE RESPONSE FROM "Create Payment Session"]'
 *      };
 *      var url = 'https://generator.[ENVIRONMENT].transpayrent.cloud/v1/'+ transpayrentConfig.merchantId +'/system/PAYMENT_GATEWAY/sdk/CLIENT';
 * 
 *      // Instantiate SDK
 *      var sdk = new Transpayrent(url, transpayrentConfig);
 * 
 *      // List of payment methods supported by Google Pay as returned in the response from "Create Payment Session"
 *      const paymentMethodIds = [ 103,   // VISA Dankort
 *                                 107,   // Maestro
 *                                 108,   // MasterCard
 *                                 109,   // VISA
 *                                 110    // VISA Electron
 *                               ];
 *      // Display the Payment Sheet for Google Pay 
 *      var config = { buttonColor : 'black',
 *                     buttonType : 'buy',
 *                     onClick : () => {
 *                          const paymentMethodId = 203;                                       // Google Pay
 *                          var paymentSheetDetails = { merchant_id : transpayrentConfig.merchantId,
 *                                                      merchant_name : '[MY MERCHANT]',
 *                                                      country : 208,                         // Denmark
 *                                                      payment_method_ids : paymentMethodIds,
 *                                                      amount : { currency : 208,             // DKK
 *                                                                 value : 1000 },
 *                                                      save : false };
 *                           sdk.displayPaymentSheet(paymentMethodId, paymentSheetDetails)
 *                              .then(token => completePayment(paymentMethodId) )
 *                              .catch(reason => {
 *                                  document.getElementById('container').style.visibility = 'hidden';
 *                                  alert('API: '+ reason.api +' failed with HTTP Status Code: '+ reason.status +' and error: '+ reason.messages[0].message +'('+ reason.messages[0].code +')');
 *                              });
 *                   } };
 *      // Display the "Buy with Google Pay" button
 *      sdk.displayPaymentButton(203, document.getElementById('google-pay-button-container'), paymentMethodIds, config);
 *  </script>
 * 
 * @param {String} environment                  The Google Pay environment, either PRODUCTION or TEST, that will be used by the helper SDK
 * @param {GooglePayConfig} config              The base configuration for Google Pay.
 * @param {GooglePayTransaction} transaction    The details for the payment transaction that will be authorized using Google Pay
 */
 function TranspayrentGooglePayHelper(environment, config, transaction) {
    /**
     * The Google Pay environment that will be used by the helper SDK.
     * Will be either PRODUCTION or TEST.
     * 
     * @private
     * 
     * @type {String}
     */
    this._environment = environment;
    /**
     * Base configuration for Google Pay
     * 
     * @typedef {Object} GooglePayConfig
     * @property {Integer} merchantId       Transpayrent's unique ID for the merchant.
     * @property {String} merchantName      Transpayrent's unique ID for the payment session that was returned as the `id` property in the response from the "Create Payment Session" API.
     * @property {String} merchantOrigin    The merchant's domain name registered with Google Pay. Default's to the domain name obtained from `window.location.href`
     * @property {Element} container        The container element in which the constructed "Buy with Google Pay" button will be displayed
     * @property {Function} callback        The callback function accepts a single argument:  
     *                                          `event`: The event that triggered the callback, will be either `payment-initialization-initiated` or `payment-initialization-completed`
     */
    /**
     * The base configuration for Google Pay.
     *
     * @private
     *
     * @type {GooglePayConfig}
     */
    this._config = config;
    /**
     * The details for the payment transaction that will be authorized using Google Pay
     * 
     * @typedef {Object} GooglePayTransaction
     * @property {PAYMENT_METHOD_ID[]} paymentMethodIds     List of Payment IDs returned by the Transpayrent Gateway upon initializing the payment transaction
     * @property {String} country                           The country the payment transaction takes place in
     * @property {Amount} amount                            The payment transaction's amount that will be authorized
     * @property {Boolean} save                             Flag indicating whether the consumer has elected to securely store the card details that will be retrieved from Google Pay upon successful authorization
     */
    /**
     * The details for the payment transaction that will be authorized using Google Pay
     *
     * @private
     *
     * @type {GooglePayTransaction}
     */
    this._transaction = transaction;
    /**
     * An initialized google.payments.api.PaymentsClient object or null if not yet set
     *
     * @private
     *
     * @see {@link TranspayrentGooglePayHelper#getGooglePaymentsClient}
     */
     this._client = null;
    /**
     * Define the version of the Google Pay API referenced when creating your configuration
     * 
     * @private
     * @constant
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#PaymentDataRequest|apiVersion in PaymentDataRequest}
     */
    const baseRequest = {
        apiVersion: 2,
        apiVersionMinor: 0
    };
    /* ========== COMMON METHODS START ========== */
    /**
     * Return an active Google PaymentsClient or initialize the Google PaymentsClient
     * 
     * @private
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/client#PaymentsClient|PaymentsClient constructor}
     * 
     * @returns {google.payments.api.PaymentsClient} Google Pay API client
     */
    TranspayrentGooglePayHelper.prototype.getGooglePaymentsClient = function () {
        if (this._client === null) {
            this._client = new google.payments.api.PaymentsClient({environment: this._environment});
        }
        return this._client;
    }
    /**
     * @private
     * 
     * @param {PAYMENT_METHOD_ID[]} paymentMethodIds     List of Payment IDs returned by the Transpayrent Gateway upon initializing the payment transaction
     * @param {Boolean} save                             Flag indicating whether the consumer has elected to securely store the card details that will be retrieved from Google Pay upon successful authorization
     * @returns {Object} PaymentDataRequest fields
     */
    TranspayrentGooglePayHelper.prototype.getBaseCardPaymentMethods = function (paymentMethodIds, save) {
        /**
         * Describe your site's support for the CARD payment method and its required fields
         *
         * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
         */
        return { type: 'CARD',
                 parameters: {
                    allowedAuthMethods: save ? ["PAN_ONLY"] : ["PAN_ONLY", "CRYPTOGRAM_3DS"],
                    allowedCardNetworks: [...new Set(Array.from(paymentMethodIds, (id) => {
                        var country = '';
                        try {
                            country = this._transaction.country.toUpperCase();
                        }
                        catch (ignore) { /* Ignore as this._transaction may be null when displaying the "Buy with Google Pay" button */ }
                        switch (parseInt(id) ) {
                            case 107:   // Maestro
                                return 'BR' == country ? 'MAESTRO' : 'MASTERCARD';
                            case 108:   // MasterCard
                                return 'MASTERCARD';
                            case 103:   // VISA Dankort
                            case 109:   // VISA
                                return 'VISA';
                            case 110:   // VISA Electron
                                return 'BR' == country ? 'ELECTRON' : 'VISA';
                        }
                    }).filter(name => name) )]
            }
        };
    }
    /**
     * @private
     * 
     * @param {Integer} currency The ISO-4217 numeric code for the currency
     * @returns {Integer} The number of decimals for the specified currency 
     */
    TranspayrentGooglePayHelper.prototype.getDecimals = function (currency) {
        switch (currency) {
            case 108:   // BIF
            case 152:   // CLP
            case 262:   // DJF
            case 324:   // GNF
            case 352:   // ISK
            case 392:   // JPY
            case 174:   // KMF
            case 410:   // KRW
            case 600:   // PYG
            case 646:   // RWF
            case 800:   // UGX
            case 940:   // UYI
            case 704:   // VND
            case 548:   // VUV
            case 950:   // XAF
            case 952:   // XOF
            case 953:   // XPF
                return 0;
            case 48:	// BHD
            case 368:	// IQD
            case 400:	// JOD
            case 414:	// KWD
            case 434:	// LYD
            case 512:	// OMR
            case 788:	// TND
                return 3;
            case 990:    // CLF
            case 927:    // UYW
                return 4;
            default:
                return 2;
        }
    }
    /* ========== COMMON METHODS END ========== */

    /* ========== DISPLAY PAYMENT BUTTON START ========== */
    /**
     * Configure your site's support for payment methods supported by the Google Pay API.
     *
     * Each member of allowedPaymentMethods should contain only the required fields, allowing reuse of this base request
     * when determining a viewer's ability to pay and later requesting a supported payment method.
     * 
     * @private
     *
     * @param {PAYMENT_METHOD_ID[]} paymentMethodIds    List of Payment IDs returned by the Transpayrent Gateway upon initializing the payment transaction
     * @returns {Object} Google Pay API version, payment methods supported by the site
     */
    TranspayrentGooglePayHelper.prototype.getGoogleIsReadyToPayRequest = function (paymentMethodIds) {
        return Object.assign(
            {},
            baseRequest,
            {
                allowedPaymentMethods: [ this.getBaseCardPaymentMethods(paymentMethodIds, false) ]
            }
        );
    }
    /**
     * Add a Google Pay purchase button alongside an existing checkout button.
     * 
     * @private
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#ButtonOptions|Button options}
     * @see {@link https://developers.google.com/pay/api/web/guides/brand-guidelines|Google Pay brand guidelines}
     * 
     * @param {*} container 
     * @param {*} config 
     * @param  {...any} args 
     */
    TranspayrentGooglePayHelper.prototype.addGooglePayButton = function (container, config, ...args) {
        const paymentsClient = this.getGooglePaymentsClient();
        const callback = config.onClick;
        config.onClick = () => { callback(args); };
        const button = paymentsClient.createButton(config);
        container.appendChild(button);
    }
    /**
     * Prefetch payment data to improve performance.
     * 
     * @private
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/client#prefetchPaymentData|prefetchPaymentData()}
     * 
     * @param {PAYMENT_METHOD_ID[]} paymentMethodIds     List of Payment IDs returned by the Transpayrent Gateway upon initializing the payment transaction
     */
    TranspayrentGooglePayHelper.prototype.prefetchGooglePaymentData = function (paymentMethodIds) {
        const paymentDataRequest = Object.assign({}, baseRequest);
        paymentDataRequest.allowedPaymentMethods = [ this.getBaseCardPaymentMethods(paymentMethodIds, false) ];
        // transactionInfo must be set but does not affect cache
        paymentDataRequest.transactionInfo = {
            totalPriceStatus: 'NOT_CURRENTLY_KNOWN',
            currencyCode: 'USD'
        };
        const paymentsClient = this.getGooglePaymentsClient();
        paymentsClient.prefetchPaymentData(paymentDataRequest);
    }
    /**
     * Initialize Google PaymentsClient after Google-hosted JavaScript has loaded and
     * display the Google Pay payment button after confirmation of the viewer's ability to pay.  
     * It is **strongly recommended** to use {@link Transpayrent#displayPaymentButton}.
     * 
     * @public
     * 
     * @see Transpayrent#displayPaymentButton
     * 
     * @param {*} container 
     * @param {PAYMENT_METHOD_ID[]} paymentMethodIds     List of Payment IDs returned by the Transpayrent Gateway upon initializing the payment transaction
     * @param {*} config 
     * @param  {...any} args 
     */
    TranspayrentGooglePayHelper.prototype.displayPaymentButton = function (container, paymentMethodIds, config, ...args) {
        const helper = this;
        const paymentsClient = this.getGooglePaymentsClient();
        paymentsClient.isReadyToPay(this.getGoogleIsReadyToPayRequest(paymentMethodIds) )
            .then(function(response) {
                if (response.result) {
                    helper.addGooglePayButton(container, config, args);
                    helper.prefetchGooglePaymentData(paymentMethodIds);
                }
            })
            .catch(function(err) {
                // show error in developer console for debugging
                console.error(err);
                helper._rejecter(err);
            });
    }
    /* ========== DISPLAY PAYMENT BUTTON END ========== */

    /* ========== DISPLAY PAYMENT SHEET START ========== */
    /**
     * Configure support for the Google Pay API.
     * 
     * @private
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#PaymentDataRequest|PaymentDataRequest}
     * 
     * @returns {Object} PaymentDataRequest fields
     */
     TranspayrentGooglePayHelper.prototype.getGooglePaymentDataRequest = function () {
        const paymentDataRequest = Object.assign({}, baseRequest);
        
        /**
         * Describe your site's support for the CARD payment method including optional fields
         *
         * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#CardParameters|CardParameters}
         */
        paymentDataRequest.allowedPaymentMethods = [Object.assign(
                                                        {},
                                                        this.getBaseCardPaymentMethods(this._transaction.paymentMethodIds, this._transaction.save),
                                                        {
                                                            tokenizationSpecification: {
                                                                type: 'PAYMENT_GATEWAY',
                                                                parameters: {
                                                                    'gateway': 'transpayrent',
                                                                    'gatewayMerchantId': this._config.merchantId.toString()
                                                                }
                                                            }
                                                        }
                                                    )];
        /**
         * Provide Google Pay API with a payment amount, currency, and amount status
         *
         * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#TransactionInfo|TransactionInfo}
         * @returns {Object} transaction info, suitable for use as transactionInfo property of PaymentDataRequest
         */
        paymentDataRequest.transactionInfo = {
            countryCode: this._transaction.country,
            currencyCode: this._transaction.amount.currency,
            totalPriceStatus: 'FINAL',
            totalPrice: Number(this._transaction.amount.value / Math.pow(10, this.getDecimals(this._transaction.amount.currency) ) ).toFixed(2)
        };
        paymentDataRequest.merchantInfo = {
            merchantId: this._config.externalMerchantId.toString(),
            merchantName: this._config.merchantName,
            merchantOrigin: this._config.merchantOrigin,
        };
        
        return paymentDataRequest;
    }
    /**
     * Show Google Pay payment sheet when Google Pay payment button is clicked.  
     * It is **strongly recommended** to use {@link Transpayrent#displayPaymentSheet}.
     * 
     * @public
     * 
     * @see {@link https://developers.google.com/pay/api/web/reference/response-objects#PaymentData|PaymentData object reference}
     * @see Transpayrent#displayPaymentSheet
     * 
     * @param {Function} resolver   The [resolver function]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve} for a promise
     * @param {Function} rejecter   The [rejecter function]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject} for a promise
     */
     TranspayrentGooglePayHelper.prototype.displayPaymentSheet = function (sdk, resolver, rejecter) {
        const paymentDataRequest = this.getGooglePaymentDataRequest();
        const paymentsClient = this.getGooglePaymentsClient();
        paymentsClient.loadPaymentData(paymentDataRequest)
            .then(function(paymentData) {
                resolver(btoa(paymentData.paymentMethodData.tokenizationData.token) );
            })
            .catch(function(err) {
                rejecter(err);
            });
    }
    /* ========== DISPLAY PAYMENT SHEET START ========== */
}