Source: transpayrent-click-to-pay-helper.js

/**
 * @copyright Transpayrent ApS 2026
 * @license Common Transpayrent API license
 *
 * @class
 * @classdesc   The Transpayrent Helper SDK for Mastercard Click-to-Pay is a thin wrapper around the
 * [Mastercard Unified Checkout Solutions (UCS) JavaScript SDK]{@link https://developer.mastercard.com/}.
 *
 * **This helper does not orchestrate the Click-to-Pay UI flow.** Merchants remain responsible for
 * recognition, OTP, card selection, fallback screens, and checkout popups. The helper provides:
 *
 * 1. **Button rendering** via {@link TranspayrentClickToPayHelper#displayPaymentButton}
 * 2. **Mastercard passthrough methods** with identical names (`init`, `getCards`, `idLookup`, etc.)
 * 3. **Transpayrent integration utilities** that reduce boilerplate around readiness checks,
 *    checkout result normalization, gateway handoff context, and optional session storage
 *
 * Click to Pay intentionally does **not** provide `displayPaymentSheet` orchestration.
 *
 * Recommended merchant sequence:
 * 1. Load Transpayrent SDK + Mastercard UCS script
 * 2. `helper.getReadiness()` and show/hide Click to Pay UI accordingly
 * 3. `helper.init(merchantOwnedInitRequest)` then call Mastercard passthrough methods as needed
 * 4. Call `checkoutWithCard` / `checkoutWithNewCard` — they normalize the Mastercard result and
 *    store the checkout handoff automatically when `checkoutActionCode` is `COMPLETE`
 * 5. Apply the saved handoff when building gateway `authenticate` / `authorize` payloads via
 *    `helper.applyCheckoutHandoff(paymentDetails)` (or the static
 *    `TranspayrentClickToPayHelper.applyCheckoutHandoff(paymentDetails, handoff)`)
 * 6. Continue with Transpayrent gateway `authenticate` / `authorize` or UI Helper `completePayment`
 *
 * @see [Transpayrent]{@link Transpayrent}
 * @see {@link Transpayrent#getClickToPayHelper}
 *
 * @constructor
 * @description Creates a new instance of the Transpayrent Click-to-Pay Helper, which uses the
 * Mastercard Unified Checkout Solutions SDK internally to abstract the Click-to-Pay API.
 *
 * @example <caption>Recommended merchant-owned Click-to-Pay checkout handoff</caption>
 *  var helper = sdk.getClickToPayHelper({ debugLogging: true });
 *  var readiness = helper.getReadiness();
 *  if (!readiness.ready) {
 *      console.warn('Click to Pay unavailable:', readiness.issues);
 *      return;
 *  }
 *
 *  helper.init(merchantOwnedInitRequest)
 *      .then(function() { return helper.checkoutWithCard(merchantOwnedCheckoutRequest); })
 *      .then(function(checkoutResult) {
 *          if (checkoutResult.checkoutActionCode !== 'COMPLETE') {
 *              return;
 *          }
 *          return uiHelper.completePayment(205, false);
 *      })
 *      .catch(function(error) {
 *          console.error(error.api, error.status, error.messages[0].code, error.messages[0].message);
 *      });
 *
 * @param {String} environment                 The Click-to-Pay environment, either PRODUCTION or TEST
 * @param {ClickToPayConfig} config            The base configuration for Click-to-Pay
 * @param {ClickToPayTransaction} [transaction]  Optional transaction details for Click-to-Pay
 */
