diff --git a/CLOUDFLARE_DEPLOYMENT.md b/CLOUDFLARE_DEPLOYMENT.md index 76090dc..d811374 100644 --- a/CLOUDFLARE_DEPLOYMENT.md +++ b/CLOUDFLARE_DEPLOYMENT.md @@ -56,6 +56,7 @@ Add these environment variables in Cloudflare Pages dashboard: - `STRIPE_PUBLISHABLE_KEY` - `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` - `STRIPE_WEBHOOK_SECRET` +- `SENDGRID_API_KEY` - For sending notification emails (thank you emails after purchases) ## Important Notes diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c728cf0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/app/api/create-checkout-session/route.ts b/app/api/create-checkout-session/route.ts index 4f0a38b..21094e8 100644 --- a/app/api/create-checkout-session/route.ts +++ b/app/api/create-checkout-session/route.ts @@ -3,8 +3,22 @@ import Stripe from "stripe" export const runtime = "edge" +function getBaseUrl(request: NextRequest): string { + // Use environment variable if set, otherwise construct from headers + if (process.env.NEXT_PUBLIC_BASE_URL) { + return process.env.NEXT_PUBLIC_BASE_URL + } + const host = request.headers.get("host") || request.headers.get("x-forwarded-host") + const protocol = request.headers.get("x-forwarded-proto") || "https" + if (host) { + return `${protocol}://${host}` + } + return "https://alertbaytrumpeter.com" +} + export async function POST(request: NextRequest) { try { + const baseUrl = getBaseUrl(request) const stripeSecretKey = process.env.STRIPE_SECRET_KEY if (!stripeSecretKey) { @@ -60,8 +74,8 @@ export async function POST(request: NextRequest) { quantity: 1, }, ], - success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${request.nextUrl.origin}/cancel`, + success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${baseUrl}/cancel`, }) console.log("[v0] Subscription checkout session created successfully:", session.id) @@ -89,8 +103,8 @@ export async function POST(request: NextRequest) { quantity: 1, }, ], - success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${request.nextUrl.origin}/cancel`, + success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${baseUrl}/cancel`, }) console.log("[v0] Monthly subscription checkout session created successfully:", session.id) @@ -140,8 +154,8 @@ export async function POST(request: NextRequest) { quantity: 1, }, ], - success_url: `${request.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${request.nextUrl.origin}/cancel`, + success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${baseUrl}/cancel`, }) console.log("[v0] Checkout session created successfully:", session.id) diff --git a/app/api/create-portal-session/route.ts b/app/api/create-portal-session/route.ts index bc9007b..9276654 100644 --- a/app/api/create-portal-session/route.ts +++ b/app/api/create-portal-session/route.ts @@ -3,8 +3,21 @@ import Stripe from "stripe" export const runtime = "edge" +function getBaseUrl(request: NextRequest): string { + if (process.env.NEXT_PUBLIC_BASE_URL) { + return process.env.NEXT_PUBLIC_BASE_URL + } + const host = request.headers.get("host") || request.headers.get("x-forwarded-host") + const protocol = request.headers.get("x-forwarded-proto") || "https" + if (host) { + return `${protocol}://${host}` + } + return "https://alertbaytrumpeter.com" +} + export async function POST(request: NextRequest) { try { + const baseUrl = getBaseUrl(request) const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim() if (!stripeSecretKey) { @@ -25,7 +38,7 @@ export async function POST(request: NextRequest) { const portalSession = await stripe.billingPortal.sessions.create({ customer: checkoutSession.customer as string, - return_url: `${request.nextUrl.origin}`, + return_url: baseUrl, }) return NextResponse.json({ url: portalSession.url }) diff --git a/app/api/webhook/route.ts b/app/api/webhook/route.ts index 354069b..fc469bd 100644 --- a/app/api/webhook/route.ts +++ b/app/api/webhook/route.ts @@ -3,6 +3,86 @@ import Stripe from "stripe" export const runtime = "edge" +async function sendThankYouEmail( + customerEmail: string, + customerName: string | null, + amount: number, + currency: string, + productName: string +) { + const sendgridApiKey = process.env.SENDGRID_API_KEY?.trim() + + if (!sendgridApiKey) { + console.log("⚠️ SENDGRID_API_KEY not configured, skipping email") + return + } + + const formattedAmount = new Intl.NumberFormat("en-CA", { + style: "currency", + currency: currency.toUpperCase(), + }).format(amount / 100) + + const name = customerName || "Valued Supporter" + + try { + const response = await fetch("https://api.sendgrid.com/v3/mail/send", { + method: "POST", + headers: { + "Authorization": `Bearer ${sendgridApiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + personalizations: [ + { + to: [{ email: customerEmail, name: name }], + }, + ], + from: { + email: "noreply@alertbaytrumpeter.com", + name: "Alert Bay Trumpeter", + }, + subject: "Thank You for Supporting the Alert Bay Trumpeter!", + content: [ + { + type: "text/html", + value: ` +
+

