2
0
Files
gitcaddy-server/web_src/js/features/repo-subscribe.ts
logikonline d1f20f6b46
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
feat(ci): add repository subscription monetization system
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
2026-01-31 13:37:07 -05:00

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};