function TranspayrentClickToPayHelper(environment, config, transaction) {
    /**
     * The Click-to-Pay environment that will be used by the helper SDK.
     * Will be either PRODUCTION or TEST.
     *
     * @private
     * @type {String}
     */
    this._environment = environment;

    /**
     * @typedef {Object} ClickToPayCheckoutHandoff
     * @property {String} checkoutActionCode
     * @property {String} [merchantTransactionId]
     * @property {String} [srcFlowId]
     * @property {String} [checkoutResponse]
     * @property {String} [recognitionToken]
     * @property {Object} [checkoutResponseData]
     * @property {*} [rawResult]
     */

    /**
     * @typedef {Object} ClickToPayCheckoutHandoffRef
     * @property {String} merchantTransactionId
     * @property {String} [srcFlowId]
     */

    /**
     * @typedef {Object} ClickToPayReadinessIssue
     * @property {String} code
     * @property {String} message
     */

    /**
     * @typedef {Object} ClickToPayReadiness
     * @property {Boolean} ready
     * @property {ClickToPayReadinessIssue[]} issues
     */

    /**
     * @typedef {Object} ClickToPayRecognitionSessionStoreOptions
     * @property {Storage} [storage]
     * @property {String} [recognitionTokenKeyPrefix]
     * @property {String} [rememberMeKeyPrefix]
     * @property {Boolean} [rememberMeDefault]
     * @property {Function} [identityNormalizer]
     */

    /**
     * @typedef {Object} ClickToPayRecognitionSessionStore
     * @property {Function} normalizeIdentity
     * @property {Function} getRecognitionToken
     * @property {Function} setRecognitionToken
     * @property {Function} removeRecognitionToken
     * @property {Function} getRememberMe
     * @property {Function} setRememberMe
     * @property {Function} clearIdentity
     */

    /**
     * Base configuration for Click-to-Pay
     *
     * @typedef {Object} ClickToPayConfig
     * @property {Integer} merchantId          Transpayrent's unique ID for the merchant
     * @property {Object} clickToPay           Mastercard Click-to-Pay SDK configuration
     * @property {String} [email]              Optional email address for identity lookup
     * @property {String} [mobile]             Optional mobile number for identity lookup
     * @property {Boolean} [rememberDevice]    Flag indicating whether to remember this device
     * @property {Boolean} [debugLogging]      When true, logs MC SDK request/response payloads to the browser
     *                                          console for debugging only. Do not enable in production unless
     *                                          intentionally troubleshooting a Click-to-Pay integration.
     *                                          The CVV/security code is always hidden from these logs regardless
     *                                          of this flag; every other field (including the PAN) is logged in
     *                                          full, so treat any captured output as sensitive when sharing it
     *                                          (email, tickets, etc.).
     * @property {ClickToPayRecognitionSessionStoreOptions} [recognitionStorage]
     *                                          Optional recognition storage configuration. When provided, the helper
     *                                          creates a browser-local store and exposes it through
     *                                          {@link TranspayrentClickToPayHelper#recognitionStorage}.
     */

    /**
     * The base configuration for Click-to-Pay
     *
     * @private
     * @type {ClickToPayConfig}
     */
    this._config = config;

    /**
     * The details for the payment transaction using Click-to-Pay
     *
     * @typedef {Object} ClickToPayTransaction
     * @property {Integer} amount              The payment transaction amount
     * @property {String} currency             The payment transaction currency code
     * @property {String} country              The country the payment transaction takes place in
     * @property {Boolean} [save]              Flag indicating whether the consumer elected to save the card
     */

    /**
     * The details for the payment transaction using Click-to-Pay
     *
     * @private
     * @type {ClickToPayTransaction}
     */
    this._transaction = transaction;

    /**
     * Promise from the most recent init() call, or null when not initialized.
     *
     * @private
     * @type {Promise|null}
     */
    this._initPromise = null;

    /**
     * When true, logs MC SDK request/response payloads to the console.
     *
     * @private
     * @type {Boolean}
     */
    this._debugLogging = !!(config && config.debugLogging);

    /**
     * Last normalized checkout handoff stored automatically after a successful
     * {@link TranspayrentClickToPayHelper#checkoutWithCard} or
     * {@link TranspayrentClickToPayHelper#checkoutWithNewCard} call.
     *
     * @private
     * @type {ClickToPayCheckoutHandoff|null}
     */
    this._checkoutHandoff = null;

    /**
     * Identity-scoped recognition token / remember-me storage, or null when not configured.
     *
     * @private
     * @type {ClickToPayRecognitionSessionStore|null}
     */
    this._recognitionSessionStore = (config && config.recognitionStorage)
        ? TranspayrentClickToPayHelper.createRecognitionSessionStore(config.recognitionStorage)
        : null;
    this._bindRecognitionStorage();
}

var TRANSPAYRENT_C2P_CALL_SEQUENCE = 0;
// Matches by substring (not exact key name) so prefixed/suffixed variants like
// "cardSecurityCode" (Mastercard UCS) or "cardCvv" are caught too, not just bare "cvv"/"securityCode".
var TRANSPAYRENT_C2P_CVV_KEY_PATTERN = /cvv2?|cvc|securitycode/i;

function transpayrentC2pIsCvvKey(key) {
    return TRANSPAYRENT_C2P_CVV_KEY_PATTERN.test(String(key).replace(/[^a-zA-Z0-9]/g, ''));
}

/**
 * @private
 */
TranspayrentClickToPayHelper.prototype._bindRecognitionStorage = function() {
    this.recognitionStorage = this._recognitionSessionStore || null;
};

/**
 * @private
 * @param {*} value
 * @returns {Object|null}
 */
TranspayrentClickToPayHelper._asRecord = function(value) {
    if (typeof value !== 'object' || value === null || Array.isArray(value)) {
        return null;
    }
    return value;
};

/**
 * @private
 * @param {*} value
 * @returns {String|undefined}
 */
TranspayrentClickToPayHelper._readString = function(value) {
    return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
};

/**
 * @private
 * @param {Object|undefined} headers
 * @param {String} key
 * @returns {String|undefined}
 */
