simple-js-payment-system
Version:
Simple payment modal system with Stripe integration
359 lines (330 loc) • 12.9 kB
JavaScript
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">×</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;
}
}