Thank You, ${name}!

+

Your generous support means the world to Jerry Higginson, the Alert Bay Trumpeter.

+
+

Subscription Details

+

Amount: ${formattedAmount}/month

+

Plan: ${productName}

+
+

Your monthly contribution helps Jerry continue spreading smiles at sea by serenading cruise ship passengers with his trumpet.

+

With over 1,000 nautical serenades since 1996, your support keeps the music playing!

+

+ With gratitude,
+ Jerry Higginson
+ The Alert Bay Trumpeter +

+
+

+ If you have any questions, contact us at alertbaytrumpeter@icloud.com +

+
+ `, + }, + ], + }), + }) + + if (response.ok || response.status === 202) { + console.log(`✅ Thank you email sent to ${customerEmail}`) + } else { + const error = await response.text() + console.error(`❌ Failed to send email: ${error}`) + } + } catch (error) { + console.error("❌ Error sending email:", error) + } +} + export async function POST(request: NextRequest) { try { const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim() @@ -33,12 +113,45 @@ export async function POST(request: NextRequest) { case "checkout.session.completed": const session = event.data.object as Stripe.Checkout.Session console.log(`💰 Payment successful for session: ${session.id}`) - // Handle successful payment here + + // Send thank you email for subscriptions + if (session.mode === "subscription" && session.customer_details?.email) { + // Get subscription details + const subscriptionId = session.subscription as string + if (subscriptionId) { + const subscription = await stripe.subscriptions.retrieve(subscriptionId) + const item = subscription.items.data[0] + const amount = item.price.unit_amount || 0 + const currency = item.price.currency + const productId = item.price.product as string + const product = await stripe.products.retrieve(productId) + + await sendThankYouEmail( + session.customer_details.email, + session.customer_details.name, + amount, + currency, + product.name + ) + } + } break - case "payment_intent.succeeded": - const paymentIntent = event.data.object as Stripe.PaymentIntent - console.log(`💰 Payment succeeded: ${paymentIntent.id}`) + + case "customer.subscription.created": + const newSubscription = event.data.object as Stripe.Subscription + console.log(`🎉 New subscription created: ${newSubscription.id}`) break + + case "invoice.paid": + const invoice = event.data.object as Stripe.Invoice + console.log(`💵 Invoice paid: ${invoice.id}`) + break + + case "customer.subscription.deleted": + const cancelledSubscription = event.data.object as Stripe.Subscription + console.log(`😢 Subscription cancelled: ${cancelledSubscription.id}`) + break + default: console.log(`Unhandled event type ${event.type}`) } diff --git a/app/page.tsx b/app/page.tsx index 5e207e9..c096064 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -50,21 +50,14 @@ export default function HomePage() { try { console.log("[v0] Starting donation process for product:", product.name) - let requestBody: any - - if (product.lookup_key) { - // Use lookup_key for one-time donations (legacy sponsor tiers) - requestBody = { lookup_key: product.lookup_key } - } else { - // Use product info for subscription checkouts - requestBody = { - productId: product.id, - productName: product.name, - price: product.price, - currency: product.currency, - description: product.description, - mode: "subscription", - } + // Always use subscription mode for products fetched from Stripe + const requestBody = { + productId: product.id, + productName: product.name, + price: product.price, + currency: product.currency, + description: product.description, + mode: "subscription", } const response = await fetch("/api/create-checkout-session", { diff --git a/next.config.mjs b/next.config.mjs index f5cbc38..5d9ff6f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', eslint: { ignoreDuringBuilds: true, },