TranspayrentClickToPayHelper._getHeaderValue = function(headers, key) {
    if (!headers) {
        return undefined;
    }
    var normalizedKey = key.toLowerCase();
    var headerKey;
    for (headerKey in headers) {
        if (headers.hasOwnProperty(headerKey) && headerKey.toLowerCase() === normalizedKey) {
            return TranspayrentClickToPayHelper._readString(headers[headerKey]);
        }
    }
    return undefined;
};

/**
 * @private
 * @param {String} api
 * @param {String} code
 * @param {String} message
 * @param {Number} [status]
 * @param {*} [rawError]
 * @returns {Object}
 */
TranspayrentClickToPayHelper._createNormalizedError = function(api, code, message, status, rawError) {
    return {
        api: api,
        status: typeof status === 'number' ? status : 400,
        transaction_id: undefined,
        rawError: rawError,
        messages: [{ code: code, message: message, system: 'CLICK_TO_PAY' }]
    };
};

/**
 * @private
 * @returns {Object}
 */
TranspayrentClickToPayHelper._createMemoryStorage = function() {
    var values = {};
    return {
        getItem: function(key) {
            return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null;
        },
        setItem: function(key, value) {
            values[key] = String(value);
        },
        removeItem: function(key) {
            delete values[key];
        }
    };
};

/**
 * Normalizes an identity value for recognition/session storage keys.
 *
 * @static
 * @param {String} identity
 * @param {Function} [identityNormalizer]
 * @returns {String}
 */
TranspayrentClickToPayHelper.normalizeIdentity = function(identity, identityNormalizer) {
    if (typeof identityNormalizer === 'function') {
        return identityNormalizer(identity);
    }

    var trimmed = typeof identity === 'string' ? identity.trim() : '';
    if (!trimmed) {
        return '';
    }

    if (trimmed.indexOf('@') >= 0) {
        return trimmed.toLowerCase();
    }

    return trimmed.replace(/\s+/g, '');
};

/**
 * Returns readiness diagnostics for the Mastercard UCS browser SDK.
 *
 * @public
 * @returns {ClickToPayReadiness}
 *
 * @example
 *  var readiness = helper.getReadiness();
 *  if (!readiness.ready) {
 *      console.warn(readiness.issues);
 *  }
 */
TranspayrentClickToPayHelper.prototype.getReadiness = function() {
    var issues = [];

    if (typeof window === 'undefined') {
        issues.push({
            code: 'SDK_ENVIRONMENT_ERROR',
            message: 'Mastercard UCS SDK is not available outside a browser environment.'
        });
        return { ready: false, issues: issues };
    }

    if (typeof window.MastercardCheckoutServices !== 'function' &&
        typeof window.Click2Pay !== 'function') {
        issues.push({
            code: 'SDK_NOT_READY',
            message: 'Mastercard UCS SDK globals were not found. Load the Mastercard script before calling Click-to-Pay methods.'
        });
    }

    return {
        ready: issues.length === 0,
        issues: issues
    };
};

/**
 * Static readiness check for use before a helper instance is created.
 *
 * @static
 * @returns {ClickToPayReadiness}
 */
TranspayrentClickToPayHelper.getReadiness = function() {
    return TranspayrentClickToPayHelper.prototype.getReadiness();
};

/**
 * @private
 * @param {*} handoff
 * @returns {Boolean}
 */
TranspayrentClickToPayHelper._isValidHandoffRef = function(handoff) {
    return !!(
        handoff &&
        typeof handoff === 'object' &&
        TranspayrentClickToPayHelper._readString(handoff.merchantTransactionId) &&
        TranspayrentClickToPayHelper._readString(handoff.srcFlowId)
    );
};

/**
 * @private
 * @returns {Object}
 */
TranspayrentClickToPayHelper._createHandoffMissingError = function() {
    return TranspayrentClickToPayHelper._createNormalizedError(
        'click_to_pay_checkout_handoff',
        'CHECKOUT_HANDOFF_MISSING',
        'Click to Pay checkout handoff is missing or incomplete. Both merchantTransactionId and srcFlowId are required.',
        400
    );
};

/**
 * Applies a saved Click-to-Pay checkout handoff to an authenticate/authorize payload.
 *
 * @static
 * @param {Object} paymentDetails
 * @param {ClickToPayCheckoutHandoffRef} handoff
 * @returns {Object}
 * @throws {Object} Transpayrent-normalized error when `handoff` is missing or incomplete
 */
TranspayrentClickToPayHelper.applyCheckoutHandoff = function(paymentDetails, handoff) {
    if (!paymentDetails || typeof paymentDetails !== 'object') {
        throw TranspayrentClickToPayHelper._createHandoffMissingError();
    }
    if (!TranspayrentClickToPayHelper._isValidHandoffRef(handoff)) {
        throw TranspayrentClickToPayHelper._createHandoffMissingError();
    }

    var merged = Object.assign({}, paymentDetails);
    var handoffFields = {
        merchant_transaction_id: handoff.merchantTransactionId,
        src_flow_id: handoff.srcFlowId
    };

    if (merged.payment_details && typeof merged.payment_details === 'object') {
        merged.payment_details = Object.assign({}, merged.payment_details, handoffFields);
        return merged;
    }

    return Object.assign(merged, handoffFields);
};

/**
 * Applies a saved checkout handoff to an authenticate/authorize payload.
 * When `handoff` is omitted, uses the handoff stored by the last successful checkout call.
 *
 * @public
 * @param {Object} paymentDetails
 * @param {ClickToPayCheckoutHandoffRef} [handoff]
 * @returns {Object}
 * @throws {Object} Transpayrent-normalized error when handoff is missing or incomplete
 */
TranspayrentClickToPayHelper.prototype.applyCheckoutHandoff = function(paymentDetails, handoff) {
    return TranspayrentClickToPayHelper.applyCheckoutHandoff(
        paymentDetails,
        handoff || this._checkoutHandoff
    );
};

/**
 * Creates an opt-in identity-scoped recognition token / remember-me store.
 *
 * @static
 * @param {ClickToPayRecognitionSessionStoreOptions} [options]
 * @returns {ClickToPayRecognitionSessionStore}
 */
TranspayrentClickToPayHelper.createRecognitionSessionStore = function(options) {
    options = options || {};
    var storage = options.storage;
    if (!storage) {
        storage = typeof localStorage !== 'undefined'
            ? localStorage
            : TranspayrentClickToPayHelper._createMemoryStorage();
    }

    var recognitionTokenKeyPrefix = options.recognitionTokenKeyPrefix || 'transpayrent.c2p.recognitionToken:';
    var rememberMeKeyPrefix = options.rememberMeKeyPrefix || 'transpayrent.c2p.rememberMe:';
    var rememberMeDefault = options.rememberMeDefault !== false;
    var identityNormalizer = options.identityNormalizer;

    function normalize(identity) {
        return TranspayrentClickToPayHelper.normalizeIdentity(identity, identityNormalizer);
    }

    function recognitionKey(identity) {
        return recognitionTokenKeyPrefix + normalize(identity);
    }

    function rememberMeKey(identity) {
        return rememberMeKeyPrefix + normalize(identity);
    }

    return {
        normalizeIdentity: normalize,
        getRecognitionToken: function(identity) {
            if (!identity) {
                return undefined;
            }
            var value = storage.getItem(recognitionKey(identity));
            return value ? value : undefined;
        },
        setRecognitionToken: function(identity, recognitionToken) {
            if (!identity || !recognitionToken) {
                return;
            }
            storage.setItem(recognitionKey(identity), recognitionToken);
        },
        removeRecognitionToken: function(identity) {
            if (!identity) {
                return;
            }
            storage.removeItem(recognitionKey(identity));
        },
        getRememberMe: function(identity) {
            if (!identity) {
                return rememberMeDefault;
            }
            var key = rememberMeKey(identity);
            var stored = storage.getItem(key);
            if (stored === null) {
                storage.setItem(key, rememberMeDefault ? 'true' : 'false');
                return rememberMeDefault;
            }
            return stored === 'true';
        },
        setRememberMe: function(identity, value) {
            if (!identity) {
                return;
            }
            storage.setItem(rememberMeKey(identity), value ? 'true' : 'false');
        },
        clearIdentity: function(identity) {
            if (!identity) {
                return;
            }
            storage.removeItem(recognitionKey(identity));
            storage.removeItem(rememberMeKey(identity));
        }
    };
};

/**
 * @private
 *
 * Returns the Mastercard UCS SDK constructor function.
 * Looks for either window.MastercardCheckoutServices or window.Click2Pay.
 *
 * @throws {Object} Transpayrent-normalized error if SDK constructor not found
 * @returns {Function} The Mastercard SDK constructor
 */
TranspayrentClickToPayHelper.prototype._getConstructor = function() {
    if (typeof window === 'undefined') {
        throw this._normalizeError('click_to_pay_init', {
            name: 'click2PayError',
            reason: 'SDK_ENVIRONMENT_ERROR',
            message: 'Mastercard UCS SDK is not available in this environment.'
        });
    }
    if (typeof window.MastercardCheckoutServices === 'function') {
        return window.MastercardCheckoutServices;
    }
    if (typeof window.Click2Pay === 'function') {
        return window.Click2Pay;
    }
    throw this._normalizeError('click_to_pay_init', {
        name: 'click2PayError',
        reason: 'SDK_NOT_READY',
        message: 'Mastercard UCS SDK is not ready. Verify script URL, srcDpaId, network policy, and SDK globals (MastercardCheckoutServices or Click2Pay).'
    });
};

/**
 * @private
 *
 * Returns or creates the Mastercard UCS service instance cached in window.mcCheckoutService.
 *
 * @returns {Object} The Mastercard UCS service instance
 */
