Merge pull request #21 from neynarxyz/veganbeef/reapply-quick-auth

feat: reapply quickauth changes conditionally
This commit is contained in:
Shreyaschorge 2025-07-17 00:08:18 +05:30 committed by GitHub
commit aac3a739cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 650 additions and 250 deletions

View File

@ -229,7 +229,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
useTunnel: true, useTunnel: true,
enableAnalytics: true, enableAnalytics: true,
seedPhrase: null, seedPhrase: null,
sponsorSigner: false, useSponsoredSigner: false,
}; };
} else { } else {
// If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options // If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options
@ -351,14 +351,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
type: 'confirm', type: 'confirm',
name: 'useSponsoredSigner', name: 'useSponsoredSigner',
message: message:
'Would you like to use Neynar Sponsored Signers and/or Sign In With Neynar (SIWN)?\n' + 'Would you like to write data to Farcaster on behalf of your miniapp users? This involves using Neynar Sponsored Signers and SIWN.\n' +
'This enables the simplest, most secure, and most user-friendly Farcaster authentication for your app.\n\n' +
'Benefits of using Neynar Sponsored Signers/SIWN:\n' +
'- No auth buildout or signer management required for developers\n' +
'- Cost-effective for users (no gas for signers)\n' +
'- Users can revoke signers at any time\n' +
'- Plug-and-play for web and React Native\n' +
'- Recommended for most developers\n' +
'\n⚠ A seed phrase is required for this option.\n', '\n⚠ A seed phrase is required for this option.\n',
default: false, default: false,
}, },
@ -453,13 +446,14 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
delete packageJson.devDependencies; delete packageJson.devDependencies;
// Add dependencies // Add dependencies
// question: remove auth-client?
packageJson.dependencies = { packageJson.dependencies = {
'@farcaster/auth-client': '>=0.3.0 <1.0.0', '@farcaster/auth-client': '>=0.3.0 <1.0.0',
'@farcaster/auth-kit': '>=0.6.0 <1.0.0',
'@farcaster/miniapp-node': '>=0.1.5 <1.0.0', '@farcaster/miniapp-node': '>=0.1.5 <1.0.0',
'@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0', '@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0',
'@farcaster/miniapp-wagmi-connector': '^1.0.0', '@farcaster/miniapp-wagmi-connector': '^1.0.0',
'@farcaster/mini-app-solana': '>=0.0.17 <1.0.0', '@farcaster/mini-app-solana': '>=0.0.17 <1.0.0',
'@farcaster/quick-auth': '>=0.0.7 <1.0.0',
'@neynar/react': '^1.2.5', '@neynar/react': '^1.2.5',
'@radix-ui/react-label': '^2.1.1', '@radix-ui/react-label': '^2.1.1',
'@solana/wallet-adapter-react': '^0.15.38', '@solana/wallet-adapter-react': '^0.15.38',
@ -471,7 +465,6 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
'lucide-react': '^0.469.0', 'lucide-react': '^0.469.0',
mipd: '^0.0.7', mipd: '^0.0.7',
next: '^15', next: '^15',
'next-auth': '^4.24.11',
react: '^19', react: '^19',
'react-dom': '^19', 'react-dom': '^19',
'tailwind-merge': '^2.6.0', 'tailwind-merge': '^2.6.0',
@ -483,6 +476,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
}; };
packageJson.devDependencies = { packageJson.devDependencies = {
"@types/inquirer": "^9.0.8",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@ -494,8 +488,8 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5", "ts-node": "^10.9.2",
"ts-node": "^10.9.2" "typescript": "^5"
}; };
// Add Neynar SDK if selected // Add Neynar SDK if selected
@ -503,6 +497,12 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0'; packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
} }
// Add auth-kit and next-auth dependencies if useSponsoredSigner is true
if (answers.useSponsoredSigner) {
packageJson.dependencies['@farcaster/auth-kit'] = '>=0.6.0 <1.0.0';
packageJson.dependencies['next-auth'] = '^4.24.11';
}
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
// Handle .env file // Handle .env file
@ -632,10 +632,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
console.log('⚠️ constants.ts not found, skipping constants update'); console.log('⚠️ constants.ts not found, skipping constants update');
} }
fs.appendFileSync(
envPath,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
);
if (useNeynar && neynarApiKey && neynarClientId) { if (useNeynar && neynarApiKey && neynarClientId) {
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`); fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`); fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
@ -648,6 +645,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`); fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
} }
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`); fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
if (answers.useSponsoredSigner) {
fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="true"`);
fs.appendFileSync(
envPath,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
);
}
fs.unlinkSync(envExamplePath); fs.unlinkSync(envExamplePath);
} else { } else {
@ -691,6 +695,32 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.rmSync(binPath, { recursive: true, force: true }); fs.rmSync(binPath, { recursive: true, force: true });
} }
// Remove NeynarAuthButton directory, NextAuth API routes, and auth directory if useSponsoredSigner is false
if (!answers.useSponsoredSigner) {
console.log('\nRemoving NeynarAuthButton directory, NextAuth API routes, and auth directory (useSponsoredSigner is false)...');
const neynarAuthButtonPath = path.join(projectPath, 'src', 'components', 'ui', 'NeynarAuthButton');
if (fs.existsSync(neynarAuthButtonPath)) {
fs.rmSync(neynarAuthButtonPath, { recursive: true, force: true });
}
// Remove NextAuth API routes
const nextAuthRoutePath = path.join(projectPath, 'src', 'app', 'api', 'auth', '[...nextauth]', 'route.ts');
if (fs.existsSync(nextAuthRoutePath)) {
fs.rmSync(nextAuthRoutePath, { force: true });
// Remove the directory if it's empty
const nextAuthDir = path.dirname(nextAuthRoutePath);
if (fs.readdirSync(nextAuthDir).length === 0) {
fs.rmSync(nextAuthDir, { recursive: true, force: true });
}
}
// Remove src/auth.ts file
const authFilePath = path.join(projectPath, 'src', 'auth.ts');
if (fs.existsSync(authFilePath)) {
fs.rmSync(authFilePath, { force: true });
}
}
// Initialize git repository // Initialize git repository
console.log('\nInitializing git repository...'); console.log('\nInitializing git repository...');
execSync('git init', { cwd: projectPath }); execSync('git init', { cwd: projectPath });

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.7.5", "version": "1.7.6",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",

View File

@ -73,18 +73,20 @@ async function checkRequiredEnvVars(): Promise<void> {
name: 'NEXT_PUBLIC_MINI_APP_NAME', name: 'NEXT_PUBLIC_MINI_APP_NAME',
message: 'Enter the name for your frame (e.g., My Cool Mini App):', message: 'Enter the name for your frame (e.g., My Cool Mini App):',
default: APP_NAME, default: APP_NAME,
validate: (input: string) => input.trim() !== '' || 'Mini app name cannot be empty' validate: (input: string) =>
input.trim() !== '' || 'Mini app name cannot be empty',
}, },
{ {
name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT',
message: 'Enter the text for your frame button:', message: 'Enter the text for your frame button:',
default: APP_BUTTON_TEXT ?? 'Launch Mini App', default: APP_BUTTON_TEXT ?? 'Launch Mini App',
validate: (input: string) => input.trim() !== '' || 'Button text cannot be empty' validate: (input: string) =>
} input.trim() !== '' || 'Button text cannot be empty',
},
]; ];
const missingVars = requiredVars.filter( const missingVars = requiredVars.filter(
(varConfig) => !process.env[varConfig.name] varConfig => !process.env[varConfig.name],
); );
if (missingVars.length > 0) { if (missingVars.length > 0) {
@ -110,7 +112,7 @@ async function checkRequiredEnvVars(): Promise<void> {
const newLine = envContent ? '\n' : ''; const newLine = envContent ? '\n' : '';
fs.appendFileSync( fs.appendFileSync(
'.env', '.env',
`${newLine}${varConfig.name}="${value.trim()}"` `${newLine}${varConfig.name}="${value.trim()}"`,
); );
} }
@ -130,14 +132,64 @@ async function checkRequiredEnvVars(): Promise<void> {
process.env.SPONSOR_SIGNER = sponsorSigner.toString(); process.env.SPONSOR_SIGNER = sponsorSigner.toString();
if (storeSeedPhrase) { if (process.env.SEED_PHRASE) {
fs.appendFileSync( fs.appendFileSync(
'.env.local', '.env.local',
`\nSPONSOR_SIGNER="${sponsorSigner}"` `\nSPONSOR_SIGNER="${sponsorSigner}"`,
); );
console.log('✅ Sponsor signer preference stored in .env.local'); console.log('✅ Sponsor signer preference stored in .env.local');
} }
} }
// Ask about required chains
const { useRequiredChains } = await inquirer.prompt([
{
type: 'confirm',
name: 'useRequiredChains',
message:
'Does your mini app require support for specific blockchains?\n' +
'If yes, the host will only render your mini app if it supports all the chains you specify.\n' +
'If no, the mini app will be rendered regardless of chain support.',
default: false,
},
]);
let requiredChains: string[] = [];
if (useRequiredChains) {
const { selectedChains } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedChains',
message: 'Select the required chains (CAIP-2 identifiers):',
choices: [
{ name: 'Ethereum Mainnet (eip155:1)', value: 'eip155:1' },
{ name: 'Polygon (eip155:137)', value: 'eip155:137' },
{ name: 'Arbitrum One (eip155:42161)', value: 'eip155:42161' },
{ name: 'Optimism (eip155:10)', value: 'eip155:10' },
{ name: 'Base (eip155:8453)', value: 'eip155:8453' },
{ name: 'Solana (solana:mainnet)', value: 'solana:mainnet' },
{ name: 'Solana Devnet (solana:devnet)', value: 'solana:devnet' },
],
},
]);
requiredChains = selectedChains;
}
// Update constants.ts with required chains
const constantsPath = path.join(projectRoot, 'src', 'lib', 'constants.ts');
if (fs.existsSync(constantsPath)) {
let constantsContent = fs.readFileSync(constantsPath, 'utf8');
// Replace the APP_REQUIRED_CHAINS line
const requiredChainsString = JSON.stringify(requiredChains);
constantsContent = constantsContent.replace(
/^export const APP_REQUIRED_CHAINS\s*:\s*string\[\]\s*=\s*\[[^\]]*\];$/m,
`export const APP_REQUIRED_CHAINS: string[] = ${requiredChainsString};`,
);
fs.writeFileSync(constantsPath, constantsContent);
console.log('✅ Required chains updated in constants.ts');
}
} }
} }
@ -172,7 +224,7 @@ async function getGitRemote(): Promise<string | null> {
async function checkVercelCLI(): Promise<boolean> { async function checkVercelCLI(): Promise<boolean> {
try { try {
execSync('vercel --version', { execSync('vercel --version', {
stdio: 'ignore' stdio: 'ignore',
}); });
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
@ -186,7 +238,7 @@ async function checkVercelCLI(): Promise<boolean> {
async function installVercelCLI(): Promise<void> { async function installVercelCLI(): Promise<void> {
console.log('Installing Vercel CLI...'); console.log('Installing Vercel CLI...');
execSync('npm install -g vercel', { execSync('npm install -g vercel', {
stdio: 'inherit' stdio: 'inherit',
}); });
} }
@ -222,7 +274,9 @@ async function getVercelToken(): Promise<string | null> {
return null; // We'll fall back to CLI operations return null; // We'll fall back to CLI operations
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
throw new Error('Not logged in to Vercel CLI. Please run this script again to login.'); throw new Error(
'Not logged in to Vercel CLI. Please run this script again to login.',
);
} }
throw error; throw error;
} }
@ -239,7 +293,7 @@ async function loginToVercel(): Promise<boolean> {
console.log('3. Complete the Vercel account setup in your browser'); console.log('3. Complete the Vercel account setup in your browser');
console.log('4. Return here once your Vercel account is created\n'); console.log('4. Return here once your Vercel account is created\n');
console.log( console.log(
'\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account' '\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account',
); );
const child = spawn('vercel', ['login'], { const child = spawn('vercel', ['login'], {
@ -247,14 +301,14 @@ async function loginToVercel(): Promise<boolean> {
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
child.on('close', (code) => { child.on('close', code => {
resolve(); resolve();
}); });
}); });
console.log('\n📱 Waiting for login to complete...'); console.log('\n📱 Waiting for login to complete...');
console.log( console.log(
"If you're creating a new account, please complete the Vercel account setup in your browser first." "If you're creating a new account, please complete the Vercel account setup in your browser first.",
); );
for (let i = 0; i < 150; i++) { for (let i = 0; i < 150; i++) {
@ -263,10 +317,13 @@ async function loginToVercel(): Promise<boolean> {
console.log('✅ Successfully logged in to Vercel!'); console.log('✅ Successfully logged in to Vercel!');
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error && error.message.includes('Account not found')) { if (
error instanceof Error &&
error.message.includes('Account not found')
) {
console.log(' Waiting for Vercel account setup to complete...'); console.log(' Waiting for Vercel account setup to complete...');
} }
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
} }
} }
@ -277,7 +334,12 @@ async function loginToVercel(): Promise<boolean> {
return false; return false;
} }
async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key: string, value: string | object): Promise<boolean> { async function setVercelEnvVarSDK(
vercelClient: Vercel,
projectId: string,
key: string,
value: string | object,
): Promise<boolean> {
try { try {
let processedValue: string; let processedValue: string;
if (typeof value === 'object') { if (typeof value === 'object') {
@ -287,17 +349,26 @@ async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key:
} }
// Get existing environment variables // Get existing environment variables
const existingVars = await vercelClient.projects.getEnvironmentVariables({ const existingVars = await vercelClient.projects.filterProjectEnvs({
idOrName: projectId, idOrName: projectId,
}); });
const existingVar = existingVars.envs?.find((env: any) => // Handle different response types
env.key === key && env.target?.includes('production') let envs: any[] = [];
if ('envs' in existingVars && Array.isArray(existingVars.envs)) {
envs = existingVars.envs;
} else if ('target' in existingVars && 'key' in existingVars) {
// Single environment variable response
envs = [existingVars];
}
const existingVar = envs.find(
(env: any) => env.key === key && env.target?.includes('production'),
); );
if (existingVar) { if (existingVar && existingVar.id) {
// Update existing variable // Update existing variable
await vercelClient.projects.editEnvironmentVariable({ await vercelClient.projects.editProjectEnv({
idOrName: projectId, idOrName: projectId,
id: existingVar.id, id: existingVar.id,
requestBody: { requestBody: {
@ -308,7 +379,7 @@ async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key:
console.log(`✅ Updated environment variable: ${key}`); console.log(`✅ Updated environment variable: ${key}`);
} else { } else {
// Create new variable // Create new variable
await vercelClient.projects.createEnvironmentVariable({ await vercelClient.projects.createProjectEnv({
idOrName: projectId, idOrName: projectId,
requestBody: { requestBody: {
key: key, key: key,
@ -323,14 +394,21 @@ async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key:
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn(`⚠️ Warning: Failed to set environment variable ${key}:`, error.message); console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`,
error.message,
);
return false; return false;
} }
throw error; throw error;
} }
} }
async function setVercelEnvVarCLI(key: string, value: string | object, projectRoot: string): Promise<boolean> { async function setVercelEnvVarCLI(
key: string,
value: string | object,
projectRoot: string,
): Promise<boolean> {
try { try {
// Remove existing env var // Remove existing env var
try { try {
@ -365,7 +443,7 @@ async function setVercelEnvVarCLI(key: string, value: string | object, projectRo
execSync(command, { execSync(command, {
cwd: projectRoot, cwd: projectRoot,
stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts
env: process.env env: process.env,
}); });
fs.unlinkSync(tempFilePath); fs.unlinkSync(tempFilePath);
@ -377,14 +455,22 @@ async function setVercelEnvVarCLI(key: string, value: string | object, projectRo
fs.unlinkSync(tempFilePath); fs.unlinkSync(tempFilePath);
} }
if (error instanceof Error) { if (error instanceof Error) {
console.warn(`⚠️ Warning: Failed to set environment variable ${key}:`, error.message); console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`,
error.message,
);
return false; return false;
} }
throw error; throw error;
} }
} }
async function setEnvironmentVariables(vercelClient: Vercel | null, projectId: string | null, envVars: Record<string, string | object>, projectRoot: string): Promise<Array<{ key: string; success: boolean }>> { async function setEnvironmentVariables(
vercelClient: Vercel | null,
projectId: string | null,
envVars: Record<string, string | object>,
projectRoot: string,
): Promise<Array<{ key: string; success: boolean }>> {
console.log('\n📝 Setting up environment variables...'); console.log('\n📝 Setting up environment variables...');
const results: Array<{ key: string; success: boolean }> = []; const results: Array<{ key: string; success: boolean }> = [];
@ -408,25 +494,30 @@ async function setEnvironmentVariables(vercelClient: Vercel | null, projectId: s
} }
// Report results // Report results
const failed = results.filter((r) => !r.success); const failed = results.filter(r => !r.success);
if (failed.length > 0) { if (failed.length > 0) {
console.warn(`\n⚠ Failed to set ${failed.length} environment variables:`); console.warn(`\n⚠ Failed to set ${failed.length} environment variables:`);
failed.forEach((r) => console.warn(` - ${r.key}`)); failed.forEach(r => console.warn(` - ${r.key}`));
console.warn( console.warn(
'\nYou may need to set these manually in the Vercel dashboard.' '\nYou may need to set these manually in the Vercel dashboard.',
); );
} }
return results; return results;
} }
async function waitForDeployment(vercelClient: Vercel | null, projectId: string, maxWaitTime = 300000): Promise<any> { // 5 minutes async function waitForDeployment(
vercelClient: Vercel | null,
projectId: string,
maxWaitTime = 300000,
): Promise<any> {
// 5 minutes
console.log('\n⏳ Waiting for deployment to complete...'); console.log('\n⏳ Waiting for deployment to complete...');
const startTime = Date.now(); const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) { while (Date.now() - startTime < maxWaitTime) {
try { try {
const deployments = await vercelClient?.deployments.list({ const deployments = await vercelClient?.deployments.getDeployments({
projectId: projectId, projectId: projectId,
limit: 1, limit: 1,
}); });
@ -445,10 +536,10 @@ async function waitForDeployment(vercelClient: Vercel | null, projectId: string,
} }
// Still building, wait and check again // Still building, wait and check again
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
} else { } else {
console.log('⏳ No deployment found yet, waiting...'); console.log('⏳ No deployment found yet, waiting...');
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -478,18 +569,18 @@ async function deployToVercel(useGitHub = false): Promise<void> {
framework: 'nextjs', framework: 'nextjs',
}, },
null, null,
2 2,
) ),
); );
} }
// Set up Vercel project // Set up Vercel project
console.log('\n📦 Setting up Vercel project...'); console.log('\n📦 Setting up Vercel project...');
console.log( console.log(
'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n' 'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n',
); );
console.log( console.log(
'\n⚠ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n' '\n⚠ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n',
); );
// Use spawn instead of execSync for better error handling // Use spawn instead of execSync for better error handling
@ -497,11 +588,11 @@ async function deployToVercel(useGitHub = false): Promise<void> {
const vercelSetup = spawn('vercel', [], { const vercelSetup = spawn('vercel', [], {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32' ? true : undefined shell: process.platform === 'win32' ? true : undefined,
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelSetup.on('close', (code) => { vercelSetup.on('close', code => {
if (code === 0 || code === null) { if (code === 0 || code === null) {
console.log('✅ Vercel project setup completed'); console.log('✅ Vercel project setup completed');
resolve(); resolve();
@ -511,25 +602,27 @@ async function deployToVercel(useGitHub = false): Promise<void> {
} }
}); });
vercelSetup.on('error', (error) => { vercelSetup.on('error', error => {
console.log('⚠️ Vercel setup command completed (this is normal)'); console.log('⚠️ Vercel setup command completed (this is normal)');
resolve(); // Don't reject, as this is often expected resolve(); // Don't reject, as this is often expected
}); });
}); });
// Wait a moment for project files to be written // Wait a moment for project files to be written
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
// Load project info // Load project info
let projectId: string; let projectId: string;
try { try {
const projectJson = JSON.parse( const projectJson = JSON.parse(
fs.readFileSync('.vercel/project.json', 'utf8') fs.readFileSync('.vercel/project.json', 'utf8'),
); );
projectId = projectJson.projectId; projectId = projectJson.projectId;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.'); throw new Error(
'Failed to load project info. Please ensure the Vercel project was created successfully.',
);
} }
throw error; throw error;
} }
@ -540,13 +633,15 @@ async function deployToVercel(useGitHub = false): Promise<void> {
const token = await getVercelToken(); const token = await getVercelToken();
if (token) { if (token) {
vercelClient = new Vercel({ vercelClient = new Vercel({
bearerToken: token bearerToken: token,
}); });
console.log('✅ Initialized Vercel SDK client'); console.log('✅ Initialized Vercel SDK client');
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn('⚠️ Could not initialize Vercel SDK, falling back to CLI operations'); console.warn(
'⚠️ Could not initialize Vercel SDK, falling back to CLI operations',
);
} }
throw error; throw error;
} }
@ -558,15 +653,22 @@ async function deployToVercel(useGitHub = false): Promise<void> {
if (vercelClient) { if (vercelClient) {
try { try {
const project = await vercelClient.projects.get({ const projects = await vercelClient.projects.getProjects({});
idOrName: projectId, const project = projects.projects.find(
}); (p: any) => p.id === projectId || p.name === projectId,
);
if (project) {
projectName = project.name; projectName = project.name;
domain = `${projectName}.vercel.app`; domain = `${projectName}.vercel.app`;
console.log('🌐 Using project name for domain:', domain); console.log('🌐 Using project name for domain:', domain);
} else {
throw new Error('Project not found');
}
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn('⚠️ Could not get project details via SDK, using CLI fallback'); console.warn(
'⚠️ Could not get project details via SDK, using CLI fallback',
);
} }
throw error; throw error;
} }
@ -580,7 +682,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
{ {
cwd: projectRoot, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
} },
); );
const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/);
@ -596,7 +698,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
console.log('🌐 Using project name for domain:', domain); console.log('🌐 Using project name for domain:', domain);
} else { } else {
console.warn( console.warn(
'⚠️ Could not determine project name from inspection, using fallback' '⚠️ Could not determine project name from inspection, using fallback',
); );
// Use a fallback domain based on project ID // Use a fallback domain based on project ID
domain = `project-${projectId.slice(-8)}.vercel.app`; domain = `project-${projectId.slice(-8)}.vercel.app`;
@ -618,19 +720,29 @@ async function deployToVercel(useGitHub = false): Promise<void> {
const nextAuthSecret = const nextAuthSecret =
process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex'); process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex');
const vercelEnv = { const vercelEnv = {
NEXT_PUBLIC_URL: `https://${domain}`,
...(process.env.NEYNAR_API_KEY && {
NEYNAR_API_KEY: process.env.NEYNAR_API_KEY,
}),
...(process.env.NEYNAR_CLIENT_ID && {
NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID,
}),
...(process.env.SPONSOR_SIGNER && {
SPONSOR_SIGNER: process.env.SPONSOR_SIGNER,
}),
// Include NextAuth environment variables if SEED_PHRASE is present or SPONSOR_SIGNER is true
...((process.env.SEED_PHRASE || process.env.SPONSOR_SIGNER === 'true') && {
NEXTAUTH_SECRET: nextAuthSecret, NEXTAUTH_SECRET: nextAuthSecret,
AUTH_SECRET: nextAuthSecret, AUTH_SECRET: nextAuthSecret,
NEXTAUTH_URL: `https://${domain}`, NEXTAUTH_URL: `https://${domain}`,
NEXT_PUBLIC_URL: `https://${domain}`, }),
...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }),
...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }),
...(process.env.SPONSOR_SIGNER && { SPONSOR_SIGNER: process.env.SPONSOR_SIGNER }),
...Object.fromEntries( ...Object.fromEntries(
Object.entries(process.env).filter(([key]) => Object.entries(process.env).filter(([key]) =>
key.startsWith('NEXT_PUBLIC_') key.startsWith('NEXT_PUBLIC_'),
) ),
), ),
}; };
@ -639,7 +751,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
vercelClient, vercelClient,
projectId, projectId,
vercelEnv, vercelEnv,
projectRoot projectRoot,
); );
// Deploy the project // Deploy the project
@ -663,7 +775,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelDeploy.on('close', (code) => { vercelDeploy.on('close', code => {
if (code === 0) { if (code === 0) {
console.log('✅ Vercel deployment command completed'); console.log('✅ Vercel deployment command completed');
resolve(); resolve();
@ -673,7 +785,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
} }
}); });
vercelDeploy.on('error', (error) => { vercelDeploy.on('error', error => {
console.error('❌ Vercel deployment error:', error.message); console.error('❌ Vercel deployment error:', error.message);
reject(error); reject(error);
}); });
@ -686,7 +798,10 @@ async function deployToVercel(useGitHub = false): Promise<void> {
deployment = await waitForDeployment(vercelClient, projectId); deployment = await waitForDeployment(vercelClient, projectId);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn('⚠️ Could not verify deployment completion:', error.message); console.warn(
'⚠️ Could not verify deployment completion:',
error.message,
);
console.log(' Proceeding with domain verification...'); console.log(' Proceeding with domain verification...');
} }
throw error; throw error;
@ -703,7 +818,9 @@ async function deployToVercel(useGitHub = false): Promise<void> {
console.log('🌐 Verified actual domain:', actualDomain); console.log('🌐 Verified actual domain:', actualDomain);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.warn('⚠️ Could not verify domain via SDK, using assumed domain'); console.warn(
'⚠️ Could not verify domain via SDK, using assumed domain',
);
} }
throw error; throw error;
} }
@ -714,11 +831,20 @@ async function deployToVercel(useGitHub = false): Promise<void> {
console.log('🔄 Updating environment variables with correct domain...'); console.log('🔄 Updating environment variables with correct domain...');
const updatedEnv: Record<string, string | object> = { const updatedEnv: Record<string, string | object> = {
NEXTAUTH_URL: `https://${actualDomain}`,
NEXT_PUBLIC_URL: `https://${actualDomain}`, NEXT_PUBLIC_URL: `https://${actualDomain}`,
}; };
await setEnvironmentVariables(vercelClient, projectId, updatedEnv, projectRoot); // Include NextAuth URL if SEED_PHRASE is present or SPONSOR_SIGNER is true
if (process.env.SEED_PHRASE || process.env.SPONSOR_SIGNER === 'true') {
updatedEnv.NEXTAUTH_URL = `https://${actualDomain}`;
}
await setEnvironmentVariables(
vercelClient,
projectId,
updatedEnv,
projectRoot,
);
console.log('\n📦 Redeploying with correct domain...'); console.log('\n📦 Redeploying with correct domain...');
const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], { const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], {
@ -728,7 +854,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelRedeploy.on('close', (code) => { vercelRedeploy.on('close', code => {
if (code === 0) { if (code === 0) {
console.log('✅ Redeployment completed'); console.log('✅ Redeployment completed');
resolve(); resolve();
@ -738,7 +864,7 @@ async function deployToVercel(useGitHub = false): Promise<void> {
} }
}); });
vercelRedeploy.on('error', (error) => { vercelRedeploy.on('error', error => {
console.error('❌ Redeployment error:', error.message); console.error('❌ Redeployment error:', error.message);
reject(error); reject(error);
}); });
@ -749,13 +875,24 @@ async function deployToVercel(useGitHub = false): Promise<void> {
console.log('\n✨ Deployment complete! Your mini app is now live at:'); console.log('\n✨ Deployment complete! Your mini app is now live at:');
console.log(`🌐 https://${domain}`); console.log(`🌐 https://${domain}`);
console.log('\n📝 You can manage your project at https://vercel.com/dashboard'); console.log(
'\n📝 You can manage your project at https://vercel.com/dashboard',
);
// Prompt user to sign manifest in browser and paste accountAssociation // Prompt user to sign manifest in browser and paste accountAssociation
console.log(`\n⚠ To complete your mini app manifest, you must sign it using the Farcaster developer portal.`); console.log(
console.log('1. Go to: https://farcaster.xyz/~/developers/mini-apps/manifest?domain=' + domain); `\n⚠ To complete your mini app manifest, you must sign it using the Farcaster developer portal.`,
console.log('2. Click "Transfer Ownership" and follow the instructions to sign the manifest.'); );
console.log('3. Copy the resulting accountAssociation JSON from the browser.'); console.log(
'1. Go to: https://farcaster.xyz/~/developers/mini-apps/manifest?domain=' +
domain,
);
console.log(
'2. Click "Transfer Ownership" and follow the instructions to sign the manifest.',
);
console.log(
'3. Copy the resulting accountAssociation JSON from the browser.',
);
console.log('4. Paste it below when prompted.'); console.log('4. Paste it below when prompted.');
const { userAccountAssociation } = await inquirer.prompt([ const { userAccountAssociation } = await inquirer.prompt([
@ -773,8 +910,8 @@ async function deployToVercel(useGitHub = false): Promise<void> {
} catch (e) { } catch (e) {
return 'Invalid JSON'; return 'Invalid JSON';
} }
} },
} },
]); ]);
const parsedAccountAssociation = JSON.parse(userAccountAssociation); const parsedAccountAssociation = JSON.parse(userAccountAssociation);
@ -786,11 +923,10 @@ async function deployToVercel(useGitHub = false): Promise<void> {
const newAccountAssociation = `export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = ${JSON.stringify(parsedAccountAssociation, null, 2)};`; const newAccountAssociation = `export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = ${JSON.stringify(parsedAccountAssociation, null, 2)};`;
constantsContent = constantsContent.replace( constantsContent = constantsContent.replace(
/^export const APP_ACCOUNT_ASSOCIATION\s*:\s*AccountAssociation \| undefined\s*=\s*[^;]*;/m, /^export const APP_ACCOUNT_ASSOCIATION\s*:\s*AccountAssociation \| undefined\s*=\s*[^;]*;/m,
newAccountAssociation newAccountAssociation,
); );
fs.writeFileSync(constantsPath, constantsContent); fs.writeFileSync(constantsPath, constantsContent);
console.log('\n✅ APP_ACCOUNT_ASSOCIATION updated in src/lib/constants.ts'); console.log('\n✅ APP_ACCOUNT_ASSOCIATION updated in src/lib/constants.ts');
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.error('\n❌ Deployment failed:', error.message); console.error('\n❌ Deployment failed:', error.message);
@ -804,7 +940,7 @@ async function main(): Promise<void> {
try { try {
console.log('🚀 Vercel Mini App Deployment (SDK Edition)'); console.log('🚀 Vercel Mini App Deployment (SDK Edition)');
console.log( console.log(
'This script will deploy your mini app to Vercel using the Vercel SDK.' 'This script will deploy your mini app to Vercel using the Vercel SDK.',
); );
console.log('\nThe script will:'); console.log('\nThe script will:');
console.log('1. Check for required environment variables'); console.log('1. Check for required environment variables');
@ -820,7 +956,7 @@ async function main(): Promise<void> {
console.log('📦 Installing @vercel/sdk...'); console.log('📦 Installing @vercel/sdk...');
execSync('npm install @vercel/sdk', { execSync('npm install @vercel/sdk', {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit' stdio: 'inherit',
}); });
console.log('✅ @vercel/sdk installed successfully'); console.log('✅ @vercel/sdk installed successfully');
} }
@ -880,7 +1016,6 @@ async function main(): Promise<void> {
} }
await deployToVercel(useGitHub); await deployToVercel(useGitHub);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.error('\n❌ Error:', error.message); console.error('\n❌ Error:', error.message);

View File

@ -1,46 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '~/auth';
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
);
}
const body = await request.json();
const { signers, user } = body;
if (!signers || !user) {
return NextResponse.json(
{ error: 'Signers and user are required' },
{ status: 400 }
);
}
// For NextAuth to update the session, we need to trigger the JWT callback
// This is typically done by calling the session endpoint with updated data
// However, we can't directly modify the session token from here
// Instead, we'll store the data temporarily and let the client refresh the session
// The session will be updated when the JWT callback is triggered
return NextResponse.json({
success: true,
message: 'Session update prepared',
signers,
user,
});
} catch (error) {
console.error('Error preparing session update:', error);
return NextResponse.json(
{ error: 'Failed to prepare session update' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,46 @@
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 },
);
}
}

View File

@ -1,6 +1,5 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getSession } from "~/auth"
import "~/app/globals.css"; import "~/app/globals.css";
import { Providers } from "~/app/providers"; import { Providers } from "~/app/providers";
import { APP_NAME, APP_DESCRIPTION } from "~/lib/constants"; import { APP_NAME, APP_DESCRIPTION } from "~/lib/constants";
@ -15,7 +14,19 @@ export default async function RootLayout({
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getSession() // Only get session if sponsored signer is enabled or seed phrase is provided
const sponsorSigner = process.env.SPONSOR_SIGNER === 'true';
const hasSeedPhrase = !!process.env.SEED_PHRASE;
let session = null;
if (sponsorSigner || hasSeedPhrase) {
try {
const { getSession } = await import("~/auth");
session = await getSession();
} catch (error) {
console.warn('Failed to get session:', error);
}
}
return ( return (
<html lang="en"> <html lang="en">

View File

@ -24,6 +24,9 @@ export function Providers({
}) { }) {
const solanaEndpoint = const solanaEndpoint =
process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com'; process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com';
// Only wrap with SessionProvider if session is provided
if (session) {
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<WagmiProvider> <WagmiProvider>
@ -38,4 +41,19 @@ export function Providers({
</WagmiProvider> </WagmiProvider>
</SessionProvider> </SessionProvider>
); );
}
// Return without SessionProvider if no session
return (
<WagmiProvider>
<MiniAppProvider
analyticsEnabled={ANALYTICS_ENABLED}
backButtonEnabled={true}
>
<SafeFarcasterSolanaProvider endpoint={solanaEndpoint}>
<AuthKitProvider config={{}}>{children}</AuthKitProvider>
</SafeFarcasterSolanaProvider>
</MiniAppProvider>
</WagmiProvider>
);
} }

View File

@ -1,13 +1,23 @@
'use client'; 'use client';
import { useCallback, useState } from 'react'; import { useCallback, useState, type ComponentType } from 'react';
import { useMiniApp } from '@neynar/react'; import { useMiniApp } from '@neynar/react';
import { ShareButton } from '../Share'; import { ShareButton } from '../Share';
import { Button } from '../Button'; import { Button } from '../Button';
import { SignIn } from '../wallet/SignIn'; import { SignIn } from '../wallet/SignIn';
import { type Haptics } from '@farcaster/miniapp-sdk'; import { type Haptics } from '@farcaster/miniapp-sdk';
import { APP_URL } from '~/lib/constants'; import { APP_URL } from '~/lib/constants';
import { NeynarAuthButton } from '../NeynarAuthButton/index';
// Optional import for NeynarAuthButton - may not exist in all templates
let NeynarAuthButton: ComponentType | null = null;
try {
const module = require('../NeynarAuthButton/index');
NeynarAuthButton = module.NeynarAuthButton;
} catch (error) {
// Component doesn't exist, that's okay
console.log('NeynarAuthButton not available in this template');
}
/** /**
* ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback.
@ -140,7 +150,7 @@ export function ActionsTab() {
<SignIn /> <SignIn />
{/* Neynar Authentication */} {/* Neynar Authentication */}
<NeynarAuthButton /> {NeynarAuthButton && <NeynarAuthButton />}
{/* Mini app actions */} {/* Mini app actions */}
<Button <Button

View File

@ -1,22 +1,20 @@
'use client'; 'use client';
import { useCallback, useState } from "react"; import { useCallback, useState } from 'react';
import { signIn, signOut, getCsrfToken } from "next-auth/react"; import { SignIn as SignInCore } from '@farcaster/miniapp-sdk';
import sdk, { SignIn as SignInCore } from "@farcaster/miniapp-sdk"; import { useQuickAuth } from '~/hooks/useQuickAuth';
import { useSession } from "next-auth/react"; import { Button } from '../Button';
import { Button } from "../Button";
/** /**
* SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF). * SignIn component handles Farcaster authentication using QuickAuth.
* *
* This component provides a complete authentication flow for Farcaster users: * This component provides a complete authentication flow for Farcaster users:
* - Generates nonces for secure authentication * - Uses the built-in QuickAuth functionality from the Farcaster SDK
* - Handles the SIWF flow using the Farcaster SDK * - Manages authentication state in memory (no persistence)
* - Manages NextAuth session state
* - Provides sign-out functionality * - Provides sign-out functionality
* - Displays authentication status and results * - Displays authentication status and results
* *
* The component integrates with both the Farcaster Frame SDK and NextAuth * The component integrates with the Farcaster Frame SDK and QuickAuth
* to provide seamless authentication within mini apps. * to provide seamless authentication within mini apps.
* *
* @example * @example
@ -36,52 +34,32 @@ export function SignIn() {
signingIn: false, signingIn: false,
signingOut: false, signingOut: false,
}); });
const [signInResult, setSignInResult] = useState<SignInCore.SignInResult>();
const [signInFailure, setSignInFailure] = useState<string>(); const [signInFailure, setSignInFailure] = useState<string>();
// --- Hooks --- // --- Hooks ---
const { data: session, status } = useSession(); const { authenticatedUser, status, signIn, signOut } = useQuickAuth();
// --- Handlers --- // --- Handlers ---
/** /**
* Generates a nonce for the sign-in process. * Handles the sign-in process using QuickAuth.
* *
* This function retrieves a CSRF token from NextAuth to use as a nonce * This function uses the built-in QuickAuth functionality:
* for the SIWF authentication flow. The nonce ensures the authentication * 1. Gets a token from QuickAuth (handles SIWF flow automatically)
* request is fresh and prevents replay attacks. * 2. Validates the token with our server
* * 3. Updates the session state
* @returns Promise<string> - The generated nonce token
* @throws Error if unable to generate nonce
*/
const getNonce = useCallback(async () => {
const nonce = await getCsrfToken();
if (!nonce) throw new Error('Unable to generate nonce');
return nonce;
}, []);
/**
* Handles the sign-in process using Farcaster SDK.
*
* This function orchestrates the complete SIWF flow:
* 1. Generates a nonce for security
* 2. Calls the Farcaster SDK to initiate sign-in
* 3. Submits the result to NextAuth for session management
* 4. Handles various error conditions including user rejection
* *
* @returns Promise<void> * @returns Promise<void>
*/ */
const handleSignIn = useCallback(async () => { const handleSignIn = useCallback(async () => {
try { try {
setAuthState((prev) => ({ ...prev, signingIn: true })); setAuthState(prev => ({ ...prev, signingIn: true }));
setSignInFailure(undefined); setSignInFailure(undefined);
const nonce = await getNonce();
const result = await sdk.actions.signIn({ nonce }); const success = await signIn();
setSignInResult(result);
await signIn('farcaster', { if (!success) {
message: result.message, setSignInFailure('Authentication failed');
signature: result.signature, }
redirect: false,
});
} catch (e) { } catch (e) {
if (e instanceof SignInCore.RejectedByUser) { if (e instanceof SignInCore.RejectedByUser) {
setSignInFailure('Rejected by user'); setSignInFailure('Rejected by user');
@ -89,52 +67,49 @@ export function SignIn() {
} }
setSignInFailure('Unknown error'); setSignInFailure('Unknown error');
} finally { } finally {
setAuthState((prev) => ({ ...prev, signingIn: false })); setAuthState(prev => ({ ...prev, signingIn: false }));
} }
}, [getNonce]); }, [signIn]);
/** /**
* Handles the sign-out process. * Handles the sign-out process.
* *
* This function clears the NextAuth session only if the current session * This function clears the QuickAuth session and resets the local state.
* is using the Farcaster provider, and resets the local sign-in result state.
* *
* @returns Promise<void> * @returns Promise<void>
*/ */
const handleSignOut = useCallback(async () => { const handleSignOut = useCallback(async () => {
try { try {
setAuthState((prev) => ({ ...prev, signingOut: true })); setAuthState(prev => ({ ...prev, signingOut: true }));
// Only sign out if the current session is from Farcaster provider await signOut();
if (session?.provider === 'farcaster') {
await signOut({ redirect: false });
}
setSignInResult(undefined);
} finally { } finally {
setAuthState((prev) => ({ ...prev, signingOut: false })); setAuthState(prev => ({ ...prev, signingOut: false }));
} }
}, [session]); }, [signOut]);
// --- Render --- // --- Render ---
return ( return (
<> <>
{/* Authentication Buttons */} {/* Authentication Buttons */}
{(status !== 'authenticated' || session?.provider !== 'farcaster') && ( {status !== 'authenticated' && (
<Button onClick={handleSignIn} disabled={authState.signingIn}> <Button onClick={handleSignIn} disabled={authState.signingIn}>
Sign In with Farcaster Sign In with Farcaster
</Button> </Button>
)} )}
{status === 'authenticated' && session?.provider === 'farcaster' && ( {status === 'authenticated' && (
<Button onClick={handleSignOut} disabled={authState.signingOut}> <Button onClick={handleSignOut} disabled={authState.signingOut}>
Sign out Sign out
</Button> </Button>
)} )}
{/* Session Information */} {/* Session Information */}
{session && ( {authenticatedUser && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono"> <div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">Session</div> <div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
Authenticated User
</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200"> <div className="whitespace-pre text-gray-700 dark:text-gray-200">
{JSON.stringify(session, null, 2)} {JSON.stringify(authenticatedUser, null, 2)}
</div> </div>
</div> </div>
)} )}
@ -142,17 +117,11 @@ export function SignIn() {
{/* Error Display */} {/* Error Display */}
{signInFailure && !authState.signingIn && ( {signInFailure && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono"> <div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div> <div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
<div className="whitespace-pre text-gray-700 dark:text-gray-200">{signInFailure}</div> Authentication Error
</div> </div>
)}
{/* Success Result Display */}
{signInResult && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200"> <div className="whitespace-pre text-gray-700 dark:text-gray-200">
{JSON.stringify(signInResult, null, 2)} {signInFailure}
</div> </div>
</div> </div>
)} )}

207
src/hooks/useQuickAuth.ts Normal file
View File

@ -0,0 +1,207 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { sdk } from '@farcaster/miniapp-sdk';
/**
* Represents the current authenticated user state
*/
interface AuthenticatedUser {
/** The user's Farcaster ID (FID) */
fid: number;
}
/**
* Possible authentication states for QuickAuth
*/
type QuickAuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
/**
* Return type for the useQuickAuth hook
*/
interface UseQuickAuthReturn {
/** Current authenticated user data, or null if not authenticated */
authenticatedUser: AuthenticatedUser | null;
/** Current authentication status */
status: QuickAuthStatus;
/** Function to initiate the sign-in process using QuickAuth */
signIn: () => Promise<boolean>;
/** Function to sign out and clear the current authentication state */
signOut: () => Promise<void>;
/** Function to retrieve the current authentication token */
getToken: () => Promise<string | null>;
}
/**
* Custom hook for managing QuickAuth authentication state
*
* This hook provides a complete authentication flow using Farcaster's QuickAuth:
* - Automatically checks for existing authentication on mount
* - Validates tokens with the server-side API
* - Manages authentication state in memory (no persistence)
* - Provides sign-in/sign-out functionality
*
* QuickAuth tokens are managed in memory only, so signing out of the Farcaster
* client will automatically sign the user out of this mini app as well.
*
* @returns {UseQuickAuthReturn} Object containing user state and authentication methods
*
* @example
* ```tsx
* const { authenticatedUser, status, signIn, signOut } = useQuickAuth();
*
* if (status === 'loading') return <div>Loading...</div>;
* if (status === 'unauthenticated') return <button onClick={signIn}>Sign In</button>;
*
* return (
* <div>
* <p>Welcome, FID: {authenticatedUser?.fid}</p>
* <button onClick={signOut}>Sign Out</button>
* </div>
* );
* ```
*/
export function useQuickAuth(): UseQuickAuthReturn {
// Current authenticated user data
const [authenticatedUser, setAuthenticatedUser] =
useState<AuthenticatedUser | null>(null);
// Current authentication status
const [status, setStatus] = useState<QuickAuthStatus>('loading');
/**
* Validates a QuickAuth token with the server-side API
*
* @param {string} authToken - The JWT token to validate
* @returns {Promise<AuthenticatedUser | null>} User data if valid, null otherwise
*/
const validateTokenWithServer = async (
authToken: string,
): Promise<AuthenticatedUser | null> => {
try {
const validationResponse = await fetch('/api/auth/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: authToken }),
});
if (validationResponse.ok) {
const responseData = await validationResponse.json();
return responseData.user;
}
return null;
} catch (error) {
console.error('Token validation failed:', error);
return null;
}
};
/**
* Checks for existing authentication token and validates it on component mount
* This runs automatically when the hook is first used
*/
useEffect(() => {
const checkExistingAuthentication = async () => {
try {
// Attempt to retrieve existing token from QuickAuth SDK
const { token } = await sdk.quickAuth.getToken();
if (token) {
// Validate the token with our server-side API
const validatedUserSession = await validateTokenWithServer(token);
if (validatedUserSession) {
// Token is valid, set authenticated state
setAuthenticatedUser(validatedUserSession);
setStatus('authenticated');
} else {
// Token is invalid or expired, clear authentication state
setStatus('unauthenticated');
}
} else {
// No existing token found, user is not authenticated
setStatus('unauthenticated');
}
} catch (error) {
console.error('Error checking existing authentication:', error);
setStatus('unauthenticated');
}
};
checkExistingAuthentication();
}, []);
/**
* Initiates the QuickAuth sign-in process
*
* Uses sdk.quickAuth.getToken() to get a QuickAuth session token.
* If there is already a session token in memory that hasn't expired,
* it will be immediately returned, otherwise a fresh one will be acquired.
*
* @returns {Promise<boolean>} True if sign-in was successful, false otherwise
*/
const signIn = useCallback(async (): Promise<boolean> => {
try {
setStatus('loading');
// Get QuickAuth session token
const { token } = await sdk.quickAuth.getToken();
if (token) {
// Validate the token with our server-side API
const validatedUserSession = await validateTokenWithServer(token);
if (validatedUserSession) {
// Authentication successful, update user state
setAuthenticatedUser(validatedUserSession);
setStatus('authenticated');
return true;
}
}
// Authentication failed, clear user state
setStatus('unauthenticated');
return false;
} catch (error) {
console.error('Sign-in process failed:', error);
setStatus('unauthenticated');
return false;
}
}, []);
/**
* Signs out the current user and clears the authentication state
*
* Since QuickAuth tokens are managed in memory only, this simply clears
* the local user state. The actual token will be cleared when the
* user signs out of their Farcaster client.
*/
const signOut = useCallback(async (): Promise<void> => {
// Clear local user state
setAuthenticatedUser(null);
setStatus('unauthenticated');
}, []);
/**
* Retrieves the current authentication token from QuickAuth
*
* @returns {Promise<string | null>} The current auth token, or null if not authenticated
*/
const getToken = useCallback(async (): Promise<string | null> => {
try {
const { token } = await sdk.quickAuth.getToken();
return token;
} catch (error) {
console.error('Failed to retrieve authentication token:', error);
return null;
}
}, []);
return {
authenticatedUser,
status,
signIn,
signOut,
getToken,
};
}

View File

@ -111,6 +111,18 @@ export const USE_WALLET: boolean = true;
*/ */
export const ANALYTICS_ENABLED: boolean = true; export const ANALYTICS_ENABLED: boolean = true;
/**
* Required chains for the mini app.
*
* Contains an array of CAIP-2 identifiers for blockchains that the mini app requires.
* If the host does not support all chains listed here, it will not render the mini app.
* If empty or undefined, the mini app will be rendered regardless of chain support.
*
* Supported chains: eip155:1, eip155:137, eip155:42161, eip155:10, eip155:8453,
* solana:mainnet, solana:devnet
*/
export const APP_REQUIRED_CHAINS: string[] = [];
// PLEASE DO NOT UPDATE THIS // PLEASE DO NOT UPDATE THIS
export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
name: 'Farcaster SignedKeyRequestValidator', name: 'Farcaster SignedKeyRequestValidator',

View File

@ -13,6 +13,7 @@ import {
APP_TAGS, APP_URL, APP_TAGS, APP_URL,
APP_WEBHOOK_URL, APP_WEBHOOK_URL,
APP_ACCOUNT_ASSOCIATION, APP_ACCOUNT_ASSOCIATION,
APP_REQUIRED_CHAINS,
} from './constants'; } from './constants';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@ -23,6 +24,9 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) {
return { return {
version: "next", version: "next",
imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL, imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL,
ogTitle: APP_NAME,
ogDescription: APP_DESCRIPTION,
ogImageUrl: ogImageUrl ?? APP_OG_IMAGE_URL,
button: { button: {
title: APP_BUTTON_TEXT, title: APP_BUTTON_TEXT,
action: { action: {
@ -56,6 +60,10 @@ export async function getFarcasterDomainManifest(): Promise<Manifest> {
description: APP_DESCRIPTION, description: APP_DESCRIPTION,
primaryCategory: APP_PRIMARY_CATEGORY, primaryCategory: APP_PRIMARY_CATEGORY,
tags: APP_TAGS, tags: APP_TAGS,
requiredChains: APP_REQUIRED_CHAINS.length > 0 ? APP_REQUIRED_CHAINS : undefined,
ogTitle: APP_NAME,
ogDescription: APP_DESCRIPTION,
ogImageUrl: APP_OG_IMAGE_URL,
}, },
}; };
} }