Skip to content

Commit 98c6431

Browse files
authored
Merge pull request #38 from NillionNetwork/feat/utm
feat: utm tracking for signups
2 parents 7b22d3a + 25fdfae commit 98c6431

File tree

9 files changed

+260
-2
lines changed

9 files changed

+260
-2
lines changed

src/app/api/createUser/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,23 @@ export async function POST(request: NextRequest) {
4949
}
5050

5151
const creationTime = new Date().toISOString();
52+
53+
// Get UTM parameters from request body if provided
54+
let utmData: USER_SCHEMA["utm"];
55+
try {
56+
const body = await request.json();
57+
if (body.utm && Object.keys(body.utm).length > 0) {
58+
utmData = body.utm;
59+
}
60+
} catch (_error) {
61+
// If no body or invalid JSON, continue without UTM data
62+
}
63+
5264
const userData: USER_SCHEMA = {
5365
_id: recordId,
5466
provider: auth.authProvider,
5567
created_at: creationTime,
68+
...(utmData && { utm: utmData }),
5669
};
5770

5871
await writeRecord(builder, process.env.USER_COLLECTION_ID, userData);

src/app/app/layout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SidebarIcon from "@/components/chat/SidebarIcon";
88
import SignInButton from "@/components/chat/SignInButton";
99
import SignOutButton from "@/components/chat/SignOutButton";
1010
import SignUpButton from "@/components/chat/SignUpButton";
11+
import UserCreationHandler from "@/components/UserCreationHandler";
1112
import { AppProvider, useApp } from "@/contexts/AppContext";
1213
import { UnifiedAuthProvider, useAuth } from "@/contexts/UnifiedAuthProvider";
1314
import PrivyProvider from "@/providers/PrivyProvider";
@@ -66,6 +67,9 @@ function LayoutContent({ children }: { children: React.ReactNode }) {
6667
{authMode && (
6768
<AuthModal mode={authMode} onClose={() => setAuthMode(null)} />
6869
)}
70+
71+
{/* Handle automatic user creation in nilDB */}
72+
<UserCreationHandler />
6973
</div>
7074
);
7175
}

src/app/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use client";
22

33
import { Download } from "lucide-react";
4+
import { useEffect } from "react";
45
import Footer from "@/components/landingPage/Footer";
56
import PWAInstallInstructionsModal from "@/components/PWAInstallInstructionsModal";
67
import { Button } from "@/components/ui/button";
78
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
89
import usePWAInstallInstructionsModal from "@/hooks/usePWAInstallInstructionsModal";
10+
import { captureAndStoreUTMParameters } from "@/utils/utmTracking";
911
import FAQSection from "../components/landingPage/FAQSection";
1012
import FeaturesSection from "../components/landingPage/FeaturesSection";
1113
import Header from "../components/landingPage/Header";
@@ -19,6 +21,14 @@ export default function Home() {
1921
setIsPWAInstallInstructionsModalOpen,
2022
} = usePWAInstallInstructionsModal();
2123

24+
// Capture and store UTM parameters on landing page load
25+
useEffect(() => {
26+
const utmParams = captureAndStoreUTMParameters();
27+
if (Object.keys(utmParams).length > 0) {
28+
console.log("UTM parameters captured:", utmParams);
29+
}
30+
}, []);
31+
2232
return (
2333
<main className="relative">
2434
<Header />
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use client";
2+
3+
import { useEffect, useRef } from "react";
4+
import { useAuth } from "@/contexts/UnifiedAuthProvider";
5+
import { getUTMParametersForRegistration } from "@/utils/utmTracking";
6+
7+
/**
8+
* Component that automatically creates users in nilDB when they first access the app
9+
* This handles both Supabase and Privy users who haven't been created in nilDB yet
10+
*/
11+
export default function UserCreationHandler() {
12+
const { user } = useAuth();
13+
const hasAttemptedCreation = useRef(false);
14+
15+
useEffect(() => {
16+
// Only run if user is authenticated and we haven't attempted creation yet
17+
if (!user?.isAuthenticated || !user?.id || hasAttemptedCreation.current) {
18+
return;
19+
}
20+
21+
hasAttemptedCreation.current = true;
22+
23+
const createUserInNilDB = async () => {
24+
try {
25+
// Get UTM parameters for user registration
26+
const utmParams = getUTMParametersForRegistration();
27+
const requestBody =
28+
Object.keys(utmParams).length > 0 ? { utm: utmParams } : {};
29+
30+
const response = await fetch("/api/createUser", {
31+
method: "POST",
32+
headers: {
33+
"Content-Type": "application/json",
34+
},
35+
body:
36+
Object.keys(requestBody).length > 0
37+
? JSON.stringify(requestBody)
38+
: undefined,
39+
});
40+
41+
const result = await response.json();
42+
43+
if (response.ok) {
44+
if (result.userExists) {
45+
console.log("User already exists in nilDB");
46+
} else {
47+
console.log("User created in nilDB successfully");
48+
}
49+
} else {
50+
console.error("Error creating user in nilDB:", result.error);
51+
}
52+
} catch (error) {
53+
console.error("Network error creating user in nilDB:", error);
54+
}
55+
};
56+
57+
// Small delay to ensure auth state is fully settled
58+
const timeoutId = setTimeout(createUserInNilDB, 1000);
59+
60+
return () => clearTimeout(timeoutId);
61+
}, [user?.isAuthenticated, user?.id]);
62+
63+
// This component doesn't render anything
64+
return null;
65+
}

src/components/auth/WalletButton.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useLogin, usePrivy } from "@privy-io/react-auth";
22
import { useRouter } from "next/navigation";
33
import { useCallback, useEffect, useState } from "react";
4+
import { getUTMParametersForRegistration } from "@/utils/utmTracking";
45

56
interface WalletButtonProps {
67
onClose?: () => void;
@@ -20,9 +21,18 @@ function WalletButton({ onClose }: WalletButtonProps) {
2021
"Content-Type": "application/json",
2122
};
2223

24+
// Get UTM parameters for user registration
25+
const utmParams = getUTMParametersForRegistration();
26+
const requestBody =
27+
Object.keys(utmParams).length > 0 ? { utm: utmParams } : {};
28+
2329
const response = await fetch("/api/createUser", {
2430
method: "POST",
2531
headers,
32+
body:
33+
Object.keys(requestBody).length > 0
34+
? JSON.stringify(requestBody)
35+
: undefined,
2636
});
2737

2838
const result = await response.json();

src/contexts/UnifiedAuthProvider.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { User } from "@supabase/supabase-js";
55
import { useRouter } from "next/navigation";
66
import { createContext, useContext, useEffect, useState } from "react";
77
import { supabase } from "@/lib/supabase/client";
8+
import { getUTMParametersForRegistration } from "@/utils/utmTracking";
89

910
interface UnifiedUser {
1011
id: string;
@@ -105,11 +106,26 @@ export function UnifiedAuthProvider({
105106
name: string,
106107
emailConsent: boolean,
107108
) => {
109+
// Get UTM parameters to include in email confirmation link
110+
const utmParams = getUTMParametersForRegistration();
111+
112+
// Build the redirect URL with UTM parameters
113+
let redirectUrl = `${window.location.origin}/app`;
114+
if (Object.keys(utmParams).length > 0) {
115+
const urlParams = new URLSearchParams();
116+
Object.entries(utmParams).forEach(([key, value]) => {
117+
if (value) {
118+
urlParams.append(key, value);
119+
}
120+
});
121+
redirectUrl += `?${urlParams.toString()}`;
122+
}
123+
108124
const { data, error } = await supabase.auth.signUp({
109125
email,
110126
password,
111127
options: {
112-
emailRedirectTo: `${window.location.origin}/app`,
128+
emailRedirectTo: redirectUrl,
113129
data: {
114130
name: name,
115131
email_consent: emailConsent,

src/lib/nildb/deleteRecord.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function deleteRecord(
1818
}
1919

2020
const deleteRequest: DeleteDataRequest = {
21-
collection: collectionId as any, // Cast to Uuid type
21+
collection: collectionId,
2222
filter,
2323
};
2424

src/types/schemas.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ export interface USER_SCHEMA {
22
_id: string;
33
provider: string;
44
created_at: string;
5+
utm?: {
6+
utm_source?: string;
7+
utm_medium?: string;
8+
utm_campaign?: string;
9+
utm_term?: string;
10+
utm_content?: string;
11+
};
512
}
613

714
export interface CHAT_SCHEMA {

src/utils/utmTracking.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* UTM Tracking Utilities
3+
* Handles extraction and storage of UTM parameters for user registration tracking
4+
*/
5+
6+
export interface UTMParameters {
7+
utm_source?: string;
8+
utm_medium?: string;
9+
utm_campaign?: string;
10+
utm_term?: string;
11+
utm_content?: string;
12+
}
13+
14+
const UTM_STORAGE_KEY = "nilgpt_utm_params";
15+
16+
/**
17+
* Extracts UTM parameters from the current URL
18+
*/
19+
export function extractUTMParameters(): UTMParameters {
20+
if (typeof window === "undefined") {
21+
return {};
22+
}
23+
24+
const urlParams = new URLSearchParams(window.location.search);
25+
const utmParams: UTMParameters = {};
26+
27+
// Standard UTM parameters
28+
const utmKeys = [
29+
"utm_source",
30+
"utm_medium",
31+
"utm_campaign",
32+
"utm_term",
33+
"utm_content",
34+
];
35+
36+
utmKeys.forEach((key) => {
37+
const value = urlParams.get(key);
38+
if (value) {
39+
utmParams[key as keyof UTMParameters] = value;
40+
}
41+
});
42+
43+
return utmParams;
44+
}
45+
46+
/**
47+
* Stores UTM parameters in session storage
48+
*/
49+
export function storeUTMParameters(utmParams: UTMParameters): void {
50+
if (typeof window === "undefined") {
51+
return;
52+
}
53+
54+
// Only store if there are actual UTM parameters
55+
const hasUTMParams = Object.values(utmParams).some(
56+
(value) => value !== undefined,
57+
);
58+
59+
if (hasUTMParams) {
60+
try {
61+
sessionStorage.setItem(UTM_STORAGE_KEY, JSON.stringify(utmParams));
62+
} catch (error) {
63+
console.warn("Failed to store UTM parameters in session storage:", error);
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Retrieves stored UTM parameters from session storage
70+
*/
71+
export function getStoredUTMParameters(): UTMParameters {
72+
if (typeof window === "undefined") {
73+
return {};
74+
}
75+
76+
try {
77+
const stored = sessionStorage.getItem(UTM_STORAGE_KEY);
78+
if (stored) {
79+
return JSON.parse(stored);
80+
}
81+
} catch (error) {
82+
console.warn(
83+
"Failed to retrieve UTM parameters from session storage:",
84+
error,
85+
);
86+
}
87+
88+
return {};
89+
}
90+
91+
/**
92+
* Clears stored UTM parameters from session storage
93+
*/
94+
export function clearStoredUTMParameters(): void {
95+
if (typeof window === "undefined") {
96+
return;
97+
}
98+
99+
try {
100+
sessionStorage.removeItem(UTM_STORAGE_KEY);
101+
} catch (error) {
102+
console.warn("Failed to clear UTM parameters from session storage:", error);
103+
}
104+
}
105+
106+
/**
107+
* Captures and stores UTM parameters from the current URL
108+
* Call this on landing page load
109+
*/
110+
export function captureAndStoreUTMParameters(): UTMParameters {
111+
const utmParams = extractUTMParameters();
112+
storeUTMParameters(utmParams);
113+
return utmParams;
114+
}
115+
116+
/**
117+
* Gets UTM parameters for user registration
118+
* First tries to get from current URL, then falls back to stored parameters
119+
* This handles the case where user comes from email confirmation with UTM params
120+
*/
121+
export function getUTMParametersForRegistration(): UTMParameters {
122+
// First, try to get UTM parameters from current URL (for email confirmation flow)
123+
const currentUrlParams = extractUTMParameters();
124+
125+
// If we have UTM params in current URL, use those and store them
126+
if (Object.keys(currentUrlParams).length > 0) {
127+
storeUTMParameters(currentUrlParams);
128+
return currentUrlParams;
129+
}
130+
131+
// Otherwise, fall back to stored parameters (for wallet signup flow)
132+
return getStoredUTMParameters();
133+
}

0 commit comments

Comments
 (0)