TranspayrentClickToPayHelper.prototype._getService = function() {
    if (typeof window === 'undefined') {
        throw this._normalizeError('click_to_pay_init', {
            name: 'click2PayError',
            reason: 'SDK_ENVIRONMENT_ERROR',
            message: 'Mastercard UCS SDK service is not available in this environment.'
        });
    }
    if (!window.mcCheckoutService) {
        window.mcCheckoutService = new (this._getConstructor())();
    }
    return window.mcCheckoutService;
};

/**
 * @private
 *
 * Resets the Mastercard UCS service instance by creating a fresh one for init().
 *
 * @returns {Object} The new Mastercard UCS service instance
 */
TranspayrentClickToPayHelper.prototype._resetService = function() {
    window.mcCheckoutService = new (this._getConstructor())();
    return window.mcCheckoutService;
};

/**
 * @private
 *
 * Normalizes Mastercard UCS SDK errors into Transpayrent error format.
 *
 * @param {String} api          The API method name that failed
 * @param {Error|Object} error  The error thrown by the MC SDK
 * @returns {Object}            Transpayrent-normalized error object
 */
TranspayrentClickToPayHelper.prototype._normalizeError = function(api, error) {
    if (error && typeof error === 'object' && Array.isArray(error.messages) && typeof error.api === 'string') {
        return error;
    }

    var code = 'UNKNOWN_ERROR';
    var message = 'An unexpected error occurred.';
    var status = 400;

    if (error && typeof error === 'object') {
        if (typeof error.reason === 'string') {
            code = error.reason;
        } else if (typeof error.code === 'string') {
            code = error.code;
        } else if (Array.isArray(error.messages) && error.messages[0] && typeof error.messages[0].code === 'string') {
            code = error.messages[0].code;
        }
        if (typeof error.message === 'string') {
            message = error.message;
        } else if (Array.isArray(error.messages) && error.messages[0] && typeof error.messages[0].message === 'string') {
            message = error.messages[0].message;
        }
        if (typeof error.status === 'number') {
            status = error.status;
        }
        if (typeof error.statusCode === 'string' && error.statusCode.toUpperCase() === 'CANCELED') {
            code = 'CANCELED';
            message = message || 'Click to Pay checkout was canceled.';
            status = 400;
        }
        if (error.details && typeof error.details.status === 'number') {
            status = error.details.status;
        }
        if (typeof error.transaction_id !== 'undefined') {
            return {
                api: api,
                status: status,
                transaction_id: error.transaction_id,
                rawError: error,
                messages: [{ code: code, message: message, system: 'CLICK_TO_PAY' }]
            };
        }
    } else if (error instanceof Error) {
        message = error.message;
    }

    return TranspayrentClickToPayHelper._createNormalizedError(api, code, message, status, error);
};

/**
 * @private
 *
 * Sanitizes a value for request/response logging, returning a plain (deep-cloned) value rather
 * than a JSON string - logged as its own console.log/console.error argument, it renders as an
 * expandable, individually copyable object in the browser console instead of one long wrapped
 * string. The CVV/security code is always hidden, regardless of `debugLogging`; every other field
 * (including the PAN) is included in full.
 *
 * @param {*} value
 * @returns {*}
 */
TranspayrentClickToPayHelper.prototype._safeSanitize = function(value) {
    try {
        var seen = typeof WeakSet === 'function' ? new WeakSet() : null;
        var json = JSON.stringify(value, function(key, val) {
            if (transpayrentC2pIsCvvKey(key)) {
                return '[HIDDEN]';
            }
            if (typeof val === 'function') {
                return '[Function]';
            }
            if (val !== null && typeof val === 'object') {
                // Cross-realm safe: popup windowRef is not instanceof global Window.
                if (key === 'windowRef' || val.window === val) {
                    return '[Window]';
                }
                if (typeof val.nodeType === 'number') {
                    return '[DOMNode]';
                }
                if (seen) {
                    if (seen.has(val)) {
                        return '[Circular]';
                    }
                    seen.add(val);
                }
            }
            return val;
        });
        // JSON.stringify(undefined) returns undefined (e.g. calls with no request/result) - nothing
        // to parse back in that case, just log the value as-is.
        return json === undefined ? value : JSON.parse(json);
    } catch (e) {
        return '[unserializable]';
    }
};

/**
 * @private
 *
 * @param {String} method  Mastercard SDK method name
 * @param {Number} callId  Diagnostic call sequence id
 * @param {Object} [request]
 */
TranspayrentClickToPayHelper.prototype._logMcRequest = function(method, callId, request) {
    if (!this._debugLogging) {
        return;
    }
    try {
        // Label/timestamp as one plain string, body as its own arg (an object, not a JSON string) so
        // the browser console renders it expandable/collapsible with its own "Copy object" entry,
        // instead of one giant wrapped line that has to be selected by hand.
        console.log('[TranspayrentClickToPayHelper] >>> MC SDK request: ' + method + ' callId=' + callId +
            ' timestamp=' + new Date().toISOString(), this._safeSanitize(request));
    } catch (_) {}
};

