Source: transpayrent-apple-pay-helper.js

/**
 * @copyright Transpayrent ApS 2022
 * @license Common Transpayrent API license
 * @version 1.0.3
 * 
 * @class
 * @classdesc   The Transpayrent Helper SDK for Apple Pay<sup>TM</sup> abstracts the [Apple Pay Javascript Client]{@link https://developers.google.com/pay/api/web/reference/client}
 * into 2 simple methods:
 *  * [Display the "Buy with Apple Pay" button]{@link TranspayrentApplePayHelper#displayPaymentButton}
 *  * [Display the payment sheet for Apple Pay]{@link TranspayrentApplePayHelper#displayPaymentSheet} allowing the consumer to select a payment card from Apple 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 Apple 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 Apple Pay:
 *  * [Apple Pay Web developer documentation]{@link https://developers.google.com/pay/api/web/}
 *  * [Apple Pay Web integration checklist]{@link https://developers.google.com/pay/api/web/guides/test-and-deploy/integration-checklist}
 *  * [Apple Pay Web Brand Guidelines]{@link https://developers.google.com/pay/api/web/guides/brand-guidelines}
 * 
 * Configuration of Apple 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 Apple Pay
 *  * Dynamically enable supported card networks *(MasterCard, VISA etc.)* for Apple 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 Apple Pay internally to abstract the Apple Pay Javascript Client.
 * @example <caption>Instantiate the Transpayrent SDK and display the "Buy with Apple 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>
 *      // 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 Apple 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 Apple Pay 
 *      var config = { buttonstyle : 'black',
 *                     type : 'pay',
 *                     locale : 'en',
 *                     onClick : () => {
 *                          const paymentMethodId = 204;                                       // Apple 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 Apple Pay" button
 *      sdk.displayPaymentButton(204, document.getElementById('apple-pay-button-container'), paymentMethodIds, config);
 *  </script>
 * 
 * @param {String} environment                 The Apple Pay environment, either PRODUCTION or TEST, that will be used by the helper SDK
 * @param {ApplePayConfig} config              The base configuration for Apple Pay.
 * @param {ApplePayTransaction} transaction    The details for the payment transaction that will be authorized using Apple Pay
 */
