UNPKG

@collectapp/checkout-sdk

Version:
214 lines (213 loc) 8.46 kB
/** * Constants for the CollectCheckoutService */ const CONSTANTS = { HOST_KEY: 'collect-pv-data', DEFAULT_CURRENCY: 'NGN', DEFAULT_ITEM_IMAGE: '/development/shoe.png', MIN_AMOUNT: 0.1, Z_INDEX: '9999999', ID_PREFIX: 'collect-pv-build-', WIDGET_ID_PREFIX: 'collect-pv-widget-', LOADER_ID: 'ct-px__app-loader', RANDOM_ID_MIN: 10000, RANDOM_ID_MAX: 9000, }; /** * Service for handling Collect App checkout functionality */ export class CollectCheckoutService { /** * Creates a new instance of the CollectCheckoutService * @param clientConfiguration Configuration for the checkout */ constructor(clientConfiguration) { this.clientConfiguration = clientConfiguration; this.hostKey = CONSTANTS.HOST_KEY; // Type-safe event handler this._handleMessageAddEventListener = (e) => this.handleMessageAddEventListener(e); // Validate configuration const clientError = this.validateConfiguration(this.clientConfiguration); if (clientError !== undefined) { throw new Error(`Configuration is invalid: ${clientError.join(', ')}`); } // Ensure we're in a browser environment if (typeof window === 'undefined') { throw new Error('Window object does not exist'); } // Determine host URL based on configuration if (this.clientConfiguration.localUrl) { this.hostUrl = this.clientConfiguration.localUrl; } else { const isLiveEnvironment = this.clientConfiguration.publicKey.startsWith('pk_live_'); this.hostUrl = `https://${isLiveEnvironment ? '{{process.env.BASE_URL}}' : '{{process.env.BASE_URL}}'}`; } // Initialize DOM elements with random ID this.collectAppID = Math.floor(CONSTANTS.RANDOM_ID_MIN + Math.random() * CONSTANTS.RANDOM_ID_MAX); this.collectAppLoader = document.createElement('div'); this.collectAppWidget = document.createElement('iframe'); } /** * Setup collect checkout UI elements */ setup() { // Set up loader element this.collectAppLoader.id = `${CONSTANTS.ID_PREFIX}${this.collectAppID}`; Object.assign(this.collectAppLoader.style, { position: 'fixed', left: '0px', top: '0px', height: '100%', width: '100%', overflow: 'hidden', zIndex: CONSTANTS.Z_INDEX, backgroundColor: 'rgba(0, 0, 0, 0.75)', }); this.collectAppLoader.innerHTML = ` <div id="${CONSTANTS.LOADER_ID}" style="position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);"> <div style="border: 3px solid #f3f3f3;border-radius: 50%;border-top: 3px solid #3498db;width: 30px;height: 30px;-webkit-animation: spin 2s linear infinite; animation: spin 2s linear infinite;"></div> <style> @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </div> `; // Set up widget element this.collectAppWidget.id = `${CONSTANTS.WIDGET_ID_PREFIX}${this.collectAppID}`; this.collectAppWidget.src = this.hostUrl; this.collectAppWidget.setAttribute('allowtransparency', 'true'); Object.assign(this.collectAppWidget.style, { position: 'fixed', left: '0px', top: '0px', margin: '0px', padding: '0px', height: '100%', width: '100%', overflow: 'hidden', zIndex: CONSTANTS.Z_INDEX, visibility: 'visible', backgroundColor: 'transparent', borderStyle: 'none', borderWidth: '0px', }); this.collectAppWidget.style.setProperty('-webkit-tap-highlight-color', 'transparent'); // Set up onload handler this.collectAppWidget.onload = () => { var _a; // Send configuration to iframe const { email, firstName, lastName = '', reference, amount, currency = CONSTANTS.DEFAULT_CURRENCY, paymentLinkCode = '', invoiceCode = '', planCode = '', itemImage = CONSTANTS.DEFAULT_ITEM_IMAGE, publicKey, } = this.clientConfiguration; (_a = this.collectAppWidget.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage({ key: this.hostKey, email, firstName, lastName, reference, amount, currency, paymentLinkCode, invoiceCode, planCode, itemImage, publicKey, }, this.hostUrl); // Add event listener for messages window.addEventListener('message', this._handleMessageAddEventListener, false); }; } /** * Open collect checkout */ open() { document.body.appendChild(this.collectAppLoader); document.body.appendChild(this.collectAppWidget); } /** * Close collect checkout */ async close() { // Call beforeClose callback if defined if (typeof this.clientConfiguration.beforeClose === 'function') { await this.clientConfiguration.beforeClose('Before close checkout'); } // Remove DOM elements if (document.body.contains(this.collectAppLoader)) { document.body.removeChild(this.collectAppLoader); } if (document.body.contains(this.collectAppWidget)) { document.body.removeChild(this.collectAppWidget); } // Remove event listener window.removeEventListener('message', this._handleMessageAddEventListener, false); // Call onClose callback if defined if (typeof this.clientConfiguration.onClose === 'function') { this.clientConfiguration.onClose('closed checkout'); } } // ========== PRIVATE METHODS ========== /** * Handle messages from the checkout iframe */ handleMessageAddEventListener(event) { var _a; // Validate message origin and key if (event.origin !== this.hostUrl || ((_a = event.data) === null || _a === void 0 ? void 0 : _a.key) !== this.hostKey) { return; } const { type, data = '' } = event.data; // Handle different message types switch (type) { case 'loading': // Hide the loader when the iframe is ready const loaderElement = this.collectAppLoader.querySelector(`div#${CONSTANTS.LOADER_ID}`); if (loaderElement) { loaderElement.style.display = 'none'; } break; case 'close': this.close(); break; case 'success': if (typeof this.clientConfiguration.onSuccess === 'function') { this.clientConfiguration.onSuccess(data); } break; case 'error': if (typeof this.clientConfiguration.onError === 'function') { this.clientConfiguration.onError(data); } break; } } /** * Validate the client configuration * @param clientConfiguration Configuration to validate * @returns Array of error messages or undefined if valid */ validateConfiguration(clientConfiguration) { const errors = []; // Required fields validation if (!clientConfiguration.email) { errors.push('email is required'); } if (!clientConfiguration.firstName) { errors.push('first name is required'); } if (!clientConfiguration.reference) { errors.push('reference is required'); } if (!clientConfiguration.amount) { errors.push('amount is required'); } else if (clientConfiguration.amount < CONSTANTS.MIN_AMOUNT) { errors.push('amount must be greater than 100 NGN'); } if (!clientConfiguration.publicKey) { errors.push('public key is required'); } return errors.length > 0 ? errors : undefined; } }