/**
 * @private
 *
 * Wraps an async operation with Transpayrent error normalization and response logging.
 *
 * @param {String} api
 * @param {Function} work  Receives callId and returns a Promise
 * @returns {Promise}
 */
TranspayrentClickToPayHelper.prototype._safeCall = function(api, work) {
    var self = this;
    var callId = ++TRANSPAYRENT_C2P_CALL_SEQUENCE;
    return Promise.resolve().then(function() {
        return work(callId);
    }).then(function(result) {
        if (self._debugLogging) {
            try {
                console.log('[TranspayrentClickToPayHelper] <<< ' + api + ' callId=' + callId +
                    ' timestamp=' + new Date().toISOString(), self._safeSanitize(result));
            } catch (_) {}
        }
        return result;
    }).catch(function(error) {
        var normalized = self._normalizeError(api, error);
        if (self._debugLogging) {
            try {
                console.error('[TranspayrentClickToPayHelper] <<< ' + api + ' callId=' + callId +
                    ' timestamp=' + new Date().toISOString(), self._safeSanitize(normalized));
            } catch (_) {}
        }
        throw normalized;
    });
};

/**
 * @private
 *
 * Waits until the current init() call has settled.
 *
 * @returns {Promise}
 */
TranspayrentClickToPayHelper.prototype.ensureInitSettled = function() {
    if (!this._initPromise) {
        return Promise.reject(this._normalizeError('click_to_pay_init', {
            name: 'click2PayError',
            reason: 'NOT_INITIALIZED',
            message: 'Click-to-Pay has not been initialized. Call init() first.'
        }));
    }
    return this._initPromise;
};

/**
 * @private
 *
 * Invokes a Mastercard UCS service method after init has settled.
 *
 * @param {String} api     Transpayrent API label for logging and errors
 * @param {String} method  Mastercard service method name
 * @param {Object} [request]
 * @returns {Promise}
 */
TranspayrentClickToPayHelper.prototype._invokeMc = function(api, method, request) {
    var self = this;
    return self.ensureInitSettled().then(function() {
        return self._safeCall(api, function(callId) {
            self._logMcRequest(method, callId, request);
            return self._getService()[method](request);
        });
    });
};

/**
 * Initializes Click-to-Pay with a fresh service instance and optional configuration.
 * This method must be called before any other Click-to-Pay operations.
 *
 * @public
 *
 * @param {Object} request                  The initialization request object (Mastercard UCS init schema)
 * @returns {Promise}                       Promise that resolves with the initialization result
 *
 * @example
 *  helper.init({
 *      requestId: 'req-12345',
 *      consumerLanguage: 'en-US'
 *  }).then(function(result) {
 *      return helper.getCards();
 *  }).catch(function(error) {
 *      console.error('Init failed:', error.messages[0].message);
 *  });
 */
TranspayrentClickToPayHelper.prototype.init = function(request) {
    var self = this;
    if (self._initPromise) {
        return self._initPromise;
    }

    var initPromise = self._safeCall('click_to_pay_init', function(callId) {
        var service = self._resetService();
        self._logMcRequest('init', callId, request);
        return service.init(request);
    }).catch(function(err) {
        if (self._initPromise === initPromise) {
            self._initPromise = null;
        }
        throw err;
    });

    self._initPromise = initPromise;
    return initPromise;
};

/**
 * Clears init state and saved checkout handoff so the next session starts from a clean runtime.
 *
 * @public
 */
TranspayrentClickToPayHelper.prototype.invalidateSession = function() {
    this._initPromise = null;
    this._checkoutHandoff = null;
    if (typeof window !== 'undefined') {
        window.mcCheckoutService = null;
    }
};

/**
 * Returns whether the Mastercard UCS browser SDK is available.
 *
 * @public
 * @returns {Boolean}
 */
TranspayrentClickToPayHelper.prototype.isReady = function() {
    return this.getReadiness().ready;
};

/**
 * Throws a normalized Click-to-Pay error when the Mastercard UCS SDK is not ready.
 *
 * @public
 */
TranspayrentClickToPayHelper.prototype.assertReady = function() {
    var readiness = this.getReadiness();
    if (!readiness.ready) {
        var issue = readiness.issues[0] || {
            code: 'SDK_NOT_READY',
            message: 'Mastercard UCS SDK is not ready.'
        };
        throw this._normalizeError('click_to_pay_readiness', {
            name: 'click2PayError',
            reason: issue.code,
            message: issue.message
        });
    }
};

/**
 * Replaces the helper's recognition storage implementation.
 * Use {@link TranspayrentClickToPayHelper#recognitionStorage} for normal read/write access.
 *
 * @public
 * @param {ClickToPayRecognitionSessionStore} store
 */