function TranspayrentApplePayHelper(environment, config, transaction) {
    /**
     * The Apple 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 Apple Pay
     * 
     * @typedef {Object} ApplePayConfig
     * @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 {Element} container        The container element in which the constructed "Buy with Apple 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 Apple Pay.
     *
     * @private
     *
     * @type {ApplePayConfig}
     */
    this._config = config;
    /**
     * The details for the payment transaction that will be authorized using Apple Pay
     * 
     * @typedef {Object} ApplePayTransaction
     * @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 Apple Pay upon successful authorization
     */
    /**
     * The details for the payment transaction that will be authorized using Apple Pay
     *
     * @private
     *
     * @type {ApplePayTransaction}
     */
    this._transaction = transaction;

    /* ========== COMMON METHODS START ========== */
    /**
     * @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 Apple Pay upon successful authorization
     * @returns {Object} PaymentDataRequest fields
     */
    TranspayrentApplePayHelper.prototype.getBaseCardPaymentMethods = function (paymentMethodIds, save) {
        /**
         * @see {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916122-supportednetworks|supportedNetworks}
         */
        return { supportedNetworks: [...new Set(Array.from(paymentMethodIds, (id) => {
                    switch (parseInt(id) ) {
                        case 107:   // Maestro
                            return 'maestro';
                        case 108:   // MasterCard
                            return 'masterCard';
                        case 103:   // VISA Dankort
                        case 109:   // VISA
                        case 110:   // VISA Electron
                            return '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 
     */
    TranspayrentApplePayHelper.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 ========== */
    /**
     * Add a Apple Pay purchase button alongside an existing checkout button.
     * 
     * @private
     *
     * @see {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaybutton|ApplePayButton}
     * @see {@link https://developer.apple.com/design/human-interface-guidelines/technologies/apple-pay/buttons-and-marks/|Buttons and marks}
     * 
     * @param {*} container 
     * @param {*} config 
     * @param  {...any} args 
     */
    TranspayrentApplePayHelper.prototype.addApplePayButton = function (container, config, ...args) {
        const callback = config.onClick;
        config.onClick = () => { callback(args); };
        var button = document.createElement('apple-pay-button');
        Object.entries(config).forEach( ([key, value]) => {
            button.setAttribute(key, value);
        });
        button.onclick = config.onClick;
        container.appendChild(button);
    }
    /**
     * Displays the Apple 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 
     */
    TranspayrentApplePayHelper.prototype.displayPaymentButton = function (container, paymentMethodIds, config, ...args) {
        const helper = this;
        helper.addApplePayButton(container, config, args);
    }
    /* ========== DISPLAY PAYMENT BUTTON END ========== */

    /* ========== DISPLAY PAYMENT SHEET START ========== */
    /**
     * Configure support for the Apple Pay API.
     * 
     * @private
     *
     * @see {@link https://developers.google.com/pay/api/web/reference/request-objects#PaymentDataRequest|PaymentDataRequest}
     * 
     * @returns {Object} PaymentDataRequest fields
     */
     TranspayrentApplePayHelper.prototype.getApplePaymentDataRequest = function () {
        // Define PaymentMethodData
        const paymentMethodData = [ Object.assign(
                                        {},
                                        { supportedMethods : 'https://apple.com/apple-pay',
                                          data : Object.assign(
                                                     {},
                                                     { version : 3,
                                                       merchantIdentifier : this._config.merchantId.toString(),
                                                       merchantCapabilities : [
                                                            'supports3DS'
                                                       ],
                                                       countryCode : this._transaction.country },
                                                       this.getBaseCardPaymentMethods(this._transaction.paymentMethodIds, this._transaction.save) )
                                        } ) ];        
        // Define PaymentDetails
        const paymentDetails = Object.assign(
                                    {},
                                    { displayItems : [ { label : this._config.merchantName,
                                                         amount : { value : Number(this._transaction.amount.value / Math.pow(10, this.getDecimals(this._transaction.amount.currency) ) ).toFixed(2),
                                                                    currency : this._transaction.amount.currency } } ],
                                      total : { label : this._config.merchantName,
                                                amount : { value : Number(this._transaction.amount.value / Math.pow(10, this.getDecimals(this._transaction.amount.currency) ) ).toFixed(2),
                                                           currency : this._transaction.amount.currency } } },
                                      this._transaction.save ? { modifiers : [ { supportedMethods : 'https://apple.com/apple-pay',
                                                                                 data : { recurringPaymentRequest : { paymentDescription : 'Subscription',
                                                                                                                      regularBilling : { paymentTiming : 'recurring',
                                                                                                                                         label : 'Recurring',
                                                                                                                                         amount : Number(this._transaction.amount.value / 100).toFixed(2),
                                                                                                                                         recurringPaymentStartDate : new Date().toISOString() },
                                                                                                                      managementURL : 'https://api-gateway.transpayrent.cloud',
                                                                                                                      tokenNotificationURL  : 'https://api-gateway.transpayrent.cloud'
                                                                                                                    }
                                                                                        }
                                                                                } ]
                                                               } : { }
                                    );
        // Define PaymentOptions
        const paymentOptions = { requestPayerName : false,
                                 requestBillingAddress : false,
                                 requestPayerEmail : false,
                                 requestPayerPhone : false,
                                 requestShipping : false,
                                 shippingType : 'shipping' };
        
        return { paymentMethodData : paymentMethodData,
                 paymentDetails : paymentDetails,
                 paymentOptions : paymentOptions };
    }
    /**
     * Show Apple Pay payment sheet when Apple 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 {Transpayrent} sdk    An instance of the Transpayrent Browser SDK, which may be used to interact with the Transpayrent Payment Gateway
     * @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
     */
    TranspayrentApplePayHelper.prototype.displayPaymentSheet = function (sdk, resolver, rejecter) {
        try {
            const request = this.getApplePaymentDataRequest();
            // Create PaymentRequest
            const payment = new PaymentRequest(request.paymentMethodData, request.paymentDetails, request.paymentOptions);
            payment.onmerchantvalidation = event => {
                if (this._transaction.merchantSession) {
                    var session = this._transaction.merchantSession;
                    if (typeof session === 'string' || session instanceof String) {
                        session = JSON.parse(session);
                    }
                    event.complete(session);
                }
                else {
                    const paymentMethodId = 204;
                    var request = { correlation_id : this._transaction.correlationId,
                                    amount : { currency : this._transaction.amount.currency_id,
                                               value : this._transaction.amount.value } };
                    sdk.createTransaction(paymentMethodId, request)
                       .then(transaction => {
                                // Creation of Payment Transaction failed - Display status message
                                if (transaction.status) {
                                    return rejecter(`API: ${transaction.api} failed with HTTP Status Code: ${transaction.status} and error: ${transaction.messages[0].message}(${transaction.messages[0].code})`);
                                }
                                else {
                                    var request = { payment_method_id : paymentMethodId,
                                                    save : this._transaction.save,
                                                    domain : this._config.merchantOrigin };
                                    return sdk.initialize(transaction.id, request);
                                }
                            })
                       .then(initialization => {
                                // Initialization of Payment Transaction failed - Display status message
                                if (initialization.status) {
                                    return rejecter(`API: ${initialization.api} failed with HTTP Status Code: ${initialization.status} and error: ${initialization.messages[0].message}(${initialization.messages[0].code})`);
                                }
                                else {
                                    event.complete(JSON.parse(initialization.connection.data.merchant_session) );
                                }
                            })
                        .catch(reason => rejecter(reason) );
                }
            };
            payment.onpaymentmethodchange = event => {
                if (event.methodDetails.type !== undefined) {
                    // Define PaymentDetailsUpdate based on the selected payment method.
                    // No updates or errors needed, pass an object with the same total.
                    const paymentDetailsUpdate = {
                        total : request.paymentDetails.total
                    };
                    event.updateWith(paymentDetailsUpdate);
                }
            };
            payment.show()
                .then(response => {
                    const status = 'success';
                    return response.complete(status)
                        .then(r => resolver(btoa(JSON.stringify(response.details) ) ) )
                        .catch(reason => rejecter(reason) );
                })
                .catch(reason => rejecter(reason) );
        }
        catch (e) {
            rejecter(e);
        }
    }
    /* ========== DISPLAY PAYMENT SHEET END ========== */
}