feat(koha): implement Stripe Customer Portal integration
- Add createPortalSession endpoint to koha.controller.js - Add POST /api/koha/portal route with rate limiting - Add 'Manage Your Subscription' section to koha.html - Implement handleManageSubscription() in koha-donation.js - Add Koha link to navigation menu in navbar.js - Allow donors to self-manage subscriptions via Stripe portal - Portal supports: payment method updates, cancellation, invoice history Ref: Customer Portal setup docs in docs/STRIPE_CUSTOMER_PORTAL_NEXT_STEPS.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
71f1b05494
commit
f042fa67b5
6 changed files with 324 additions and 71 deletions
|
|
@ -95,6 +95,9 @@ class TractatusNavbar {
|
|||
<a href="/about.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
||||
<span class="text-sm font-semibold">ℹ️ About</span>
|
||||
</a>
|
||||
<a href="/koha.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
|
||||
<span class="text-sm font-semibold">🤝 Support (Koha)</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,10 +105,11 @@ class TractatusNavbar {
|
|||
</nav>
|
||||
`;
|
||||
|
||||
// Insert navbar at the beginning of body (or replace existing nav)
|
||||
const existingNav = document.querySelector('nav');
|
||||
if (existingNav) {
|
||||
existingNav.outerHTML = navHTML;
|
||||
// Always insert navbar at the very beginning of body
|
||||
// Check if there's already a tractatus navbar (to avoid duplicates)
|
||||
const existingNavbar = document.querySelector('nav.bg-white.border-b.border-gray-200.sticky');
|
||||
if (existingNavbar) {
|
||||
existingNavbar.outerHTML = navHTML;
|
||||
} else {
|
||||
document.body.insertAdjacentHTML('afterbegin', navHTML);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,3 +287,129 @@ async function handleFormSubmit(e) {
|
|||
submitBtn.textContent = 'Proceed to Secure Payment';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize manage subscription functionality
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const manageBtn = document.getElementById('manage-subscription-btn');
|
||||
const emailInput = document.getElementById('manage-email');
|
||||
|
||||
if (manageBtn && emailInput) {
|
||||
manageBtn.addEventListener('click', handleManageSubscription);
|
||||
emailInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleManageSubscription();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle manage subscription button click
|
||||
*/
|
||||
async function handleManageSubscription() {
|
||||
const emailInput = document.getElementById('manage-email');
|
||||
const manageBtn = document.getElementById('manage-subscription-btn');
|
||||
const errorDiv = document.getElementById('manage-error');
|
||||
const loadingDiv = document.getElementById('manage-loading');
|
||||
|
||||
const email = emailInput.value.trim();
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
showManageError('Please enter your email address.');
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
showManageError('Please enter a valid email address.');
|
||||
emailInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide error, show loading
|
||||
hideManageError();
|
||||
showManageLoading();
|
||||
manageBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/koha/portal', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data.url) {
|
||||
// Redirect to Stripe Customer Portal
|
||||
window.location.href = data.data.url;
|
||||
} else if (response.status === 404) {
|
||||
showManageError('No subscription found for this email address. Please check your email and try again.');
|
||||
manageBtn.disabled = false;
|
||||
hideManageLoading();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to access subscription portal');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Manage subscription error:', error);
|
||||
showManageError('An error occurred. Please try again or contact support@agenticgovernance.digital');
|
||||
manageBtn.disabled = false;
|
||||
hideManageLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
function isValidEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
function showManageError(message) {
|
||||
const errorDiv = document.getElementById('manage-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide error message
|
||||
*/
|
||||
function hideManageError() {
|
||||
const errorDiv = document.getElementById('manage-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading indicator
|
||||
*/
|
||||
function showManageLoading() {
|
||||
const loadingDiv = document.getElementById('manage-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading indicator
|
||||
*/
|
||||
function hideManageLoading() {
|
||||
const loadingDiv = document.getElementById('manage-loading');
|
||||
if (loadingDiv) {
|
||||
loadingDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
191
public/koha.html
191
public/koha.html
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Support Tractatus | Koha Donation System</title>
|
||||
<meta name="description" content="Support the Tractatus AI Safety Framework. Help fund hosting, development, research, and community building for architectural AI safety.">
|
||||
<title>Koha — Reciprocal Support | Tractatus AI Safety</title>
|
||||
<meta name="description" content="Join a relationship of mutual support for AI safety. Koha is reciprocal giving that maintains community bonds — your contribution sustains this work; our work serves you and the commons.">
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1760254958072">
|
||||
<style>
|
||||
.gradient-text { background: linear-gradient(120deg, #3b82f6 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
|
|
@ -57,19 +57,31 @@
|
|||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
|
||||
Support Tractatus
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4" data-i18n="heading">
|
||||
Koha — Reciprocal Support
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Your donation helps fund the development, hosting, and research behind the world's first production implementation of architectural AI safety constraints.
|
||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto" data-i18n="subheading">
|
||||
Join a relationship of mutual support and shared responsibility for AI safety. Your contribution sustains this work, and this work serves you and our shared future.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Koha Explanation -->
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 mb-12 rounded">
|
||||
<h2 class="text-lg font-semibold text-blue-900 mb-2">What is Koha?</h2>
|
||||
<p class="text-blue-800">
|
||||
<strong>Koha</strong> (koh-hah) is a Māori word meaning "gift" or "donation." In the spirit of Te Tiriti partnership and reciprocity, we use this term to honor the indigenous wisdom that informs our approach to technology governance and human agency.
|
||||
<h2 class="text-lg font-semibold text-blue-900 mb-2" data-i18n="understanding_koha.title">Understanding Koha</h2>
|
||||
<p class="text-blue-800 mb-3" data-i18n="understanding_koha.intro">
|
||||
<strong>Koha</strong> (koh-hah) is a Māori practice of reciprocal giving that maintains and strengthens relationships. Unlike a one-sided donation, koha recognizes the mutual bond between giver and receiver — it affirms our shared humanity and interdependence.
|
||||
</p>
|
||||
<p class="text-blue-800 mb-3" data-i18n="understanding_koha.participation">
|
||||
When you offer koha to support this work, you are not simply paying for a service. You are participating in <em>whanaungatanga</em> (relationship-building) and <em>manaakitanga</em> (reciprocal care). In return, you receive:
|
||||
</p>
|
||||
<ul class="text-blue-800 space-y-2 ml-6 mb-3">
|
||||
<li data-i18n="understanding_koha.benefits.0">• Open access to all research, documentation, and code</li>
|
||||
<li data-i18n="understanding_koha.benefits.1">• Participation in a community committed to value pluralism and AI safety</li>
|
||||
<li data-i18n="understanding_koha.benefits.2">• Tools and frameworks that serve your needs and values</li>
|
||||
<li data-i18n="understanding_koha.benefits.3">• Transparent governance and ongoing dialogue about this work's direction</li>
|
||||
</ul>
|
||||
<p class="text-blue-800 text-sm" data-i18n="understanding_koha.spirit">
|
||||
The spirit of koha is not about the amount given, but about maintaining balance, mutual respect, and <em>aroha</em> (compassion) in our shared work. Your koha sustains us; our work serves you and the broader community. Together, we uphold the commons.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -82,29 +94,29 @@
|
|||
|
||||
<!-- Frequency Selection -->
|
||||
<div class="mb-8">
|
||||
<label class="block text-lg font-semibold text-gray-900 mb-4">Donation Type</label>
|
||||
<label class="block text-lg font-semibold text-gray-900 mb-4" data-i18n="form.participation_label">How Would You Like to Participate?</label>
|
||||
<div class="flex gap-4">
|
||||
<button type="button" id="freq-monthly" class="flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition">
|
||||
Monthly Support
|
||||
<button type="button" id="freq-monthly" class="flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition" data-i18n="form.monthly">
|
||||
Ongoing Koha (Monthly)
|
||||
</button>
|
||||
<button type="button" id="freq-onetime" class="flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition">
|
||||
One-Time Gift
|
||||
<button type="button" id="freq-onetime" class="flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition" data-i18n="form.one_time">
|
||||
One-Time Koha
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tier Selection (Monthly) -->
|
||||
<div id="tier-selection" class="mb-8">
|
||||
<label class="block text-lg font-semibold text-gray-900 mb-4">Choose Your Monthly Support Level</label>
|
||||
<label class="block text-lg font-semibold text-gray-900 mb-4" data-i18n="form.tier_selection">Select Your Ongoing Contribution</label>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Tier: $5 NZD -->
|
||||
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6" data-tier="5" data-amount="500">
|
||||
<div class="text-center">
|
||||
<div class="tier-badge mb-3">Foundation</div>
|
||||
<div class="tier-badge mb-3" data-i18n="form.tiers.foundation.name">Foundation</div>
|
||||
<div class="text-4xl font-bold text-gray-900 mb-2">$5</div>
|
||||
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
||||
<p class="text-gray-600 text-sm">
|
||||
<p class="text-gray-600 text-sm" data-i18n="form.tiers.foundation.description">
|
||||
Essential support for hosting and infrastructure. Every contribution matters.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -113,10 +125,10 @@
|
|||
<!-- Tier: $15 NZD -->
|
||||
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6 selected" data-tier="15" data-amount="1500">
|
||||
<div class="text-center">
|
||||
<div class="tier-badge mb-3">Advocate</div>
|
||||
<div class="tier-badge mb-3" data-i18n="form.tiers.advocate.name">Advocate</div>
|
||||
<div class="text-4xl font-bold text-gray-900 mb-2">$15</div>
|
||||
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
||||
<p class="text-gray-600 text-sm">
|
||||
<p class="text-gray-600 text-sm" data-i18n="form.tiers.advocate.description">
|
||||
Support development and research. Help expand the framework's capabilities.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -125,10 +137,10 @@
|
|||
<!-- Tier: $50 NZD -->
|
||||
<div class="tier-card bg-white border-2 border-gray-200 rounded-lg p-6" data-tier="50" data-amount="5000">
|
||||
<div class="text-center">
|
||||
<div class="tier-badge mb-3">Champion</div>
|
||||
<div class="tier-badge mb-3" data-i18n="form.tiers.champion.name">Champion</div>
|
||||
<div class="text-4xl font-bold text-gray-900 mb-2">$50</div>
|
||||
<div class="text-sm text-gray-500 mb-4">NZD / month</div>
|
||||
<p class="text-gray-600 text-sm">
|
||||
<p class="text-gray-600 text-sm" data-i18n="form.tiers.champion.description">
|
||||
Sustaining support for community building and advanced features.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -139,7 +151,7 @@
|
|||
|
||||
<!-- Custom Amount (One-Time) -->
|
||||
<div id="custom-amount" class="mb-8 hidden">
|
||||
<label for="amount-input" class="block text-lg font-semibold text-gray-900 mb-2">Donation Amount (NZD)</label>
|
||||
<label for="amount-input" class="block text-lg font-semibold text-gray-900 mb-2" data-i18n="form.amount_label">Your Koha Amount (NZD)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-2xl text-gray-700">$</span>
|
||||
<input
|
||||
|
|
@ -148,53 +160,57 @@
|
|||
min="1"
|
||||
step="0.01"
|
||||
placeholder="Enter amount"
|
||||
data-i18n-placeholder="form.amount_placeholder"
|
||||
class="flex-1 px-4 py-3 border-2 border-gray-300 rounded-lg text-lg focus:border-blue-600 focus:outline-none"
|
||||
aria-label="Donation amount in NZD"
|
||||
aria-label="Koha amount in NZD"
|
||||
aria-describedby="amount-help"
|
||||
>
|
||||
<span class="text-lg text-gray-600">NZD</span>
|
||||
</div>
|
||||
<p id="amount-help" class="text-sm text-gray-500 mt-2">Minimum donation: $1.00 NZD</p>
|
||||
<p id="amount-help" class="text-sm text-gray-500 mt-2" data-i18n="form.amount_help">The spirit of koha is in the giving, not the amount. Minimum: $1.00 NZD</p>
|
||||
</div>
|
||||
|
||||
<!-- Donor Information -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Your Information</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4" data-i18n="form.donor_info">Your Information</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="donor-name" class="block text-sm font-medium text-gray-700 mb-1">Name (optional)</label>
|
||||
<label for="donor-name" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="form.name_label">Name (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="donor-name"
|
||||
placeholder="Leave blank to donate anonymously"
|
||||
data-i18n-placeholder="form.name_placeholder"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
||||
aria-describedby="name-help"
|
||||
>
|
||||
<p id="name-help" class="text-xs text-gray-500 mt-1">Your name will only be used if you opt-in for public acknowledgement below</p>
|
||||
<p id="name-help" class="text-xs text-gray-500 mt-1" data-i18n="form.name_help">Your name will only be used if you opt-in for public acknowledgement below</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="donor-email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email <span class="text-red-500" aria-label="required">*</span>
|
||||
<span data-i18n="form.email_label">Email</span> <span class="text-red-500" aria-label="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="donor-email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
data-i18n-placeholder="form.email_placeholder"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
||||
aria-required="true"
|
||||
aria-describedby="email-help"
|
||||
>
|
||||
<p id="email-help" class="text-xs text-gray-500 mt-1">Required for payment receipt. We never share your email.</p>
|
||||
<p id="email-help" class="text-xs text-gray-500 mt-1" data-i18n="form.email_help">Required for payment receipt. We never share your email.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="donor-country" class="block text-sm font-medium text-gray-700 mb-1">Country (optional)</label>
|
||||
<label for="donor-country" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="form.country_label">Country (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="donor-country"
|
||||
placeholder="New Zealand"
|
||||
data-i18n-placeholder="form.country_placeholder"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
||||
>
|
||||
</div>
|
||||
|
|
@ -207,54 +223,55 @@
|
|||
type="checkbox"
|
||||
id="public-acknowledgement"
|
||||
class="mt-1 h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
|
||||
|
||||
>
|
||||
<div class="flex-1">
|
||||
<label for="public-acknowledgement" class="font-medium text-gray-900 cursor-pointer">
|
||||
<label for="public-acknowledgement" class="font-medium text-gray-900 cursor-pointer" data-i18n="form.public_ack_label">
|
||||
Include me in the public supporters list
|
||||
</label>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
<p class="text-sm text-gray-600 mt-1" data-i18n="form.public_ack_help">
|
||||
We value privacy. By default, all donations are anonymous. Check this box to be listed as a supporter on our transparency dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="public-name-field" class="mt-4 hidden">
|
||||
<label for="public-name" class="block text-sm font-medium text-gray-700 mb-1">Name to display publicly</label>
|
||||
<label for="public-name" class="block text-sm font-medium text-gray-700 mb-1" data-i18n="form.public_name_label">Name to display publicly</label>
|
||||
<input
|
||||
type="text"
|
||||
id="public-name"
|
||||
placeholder="How you'd like to be acknowledged"
|
||||
data-i18n-placeholder="form.public_name_placeholder"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
||||
aria-describedby="public-name-help"
|
||||
>
|
||||
<p id="public-name-help" class="text-xs text-gray-500 mt-1">Can be your real name, organization, or pseudonym</p>
|
||||
<p id="public-name-help" class="text-xs text-gray-500 mt-1" data-i18n="form.public_name_help">Can be your real name, organization, or pseudonym</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allocation Transparency -->
|
||||
<div class="mb-8 bg-blue-50 p-6 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-900 mb-3">How Your Donation is Used</h3>
|
||||
<h3 class="font-semibold text-gray-900 mb-3" data-i18n="form.allocation_heading">What Your Koha Sustains</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<div class="font-bold text-blue-900">40%</div>
|
||||
<div class="text-gray-700">Development</div>
|
||||
<div class="text-gray-700" data-i18n="form.allocation.development">Framework & website development</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-blue-900">30%</div>
|
||||
<div class="text-gray-700">Hosting</div>
|
||||
<div class="text-gray-700" data-i18n="form.allocation.hosting">Server & infrastructure costs</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-blue-900">20%</div>
|
||||
<div class="text-gray-700">Research</div>
|
||||
<div class="text-gray-700" data-i18n="form.allocation.research">AI safety research</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-blue-900">10%</div>
|
||||
<div class="text-gray-700">Community</div>
|
||||
<div class="text-gray-700" data-i18n="form.allocation.operations">Operations & expansion</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-3">
|
||||
<a href="/koha/transparency" class="text-blue-600 hover:underline">View full transparency dashboard →</a>
|
||||
<a href="/koha/transparency.html" class="text-blue-600 hover:underline" data-i18n="form.transparency_link">View full transparency dashboard →</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -263,10 +280,11 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="bg-blue-600 text-white px-12 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 transition shadow-lg hover:shadow-xl"
|
||||
data-i18n="form.submit"
|
||||
>
|
||||
Proceed to Secure Payment
|
||||
Offer Koha — Join Our Community
|
||||
</button>
|
||||
<p class="text-sm text-gray-500 mt-3">
|
||||
<p class="text-sm text-gray-500 mt-3" data-i18n="form.secure_payment">
|
||||
🔒 Secure payment via Stripe. We don't store your card details.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -274,43 +292,85 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Manage Subscription Section -->
|
||||
<div class="bg-white shadow rounded-lg p-8 mb-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4" data-i18n="manage_subscription.heading">Manage Your Subscription</h2>
|
||||
<p class="text-gray-700 mb-6" data-i18n="manage_subscription.description">
|
||||
Already supporting us with ongoing koha? You can manage your subscription, update payment methods, or cancel anytime — no questions asked.
|
||||
</p>
|
||||
|
||||
<div id="manage-subscription-form" class="max-w-md">
|
||||
<label for="manage-email" class="block text-sm font-medium text-gray-700 mb-2" data-i18n="manage_subscription.email_label">
|
||||
Enter your email to manage your subscription:
|
||||
</label>
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
type="email"
|
||||
id="manage-email"
|
||||
placeholder="your@email.com"
|
||||
data-i18n-placeholder="form.email_placeholder"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:border-blue-600 focus:outline-none"
|
||||
aria-label="Email address for subscription management"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id="manage-subscription-btn"
|
||||
class="bg-gray-800 text-white px-6 py-2 rounded-lg font-semibold hover:bg-gray-900 transition"
|
||||
data-i18n="manage_subscription.button"
|
||||
>
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2" data-i18n="manage_subscription.help">
|
||||
We'll send you to a secure portal where you can update your payment method, view invoices, or cancel your subscription.
|
||||
</p>
|
||||
<div id="manage-error" class="hidden mt-3 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700"></div>
|
||||
<div id="manage-loading" class="hidden mt-3 text-sm text-gray-600">
|
||||
<span class="inline-block animate-pulse">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Section -->
|
||||
<div class="bg-white shadow rounded-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Frequently Asked Questions</h2>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6" data-i18n="faq.heading">Frequently Asked Questions</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Why do you use the term "Koha"?</h3>
|
||||
<p class="text-gray-700">
|
||||
Koha is a Māori concept of gift-giving and reciprocity. We use it to honor the indigenous wisdom of Aotearoa New Zealand, where our lead developer is based, and to acknowledge that technology governance should be rooted in values of reciprocity and collective responsibility.
|
||||
<h3 class="font-semibold text-gray-900 mb-2" data-i18n="faq.why_koha.question">Why do you use the term "Koha"?</h3>
|
||||
<p class="text-gray-700 mb-2" data-i18n="faq.why_koha.answer_p1">
|
||||
Koha is a Māori practice of reciprocal giving that maintains relationships and affirms mutual respect. It recognizes that what sustains a community is not one-way charity, but ongoing exchange that honors both giver and receiver.
|
||||
</p>
|
||||
<p class="text-gray-700" data-i18n="faq.why_koha.answer_p2">
|
||||
We use this term to honor the indigenous wisdom of Aotearoa New Zealand, where our lead developer is based, and to acknowledge that technology governance should be rooted in values of <em>whanaungatanga</em> (relationship), <em>manaakitanga</em> (reciprocal care), and <em>aroha</em> (compassion) — not transactional exchange. Your koha sustains our work; our work serves you and the commons.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Is this tax-deductible?</h3>
|
||||
<p class="text-gray-700">
|
||||
<h3 class="font-semibold text-gray-900 mb-2" data-i18n="faq.tax.question">Is this tax-deductible?</h3>
|
||||
<p class="text-gray-700" data-i18n="faq.tax.answer">
|
||||
We are currently operating as an open-source project, not a registered charity. Donations are not tax-deductible at this time. You will receive a receipt for your records.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">How do I cancel my monthly donation?</h3>
|
||||
<p class="text-gray-700">
|
||||
You can cancel anytime by emailing <a href="mailto:support@agenticgovernance.digital" class="text-blue-600 hover:underline">support@agenticgovernance.digital</a> with your email address. We'll process your cancellation within 24 hours.
|
||||
<h3 class="font-semibold text-gray-900 mb-2" data-i18n="faq.manage.question">How do I pause or end my monthly koha?</h3>
|
||||
<p class="text-gray-700" data-i18n="faq.manage.answer">
|
||||
Relationships change, and we understand. You can manage your subscription using the "Manage Your Subscription" section above — just enter your email and you'll be taken to a secure portal where you can update payment methods, pause, or cancel anytime. No questions asked, and with gratitude for the time we shared.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">What currencies do you accept?</h3>
|
||||
<p class="text-gray-700">
|
||||
We accept 10 major currencies: NZD, USD, EUR, GBP, AUD, CAD, JPY, CHF, SGD, and HKD. Prices are automatically converted from our base currency (NZD) using current exchange rates. All donations are tracked in both your chosen currency and NZD for transparency reporting.
|
||||
<h3 class="font-semibold text-gray-900 mb-2" data-i18n="faq.currencies.question">What currencies do you accept?</h3>
|
||||
<p class="text-gray-700" data-i18n="faq.currencies.answer">
|
||||
We accept 10 major currencies: NZD, USD, EUR, GBP, AUD, CAD, JPY, CHF, SGD, and HKD. When you donate, you pay in your chosen currency (e.g., $10 USD), Stripe processes the payment in that currency, and then transfers funds to our NZD account after conversion. Your donation is tracked in both your original currency and NZD equivalent for complete transparency reporting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 mb-2">Can I make a one-time donation instead?</h3>
|
||||
<p class="text-gray-700">
|
||||
Yes! Click the "One-Time Gift" button at the top of the form to make a single donation of any amount.
|
||||
<h3 class="font-semibold text-gray-900 mb-2" data-i18n="faq.one_time.question">Can I offer koha just once rather than monthly?</h3>
|
||||
<p class="text-gray-700" data-i18n="faq.one_time.answer">
|
||||
Absolutely. Both one-time and ongoing koha honor the reciprocal relationship between us. Click "One-Time Koha" at the top of the form to offer a single contribution of any amount.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -319,16 +379,17 @@
|
|||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<script src="/js/components/footer.js"></script>
|
||||
<script src="/js/components/footer.js?v=1.1.4"></script>
|
||||
|
||||
<!-- Currency utilities and selector -->
|
||||
<script src="/js/utils/currency.js"></script>
|
||||
<script src="/js/components/currency-selector.js"></script>
|
||||
|
||||
<!-- Coming Soon Overlay (remove when Stripe is configured) -->
|
||||
<script src="/js/components/coming-soon-overlay.js"></script>
|
||||
<script src="/js/utils/currency.js?v=1.1.4"></script>
|
||||
<script src="/js/components/currency-selector.js?v=1.1.4"></script>
|
||||
|
||||
<!-- Donation form functionality -->
|
||||
<script src="/js/koha-donation.js"></script>
|
||||
<script src="/js/koha-donation.js?v=1.1.4"></script>
|
||||
|
||||
<!-- Internationalization -->
|
||||
<script src="/js/i18n-simple.js?v=1.1.4"></script>
|
||||
<script src="/js/components/language-selector.js?v=1.1.4"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -249,3 +249,59 @@ exports.verifySession = async (req, res) => {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Stripe Customer Portal session
|
||||
* POST /api/koha/portal
|
||||
* Allows donors to manage their subscription (update payment, cancel, etc.)
|
||||
*/
|
||||
exports.createPortalSession = async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Email is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Find customer by email
|
||||
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
||||
const customers = await stripe.customers.list({
|
||||
email: email,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (customers.data.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'No subscription found for this email address'
|
||||
});
|
||||
}
|
||||
|
||||
const customer = customers.data[0];
|
||||
|
||||
// Create portal session
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customer.id,
|
||||
return_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha.html`,
|
||||
});
|
||||
|
||||
logger.info(`[KOHA] Customer portal session created for ${email}`);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
url: session.url
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[KOHA] Create portal session error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to create portal session'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ router.get('/transparency', kohaController.getTransparency);
|
|||
// Rate limited to prevent abuse/guessing of subscription IDs
|
||||
router.post('/cancel', donationLimiter, kohaController.cancelDonation);
|
||||
|
||||
// Create Stripe Customer Portal session
|
||||
// POST /api/koha/portal
|
||||
// Body: { email }
|
||||
// Rate limited to prevent abuse
|
||||
router.post('/portal', donationLimiter, kohaController.createPortalSession);
|
||||
|
||||
// Verify donation session (after Stripe redirect)
|
||||
// GET /api/koha/verify/:sessionId
|
||||
router.get('/verify/:sessionId', kohaController.verifySession);
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ class KohaService {
|
|||
payment_method_types: ['card'],
|
||||
customer: stripeCustomer.id,
|
||||
mode: frequency === 'monthly' ? 'subscription' : 'payment',
|
||||
success_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha/success?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha`,
|
||||
success_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha/success.html?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha.html`,
|
||||
metadata: {
|
||||
frequency: frequency,
|
||||
tier: tier,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue