UNPKG

simple-js-payment-system

Version:

Simple payment modal system with Stripe integration

359 lines (330 loc) 12.9 kB
class PaymentSystem { constructor() { // Defining state attributes const attrs = { isLive: true, modalTitle: '❤️ Support Our Work', modalDescription: 'If you find this tool useful, please consider supporting us with a small donation. Your contribution helps us maintain and improve the platform for everyone.', modalSubDescription: 'One-time donation • Lifetime access • Support independent creators', modalActionText: 'Remove this annoying popup $2.99', modalDiscardText: 'Maybe Later', minVisitCount: 100, discardVisitCount: 10, getDeviceId: () => { const metrics = { cores: navigator.hardwareConcurrency || '', memory: navigator.deviceMemory || '', screen: `${screen.width},${screen.height},${screen.colorDepth}`, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, touch: navigator.maxTouchPoints, lang: navigator.language }; // Create a string from all metrics and encode it const metricsString = Object.values(metrics).join('|'); return btoa(metricsString).replace(/=/g, ''); }, successUrl: function () { const deviceId = attrs.getDeviceId(); return `${window.location.origin}${window.location.pathname}?payment=success&deviceId=${encodeURIComponent(deviceId)}`; }, cancelUrl: () => `${window.location.origin}${window.location.pathname}?payment=cancelled`, loadPaymentStatus: function () { const stored = JSON.parse(localStorage.getItem('access_data') || '{}'); const currentDeviceId = attrs.getDeviceId(); return stored.paid && stored.deviceId === currentDeviceId; }, savePaymentStatus: function (status) { const deviceId = attrs.getDeviceId(); localStorage.setItem('access_data', JSON.stringify({ deviceId, paid: status, timestamp: Date.now() })); }, stripeConfig: { test: { publishableKey: 'pk_test_51OvPGD2L08TdNw1TA3BmJZSWNhbUg3HaW647yhsF8dHXm6MSN5JyxNyUC7aaxEOIrrZLNuwq0FNMWVNwdufiBu7l00s1r44mAQ', priceId: 'price_1Qmau92L08TdNw1TP7Jc7KcC' }, live: { publishableKey: 'pk_live_51OvPGD2L08TdNw1TGVlkaf8f4mEsqkwCHMF3Av110O79YdG578m1L18WbKyjZFbn6lHwRmNR1RiiHU00IEIt0Wpb00iWR2ouIQ', priceId: 'price_1QoAfz2L08TdNw1TcUnXI3ms' } }, modal: null, modalStyles: ` .payment-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10002; opacity: 0; transition: opacity 0.2s ease; } .payment-modal.show { display: flex; opacity: 1; } .payment-modal-content { position: relative; background: #2d2d2d; margin: auto; padding: 25px; border-radius: 8px; width: 90%; max-width: 600px; color: #fff; box-shadow: 0 4px 20px rgba(0,0,0,0.3); font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } .payment-modal-close { position: absolute; top: 15px; right: 15px; background: none; border: none; color: #999; cursor: pointer; font-size: 24px; line-height: 1; border-radius: 50%; transition: all 0.2s ease; } .payment-modal-close:hover { background: rgba(255,255,255,0.1); color: #fff; } .payment-modal h2 { margin: 0 0 20px 0; font-size: 24px; font-weight: 600; } .payment-modal p { margin: 0 0 20px 0; line-height: 1.6; color: #e0e0e0; } .payment-error { color: #ff4444; margin-bottom: 15px; display: none; } .payment-button { display: inline-block; background: #4CAF50; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .payment-button:hover { background: #45a049; transform: translateY(-1px); } .later-button { display: inline-block; background: transparent; color: #999; padding: 12px 24px; border: 1px solid #666; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; margin-left: 10px; transition: all 0.2s ease; } .later-button:hover { background: rgba(255,255,255,0.1); color: #fff; } `, pageName: window.location.pathname.split('/').pop().split('.')[0] || 'home', trackEvent: (eventName) => { if (window.gtag) { window.gtag('event', eventName); } if (window.plausible) { window.plausible(eventName); } } }; // Defining accessors this.getState = () => attrs; this.setState = (d) => Object.assign(attrs, d); // Automatically generate getter and setters for chart object based on the state properties; Object.keys(attrs).forEach((key) => { //@ts-ignore this[key] = function (_) { if (!arguments.length) { return attrs[key]; } attrs[key] = _; return this; }; }); // Custom enter exit update pattern initialization (prototype method) } injectStyles() { const { modalStyles } = this.getState(); if (!document.querySelector('#payment-modal-styles')) { const style = document.createElement('style'); style.id = 'payment-modal-styles'; style.textContent = modalStyles; document.head.appendChild(style); } } async handlePayment() { const { stripe, trackEvent, pageName, successUrl, cancelUrl } = this.getState(); trackEvent(`custom_pay_${pageName}`); const currentConfig = this.getCurrentConfig(); try { const { error } = await stripe.redirectToCheckout({ lineItems: [{ price: currentConfig.priceId, quantity: 1, }], mode: 'payment', successUrl: successUrl(), cancelUrl: cancelUrl(), }); if (error) { throw new Error(error.message); } } catch (error) { console.error('Payment failed:', error); const errorElement = document.querySelector('.payment-error'); if (errorElement) { errorElement.textContent = error.message; errorElement.style.display = 'block'; } } } hideModal() { const { modal, trackEvent, pageName } = this.getState(); if (modal) { modal.classList.remove('show'); this.updateVisitOnDiscard(); trackEvent(`custom_modal_close_${pageName}`); } } showModal() { const { modal } = this.getState(); setTimeout(() => modal.classList.add('show'), 10); } showModalIfNotPaid() { const { loadPaymentStatus, trackEvent, pageName } = this.getState(); trackEvent(`custom_modal_show_${pageName}`); if (!loadPaymentStatus()) { this.showModal(); } } createModal() { const { modalTitle, modalDescription, modalSubDescription, modalActionText, modalDiscardText } = this.getState(); const modal = document.createElement('div'); modal.className = 'payment-modal'; modal.innerHTML = ` <div class="payment-modal-content"> <button class="payment-modal-close">&times;</button> <h2 style='color: #fff;'>${modalTitle}</h2> <p>${modalDescription}</p> <p style="font-size: 14px; color: #999;">${modalSubDescription}</p> <div class="payment-error"></div> <div> <button class="payment-button">${modalActionText}</button> <button class="later-button">${modalDiscardText}</button> </div> </div> `; document.body.appendChild(modal); // Add event listeners const closeBtn = modal.querySelector('.payment-modal-close'); const laterBtn = modal.querySelector('.later-button'); const paymentBtn = modal.querySelector('.payment-button'); closeBtn.addEventListener('click', () => { this.hideModal(); }); laterBtn.addEventListener('click', () => { this.hideModal(); }); paymentBtn.addEventListener('click', () => { this.handlePayment(); }); modal.addEventListener('click', (e) => { if (e.target === modal) { this.hideModal(); } }); // Add ESC key listener document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.hideModal(); } }); this.setState({ modal }); return this; } // Check payment status async checkPaymentStatus() { const { loadPaymentStatus, savePaymentStatus, getDeviceId } = this.getState(); const urlParams = new URLSearchParams(window.location.search); const paymentStatus = urlParams.get('payment'); const urlDeviceId = urlParams.get('deviceId'); const currentDeviceId = getDeviceId(); if (paymentStatus === 'success' && urlDeviceId === currentDeviceId) { savePaymentStatus(true); // Clean up URL const cleanUrl = `${window.location.origin}${window.location.pathname}`; window.history.replaceState({}, document.title, cleanUrl); return true; } return loadPaymentStatus(); } getCurrentConfig() { const { isLive, stripeConfig } = this.getState(); return isLive ? stripeConfig.live : stripeConfig.test; } updateVisitCount() { const { pageName, minVisitCount } = this.getState(); const visitCount = localStorage.getItem(`${pageName}_visit_count`) || 0; localStorage.setItem(`${pageName}_visit_count`, parseInt(visitCount) + 1); if (minVisitCount && parseInt(visitCount) >= minVisitCount) { this.showModalIfNotPaid(); } } updateVisitOnDiscard() { const { pageName, discardVisitCount } = this.getState(); const visitCount = localStorage.getItem(`${pageName}_visit_count`) || 0; localStorage.setItem(`${pageName}_visit_count`, parseInt(visitCount) + discardVisitCount); } hasPaid() { const { loadPaymentStatus } = this.getState(); return loadPaymentStatus(); } run() { const currentConfig = this.getCurrentConfig(); console.log('Payment System Running', currentConfig); this.injectStyles(); this.createModal(); const stripe = Stripe(currentConfig.publishableKey); this.setState({ stripe }); this.checkPaymentStatus() this.updateVisitCount(); window._PaymentSystem = this; return this; } }