formatting

This commit is contained in:
Shreyaschorge
2025-07-07 14:10:47 +05:30
parent f42a5f8d33
commit 193dffe03a
64 changed files with 6398 additions and 985 deletions

View File

@@ -1,10 +1,10 @@
/**
* Application constants and configuration values.
*
*
* This file contains all the configuration constants used throughout the mini app.
* These values are either sourced from environment variables or hardcoded and provide
* configuration for the app's appearance, behavior, and integration settings.
*
*
* NOTE: This file is automatically updated by the init script.
* Manual changes may be overwritten during project initialization.
*/
@@ -63,7 +63,7 @@ export const APP_SPLASH_URL = `${APP_URL}/splash.png`;
* Background color for the splash screen.
* Used as fallback when splash image is loading.
*/
export const APP_SPLASH_BACKGROUND_COLOR = "#f7f7f7";
export const APP_SPLASH_BACKGROUND_COLOR = '#f7f7f7';
// --- UI Configuration ---
/**
@@ -75,18 +75,19 @@ export const APP_BUTTON_TEXT = 'Launch NSK';
// --- Integration Configuration ---
/**
* Webhook URL for receiving events from Neynar.
*
*
* If Neynar API key and client ID are configured, uses the official
* Neynar webhook endpoint. Otherwise, falls back to a local webhook
* endpoint for development and testing.
*/
export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
export const APP_WEBHOOK_URL =
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `${APP_URL}/api/webhook`;
/**
* Flag to enable/disable wallet functionality.
*
*
* When true, wallet-related components and features are rendered.
* When false, wallet functionality is completely hidden from the UI.
* Useful for mini apps that don't require wallet integration.
@@ -95,7 +96,7 @@ export const USE_WALLET = true;
/**
* Flag to enable/disable analytics tracking.
*
*
* When true, usage analytics are collected and sent to Neynar.
* When false, analytics collection is disabled.
* Useful for privacy-conscious users or development environments.

View File

@@ -1,9 +1,9 @@
import { type ReactElement } from "react";
import { BaseError, UserRejectedRequestError } from "viem";
import { type ReactElement } from 'react';
import { BaseError, UserRejectedRequestError } from 'viem';
/**
* Renders an error object in a user-friendly format.
*
*
* This utility function takes an error object and renders it as a React element
* with consistent styling. It handles different types of errors including:
* - Error objects with message properties
@@ -11,14 +11,14 @@ import { BaseError, UserRejectedRequestError } from "viem";
* - String errors
* - Unknown error types
* - User rejection errors (special handling for wallet rejections)
*
*
* The rendered error is displayed in a gray container with monospace font
* for better readability of technical error details. User rejections are
* displayed with a simpler, more user-friendly message.
*
*
* @param error - The error object to render
* @returns ReactElement - A styled error display component, or null if no error
*
*
* @example
* ```tsx
* {isError && renderError(error)}
@@ -27,11 +27,11 @@ import { BaseError, UserRejectedRequestError } from "viem";
export function renderError(error: unknown): ReactElement | null {
// Handle null/undefined errors
if (!error) return null;
// Special handling for user rejections in wallet operations
if (error instanceof BaseError) {
const isUserRejection = error.walk(
(e) => e instanceof UserRejectedRequestError
e => e instanceof UserRejectedRequestError
);
if (isUserRejection) {
@@ -43,10 +43,10 @@ export function renderError(error: unknown): ReactElement | null {
);
}
}
// Extract error message from different error types
let errorMessage: string;
if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === 'object' && error !== null && 'error' in error) {
@@ -63,4 +63,4 @@ export function renderError(error: unknown): ReactElement | null {
<div className="whitespace-pre-wrap break-words">{errorMessage}</div>
</div>
);
}
}

View File

@@ -1,16 +1,18 @@
import { FrameNotificationDetails } from "@farcaster/frame-sdk";
import { Redis } from "@upstash/redis";
import { APP_NAME } from "./constants";
import { FrameNotificationDetails } from '@farcaster/frame-sdk';
import { Redis } from '@upstash/redis';
import { APP_NAME } from './constants';
// In-memory fallback storage
const localStore = new Map<string, FrameNotificationDetails>();
// Use Redis if KV env vars are present, otherwise use in-memory
const useRedis = process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN;
const redis = useRedis ? new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
}) : null;
const redis = useRedis
? new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
})
: null;
function getUserNotificationDetailsKey(fid: number): string {
return `${APP_NAME}:user:${fid}`;

View File

@@ -1,11 +1,15 @@
import { NeynarAPIClient, Configuration, WebhookUserCreated } from '@neynar/nodejs-sdk';
import {
NeynarAPIClient,
Configuration,
WebhookUserCreated,
} from '@neynar/nodejs-sdk';
import { APP_URL } from './constants';
let neynarClient: NeynarAPIClient | null = null;
// Example usage:
// const client = getNeynarClient();
// const user = await client.lookupUserByFid(fid);
// const user = await client.lookupUserByFid(fid);
export function getNeynarClient() {
if (!neynarClient) {
const apiKey = process.env.NEYNAR_API_KEY;
@@ -33,12 +37,12 @@ export async function getNeynarUser(fid: number): Promise<User | null> {
type SendMiniAppNotificationResult =
| {
state: "error";
state: 'error';
error: unknown;
}
| { state: "no_token" }
| { state: "rate_limit" }
| { state: "success" };
| { state: 'no_token' }
| { state: 'rate_limit' }
| { state: 'success' };
export async function sendNeynarMiniAppNotification({
fid,
@@ -58,19 +62,19 @@ export async function sendNeynarMiniAppNotification({
target_url: APP_URL,
};
const result = await client.publishFrameNotifications({
targetFids,
notification
const result = await client.publishFrameNotifications({
targetFids,
notification,
});
if (result.notification_deliveries.length > 0) {
return { state: "success" };
return { state: 'success' };
} else if (result.notification_deliveries.length === 0) {
return { state: "no_token" };
return { state: 'no_token' };
} else {
return { state: "error", error: result || "Unknown error" };
return { state: 'error', error: result || 'Unknown error' };
}
} catch (error) {
return { state: "error", error };
return { state: 'error', error };
}
}
}

View File

@@ -1,18 +1,18 @@
import {
SendNotificationRequest,
sendNotificationResponseSchema,
} from "@farcaster/frame-sdk";
import { getUserNotificationDetails } from "~/lib/kv";
import { APP_URL } from "./constants";
} from '@farcaster/frame-sdk';
import { getUserNotificationDetails } from '~/lib/kv';
import { APP_URL } from './constants';
type SendMiniAppNotificationResult =
| {
state: "error";
state: 'error';
error: unknown;
}
| { state: "no_token" }
| { state: "rate_limit" }
| { state: "success" };
| { state: 'no_token' }
| { state: 'rate_limit' }
| { state: 'success' };
export async function sendMiniAppNotification({
fid,
@@ -25,13 +25,13 @@ export async function sendMiniAppNotification({
}): Promise<SendMiniAppNotificationResult> {
const notificationDetails = await getUserNotificationDetails(fid);
if (!notificationDetails) {
return { state: "no_token" };
return { state: 'no_token' };
}
const response = await fetch(notificationDetails.url, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({
notificationId: crypto.randomUUID(),
@@ -48,17 +48,17 @@ export async function sendMiniAppNotification({
const responseBody = sendNotificationResponseSchema.safeParse(responseJson);
if (responseBody.success === false) {
// Malformed response
return { state: "error", error: responseBody.error.errors };
return { state: 'error', error: responseBody.error.errors };
}
if (responseBody.data.result.rateLimitedTokens.length) {
// Rate limited
return { state: "rate_limit" };
return { state: 'rate_limit' };
}
return { state: "success" };
return { state: 'success' };
} else {
// Error response
return { state: "error", error: responseJson };
return { state: 'error', error: responseJson };
}
}

View File

@@ -1,4 +1,4 @@
export const truncateAddress = (address: string) => {
if (!address) return "";
if (!address) return '';
return `${address.slice(0, 14)}...${address.slice(-12)}`;
};

View File

@@ -1,7 +1,18 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { mnemonicToAccount } from 'viem/accounts';
import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants';
import {
APP_BUTTON_TEXT,
APP_DESCRIPTION,
APP_ICON_URL,
APP_NAME,
APP_OG_IMAGE_URL,
APP_PRIMARY_CATEGORY,
APP_SPLASH_BACKGROUND_COLOR,
APP_TAGS,
APP_URL,
APP_WEBHOOK_URL,
} from './constants';
import { APP_SPLASH_URL } from './constants';
interface MiniAppMetadata {
@@ -17,7 +28,7 @@ interface MiniAppMetadata {
description?: string;
primaryCategory?: string;
tags?: string[];
};
}
interface MiniAppManifest {
accountAssociation?: {
@@ -35,7 +46,7 @@ export function cn(...inputs: ClassValue[]) {
export function getSecretEnvVars() {
const seedPhrase = process.env.SEED_PHRASE;
const fid = process.env.FID;
if (!seedPhrase || !fid) {
return null;
}
@@ -45,12 +56,12 @@ export function getSecretEnvVars() {
export function getMiniAppEmbedMetadata(ogImageUrl?: string) {
return {
version: "next",
version: 'next',
imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL,
button: {
title: APP_BUTTON_TEXT,
action: {
type: "launch_frame",
type: 'launch_frame',
name: APP_NAME,
url: APP_URL,
splashImageUrl: APP_SPLASH_URL,
@@ -72,7 +83,10 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
console.log('Using pre-signed mini app metadata from environment');
return metadata;
} catch (error) {
console.warn('Failed to parse MINI_APP_METADATA from environment:', error);
console.warn(
'Failed to parse MINI_APP_METADATA from environment:',
error
);
}
}
@@ -86,7 +100,9 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
const secretEnvVars = getSecretEnvVars();
if (!secretEnvVars) {
console.warn('No seed phrase or FID found in environment variables -- generating unsigned metadata');
console.warn(
'No seed phrase or FID found in environment variables -- generating unsigned metadata'
);
}
let accountAssociation;
@@ -100,34 +116,41 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
type: 'custody',
key: custodyAddress,
};
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64');
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString(
'base64'
);
const payload = {
domain
domain,
};
const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url');
const encodedPayload = Buffer.from(
JSON.stringify(payload),
'utf-8'
).toString('base64url');
const signature = await account.signMessage({
message: `${encodedHeader}.${encodedPayload}`
const signature = await account.signMessage({
message: `${encodedHeader}.${encodedPayload}`,
});
const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url');
const encodedSignature = Buffer.from(signature, 'utf-8').toString(
'base64url'
);
accountAssociation = {
header: encodedHeader,
payload: encodedPayload,
signature: encodedSignature
signature: encodedSignature,
};
}
return {
accountAssociation,
frame: {
version: "1",
name: APP_NAME ?? "Neynar Starter Kit",
version: '1',
name: APP_NAME ?? 'Neynar Starter Kit',
iconUrl: APP_ICON_URL,
homeUrl: APP_URL,
imageUrl: APP_OG_IMAGE_URL,
buttonTitle: APP_BUTTON_TEXT ?? "Launch Mini App",
buttonTitle: APP_BUTTON_TEXT ?? 'Launch Mini App',
splashImageUrl: APP_SPLASH_URL,
splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR,
webhookUrl: APP_WEBHOOK_URL,