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:
parent
3b63d62b95
commit
8f59233918
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +1,93 @@
|
|||
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
|
||||
const updated = await updatePaymentStatus({
|
||||
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,
|
||||
paymentSessionId: paymentId,
|
||||
paymentStatus: "Paid",
|
||||
paymentMethod: payment.method || "unknown",
|
||||
amountPaid,
|
||||
paymentDate: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (updated) {
|
||||
console.log(`[Webhook] Google Sheet updated for ${metadata.name}`)
|
||||
} else {
|
||||
console.error(`[Webhook] Failed to update Google Sheet for ${metadata.name}`)
|
||||
}
|
||||
|
||||
// Send confirmation email
|
||||
if (customerEmail) {
|
||||
await sendPaymentConfirmation({
|
||||
name: metadata.name || "",
|
||||
email: customerEmail,
|
||||
stripeSessionId: session.id,
|
||||
paymentStatus: "Paid",
|
||||
paymentMethod: session.payment_method_types?.[0] || "unknown",
|
||||
amountPaid: session.amount_total
|
||||
? `€${(session.amount_total / 100).toFixed(2)}`
|
||||
: "",
|
||||
paymentDate: new Date().toISOString(),
|
||||
amountPaid,
|
||||
paymentMethod: payment.method || "card",
|
||||
contributions: metadata.contributions || "",
|
||||
dietary: metadata.dietary || "",
|
||||
})
|
||||
|
||||
if (updated) {
|
||||
console.log(`[Webhook] Google Sheet updated for ${metadata.name}`)
|
||||
} else {
|
||||
console.error(`[Webhook] Failed to update Google Sheet for ${metadata.name}`)
|
||||
}
|
||||
|
||||
// Send payment 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",
|
||||
contributions: metadata.contributions || "",
|
||||
dietary: metadata.dietary || "",
|
||||
})
|
||||
|
||||
// Add to Listmonk newsletter
|
||||
addToListmonk({
|
||||
email: customerEmail,
|
||||
name: metadata.name || "",
|
||||
attribs: {
|
||||
contact: metadata.contact,
|
||||
contributions: metadata.contributions,
|
||||
expectations: metadata.expectations,
|
||||
},
|
||||
}).catch((err) => console.error("[Webhook] Listmonk sync failed:", err))
|
||||
}
|
||||
|
||||
break
|
||||
// Add to Listmonk newsletter
|
||||
addToListmonk({
|
||||
email: customerEmail,
|
||||
name: metadata.name || "",
|
||||
attribs: {
|
||||
contact: metadata.contact,
|
||||
contributions: metadata.contributions,
|
||||
expectations: metadata.expectations,
|
||||
},
|
||||
}).catch((err) => console.error("[Webhook] Listmonk sync failed:", err))
|
||||
}
|
||||
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)
|
||||
} else if (payment.status === "failed" || payment.status === "canceled" || payment.status === "expired") {
|
||||
console.log(`[Webhook] Payment ${payment.status}: ${paymentId}`)
|
||||
|
||||
// Optionally update sheet to mark as failed
|
||||
const failedMetadata = paymentIntent.metadata || {}
|
||||
if (failedMetadata.name) {
|
||||
await updatePaymentStatus({
|
||||
name: failedMetadata.name,
|
||||
stripeSessionId: paymentIntent.id,
|
||||
paymentStatus: "Failed",
|
||||
paymentDate: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
if (metadata.name) {
|
||||
await updatePaymentStatus({
|
||||
name: metadata.name,
|
||||
paymentSessionId: paymentId,
|
||||
paymentStatus: "Failed",
|
||||
paymentDate: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
141
pnpm-lock.yaml
141
pnpm-lock.yaml
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue