@collectapp/checkout-sdk
Version:
Collect Africa Checkout library
214 lines (213 loc) • 8.46 kB
JavaScript
/**
* 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); } }
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;
}
}