Integrating Fungies with Next.js 15: A Concise Guide
1. Introduction
Fungies is a payment merchant of record platform designed specifically for SaaS developers. It offers a streamlined way to handle payments, subscriptions, and checkout experiences with key benefits including:
No hidden fees pricing structure
Beautiful checkout solutions (Overlay, Embedded, and Hosted options)
Easy integration process
No-code store builder for quick setup
This guide will walk you through integrating Fungies checkout into your Next.js 15 application. We'll use straightforward language and provide practical code examples that you can adapt to your own projects.
2. Getting Started with Fungies & Next.js
Prerequisites
Before beginning the integration process, ensure you have:
Next.js 15 Project: Create one with npx create-next-app@latest my-fungies-app
Use TypeScript (recommended)
Use App Router (to leverage Next.js 15 features)
Fungies Account:
Sign up at
For testing, use the Sandbox environment:
Create a store in your dashboard
Set up at least one product or subscription
Note your API keys (public and secret)
Create a checkout element and note the checkout URL
Development Environment:
Node.js 18.17 or later
npm, yarn, or pnpm package manager
Installation
Install the Fungies JavaScript SDK in your Next.js project:bash
# Using npm
npm install @fungies/fungies-js
# Using yarn
yarn add @fungies/fungies-js
# Using pnpm
pnpm add @fungies/fungies-js
Setup
Environment Variables
Create a .env.local file in your project root:
# .env.local
# For production
NEXT_PUBLIC_FUNGIES_PUBLIC_KEY=pub_your_public_key
FUNGIES_SECRET_KEY=sec_your_secret_key
# For sandbox testing (https://app.stage.fungies.net)
NEXT_PUBLIC_FUNGIES_SANDBOX_PUBLIC_KEY=pub_your_sandbox_key
FUNGIES_SANDBOX_SECRET_KEY=sec_your_sandbox_key
NEXT_PUBLIC_FUNGIES_ENVIRONMENT=sandbox
SDK Initialization
Create a utility file to initialize the Fungies SDK (lib/fungies.ts):typescript
// lib/fungies.ts
import { Fungies } from '@fungies/fungies-js';
// Initialize Fungies on the client side only
export const initFungies = () => {
if (typeof window !== 'undefined') {
Fungies.Initialize({
// Optional: Disable data attribute support if not needed
// enableDataAttributes: false
});
return Fungies;
}
return null;
};
// Helper function to get the Fungies instance
export const getFungies = () => {
if (typeof window !== 'undefined') {
return Fungies;
}
return null;
};
This utility ensures Fungies is only initialized on the client side, preventing server-side rendering errors.
3. Implementing Fungies Checkout
Checkout Options Overview
Fungies offers three main checkout integration options:
Overlay Checkout: Displays in a modal overlay (simplest option)
Embedded Checkout: Displays directly within your page layout
Hosted Checkout: Redirects to a Fungies-hosted checkout page
We'll focus on implementing the Overlay and Embedded options as they provide the best balance of ease and user experience.
Overlay Checkout Implementation
Create a client component for the overlay checkout (components/OverlayCheckoutButton.tsx):typescript
Webhooks allow your application to receive real-time notifications about events in your Fungies account.Create an API route to handle webhook events:typescript
// app/api/webhooks/fungies/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
// This should be stored in your environment variables
const webhookSecret = process.env.FUNGIES_WEBHOOK_SECRET;
export async function POST(request: NextRequest) {
try {
// Get the signature from the headers
const signature = request.headers.get('x-fngs-signature');
if (!signature || !webhookSecret) {
return NextResponse.json(
{ error: 'Missing signature or webhook secret' },
{ status: 401 }
);
}
// Get the raw request body
const rawBody = await request.text();
// Verify the signature
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = hmac.update(rawBody).digest('hex');
if (digest !== signature) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// Parse the webhook payload
const event = JSON.parse(rawBody);
// Handle different event types
switch (event.type) {
case 'payment_success':
await handlePaymentSuccess(event);
break;
case 'subscription_created':
await handleSubscriptionCreated(event);
break;
case 'subscription_cancelled':
await handleSubscriptionCancelled(event);
break;
// Add more event handlers as needed
default:
console.log(`Unhandled event type: ${event.type}`);
}
// Return a 200 response to acknowledge receipt of the webhook
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
// Example event handlers
async function handlePaymentSuccess(event: any) {
// Update your database, send confirmation emails, etc.
console.log('Payment successful:', event.data.id);
}
async function handleSubscriptionCreated(event: any) {
// Provision access to your service, update user status, etc.
console.log('Subscription created:', event.data.id);
}
async function handleSubscriptionCancelled(event: any) {
// Update user status, send retention emails, etc.
console.log('Subscription cancelled:', event.data.id);
}
Security Considerations for Webhooks
Always verify signatures using the webhook secret
Store secrets in environment variables, never hardcode them
Design webhook handlers to be idempotent as Fungies may retry webhook deliveries
Catch and log errors but still return a 200 status to acknowledge receipt
Keep webhook processing quick or move long-running tasks to a background job
5. Integrating with Next.js 15 Features
Server vs. Client Components
Next.js 15 uses React's Server Components by default, but Fungies SDK requires client-side JavaScript.Server Components are great for:
Fetching data from your backend or APIs
Accessing environment variables securely
Rendering static content
Client Components are necessary for:
Interacting with the Fungies SDK
Handling user interactions like button clicks
Managing local state
Here's a pattern for combining both:tsx
// app/products/[id]/page.tsx (Server Component)
import { Suspense } from 'react';
import ProductDetails from './ProductDetails';
import CheckoutSection from './CheckoutSection';
import { getProductById } from '@/lib/products';
export default async function ProductPage({ params }: { params: { id: string } }) {
// Fetch product data on the server
const product = await getProductById(params.id);
return (
<div className="max-w-6xl mx-auto p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<Suspense fallback={<div>Loading product details...</div>}>
{/* Server Component for product details */}
<ProductDetails product={product} />
</Suspense>
<Suspense fallback={<div>Loading checkout options...</div>}>
{/* Client Component for Fungies integration */}
<CheckoutSection
productId={product.id}
price={product.price}
checkoutUrl={product.checkoutUrl}
/>
</Suspense>
</div>
</div>
);
}
tsx
// app/products/[id]/CheckoutSection.tsx (Client Component)
'use client';
import { useState, useEffect } from 'react';
import { initFungies, getFungies } from '@/lib/fungies';
export default function CheckoutSection({
productId,
price,
checkoutUrl
}) {
// Client-side code for Fungies integration
// ...
}
Server Actions
Next.js 15 includes Server Actions, which allow you to run server-side code from client components. This is perfect for operations that require your secret API key.typescript
// app/actions/checkout.ts
'use server';
// This function runs on the server and can access environment variables securely
export async function createCheckoutSession(formData: FormData) {
const productId = formData.get('productId') as string;
const quantity = parseInt(formData.get('quantity') as string, 10);
if (!productId || isNaN(quantity)) {
throw new Error('Invalid product or quantity');
}
try {
// Use your secret API key securely on the server
const apiKey = process.env.FUNGIES_SECRET_KEY;
// Make API request to Fungies to create a checkout session
const response = await fetch('https://api.fungies.io/v0/checkout-sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-fngs-public-key': process.env.NEXT_PUBLIC_FUNGIES_PUBLIC_KEY!,
'x-fngs-secret-key': apiKey!,
},
body: JSON.stringify({
productId,
quantity,
successUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/checkout/success`,
cancelUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/checkout/cancel`,
}) ,
});
if (!response.ok) {
throw new Error('Failed to create checkout session');
}
const data = await response.json();
return { checkoutUrl: data.url };
} catch (error) {
console.error('Error creating checkout session:', error);
throw new Error('Failed to create checkout session');
}
}
For long-running webhook tasks, use a background process:typescript
// app/api/webhooks/fungies/route.ts
export async function POST(request: NextRequest) {
try {
// Verify the webhook (code omitted for brevity)
// Get the event data
const event = await request.json();
// For long-running tasks, use a background process
await enqueueWebhookProcessing(event);
// Return a 200 response immediately
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
// Function to enqueue webhook processing
async function enqueueWebhookProcessing(event: any) {
// This could be a message queue, database insert, etc.
console.log('Enqueueing webhook processing for event:', event.id);
}
6. Testing & Deployment
Testing with Fungies Sandbox
Fungies provides a Sandbox environment for testing:
Set up environment variables to switch between environments:typescript
Use this configuration in your API calls:typescript
// Example server action using the environment config
export async function createCheckoutSession(formData: FormData) {
const config = getFungiesConfig();
// This will use https://api.stage.fungies.net/v0/checkout-sessions in sandbox mode
// or https://api.fungies.io/v0/checkout-sessions in production
const response = await fetch(`${config.apiBase}/checkout-sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-fngs-public-key': config.publicKey!,
'x-fngs-secret-key': config.secretKey!,
},
// ...
}) ;
// ...
}
Debugging Common Issues
SDK Not Loading
typescript
// Debug helper to check if SDK is loaded
export const debugFungiesSDK = () => {
if (typeof window === 'undefined') {
console.log('Running on server - Fungies SDK not available');
return false;
}
if (typeof Fungies === 'undefined') {
console.error('Fungies SDK not loaded correctly');
return false;
}
console.log('Fungies SDK loaded successfully');
return true;
};
Webhook Verification Issues
typescript
// app/api/webhooks/fungies/debug/route.ts
export async function POST(request: NextRequest) {
try {
// Get the signature from the headers
const signature = request.headers.get('x-fngs-signature');
const webhookSecret = process.env.FUNGIES_WEBHOOK_SECRET;
console.log('Received webhook with signature:', signature);
console.log('Using webhook secret (first 4 chars):', webhookSecret?.substring(0, 4));
// Get the raw request body
const rawBody = await request.text();
// Verify the signature
const hmac = crypto.createHmac('sha256', webhookSecret || '');
const digest = hmac.update(rawBody).digest('hex');
console.log('Calculated signature:', digest);
console.log('Signatures match:', digest === signature);
// Return debug info (only in development!)
if (process.env.NODE_ENV === 'development') {
return NextResponse.json({
received: true,
signatureProvided: signature,
signatureCalculated: digest,
match: digest === signature,
});
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook debug error:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
Deployment
Environment Variables in Production
For Vercel deployments, add these environment variables in the Vercel dashboard: