Skip to main content

Next.js Integration Guide

Complete guide for integrating Loyalteez with Next.js applications, including deployment to Cloudflare Pages, Vercel, and other platforms.

🎯 What You'll Build: A Next.js SaaS application with crypto rewards for user actions (newsletter signup, profile completion, subscription upgrades).

Live Demo: saas-demo.loyalteez.app
Source Code: GitHub Repository


Quick Start

Time: 15 minutes
Difficulty: ⭐⭐⭐ Easy

1. Install Dependencies

npm install next react react-dom

2. Configure Environment

Create .env.local:

NEXT_PUBLIC_BRAND_ID=0xYourBrandWalletAddress

3. Track Events

// In your component
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.NEXT_PUBLIC_BRAND_ID,
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: 'yourdomain.com',
sourceUrl: 'https://yourdomain.com',
}),
});

Integration Patterns

Best for: Next.js on Cloudflare Pages, Vercel, or any static hosting

Why: Simple, reliable, no server-side complexity

Example:

'use client';

import { useState } from 'react';

export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');

const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');

try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase(),
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: window.location.hostname,
sourceUrl: window.location.href,
}),
});

const data = await res.json();
if (res.ok && data.success) {
setStatus('success');
}
} catch (err) {
console.error('Subscription failed:', err);
}
};

return (
<form onSubmit={handleSubscribe}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button disabled={status === 'loading'}>
{status === 'loading' ? 'Subscribing...' : 'Subscribe'}
</button>
</form>
);
}

Advantages:

  • ✅ Works on all hosting platforms
  • ✅ No server-side code needed
  • ✅ Simple error handling
  • ✅ Brand ID is public anyway (wallet address)

Pattern 2: Server-Side API Route (Optional)

Best for: When you need to hide Brand ID or add server-side validation

⚠️ Important: Edge Runtime API routes don't work well on Cloudflare Pages. Use Node.js runtime instead.

Example:

// app/api/track-event/route.ts
import { NextResponse } from 'next/server';

export const runtime = 'nodejs'; // NOT 'edge' for Cloudflare Pages

export async function POST(request: Request) {
const { eventType, userEmail, metadata } = await request.json();

const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: process.env.BRAND_ID, // Server-side only
eventType,
userEmail,
userIdentifier: userEmail,
domain: 'yourdomain.com',
sourceUrl: 'https://yourdomain.com',
metadata,
}),
});

const data = await res.json();
return NextResponse.json(data);
}

Client-side call:

const res = await fetch('/api/track-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventType: 'newsletter_subscribe',
userEmail: email,
}),
});

Cloudflare Pages Deployment

Configuration

1. Set Environment Variables:

In Cloudflare Pages Dashboard → Settings → Environment Variables:

NEXT_PUBLIC_BRAND_ID=0xYourBrandWalletAddress

2. Configure Build:

  • Build command: npx @cloudflare/next-on-pages@1
  • Build output directory: .vercel/output/static
  • Root directory: (leave empty)

3. Add wrangler.toml:

name = "your-app-name"
compatibility_date = "2024-04-05"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"

4. Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
// No special config needed for Cloudflare Pages
};

module.exports = nextConfig;

Important Notes

  • Don't use Edge Runtime for API routes on Cloudflare Pages
  • Use client-side direct API calls instead (Pattern 1)
  • Brand ID must be lowercase (brandId.toLowerCase())
  • Domain must be whitelisted in Partner Portal → Settings → Domain Configuration

Error Handling Best Practices

1. Timeout Handling

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 seconds

try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
// ... handle response
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
// Handle timeout
}
} finally {
clearTimeout(timeoutId);
}

2. CORS Error Handling

try {
const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

if (!res.ok) {
const data = await res.json();
throw new Error(data.error || `HTTP ${res.status}`);
}
} catch (err) {
if (err instanceof TypeError && err.message.includes('fetch')) {
// Network/CORS error
console.error('Network error - check CORS configuration');
}
}

3. Response Validation

const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await res.text();
throw new Error(`Invalid response: ${text}`);
}

const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Request failed');
}

