Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import type { Address } from "viem";
export async function getFarcasterProfile(
address: string,
): Promise<FarcasterProfile | null> {
// Try DB first
// Try DB first (only if user has a FID — wallet-only users have no Farcaster profile)
const dbUser = await getUserFromDB(address);
if (dbUser) {
if (dbUser && dbUser.fid != null) {
return {
fid: dbUser.fid,
username: dbUser.username ?? "",
Expand All @@ -41,6 +41,7 @@ export async function fetchAgentMetadata(

/**
* Look up a user from the DB by wallet address.
* Searches verified_addresses first, then primary_address for wallet-only users.
*/
export async function getUserFromDB(
address: string,
Expand All @@ -50,11 +51,19 @@ export async function getUserFromDB(

const normalized = address.toLowerCase();

const { data } = await supabase
const { data: byVerified } = await supabase
.from("users")
.select("*")
.contains("verified_addresses", [normalized])
.single();

return data ?? null;
if (byVerified) return byVerified;

const { data: byPrimary } = await supabase
.from("users")
.select("*")
.eq("primary_address", normalized)
.single();

return byPrimary ?? null;
}
6 changes: 3 additions & 3 deletions lib/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ export interface Database {
users: {
Row: {
id: string;
fid: number;
fid: number | null;
username: string | null;
display_name: string | null;
pfp_url: string | null;
Expand Down Expand Up @@ -429,7 +429,7 @@ export interface Database {
};
Insert: {
id?: never;
fid: number;
fid?: number | null;
username?: string | null;
display_name?: string | null;
pfp_url?: string | null;
Expand Down Expand Up @@ -464,7 +464,7 @@ export interface Database {
};
Update: {
id?: never;
fid?: number;
fid?: number | null;
username?: string | null;
display_name?: string | null;
pfp_url?: string | null;
Expand Down
19 changes: 14 additions & 5 deletions lib/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,21 @@ export async function buildUserData(opts: {
} as UserInsert;
}

if (neynarProfile) {
return {
...base,
fid: neynarProfile.fid,
username: neynarProfile.username,
display_name: neynarProfile.displayName,
pfp_url: neynarProfile.pfpUrl,
bio: neynarProfile.bio,
} as UserInsert;
}

// Wallet-only user (no Farcaster account)
return {
...base,
fid: neynarProfile!.fid,
username: neynarProfile!.username,
display_name: neynarProfile!.displayName,
pfp_url: neynarProfile!.pfpUrl,
bio: neynarProfile!.bio,
fid: null,
primary_address: verifiedAddresses[0] || null,
} as UserInsert;
}
57 changes: 41 additions & 16 deletions src/app/api/user/onboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,23 @@ export async function POST(request: NextRequest) {
);
}

// Check existing user and cooldown
const { data: existingUser } = await supabase
// Check existing user (by verified_addresses or primary_address)
let existingUser = null;
const { data: byVerified } = await supabase
.from("users")
.select("*")
.contains("verified_addresses", [normalizedAddress])
.single();
if (byVerified) {
existingUser = byVerified;
} else {
const { data: byPrimary } = await supabase
.from("users")
.select("*")
.eq("primary_address", normalizedAddress)
.single();
existingUser = byPrimary;
}

// Enforce 5-min cooldown on ALL refreshes
if (existingUser?.steemhunt_fetched_at) {
Expand All @@ -64,14 +75,7 @@ export async function POST(request: NextRequest) {
neynarProfile = await lookupByAddress(normalizedAddress);
}

if (!steemhuntUser && !neynarProfile) {
return NextResponse.json(
{ error: "No Farcaster account found for this wallet." },
{ status: 404 },
);
}

const fid = steemhuntUser?.fid ?? neynarProfile?.fid;
const fid = steemhuntUser?.fid ?? neynarProfile?.fid ?? null;

// Build verified addresses
let verifiedAddresses: string[];
Expand Down Expand Up @@ -106,12 +110,12 @@ export async function POST(request: NextRequest) {
quotientData,
});

// Upsert
// Upsert — update by existing row identity
if (existingUser) {
const { data, error } = await supabase
.from("users")
.update(userData)
.eq("fid", userData.fid)
.eq("id", existingUser.id)
.select()
.single();

Expand All @@ -124,20 +128,41 @@ export async function POST(request: NextRequest) {
}
return NextResponse.json({ success: true, user: data });
} else {
const { data, error } = await supabase
const { data: insertData, error: insertError } = await supabase
.from("users")
.insert(userData)
.select()
.single();

if (error) {
console.error("[onboard] Insert error:", error);
if (insertError) {
if (insertError.code === "23505") {
// Unique violation — update by conflicting identity
const updateQuery = supabase.from("users").update(userData);
const conditioned =
userData.fid != null
? updateQuery.eq("fid", userData.fid)
: updateQuery.eq("primary_address", normalizedAddress);

const { data: updateData, error: updateError } = await conditioned
.select()
.single();

if (updateError) {
console.error("[onboard] Update error:", updateError);
return NextResponse.json(
{ error: "Failed to save user data" },
{ status: 500 },
);
}
return NextResponse.json({ success: true, user: updateData });
}
console.error("[onboard] Insert error:", insertError);
return NextResponse.json(
{ error: "Failed to save user data" },
{ status: 500 },
);
}
return NextResponse.json({ success: true, user: data });
return NextResponse.json({ success: true, user: insertData });
}
} catch (error) {
console.error("[onboard] Error:", error);
Expand Down
51 changes: 29 additions & 22 deletions src/app/api/user/register-by-wallet/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { buildUserData } from "../../../../../lib/user-data";

/**
* POST /api/user/register-by-wallet
* Called on wallet connect — upserts all Farcaster profile fields.
* SteemHunt primary (free), Neynar fallback (paid).
* Called on wallet connect — upserts user profile fields.
* Works for both Farcaster and non-Farcaster wallet users.
*/
export async function POST(request: NextRequest) {
try {
Expand All @@ -31,13 +31,26 @@ export async function POST(request: NextRequest) {
);
}

// Check if user exists and data is fresh (< 5 min)
const { data: existingUser } = await supabase
// Check if user exists (by verified_addresses or primary_address)
let existingUser = null;
const { data: byVerified } = await supabase
.from("users")
.select("*")
.contains("verified_addresses", [normalizedAddress])
.single();

if (byVerified) {
existingUser = byVerified;
} else {
const { data: byPrimary } = await supabase
.from("users")
.select("*")
.eq("primary_address", normalizedAddress)
.single();
existingUser = byPrimary;
}

// If user exists and data is fresh (< 5 min), return cached
if (existingUser?.steemhunt_fetched_at) {
const age =
Date.now() - new Date(existingUser.steemhunt_fetched_at).getTime();
Expand All @@ -49,22 +62,12 @@ export async function POST(request: NextRequest) {
// SteemHunt lookup (primary, free)
const steemhuntUser = await getUserByWallet(normalizedAddress);

// Neynar fallback
// Neynar fallback (only if SteemHunt found nothing)
let neynarProfile = null;
if (!steemhuntUser) {
neynarProfile = await lookupByAddress(normalizedAddress);
}

if (!steemhuntUser && !neynarProfile) {
return NextResponse.json(
{
error:
"No Farcaster account found for this wallet. Please use a wallet linked to your Farcaster account.",
},
{ status: 404 },
);
}

// Build verified addresses
let verifiedAddresses: string[];
if (steemhuntUser) {
Expand All @@ -78,9 +81,9 @@ export async function POST(request: NextRequest) {
verifiedAddresses.push(normalizedAddress);
}

const fid = steemhuntUser?.fid ?? neynarProfile?.fid;
const fid = steemhuntUser?.fid ?? neynarProfile?.fid ?? null;

// Fetch Quotient Score (non-blocking, don't fail if unavailable)
// Fetch Quotient Score (non-blocking, only when FID available)
let quotientData = null;
if (fid) {
try {
Expand Down Expand Up @@ -108,11 +111,15 @@ export async function POST(request: NextRequest) {

if (insertError) {
if (insertError.code === "23505") {
// Unique violation — update existing
const { data: updateData, error: updateError } = await supabase
.from("users")
.update(userData)
.eq("fid", userData.fid)
// Unique violation — update by the conflicting identity
const updateQuery = supabase.from("users").update(userData);
const conditioned = existingUser
? updateQuery.eq("id", existingUser.id)
: userData.fid != null
? updateQuery.eq("fid", userData.fid)
: updateQuery.eq("primary_address", normalizedAddress);

const { data: updateData, error: updateError } = await conditioned
.select()
.single();

Expand Down
8 changes: 8 additions & 0 deletions supabase/migrations/00028_users_fid_nullable.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- [#567] Make fid nullable for non-Farcaster wallet users
-- PlotLink must work for ALL wallet users, not just Farcaster.

ALTER TABLE users ALTER COLUMN fid DROP NOT NULL;

-- Unique index on primary_address for wallet-only user upserts
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_primary_address_unique
ON users (primary_address) WHERE primary_address IS NOT NULL;
Loading