All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m10s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m13s
Build and Release / Lint (push) Successful in 5m25s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m13s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m42s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m30s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m36s
Implement complete subscription monetization system for repositories with Stripe and PayPal integration. Includes: - Database models and migrations for monetization settings, subscription products, and user subscriptions - Payment provider abstraction layer with Stripe and PayPal implementations - Admin UI for configuring payment providers and viewing subscriptions - Repository settings UI for managing subscription products and tiers - Subscription checkout flow and webhook handlers for payment events - Access control to gate repository code behind active subscriptions
189 lines
5.9 KiB
TypeScript
189 lines
5.9 KiB
TypeScript
// repo-subscribe.ts — handles Stripe Elements and PayPal SDK for the subscribe page.
|
|
|
|
import {POST} from '../modules/fetch.ts';
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- must use interface for global augmentation
|
|
interface Window {
|
|
_subscribeData?: {
|
|
repoLink: string;
|
|
stripeEnabled: boolean;
|
|
stripePublishableKey: string;
|
|
paypalEnabled: boolean;
|
|
paypalClientID: string;
|
|
paypalSandbox: boolean;
|
|
csrfToken: string;
|
|
};
|
|
Stripe?: (key: string) => any;
|
|
paypal?: any;
|
|
}
|
|
}
|
|
|
|
let selectedProductId = '';
|
|
let selectedPriceCents = 0;
|
|
let selectedCurrency = '';
|
|
let selectedPaypalPlanId = '';
|
|
|
|
function initSubscribePage() {
|
|
const data = window._subscribeData;
|
|
if (!data) return;
|
|
|
|
const buttons = document.querySelectorAll<HTMLButtonElement>('.subscribe-btn');
|
|
const formContainer = document.querySelector<HTMLElement>('#payment-form-container');
|
|
const productNameEl = document.querySelector('#payment-product-name');
|
|
|
|
for (const btn of buttons) {
|
|
btn.addEventListener('click', () => {
|
|
const card = btn.closest('.card') as HTMLElement;
|
|
if (!card) return;
|
|
|
|
selectedProductId = card.getAttribute('data-product-id') || '';
|
|
selectedPriceCents = parseInt(card.getAttribute('data-price-cents') || '0');
|
|
selectedCurrency = card.getAttribute('data-currency') || 'USD';
|
|
selectedPaypalPlanId = card.getAttribute('data-paypal-plan-id') || '';
|
|
|
|
if (productNameEl) {
|
|
productNameEl.textContent = card.querySelector('.header')?.textContent || '';
|
|
}
|
|
|
|
if (formContainer) {
|
|
formContainer.style.display = '';
|
|
}
|
|
|
|
// Highlight selected card
|
|
for (const c of document.querySelectorAll('#subscribe-products .card')) c.classList.remove('blue');
|
|
card.classList.add('blue');
|
|
|
|
// Show appropriate payment method
|
|
if (data.stripeEnabled) {
|
|
const stripeEl = document.querySelector<HTMLElement>('#stripe-payment');
|
|
if (stripeEl) stripeEl.style.display = '';
|
|
initStripe(data.stripePublishableKey);
|
|
}
|
|
if (data.paypalEnabled) {
|
|
const paypalEl = document.querySelector<HTMLElement>('#paypal-payment');
|
|
if (paypalEl) paypalEl.style.display = '';
|
|
initPayPal(data.paypalClientID);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
let stripeInstance: any = null;
|
|
let stripeCardElement: any = null;
|
|
|
|
function initStripe(publishableKey: string) {
|
|
if (stripeInstance) return;
|
|
|
|
if (!window.Stripe) {
|
|
// Stripe.js must be loaded dynamically since it's an external payment SDK
|
|
// eslint-disable-next-line github/no-dynamic-script-tag
|
|
const script = document.createElement('script');
|
|
script.src = 'https://js.stripe.com/v3/';
|
|
script.addEventListener('load', () => setupStripe(publishableKey));
|
|
document.head.append(script);
|
|
} else {
|
|
setupStripe(publishableKey);
|
|
}
|
|
}
|
|
|
|
function setupStripe(publishableKey: string) {
|
|
if (!window.Stripe) return;
|
|
stripeInstance = window.Stripe(publishableKey);
|
|
const elements = stripeInstance.elements();
|
|
stripeCardElement = elements.create('card');
|
|
stripeCardElement.mount('#stripe-card-element');
|
|
|
|
const submitBtn = document.querySelector<HTMLElement>('#stripe-submit-btn');
|
|
const errorEl = document.querySelector<HTMLElement>('#stripe-card-errors');
|
|
|
|
submitBtn?.addEventListener('click', async () => {
|
|
if (!stripeInstance || !stripeCardElement) return;
|
|
submitBtn.classList.add('loading');
|
|
|
|
try {
|
|
const data = window._subscribeData!;
|
|
const resp = await POST(`${data.repoLink}/subscribe`, {
|
|
data: new URLSearchParams({
|
|
product_id: selectedProductId,
|
|
provider: 'stripe',
|
|
provider_subscription_id: '',
|
|
provider_payment_id: '',
|
|
}),
|
|
});
|
|
|
|
if (resp.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
const text = await resp.text();
|
|
if (errorEl) {
|
|
errorEl.textContent = text || 'Payment failed. Please try again.';
|
|
errorEl.style.display = '';
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
if (errorEl) {
|
|
errorEl.textContent = err.message || 'An error occurred.';
|
|
errorEl.style.display = '';
|
|
}
|
|
} finally {
|
|
submitBtn.classList.remove('loading');
|
|
}
|
|
});
|
|
}
|
|
|
|
let paypalLoaded = false;
|
|
|
|
function initPayPal(clientID: string) {
|
|
if (paypalLoaded) return;
|
|
paypalLoaded = true;
|
|
|
|
// PayPal SDK must be loaded dynamically since it's an external payment SDK
|
|
// eslint-disable-next-line github/no-dynamic-script-tag
|
|
const script = document.createElement('script');
|
|
script.src = `https://www.paypal.com/sdk/js?client-id=${clientID}&vault=true&intent=subscription`;
|
|
script.addEventListener('load', () => setupPayPal());
|
|
document.head.append(script);
|
|
}
|
|
|
|
function setupPayPal() {
|
|
if (!window.paypal) return;
|
|
const data = window._subscribeData!;
|
|
|
|
window.paypal.Buttons({
|
|
style: {layout: 'vertical', color: 'blue', shape: 'rect', label: 'subscribe'},
|
|
createSubscription(_data: any, actions: any) {
|
|
if (selectedPaypalPlanId) {
|
|
return actions.subscription.create({plan_id: selectedPaypalPlanId});
|
|
}
|
|
// For one-time payments, use createOrder instead
|
|
return actions.order.create({
|
|
purchase_units: [{
|
|
amount: {
|
|
currency_code: selectedCurrency,
|
|
value: (selectedPriceCents / 100).toFixed(2),
|
|
},
|
|
}],
|
|
});
|
|
},
|
|
async onApprove(approvalData: any) {
|
|
const subID = approvalData.subscriptionID || '';
|
|
const orderID = approvalData.orderID || '';
|
|
await POST(`${data.repoLink}/subscribe`, {
|
|
data: new URLSearchParams({
|
|
product_id: selectedProductId,
|
|
provider: 'paypal',
|
|
provider_subscription_id: subID,
|
|
provider_payment_id: orderID,
|
|
}),
|
|
});
|
|
window.location.reload();
|
|
},
|
|
onError(err: any) {
|
|
console.error('PayPal error:', err);
|
|
},
|
|
}).render('#paypal-button-container');
|
|
}
|
|
|
|
export {initSubscribePage};
|