mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
fix: remove duplicate app directory
This commit is contained in:
parent
86029b2bd9
commit
b9e2087bd8
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neynar/create-farcaster-mini-app",
|
"name": "@neynar/create-farcaster-mini-app",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": false,
|
"private": false,
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getFarcasterDomainManifest } from '~/lib/utils';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const config = await getFarcasterDomainManifest();
|
|
||||||
return NextResponse.json(config);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating metadata:', error);
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getNeynarClient } from '~/lib/neynar';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const client = getNeynarClient();
|
|
||||||
const response = await client.fetchNonce();
|
|
||||||
return NextResponse.json(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching nonce:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch nonce' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getNeynarClient } from '~/lib/neynar';
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const message = searchParams.get('message');
|
|
||||||
const signature = searchParams.get('signature');
|
|
||||||
|
|
||||||
if (!message || !signature) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Message and signature are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = getNeynarClient();
|
|
||||||
const data = await client.fetchSigners({ message, signature });
|
|
||||||
const signers = data.signers;
|
|
||||||
|
|
||||||
// Fetch user data if signers exist
|
|
||||||
let user = null;
|
|
||||||
if (signers && signers.length > 0 && signers[0].fid) {
|
|
||||||
const {
|
|
||||||
users: [fetchedUser],
|
|
||||||
} = await client.fetchBulkUsers({
|
|
||||||
fids: [signers[0].fid],
|
|
||||||
});
|
|
||||||
user = fetchedUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
signers,
|
|
||||||
user,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in session-signers API:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch signers' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getNeynarClient } from '~/lib/neynar';
|
|
||||||
|
|
||||||
export async function POST() {
|
|
||||||
try {
|
|
||||||
const neynarClient = getNeynarClient();
|
|
||||||
const signer = await neynarClient.createSigner();
|
|
||||||
return NextResponse.json(signer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching signer:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch signer' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const signerUuid = searchParams.get('signerUuid');
|
|
||||||
|
|
||||||
if (!signerUuid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'signerUuid is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const neynarClient = getNeynarClient();
|
|
||||||
const signer = await neynarClient.lookupSigner({
|
|
||||||
signerUuid,
|
|
||||||
});
|
|
||||||
return NextResponse.json(signer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching signed key:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch signed key' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getNeynarClient } from '~/lib/neynar';
|
|
||||||
import { mnemonicToAccount } from 'viem/accounts';
|
|
||||||
import {
|
|
||||||
SIGNED_KEY_REQUEST_TYPE,
|
|
||||||
SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
|
|
||||||
} from '~/lib/constants';
|
|
||||||
|
|
||||||
const postRequiredFields = ['signerUuid', 'publicKey'];
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
for (const field of postRequiredFields) {
|
|
||||||
if (!body[field]) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `${field} is required` },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { signerUuid, publicKey, redirectUrl } = body;
|
|
||||||
|
|
||||||
if (redirectUrl && typeof redirectUrl !== 'string') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'redirectUrl must be a string' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the app's account from seed phrase
|
|
||||||
const seedPhrase = process.env.SEED_PHRASE;
|
|
||||||
const shouldSponsor = process.env.SPONSOR_SIGNER === 'true';
|
|
||||||
|
|
||||||
if (!seedPhrase) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'App configuration missing (SEED_PHRASE or FID)' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const neynarClient = getNeynarClient();
|
|
||||||
|
|
||||||
const account = mnemonicToAccount(seedPhrase);
|
|
||||||
|
|
||||||
const {
|
|
||||||
user: { fid },
|
|
||||||
} = await neynarClient.lookupUserByCustodyAddress({
|
|
||||||
custodyAddress: account.address,
|
|
||||||
});
|
|
||||||
|
|
||||||
const appFid = fid;
|
|
||||||
|
|
||||||
// Generate deadline (24 hours from now)
|
|
||||||
const deadline = Math.floor(Date.now() / 1000) + 86400;
|
|
||||||
|
|
||||||
// Generate EIP-712 signature
|
|
||||||
const signature = await account.signTypedData({
|
|
||||||
domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
|
|
||||||
types: {
|
|
||||||
SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
|
|
||||||
},
|
|
||||||
primaryType: 'SignedKeyRequest',
|
|
||||||
message: {
|
|
||||||
requestFid: BigInt(appFid),
|
|
||||||
key: publicKey,
|
|
||||||
deadline: BigInt(deadline),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const signer = await neynarClient.registerSignedKey({
|
|
||||||
appFid,
|
|
||||||
deadline,
|
|
||||||
signature,
|
|
||||||
signerUuid,
|
|
||||||
...(redirectUrl && { redirectUrl }),
|
|
||||||
...(shouldSponsor && { sponsor: { sponsored_by_neynar: true } }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(signer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error registering signed key:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to register signed key' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { getNeynarClient } from '~/lib/neynar';
|
|
||||||
|
|
||||||
const requiredParams = ['message', 'signature'];
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const params: Record<string, string | null> = {};
|
|
||||||
for (const param of requiredParams) {
|
|
||||||
params[param] = searchParams.get(param);
|
|
||||||
if (!params[param]) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: `${param} parameter is required`,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = params.message as string;
|
|
||||||
const signature = params.signature as string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = getNeynarClient();
|
|
||||||
const data = await client.fetchSigners({ message, signature });
|
|
||||||
const signers = data.signers;
|
|
||||||
return NextResponse.json({
|
|
||||||
signers,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching signers:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch signers' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { createClient, Errors } from '@farcaster/quick-auth';
|
|
||||||
|
|
||||||
const client = createClient();
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const { token } = await request.json();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Token is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get domain from environment or request
|
|
||||||
const domain = process.env.NEXT_PUBLIC_URL
|
|
||||||
? new URL(process.env.NEXT_PUBLIC_URL).hostname
|
|
||||||
: request.headers.get('host') || 'localhost';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use the official QuickAuth library to verify the JWT
|
|
||||||
const payload = await client.verifyJwt({
|
|
||||||
token,
|
|
||||||
domain,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
user: {
|
|
||||||
fid: payload.sub,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Errors.InvalidTokenError) {
|
|
||||||
console.info('Invalid token:', e.message);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid token' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Token validation error:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Internal server error' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const apiKey = process.env.NEYNAR_API_KEY;
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const fid = searchParams.get('fid');
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Neynar API key is not configured. Please add NEYNAR_API_KEY to your environment variables.' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fid) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'FID parameter is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.neynar.com/v2/farcaster/user/best_friends?fid=${fid}&limit=3`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"x-api-key": apiKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Neynar API error: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { users } = await response.json() as { users: { user: { fid: number; username: string } }[] };
|
|
||||||
|
|
||||||
return NextResponse.json({ bestFriends: users });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch best friends:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch best friends. Please check your Neynar API key and try again.' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { ImageResponse } from "next/og";
|
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import { getNeynarUser } from "~/lib/neynar";
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const fid = searchParams.get('fid');
|
|
||||||
|
|
||||||
const user = fid ? await getNeynarUser(Number(fid)) : null;
|
|
||||||
|
|
||||||
return new ImageResponse(
|
|
||||||
(
|
|
||||||
<div tw="flex h-full w-full flex-col justify-center items-center relative bg-primary">
|
|
||||||
{user?.pfp_url && (
|
|
||||||
<div tw="flex w-96 h-96 rounded-full overflow-hidden mb-8 border-8 border-white">
|
|
||||||
<img src={user.pfp_url} alt="Profile" tw="w-full h-full object-cover" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<h1 tw="text-8xl text-white">{user?.display_name ? `Hello from ${user.display_name ?? user.username}!` : 'Hello!'}</h1>
|
|
||||||
<p tw="text-5xl mt-4 text-white opacity-80">Powered by Neynar 🪐</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
{
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { notificationDetailsSchema } from "@farcaster/miniapp-sdk";
|
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { setUserNotificationDetails } from "~/lib/kv";
|
|
||||||
import { sendMiniAppNotification } from "~/lib/notifs";
|
|
||||||
import { sendNeynarMiniAppNotification } from "~/lib/neynar";
|
|
||||||
|
|
||||||
const requestSchema = z.object({
|
|
||||||
fid: z.number(),
|
|
||||||
notificationDetails: notificationDetailsSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
// If Neynar is enabled, we don't need to store notification details
|
|
||||||
// as they will be managed by Neynar's system
|
|
||||||
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
|
|
||||||
|
|
||||||
const requestJson = await request.json();
|
|
||||||
const requestBody = requestSchema.safeParse(requestJson);
|
|
||||||
|
|
||||||
if (requestBody.success === false) {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, errors: requestBody.error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only store notification details if not using Neynar
|
|
||||||
if (!neynarEnabled) {
|
|
||||||
await setUserNotificationDetails(
|
|
||||||
Number(requestBody.data.fid),
|
|
||||||
requestBody.data.notificationDetails
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use appropriate notification function based on Neynar status
|
|
||||||
const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification;
|
|
||||||
const sendResult = await sendNotification({
|
|
||||||
fid: Number(requestBody.data.fid),
|
|
||||||
title: "Test notification",
|
|
||||||
body: "Sent at " + new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sendResult.state === "error") {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: sendResult.error },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
} else if (sendResult.state === "rate_limit") {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: "Rate limited" },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ success: true });
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const apiKey = process.env.NEYNAR_API_KEY;
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const fids = searchParams.get('fids');
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Neynar API key is not configured. Please add NEYNAR_API_KEY to your environment variables.' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fids) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'FIDs parameter is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const neynar = new NeynarAPIClient({ apiKey });
|
|
||||||
const fidsArray = fids.split(',').map(fid => parseInt(fid.trim()));
|
|
||||||
|
|
||||||
const { users } = await neynar.fetchBulkUsers({
|
|
||||||
fids: fidsArray,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({ users });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch users:', error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch users. Please check your Neynar API key and try again.' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import {
|
|
||||||
ParseWebhookEvent,
|
|
||||||
parseWebhookEvent,
|
|
||||||
verifyAppKeyWithNeynar,
|
|
||||||
} from "@farcaster/miniapp-node";
|
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import { APP_NAME } from "~/lib/constants";
|
|
||||||
import {
|
|
||||||
deleteUserNotificationDetails,
|
|
||||||
setUserNotificationDetails,
|
|
||||||
} from "~/lib/kv";
|
|
||||||
import { sendMiniAppNotification } from "~/lib/notifs";
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
// If Neynar is enabled, we don't need to handle webhooks here
|
|
||||||
// as they will be handled by Neynar's webhook endpoint
|
|
||||||
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
|
|
||||||
if (neynarEnabled) {
|
|
||||||
return Response.json({ success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestJson = await request.json();
|
|
||||||
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = await parseWebhookEvent(requestJson, verifyAppKeyWithNeynar);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const error = e as ParseWebhookEvent.ErrorType;
|
|
||||||
|
|
||||||
switch (error.name) {
|
|
||||||
case "VerifyJsonFarcasterSignature.InvalidDataError":
|
|
||||||
case "VerifyJsonFarcasterSignature.InvalidEventDataError":
|
|
||||||
// The request data is invalid
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: error.message },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
case "VerifyJsonFarcasterSignature.InvalidAppKeyError":
|
|
||||||
// The app key is invalid
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: error.message },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
case "VerifyJsonFarcasterSignature.VerifyAppKeyError":
|
|
||||||
// Internal error verifying the app key (caller may want to try again)
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: error.message },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fid = data.fid;
|
|
||||||
const event = data.event;
|
|
||||||
|
|
||||||
// Only handle notifications if Neynar is not enabled
|
|
||||||
// When Neynar is enabled, notifications are handled through their webhook
|
|
||||||
switch (event.event) {
|
|
||||||
case "frame_added":
|
|
||||||
if (event.notificationDetails) {
|
|
||||||
await setUserNotificationDetails(fid, event.notificationDetails);
|
|
||||||
await sendMiniAppNotification({
|
|
||||||
fid,
|
|
||||||
title: `Welcome to ${APP_NAME}`,
|
|
||||||
body: "Mini app is now added to your client",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await deleteUserNotificationDetails(fid);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "frame_removed":
|
|
||||||
await deleteUserNotificationDetails(fid);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "notifications_enabled":
|
|
||||||
await setUserNotificationDetails(fid, event.notificationDetails);
|
|
||||||
await sendMiniAppNotification({
|
|
||||||
fid,
|
|
||||||
title: `Welcome to ${APP_NAME}`,
|
|
||||||
body: "Notifications are now enabled",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "notifications_disabled":
|
|
||||||
await deleteUserNotificationDetails(fid);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json({ success: true });
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { APP_NAME } from "~/lib/constants";
|
|
||||||
|
|
||||||
// note: dynamic import is required for components that use the Frame SDK
|
|
||||||
const AppComponent = dynamic(() => import("~/components/App"), {
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function App(
|
|
||||||
{ title }: { title?: string } = { title: APP_NAME }
|
|
||||||
) {
|
|
||||||
return <AppComponent title={title} />;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* DESIGN SYSTEM - DO NOT EDIT UNLESS NECESSARY
|
|
||||||
*
|
|
||||||
* This file contains the centralized design system for the mini app.
|
|
||||||
* These component classes establish the visual consistency across all components.
|
|
||||||
*
|
|
||||||
* ⚠️ AI SHOULD NOT NORMALLY EDIT THIS FILE ⚠️
|
|
||||||
*
|
|
||||||
* Instead of modifying these classes, AI should:
|
|
||||||
* 1. Use existing component classes (e.g., .btn, .card, .input)
|
|
||||||
* 2. Use Tailwind utilities for one-off styling
|
|
||||||
* 3. Create new React components rather than new CSS classes
|
|
||||||
* 4. Only edit this file for specific bug fixes or accessibility improvements
|
|
||||||
*
|
|
||||||
* When AI needs to style something:
|
|
||||||
* ✅ Good: <button className="btn btn-primary">Click me</button>
|
|
||||||
* ✅ Good: <div className="bg-primary text-white px-4 py-2 rounded">Custom</div>
|
|
||||||
* ❌ Bad: Adding new CSS classes here for component-specific styling
|
|
||||||
*
|
|
||||||
* This design system is intentionally minimal to prevent bloat and maintain consistency.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--background);
|
|
||||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
|
||||||
}
|
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
/* Global container styles for consistent layout */
|
|
||||||
.container {
|
|
||||||
@apply mx-auto max-w-md px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-wide {
|
|
||||||
@apply mx-auto max-w-lg px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container-narrow {
|
|
||||||
@apply mx-auto max-w-sm px-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global card styles */
|
|
||||||
.card {
|
|
||||||
@apply bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-primary {
|
|
||||||
@apply bg-primary/10 border-primary/20;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global button styles */
|
|
||||||
.btn {
|
|
||||||
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
@apply bg-primary text-white hover:bg-primary-dark focus:ring-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
@apply bg-secondary text-gray-900 hover:bg-gray-200 focus:ring-gray-500 dark:bg-secondary-dark dark:text-gray-100 dark:hover:bg-gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
@apply border border-gray-300 bg-transparent hover:bg-gray-50 focus:ring-gray-500 dark:border-gray-600 dark:hover:bg-gray-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global input styles */
|
|
||||||
.input {
|
|
||||||
@apply block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-gray-900 placeholder-gray-500 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global loading spinner */
|
|
||||||
.spinner {
|
|
||||||
@apply animate-spin rounded-full border-2 border-gray-300 border-t-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner-primary {
|
|
||||||
@apply animate-spin rounded-full border-2 border-white border-t-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global focus styles */
|
|
||||||
.focus-ring {
|
|
||||||
@apply focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
|
|
||||||
import "~/app/globals.css";
|
|
||||||
import { Providers } from "~/app/providers";
|
|
||||||
import { APP_NAME, APP_DESCRIPTION } from "~/lib/constants";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: APP_NAME,
|
|
||||||
description: APP_DESCRIPTION,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function RootLayout({
|
|
||||||
children,
|
|
||||||
}: Readonly<{
|
|
||||||
children: React.ReactNode;
|
|
||||||
}>) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<Providers>{children}</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { Metadata } from "next";
|
|
||||||
import App from "./app";
|
|
||||||
import { APP_NAME, APP_DESCRIPTION, APP_OG_IMAGE_URL } from "~/lib/constants";
|
|
||||||
import { getMiniAppEmbedMetadata } from "~/lib/utils";
|
|
||||||
|
|
||||||
export const revalidate = 300;
|
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
|
||||||
return {
|
|
||||||
title: APP_NAME,
|
|
||||||
openGraph: {
|
|
||||||
title: APP_NAME,
|
|
||||||
description: APP_DESCRIPTION,
|
|
||||||
images: [APP_OG_IMAGE_URL],
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
"fc:frame": JSON.stringify(getMiniAppEmbedMetadata()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (<App />);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { MiniAppProvider } from '@neynar/react';
|
|
||||||
import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider';
|
|
||||||
import { ANALYTICS_ENABLED } from '~/lib/constants';
|
|
||||||
|
|
||||||
const WagmiProvider = dynamic(
|
|
||||||
() => import('~/components/providers/WagmiProvider'),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function Providers({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const solanaEndpoint =
|
|
||||||
process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com';
|
|
||||||
return (
|
|
||||||
<WagmiProvider>
|
|
||||||
<MiniAppProvider
|
|
||||||
analyticsEnabled={ANALYTICS_ENABLED}
|
|
||||||
backButtonEnabled={true}
|
|
||||||
>
|
|
||||||
<SafeFarcasterSolanaProvider endpoint={solanaEndpoint}>
|
|
||||||
{children}
|
|
||||||
</SafeFarcasterSolanaProvider>
|
|
||||||
</MiniAppProvider>
|
|
||||||
</WagmiProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { APP_URL, APP_NAME, APP_DESCRIPTION } from "~/lib/constants";
|
|
||||||
import { getMiniAppEmbedMetadata } from "~/lib/utils";
|
|
||||||
export const revalidate = 300;
|
|
||||||
|
|
||||||
// This is an example of how to generate a dynamically generated share page based on fid:
|
|
||||||
// Sharing this route e.g. exmaple.com/share/123 will generate a share page for fid 123,
|
|
||||||
// with the image dynamically generated by the opengraph-image API route.
|
|
||||||
export async function generateMetadata({
|
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: Promise<{ fid: string }>;
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
const { fid } = await params;
|
|
||||||
const imageUrl = `${APP_URL}/api/opengraph-image?fid=${fid}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `${APP_NAME} - Share`,
|
|
||||||
openGraph: {
|
|
||||||
title: APP_NAME,
|
|
||||||
description: APP_DESCRIPTION,
|
|
||||||
images: [imageUrl],
|
|
||||||
},
|
|
||||||
other: {
|
|
||||||
"fc:frame": JSON.stringify(getMiniAppEmbedMetadata(imageUrl)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SharePage() {
|
|
||||||
// redirect to home page
|
|
||||||
redirect("/");
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user