Brand ID Format

Important: Brand ID must be a valid Ethereum address and lowercase.

// ✅ Correct
const brandId = process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase();
// Example: "0x5242b6db88a72752ac5a54cfe6a7db8244d743c9"

// ❌ Wrong
const brandId = process.env.NEXT_PUBLIC_BRAND_ID; // May have uppercase

Validation:

if (!brandId || !brandId.startsWith('0x') || brandId.length !== 42) {
throw new Error('Invalid Brand ID format');
}

Domain Configuration

Required: Add your domain to Partner Portal before deploying.

  1. Log into Partner Portal
  2. Navigate to Settings → Domain Configuration
  3. Add your domain: yourdomain.com
  4. Save configuration

Why: The API validates that requests come from authorized domains for security.


Complete Example: Newsletter Signup

'use client';

import { useState } from 'react';

export default function NewsletterForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [reward, setReward] = useState<number | null>(null);
const [errorMessage, setErrorMessage] = useState<string>('');

const handleSubscribe = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('loading');
setErrorMessage('');

try {
const brandId = process.env.NEXT_PUBLIC_BRAND_ID?.toLowerCase();

if (!brandId || !brandId.startsWith('0x') || brandId.length !== 42) {
throw new Error('Invalid Brand ID configuration');
}

const payload = {
brandId,
eventType: 'newsletter_subscribe',
userEmail: email,
userIdentifier: email,
domain: window.location.hostname,
sourceUrl: window.location.href,
metadata: {
source: 'newsletter_form',
timestamp: new Date().toISOString(),
},
};

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);

const res = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});

clearTimeout(timeoutId);

const contentType = res.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error(`Server error: ${res.status}`);
}

const data = await res.json();

if (res.ok && data.success) {
setStatus('success');
setReward(data.ltzDistributed || data.rewardAmount || 25);
} else {
setStatus('error');
setErrorMessage(data.error || 'Subscription failed');
}
} catch (err) {
setStatus('error');
if (err instanceof DOMException && err.name === 'AbortError') {
setErrorMessage('Request timed out. Please try again.');
} else if (err instanceof TypeError) {
setErrorMessage('Network error. Please check your connection.');
} else {
setErrorMessage(err instanceof Error ? err.message : 'An error occurred');
}
}
};

return (
<form onSubmit={handleSubscribe}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={status === 'success'}
/>
<button disabled={status === 'loading' || status === 'success'}>
{status === 'loading' ? 'Subscribing...' : status === 'success' ? 'Subscribed!' : 'Subscribe'}
</button>

{status === 'success' && reward && (
<p>✅ You earned {reward} LTZ points!</p>
)}

{status === 'error' && (
<p className="error">{errorMessage}</p>
)}
</form>
);
}

Common Event Types

Event TypeDefault RewardUse Case
newsletter_subscribe25 LTZNewsletter signup
account_creation100 LTZUser registration
profile_completed50 LTZProfile completion
subscription_upgrade200 LTZPaid plan upgrade
form_submit10 LTZGeneric form submission
complete_survey75 LTZSurvey completion

Troubleshooting

CORS Errors

Error: Access to fetch has been blocked by CORS policy

Solution:

  1. Ensure your domain is added in Partner Portal → Settings → Domain Configuration
  2. Contact support to whitelist your domain in the API CORS configuration

Connection Reset

Error: ERR_CONNECTION_RESET or Failed to fetch

Solutions:

  1. Verify Brand ID format (must be lowercase Ethereum address)
  2. Check domain is configured in Partner Portal
  3. Add timeout handling (see Error Handling section)
  4. Verify network connectivity

Invalid Event Data

Error: Invalid event data or Missing required field

Solution:

  1. Ensure brandId, eventType, and userEmail are provided
  2. Verify brandId is lowercase: brandId.toLowerCase()
  3. Check domain and sourceUrl are valid URLs


Next Steps

  1. Set up environment variables
  2. Add domain to Partner Portal
  3. Implement event tracking
  4. Test with real users
  5. Deploy to production

Need Help? Join our Discord or email [email protected]