feat: swap Stripe for Mollie payment integration

Replace Stripe checkout with Mollie payments API. Mollie handles
payment method selection on their hosted checkout (card, SEPA, iDEAL,
PayPal, etc). Simpler auth model — single API key, no webhook secrets.

- Rewrite /api/create-checkout-session for Mollie payment creation
- Rewrite /api/webhook for Mollie status verification flow
- Update google-sheets.ts: stripeSessionId → paymentSessionId
- Remove payment method radio buttons (Mollie shows all methods)
- Update docker-compose env vars
- Swap stripe npm package for @mollie/api-client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-04 14:30:55 -08:00
parent 3b63d62b95
commit 8f59233918
9 changed files with 249 additions and 254 deletions

View File

@ -1,9 +1,7 @@
# Stripe Payment Integration
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxx
# Mollie Payment Integration
MOLLIE_API_KEY=live_xxx
# Public URL (used for Stripe redirect URLs)
# Public URL (used for payment redirect URLs)
NEXT_PUBLIC_BASE_URL=https://cryptocommonsgather.ing
# Google Sheets Integration

View File

@ -1,138 +1,86 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
import createMollieClient from "@mollie/api-client"
// Lazy initialization to avoid build-time errors
let stripe: Stripe | null = null
let mollieClient: ReturnType<typeof createMollieClient> | null = null
function getStripe() {
if (!stripe) {
stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
})
function getMollie() {
if (!mollieClient) {
mollieClient = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY! })
}
return stripe
return mollieClient
}
// Dynamic pricing configuration (in EUR cents)
const TICKET_PRICE_CENTS = 8000 // €80 early bird
// Dynamic pricing configuration (in EUR)
const TICKET_PRICE = 80 // €80 early bird
const DORM_PRICE = 235.2 // €235.20 (€39.20/night x 6)
const DOUBLE_PRICE = 301.2 // €301.20 (€50.20/night x 6)
const FOOD_PRICE = 135 // €135 (6 days)
// Public base URL (needed because request.nextUrl.origin returns internal Docker address)
// Public base URL
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || "https://cryptocommonsgather.ing"
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const paymentMethod = formData.get("paymentMethod") as string
const registrationDataStr = formData.get("registrationData") as string
const includeAccommodation = formData.get("includeAccommodation") === "true"
const accommodationType = formData.get("accommodationType") as string || "dorm"
const accommodationType = (formData.get("accommodationType") as string) || "dorm"
const includeFood = formData.get("includeFood") === "true"
const registrationData = registrationDataStr ? JSON.parse(registrationDataStr) : null
// Accommodation pricing (in EUR cents)
const DORM_PRICE_CENTS = 23520 // €235.20 (€39.20/night x 6)
const DOUBLE_PRICE_CENTS = 30120 // €301.20 (€50.20/night x 6)
const FOOD_PRICE_CENTS = 13500 // €135 (6 days)
// Build line items dynamically
const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [
{
price_data: {
currency: "eur",
product_data: {
name: "CCG 2026 Ticket",
description: "Crypto Commons Gathering 2026 - August 16-22, Austria",
},
unit_amount: TICKET_PRICE_CENTS,
},
quantity: 1,
},
]
// Calculate total
let total = TICKET_PRICE
const descriptionParts = ["CCG 2026 Ticket (€80)"]
if (includeAccommodation) {
const isDorm = accommodationType === "dorm"
lineItems.push({
price_data: {
currency: "eur",
product_data: {
name: isDorm ? "Accommodation - Dorm (6 nights)" : "Accommodation - Double Room (6 nights)",
description: `Commons Hub, ${isDorm ? "dorm bed" : "double room"}${isDorm ? "€39.20" : "€50.20"}/night`,
},
unit_amount: isDorm ? DORM_PRICE_CENTS : DOUBLE_PRICE_CENTS,
},
quantity: 1,
})
const accomPrice = isDorm ? DORM_PRICE : DOUBLE_PRICE
total += accomPrice
descriptionParts.push(`${isDorm ? "Dorm" : "Double Room"} (€${accomPrice.toFixed(2)})`)
}
if (includeFood) {
lineItems.push({
price_data: {
currency: "eur",
product_data: {
name: "Food Package (6 days)",
description: "Breakfast, catered lunches & dinners, coffee & tea",
},
unit_amount: FOOD_PRICE_CENTS,
},
quantity: 1,
})
total += FOOD_PRICE
descriptionParts.push(`Food Package (€${FOOD_PRICE})`)
}
let paymentMethodTypes: Stripe.Checkout.SessionCreateParams.PaymentMethodType[] = ["card"]
if (paymentMethod === "sepa_debit") {
paymentMethodTypes = ["sepa_debit"]
} else if (paymentMethod === "crypto") {
paymentMethodTypes = ["customer_balance"]
// Build metadata for webhook
const metadata: Record<string, string> = {}
if (registrationData) {
metadata.name = registrationData.name || ""
metadata.contact = registrationData.contact || ""
metadata.contributions = (registrationData.contributions || "").substring(0, 500)
metadata.expectations = (registrationData.expectations || "").substring(0, 500)
metadata.howHeard = registrationData.howHeard || ""
metadata.dietary =
(registrationData.dietary || []).join(", ") +
(registrationData.dietaryOther ? `, ${registrationData.dietaryOther}` : "")
metadata.crewConsent = registrationData.crewConsent || ""
metadata.accommodation = includeAccommodation ? accommodationType : "none"
metadata.food = includeFood ? "yes" : "no"
}
const session = await getStripe().checkout.sessions.create({
payment_method_types: paymentMethodTypes,
line_items: lineItems,
mode: "payment",
success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/register`,
metadata: registrationData
? {
name: registrationData.name,
contact: registrationData.contact,
contributions: registrationData.contributions.substring(0, 500),
expectations: registrationData.expectations.substring(0, 500),
howHeard: registrationData.howHeard || "",
dietary:
registrationData.dietary.join(", ") +
(registrationData.dietaryOther ? `, ${registrationData.dietaryOther}` : ""),
crewConsent: registrationData.crewConsent,
accommodation: includeAccommodation ? accommodationType : "none",
food: includeFood ? "yes" : "no",
}
: {},
...(paymentMethod === "crypto" && {
payment_method_options: {
customer_balance: {
funding_type: "bank_transfer",
bank_transfer: {
type: "us_bank_account",
},
},
},
}),
allow_promotion_codes: true,
billing_address_collection: "required",
phone_number_collection: {
enabled: true,
const payment = await getMollie().payments.create({
amount: {
value: total.toFixed(2),
currency: "EUR",
},
description: `CCG 2026 Registration — ${descriptionParts.join(" + ")}`,
redirectUrl: `${BASE_URL}/success`,
webhookUrl: `${BASE_URL}/api/webhook`,
metadata,
})
// Use 303 redirect for POST requests (tells browser to follow with GET)
// Redirect to Mollie checkout
return new Response(null, {
status: 303,
headers: { Location: session.url! },
headers: { Location: payment.getCheckoutUrl()! },
})
} catch (err) {
console.error("Error creating checkout session:", err)
return NextResponse.json({ error: "Error creating checkout session" }, { status: 500 })
console.error("Error creating Mollie payment:", err)
return NextResponse.json({ error: "Error creating payment" }, { status: 500 })
}
}

View File

@ -1,59 +1,48 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
import createMollieClient from "@mollie/api-client"
import { updatePaymentStatus } from "@/lib/google-sheets"
import { sendPaymentConfirmation } from "@/lib/email"
import { addToListmonk } from "@/lib/listmonk"
// Lazy initialization to avoid build-time errors
let stripe: Stripe | null = null
// Lazy initialization
let mollieClient: ReturnType<typeof createMollieClient> | null = null
function getStripe() {
if (!stripe) {
stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
})
function getMollie() {
if (!mollieClient) {
mollieClient = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY! })
}
return stripe
}
function getWebhookSecret() {
return process.env.STRIPE_WEBHOOK_SECRET!
return mollieClient
}
export async function POST(request: NextRequest) {
try {
const body = await request.text()
const signature = request.headers.get("stripe-signature")!
// Mollie sends payment ID in the body as form data
const formData = await request.formData()
const paymentId = formData.get("id") as string
let event: Stripe.Event
try {
event = getStripe().webhooks.constructEvent(body, signature, getWebhookSecret())
} catch (err) {
console.error("[Webhook] Signature verification failed:", err)
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
if (!paymentId) {
console.error("[Webhook] No payment ID received")
return NextResponse.json({ error: "Missing payment ID" }, { status: 400 })
}
// Handle the event
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session
console.log("[Webhook] Payment successful:", session.id)
// Fetch the full payment from Mollie API (this is how you verify — no signature needed)
const payment = await getMollie().payments.get(paymentId)
const metadata = (payment.metadata || {}) as Record<string, string>
// Extract registration data from metadata
const metadata = session.metadata || {}
const customerEmail = session.customer_details?.email || ""
console.log(`[Webhook] Payment ${paymentId} status: ${payment.status}`)
// Update Google Sheet with payment confirmation
if (payment.status === "paid") {
const customerEmail = payment.billingAddress?.email || ""
const amountPaid = `${payment.amount.value}`
// Update Google Sheet
const updated = await updatePaymentStatus({
name: metadata.name || "",
email: customerEmail,
stripeSessionId: session.id,
paymentSessionId: paymentId,
paymentStatus: "Paid",
paymentMethod: session.payment_method_types?.[0] || "unknown",
amountPaid: session.amount_total
? `${(session.amount_total / 100).toFixed(2)}`
: "",
paymentMethod: payment.method || "unknown",
amountPaid,
paymentDate: new Date().toISOString(),
})
@ -63,15 +52,13 @@ export async function POST(request: NextRequest) {
console.error(`[Webhook] Failed to update Google Sheet for ${metadata.name}`)
}
// Send payment confirmation email
// Send confirmation email
if (customerEmail) {
await sendPaymentConfirmation({
name: metadata.name || "",
email: customerEmail,
amountPaid: session.amount_total
? `${(session.amount_total / 100).toFixed(2)}`
: "",
paymentMethod: session.payment_method_types?.[0] || "card",
amountPaid,
paymentMethod: payment.method || "card",
contributions: metadata.contributions || "",
dietary: metadata.dietary || "",
})
@ -87,35 +74,20 @@ export async function POST(request: NextRequest) {
},
}).catch((err) => console.error("[Webhook] Listmonk sync failed:", err))
}
} else if (payment.status === "failed" || payment.status === "canceled" || payment.status === "expired") {
console.log(`[Webhook] Payment ${payment.status}: ${paymentId}`)
break
}
case "payment_intent.succeeded": {
const paymentIntent = event.data.object as Stripe.PaymentIntent
console.log("[Webhook] PaymentIntent successful:", paymentIntent.id)
break
}
case "payment_intent.payment_failed": {
const paymentIntent = event.data.object as Stripe.PaymentIntent
console.error("[Webhook] Payment failed:", paymentIntent.id)
// Optionally update sheet to mark as failed
const failedMetadata = paymentIntent.metadata || {}
if (failedMetadata.name) {
if (metadata.name) {
await updatePaymentStatus({
name: failedMetadata.name,
stripeSessionId: paymentIntent.id,
name: metadata.name,
paymentSessionId: paymentId,
paymentStatus: "Failed",
paymentDate: new Date().toISOString(),
})
}
break
}
default:
console.log("[Webhook] Unhandled event type:", event.type)
}
// Mollie expects 200 OK
return NextResponse.json({ received: true })
} catch (err) {
console.error("[Webhook] Error:", err)

View File

@ -258,34 +258,10 @@ export default function RegisterPage() {
<input type="hidden" name="includeFood" value={includeFood ? "true" : "false"} />
<div className="space-y-4">
<div>
<Label className="text-base font-semibold mb-3 block">Select Payment Method</Label>
<RadioGroup name="paymentMethod" defaultValue="card" className="space-y-3">
<div className="flex items-center space-x-2 border rounded-lg p-4 hover:border-primary transition-colors">
<RadioGroupItem value="card" id="card" />
<Label htmlFor="card" className="flex-1 cursor-pointer">
<div className="font-medium">Credit Card</div>
<div className="text-sm text-muted-foreground">Visa, Mastercard, Amex</div>
</Label>
</div>
<div className="flex items-center space-x-2 border rounded-lg p-4 hover:border-primary transition-colors">
<RadioGroupItem value="sepa_debit" id="sepa" />
<Label htmlFor="sepa" className="flex-1 cursor-pointer">
<div className="font-medium">SEPA Bank Transfer</div>
<div className="text-sm text-muted-foreground">Direct debit from European banks</div>
</Label>
</div>
<div className="flex items-center space-x-2 border rounded-lg p-4 hover:border-primary transition-colors border-primary/30">
<RadioGroupItem value="crypto" id="crypto" />
<Label htmlFor="crypto" className="flex-1 cursor-pointer">
<div className="font-medium">Cryptocurrency</div>
<div className="text-sm text-muted-foreground">USDC Stablecoin (settles as EUR)</div>
</Label>
</div>
</RadioGroup>
</div>
<p className="text-sm text-muted-foreground">
You'll be redirected to Mollie's secure checkout where you can pay by credit card,
SEPA bank transfer, iDEAL, PayPal, or other methods.
</p>
<div className="flex gap-3 pt-4">
<Button type="button" variant="outline" onClick={() => setStep("form")} className="flex-1">
@ -302,7 +278,7 @@ export default function RegisterPage() {
<div className="text-center text-sm text-muted-foreground">
<p>
All payments are processed securely through Stripe. You'll receive a confirmation email after successful
All payments are processed securely through Mollie. You'll receive a confirmation email after successful
payment.
</p>
</div>

View File

@ -5,9 +5,7 @@ services:
restart: unless-stopped
environment:
- NODE_ENV=production
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
- MOLLIE_API_KEY=${MOLLIE_API_KEY}
- NEXT_PUBLIC_BASE_URL=https://staging-ccg.jeffemmett.com
- GOOGLE_SERVICE_ACCOUNT_KEY=${GOOGLE_SERVICE_ACCOUNT_KEY}
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}

View File

@ -5,9 +5,7 @@ services:
restart: unless-stopped
environment:
- NODE_ENV=production
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
- MOLLIE_API_KEY=${MOLLIE_API_KEY}
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-https://cryptocommonsgather.ing}
- GOOGLE_SERVICE_ACCOUNT_KEY=${GOOGLE_SERVICE_ACCOUNT_KEY}
- GOOGLE_SHEET_ID=${GOOGLE_SHEET_ID}

View File

@ -29,7 +29,7 @@ export interface RegistrationData {
export interface PaymentUpdateData {
email?: string
name?: string
stripeSessionId: string
paymentSessionId: string
paymentStatus: "Paid" | "Failed"
paymentMethod?: string
amountPaid?: string
@ -131,7 +131,7 @@ export async function updatePaymentStatus(data: PaymentUpdateData): Promise<bool
rows[targetRowIndex - 1][8], // I: Crew Consent (preserve)
data.paymentStatus, // J: Payment Status
data.paymentMethod || "", // K: Payment Method
data.stripeSessionId, // L: Stripe Session ID
data.paymentSessionId, // L: Stripe Session ID
data.amountPaid || "", // M: Amount Paid
data.paymentDate || new Date().toISOString(), // N: Payment Date
],
@ -180,7 +180,7 @@ export async function initializeSheetHeaders(): Promise<void> {
"Crew Consent",
"Payment Status",
"Payment Method",
"Stripe Session ID",
"Payment Session ID",
"Amount Paid",
"Payment Date",
],

View File

@ -10,6 +10,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@mollie/api-client": "^4.4.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
@ -58,7 +59,6 @@
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"stripe": "latest",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",

View File

@ -11,6 +11,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.10.0
version: 3.10.0(react-hook-form@7.69.0(react@19.2.0))
'@mollie/api-client':
specifier: ^4.4.0
version: 4.4.0
'@radix-ui/react-accordion':
specifier: 1.2.2
version: 1.2.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -155,9 +158,6 @@ importers:
sonner:
specifier: ^1.7.4
version: 1.7.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
stripe:
specifier: latest
version: 20.1.0(@types/node@22.19.3)
tailwind-merge:
specifier: ^2.5.5
version: 2.6.0
@ -411,6 +411,10 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mollie/api-client@4.4.0':
resolution: {integrity: sha512-V2OKBGS4TUKpbARV4JSjjtXqfRqfTQ5KCKGudEFEOEh/yQxrM2zXhkoq2gIbA6nGIVPh5nVaAzqLhp4C+e4aMQ==}
engines: {node: '>=8'}
'@next/env@16.0.10':
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
@ -1375,6 +1379,9 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node@22.19.3':
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
@ -1416,6 +1423,9 @@ packages:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.4.23:
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
engines: {node: ^10 || ^12 || >=14}
@ -1481,6 +1491,10 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -1554,6 +1568,10 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@ -1612,6 +1630,10 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@ -1634,6 +1656,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
@ -1699,6 +1725,10 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@ -1839,6 +1869,14 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@ -1887,6 +1925,15 @@ packages:
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -2083,6 +2130,9 @@ packages:
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
hasBin: true
ruply@1.0.1:
resolution: {integrity: sha512-p39LnaaJyuucPGlgaB0KiyifpcuOkn24+Hq5y0ejAD/LlH+mRAbkHn2tckCLgHir+S+nis1WYG+TYEC4zHX0WQ==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@ -2156,15 +2206,6 @@ packages:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
stripe@20.1.0:
resolution: {integrity: sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==}
engines: {node: '>=16'}
peerDependencies:
'@types/node': '>=16'
peerDependenciesMeta:
'@types/node':
optional: true
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@ -2196,6 +2237,9 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -2257,6 +2301,12 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -2436,6 +2486,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mollie/api-client@4.4.0':
dependencies:
'@types/node-fetch': 2.6.13
node-fetch: 2.7.0
ruply: 1.0.1
transitivePeerDependencies:
- encoding
'@next/env@16.0.10': {}
'@next/swc-darwin-arm64@16.0.10':
@ -3357,6 +3415,11 @@ snapshots:
'@types/d3-timer@3.0.2': {}
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 22.19.3
form-data: 4.0.5
'@types/node@22.19.3':
dependencies:
undici-types: 6.21.0
@ -3395,6 +3458,8 @@ snapshots:
dependencies:
tslib: 2.8.1
asynckit@0.4.0: {}
autoprefixer@10.4.23(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
@ -3464,6 +3529,10 @@ snapshots:
color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -3522,6 +3591,8 @@ snapshots:
decimal.js-light@2.5.1: {}
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@ -3574,6 +3645,13 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
escalade@3.2.0: {}
eventemitter3@4.0.7: {}
@ -3592,6 +3670,14 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
@ -3690,6 +3776,10 @@ snapshots:
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@ -3804,6 +3894,12 @@ snapshots:
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@ -3844,6 +3940,10 @@ snapshots:
node-domexception@1.0.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
@ -4036,6 +4136,8 @@ snapshots:
dependencies:
glob: 10.5.0
ruply@1.0.1: {}
safe-buffer@5.2.1: {}
scheduler@0.27.0: {}
@ -4140,12 +4242,6 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
stripe@20.1.0(@types/node@22.19.3):
dependencies:
qs: 6.14.0
optionalDependencies:
'@types/node': 22.19.3
styled-jsx@5.1.6(react@19.2.0):
dependencies:
client-only: 0.0.1
@ -4163,6 +4259,8 @@ snapshots:
tiny-invariant@1.3.3: {}
tr46@0.0.3: {}
tslib@2.8.1: {}
tw-animate-css@1.3.3: {}
@ -4226,6 +4324,13 @@ snapshots:
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0