TranspayrentClickToPayHelper.prototype.useRecognitionSessionStore = function(store) {
    this._recognitionSessionStore = store;
    this._bindRecognitionStorage();
};

/**
 * @private
 *
 * `rawResult` preserves the original, unmodified Mastercard checkout payload on the returned
 * handoff so merchants can inspect fields this normalizer doesn't extract.
 *
 * @param {*} checkoutResult
 * @returns {ClickToPayCheckoutHandoff|null}
 */
TranspayrentClickToPayHelper.prototype._normalizeCheckoutResult = function(checkoutResult) {
    var payload = TranspayrentClickToPayHelper._asRecord(checkoutResult);
    if (!payload) {
        return null;
    }

    var headers = TranspayrentClickToPayHelper._asRecord(payload.headers);
    return {
        checkoutActionCode: TranspayrentClickToPayHelper._readString(payload.checkoutActionCode) || 'UNKNOWN',
        merchantTransactionId: TranspayrentClickToPayHelper._readString(payload.merchantTransactionId) ||
            TranspayrentClickToPayHelper._getHeaderValue(headers, 'merchant-transaction-id'),
        srcFlowId: TranspayrentClickToPayHelper._readString(payload.srcFlowId) ||
            TranspayrentClickToPayHelper._getHeaderValue(headers, 'x-src-cx-flow-id'),
        checkoutResponse: TranspayrentClickToPayHelper._readString(payload.checkoutResponse),
        recognitionToken: TranspayrentClickToPayHelper._readString(payload.recognitionToken),
        checkoutResponseData: TranspayrentClickToPayHelper._asRecord(payload.checkoutResponseData),
        rawResult: checkoutResult
    };
};

/**
 * @private
 *
 * @param {*} rawResult
 * @returns {ClickToPayCheckoutHandoff|null}
 */
TranspayrentClickToPayHelper.prototype._completeCheckout = function(rawResult) {
    var normalized = this._normalizeCheckoutResult(rawResult);
    if (!normalized) {
        throw this._normalizeError('click_to_pay_checkout', {
            name: 'click2PayError',
            reason: 'CHECKOUT_RESULT_INVALID',
            message: 'Click to Pay checkout returned an unrecognizable result.'
        });
    }

    if (normalized.checkoutActionCode === 'COMPLETE') {
        if (!TranspayrentClickToPayHelper._isValidHandoffRef(normalized)) {
            throw TranspayrentClickToPayHelper._createHandoffMissingError();
        }
        this._checkoutHandoff = normalized;
    }

    return normalized;
};

/**
 * Enables or disables MC SDK request/response logging for debugging Click-to-Pay flows.
 * Intended for development and integration troubleshooting only. The CVV/security code is
 * always hidden from these logs regardless of this flag; every other field (including the
 * PAN) is logged in full, so treat any captured output as sensitive when sharing it
 * (email, tickets, etc.).
 *
 * @public
 *
 * @param {Boolean} enabled  When true, logs payloads to the browser console
 *
 * @example
 *  var helper = sdk.getClickToPayHelper({ debugLogging: true });
 *  // or at runtime:
 *  helper.setDebugLogging(true);
 */
TranspayrentClickToPayHelper.prototype.setDebugLogging = function(enabled) {
    this._debugLogging = !!enabled;
};

/**
 * Retrieves the list of recognized cards for the current consumer.
 * Should be called after init() to determine if the consumer is recognized.
 *
 * @public
 *
 * @param {Object} [request]                Optional getCards request parameters
 * @returns {Promise<Array>}                Promise that resolves with the cards array (may be empty)
 *
 * @example
 *  helper.getCards().then(function(cards) {
 *      if (cards.length > 0) {
 *          console.log('Cards:', cards);
 *      }
 *  }).catch(function(error) {
 *      console.error('Get cards failed:', error.messages[0].message);
 *  });
 */
TranspayrentClickToPayHelper.prototype.getCards = function(request) {
    return this._invokeMc('click_to_pay_get_cards', 'getCards', request).then(function(result) {
        return Array.isArray(result) ? result : [];
    });
};

/**
 * Authenticates a Click-to-Pay consumer using device fingerprint or credentials.
 *
 * @public
 *
 * @param {Object} request                  The authentication request object
 * @returns {Promise}                       Promise that resolves with authentication result
 */
TranspayrentClickToPayHelper.prototype.authenticate = function(request) {
    return this._invokeMc('click_to_pay_authenticate', 'authenticate', request);
};

/**
 * Performs an identity lookup for a consumer using email or phone number.
 *
 * @public
 *
 * @param {Object} request                  The identity lookup request
 * @returns {Promise}                       Promise that resolves with profile existence indicator
 */
