Merge branch 'main' into shreyas-formatting

This commit is contained in:
Shreyaschorge
2025-07-14 18:55:06 +05:30
34 changed files with 2479 additions and 829 deletions

View File

@@ -20,19 +20,19 @@ export const APP_URL = process.env.NEXT_PUBLIC_URL!;
* The name of the mini app as displayed to users.
* Used in titles, headers, and app store listings.
*/
export const APP_NAME = 'Starter Kit';
export const APP_NAME = 'shreyas-testing-mini-app';
/**
* A brief description of the mini app's functionality.
* Used in app store listings and metadata.
*/
export const APP_DESCRIPTION = 'A demo of the Neynar Starter Kit';
export const APP_DESCRIPTION = 'A Farcaster mini app created with Neynar';
/**
* The primary category for the mini app.
* Used for app store categorization and discovery.
*/
export const APP_PRIMARY_CATEGORY = 'developer-tools';
export const APP_PRIMARY_CATEGORY = '';
/**
* Tags associated with the mini app.
@@ -70,7 +70,7 @@ export const APP_SPLASH_BACKGROUND_COLOR = '#f7f7f7';
* Text displayed on the main action button.
* Used for the primary call-to-action in the mini app.
*/
export const APP_BUTTON_TEXT = 'Launch NSK';
export const APP_BUTTON_TEXT = 'Launch Mini App';
// --- Integration Configuration ---
/**
@@ -102,3 +102,19 @@ export const USE_WALLET = true;
* Useful for privacy-conscious users or development environments.
*/
export const ANALYTICS_ENABLED = true;
// PLEASE DO NOT UPDATE THIS
export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
name: 'Farcaster SignedKeyRequestValidator',
version: '1',
chainId: 10,
verifyingContract:
'0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`,
};
// PLEASE DO NOT UPDATE THIS
export const SIGNED_KEY_REQUEST_TYPE = [
{ name: 'requestFid', type: 'uint256' },
{ name: 'key', type: 'bytes' },
{ name: 'deadline', type: 'uint256' },
];

27
src/lib/devices.ts Normal file
View File

@@ -0,0 +1,27 @@
function isAndroid(): boolean {
return (
typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent)
);
}
function isSmallIOS(): boolean {
return (
typeof navigator !== 'undefined' && /iPhone|iPod/.test(navigator.userAgent)
);
}
function isLargeIOS(): boolean {
return (
typeof navigator !== 'undefined' &&
(/iPad/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1))
);
}
function isIOS(): boolean {
return isSmallIOS() || isLargeIOS();
}
export function isMobile(): boolean {
return isAndroid() || isIOS();
}

View File

@@ -1,25 +1,23 @@
import { FrameNotificationDetails } from '@farcaster/frame-sdk';
import { Redis } from '@upstash/redis';
import { APP_NAME } from './constants';
import { FrameNotificationDetails } from "@farcaster/miniapp-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}`;
}
export async function getUserNotificationDetails(
fid: number,
fid: number
): Promise<FrameNotificationDetails | null> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {
@@ -30,7 +28,7 @@ export async function getUserNotificationDetails(
export async function setUserNotificationDetails(
fid: number,
notificationDetails: FrameNotificationDetails,
notificationDetails: FrameNotificationDetails
): Promise<void> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {
@@ -41,7 +39,7 @@ export async function setUserNotificationDetails(
}
export async function deleteUserNotificationDetails(
fid: number,
fid: number
): Promise<void> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {
@@ -49,4 +47,4 @@ export async function deleteUserNotificationDetails(
} else {
localStore.delete(key);
}
}
}

25
src/lib/localStorage.ts Normal file
View File

@@ -0,0 +1,25 @@
export function setItem<T>(key: string, value: T) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn('Failed to save item:', error);
}
}
export function getItem<T>(key: string): T | null {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.warn('Failed to load item:', error);
return null;
}
}
export function removeItem(key: string) {
try {
localStorage.removeItem(key);
} catch (error) {
console.warn('Failed to remove item:', 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/miniapp-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,6 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { mnemonicToAccount } from 'viem/accounts';
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { mnemonicToAccount } from "viem/accounts";
import {
APP_BUTTON_TEXT,
APP_DESCRIPTION,
@@ -12,8 +12,8 @@ import {
APP_TAGS,
APP_URL,
APP_WEBHOOK_URL,
} from './constants';
import { APP_SPLASH_URL } from './constants';
} from "./constants";
import { APP_SPLASH_URL } from "./constants";
interface MiniAppMetadata {
version: string;
@@ -43,25 +43,14 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getSecretEnvVars() {
const seedPhrase = process.env.SEED_PHRASE;
const fid = process.env.FID;
if (!seedPhrase || !fid) {
return null;
}
return { seedPhrase, fid };
}
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,
@@ -80,77 +69,37 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
if (process.env.MINI_APP_METADATA) {
try {
const metadata = JSON.parse(process.env.MINI_APP_METADATA);
console.log('Using pre-signed mini app metadata from environment');
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,
"Failed to parse MINI_APP_METADATA from environment:",
error
);
}
}
if (!APP_URL) {
throw new Error('NEXT_PUBLIC_URL not configured');
throw new Error("NEXT_PUBLIC_URL not configured");
}
// Get the domain from the URL (without https:// prefix)
const domain = new URL(APP_URL).hostname;
console.log('Using domain for manifest:', domain);
const secretEnvVars = getSecretEnvVars();
if (!secretEnvVars) {
console.warn(
'No seed phrase or FID found in environment variables -- generating unsigned metadata',
);
}
let accountAssociation;
if (secretEnvVars) {
// Generate account from seed phrase
const account = mnemonicToAccount(secretEnvVars.seedPhrase);
const custodyAddress = account.address;
const header = {
fid: parseInt(secretEnvVars.fid),
type: 'custody',
key: custodyAddress,
};
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString(
'base64',
);
const payload = {
domain,
};
const encodedPayload = Buffer.from(
JSON.stringify(payload),
'utf-8',
).toString('base64url');
const signature = await account.signMessage({
message: `${encodedHeader}.${encodedPayload}`,
});
const encodedSignature = Buffer.from(signature, 'utf-8').toString(
'base64url',
);
accountAssociation = {
header: encodedHeader,
payload: encodedPayload,
signature: encodedSignature,
};
}
console.log("Using domain for manifest:", domain);
return {
accountAssociation,
accountAssociation: {
header: "",
payload: "",
signature: "",
},
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,
@@ -159,4 +108,4 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
tags: APP_TAGS,
},
};
}
}