TranspayrentClickToPayHelper.prototype.idLookup = function(request) {
    return this._invokeMc('click_to_pay_id_lookup', 'idLookup', request);
};

/**
 * Initiates identity validation by sending an OTP to the consumer.
 *
 * @public
 *
 * @param {Object} [request]                The initiate validation request
 * @returns {Promise}                       Promise that resolves with OTP delivery confirmation
 */
TranspayrentClickToPayHelper.prototype.initiateValidation = function(request) {
    return this._invokeMc('click_to_pay_initiate_validation', 'initiateValidation', request);
};

/**
 * Completes identity validation by verifying the OTP entered by the consumer.
 *
 * @public
 *
 * @param {Object} request                  The validate request with OTP value
 * @returns {Promise}                       Promise that resolves with validation result and cards
 */
TranspayrentClickToPayHelper.prototype.validate = function(request) {
    return this._invokeMc('click_to_pay_validate', 'validate', request);
};

/**
 * Encrypts a new card for storage and use with Click-to-Pay.
 *
 * @public
 *
 * @param {Object} request                  The encrypt card request with PAN and card details
 * @returns {Promise}                       Promise that resolves with encrypted card data
 */
TranspayrentClickToPayHelper.prototype.encryptCard = function(request) {
    return this._invokeMc('click_to_pay_encrypt_card', 'encryptCard', request);
};

/**
 * Initiates checkout with a saved/recognized card.
 *
 * @public
 *
 * @param {Object} request                  The checkout request with selected card
 * @returns {Promise<ClickToPayCheckoutHandoff>} Normalized checkout result; stores valid handoff on `COMPLETE`
 */
TranspayrentClickToPayHelper.prototype.checkoutWithCard = function(request) {
    var self = this;
    return this._invokeMc('click_to_pay_checkout_with_card', 'checkoutWithCard', request)
        .then(function(result) {
            return self._completeCheckout(result);
        });
};

/**
 * Initiates checkout with a newly encrypted card.
 *
 * @public
 *
 * @param {Object} request                  The checkout request with encrypted card data
 * @returns {Promise<ClickToPayCheckoutHandoff>} Normalized checkout result; stores valid handoff on `COMPLETE`
 */
TranspayrentClickToPayHelper.prototype.checkoutWithNewCard = function(request) {
    var self = this;
    return this._invokeMc('click_to_pay_checkout_with_new_card', 'checkoutWithNewCard', request)
        .then(function(result) {
            return self._completeCheckout(result);
        });
};

/**
 * Signs out the consumer from their Click-to-Pay session.
 *
 * @public
 *
 * @param {Object} [request]                Optional sign out parameters
 * @returns {Promise}                       Promise that resolves when sign out completes
 */
TranspayrentClickToPayHelper.prototype.signOut = function(request) {
    var self = this;
    return self.ensureInitSettled().then(function() {
        return self._safeCall('click_to_pay_sign_out', function(callId) {
            var service = self._getService();
            if (typeof service.signOut !== 'function') {
                throw {
                    name: 'click2PayError',
                    reason: 'METHOD_NOT_AVAILABLE',
                    message: 'signOut method is not available on the Mastercard UCS SDK service.'
                };
            }
            self._logMcRequest('signOut', callId, request);
            return service.signOut(request);
        });
    }).then(function(result) {
        self.invalidateSession();
        return result;
    });
};

/**
 * Displays the Click-to-Pay button using the SRC UI Kit src-button web component when available.
 * When `<src-button>` is unavailable, a plain HTML button is rendered as a visual/click target fallback only.
 * That fallback does **not** indicate Mastercard Click-to-Pay runtime readiness; call {@link TranspayrentClickToPayHelper#getReadiness}
 * before showing merchant-owned Click-to-Pay UI.
 * It is **strongly recommended** to use {@link Transpayrent#displayPaymentButton}.
 *
 * @public
 *
 * @see Transpayrent#displayPaymentButton
 *
 * @param {*} container                        The container element for the button
 * @param {PAYMENT_METHOD_ID[]} paymentMethodIds List of payment method IDs from the gateway
 * @param {Object} [config]                    Button-only configuration (`buttonText`, `onClick`)
 */
TranspayrentClickToPayHelper.prototype.displayPaymentButton = function(container, paymentMethodIds, config) {
    if (!container) {
        return;
    }

    var hasSourceButton = typeof window !== 'undefined' &&
        typeof window.customElements !== 'undefined' &&
        window.customElements.get('src-button') !== undefined;

    var button = hasSourceButton
        ? document.createElement('src-button')
        : (function() {
            var el = document.createElement('button');
            el.type = 'button';
            el.className = 'click-to-pay-button';
            el.textContent = config && config.buttonText ? config.buttonText : 'Click to Pay';
            return el;
        })();

    if (config && typeof config.onClick === 'function') {
        button.addEventListener('click', config.onClick);
    }

    container.innerHTML = '';
    container.appendChild(button);
};