fix: auth kit url update and seed phrase input

This commit is contained in:
veganbeef 2025-07-11 22:28:05 -07:00
parent 36d2b5d0f7
commit 5fa624a063
No known key found for this signature in database
4 changed files with 113 additions and 36 deletions

View File

@ -338,6 +338,43 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
]);
answers.useTunnel = hostingAnswer.useTunnel;
// Ask about Neynar Sponsored Signers / SIWN
const sponsoredSignerAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'useSponsoredSigner',
message:
'Would you like to use Neynar Sponsored Signers and/or Sign In With Neynar (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',
default: false,
},
]);
answers.useSponsoredSigner = sponsoredSignerAnswer.useSponsoredSigner;
if (answers.useSponsoredSigner) {
const { seedPhrase } = await inquirer.prompt([
{
type: 'password',
name: 'seedPhrase',
message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
validate: (input) => {
if (!input || input.trim().split(' ').length < 12) {
return 'Seed phrase must be at least 12 words';
}
return true;
},
},
]);
answers.seedPhrase = seedPhrase;
}
// Ask about analytics opt-out
const analyticsAnswer = await inquirer.prompt([
{
@ -601,7 +638,6 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
}
if (answers.seedPhrase) {
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="${answers.sponsorSigner}"`);
}
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);

View File

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

View File

@ -191,7 +191,8 @@ export function AuthDialog({
{content.showOpenButton && content.qrUrl && (
<button
onClick={() =>
onClick={() => {
if (content.qrUrl) {
window.open(
content.qrUrl
.replace(
@ -199,12 +200,13 @@ export function AuthDialog({
'https://client.farcaster.xyz/deeplinks/'
)
.replace(
'https://client.farcaster.xyz/deeplinks/',
'farcaster://'
'https://client.farcaster.xyz/deeplinks/signed-key-request',
'https://farcaster.xyz/~/connect'
),
'_blank'
)
}
}}
className="btn btn-outline flex items-center justify-center gap-2 w-full"
>
I&apos;m using my phone

View File

@ -1,11 +1,10 @@
'use client';
import '@farcaster/auth-kit/styles.css';
import { useSignIn } from '@farcaster/auth-kit';
import { useCallback, useEffect, useState } from 'react';
import { useSignIn, UseSignInData } from '@farcaster/auth-kit';
import { useCallback, useEffect, useState, useRef } from 'react';
import { cn } from '~/lib/utils';
import { Button } from '~/components/ui/Button';
import { isMobile } from '~/lib/devices';
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
import { getItem, removeItem, setItem } from '~/lib/localStorage';
@ -113,6 +112,8 @@ export function NeynarAuthButton() {
);
const [message, setMessage] = useState<string | null>(null);
const [signature, setSignature] = useState<string | null>(null);
const [isSignerFlowRunning, setIsSignerFlowRunning] = useState(false);
const signerFlowStartedRef = useRef(false);
// Determine which flow to use based on context
const useBackendFlow = context !== undefined;
@ -290,14 +291,46 @@ export function NeynarAuthButton() {
// Helper function to poll signer status
const startPolling = useCallback(
(signerUuid: string, message: string, signature: string) => {
// Clear any existing polling interval before starting a new one
if (pollingInterval) {
clearInterval(pollingInterval);
}
let retryCount = 0;
const maxRetries = 10; // Maximum 10 retries (20 seconds total)
const maxPollingTime = 60000; // Maximum 60 seconds of polling
const startTime = Date.now();
const interval = setInterval(async () => {
// Check if we've been polling too long
if (Date.now() - startTime > maxPollingTime) {
clearInterval(interval);
setPollingInterval(null);
return;
}
try {
const response = await fetch(
`/api/auth/signer?signerUuid=${signerUuid}`
);
if (!response.ok) {
throw new Error('Failed to poll signer status');
// Check if it's a rate limit error
if (response.status === 429) {
clearInterval(interval);
setPollingInterval(null);
return;
}
// Increment retry count for other errors
retryCount++;
if (retryCount >= maxRetries) {
clearInterval(interval);
setPollingInterval(null);
return;
}
throw new Error(`Failed to poll signer status: ${response.status}`);
}
const signerData = await response.json();
@ -319,7 +352,7 @@ export function NeynarAuthButton() {
setPollingInterval(interval);
},
[fetchAllSigners]
[fetchAllSigners, pollingInterval]
);
// Cleanup polling on unmount
@ -328,6 +361,7 @@ export function NeynarAuthButton() {
if (pollingInterval) {
clearInterval(pollingInterval);
}
signerFlowStartedRef.current = false;
};
}, [pollingInterval]);
@ -362,11 +396,11 @@ export function NeynarAuthButton() {
// Success callback - this is critical!
const onSuccessCallback = useCallback(
async (res: unknown) => {
async (res: UseSignInData) => {
if (!useBackendFlow) {
// Only handle localStorage for frontend flow
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
const user = await fetchUserData(res.fid);
const user = res.fid ? await fetchUserData(res.fid) : null;
const authState: StoredAuthState = {
...existingAuth,
isAuthenticated: true,
@ -409,6 +443,11 @@ export function NeynarAuthButton() {
useEffect(() => {
setMessage(data?.message || null);
setSignature(data?.signature || null);
// Reset the signer flow flag when message/signature change
if (data?.message && data?.signature) {
signerFlowStartedRef.current = false;
}
}, [data?.message, data?.signature]);
// Connect for frontend flow when nonce is available
@ -420,8 +459,11 @@ export function NeynarAuthButton() {
// Handle fetching signers after successful authentication
useEffect(() => {
if (message && signature) {
if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) {
signerFlowStartedRef.current = true;
const handleSignerFlow = async () => {
setIsSignerFlowRunning(true);
try {
const clientContext = context?.client as Record<string, unknown>;
const isMobileContext =
@ -437,6 +479,7 @@ export function NeynarAuthButton() {
// First, fetch existing signers
const signers = await fetchAllSigners(message, signature);
if (useBackendFlow && isMobileContext) setSignersLoading(true);
// Check if no signers exist or if we have empty signers
@ -457,8 +500,8 @@ export function NeynarAuthButton() {
setShowDialog(false);
await sdk.actions.openUrl(
signedKeyData.signer_approval_url.replace(
'https://client.farcaster.xyz/deeplinks/',
'farcaster://'
'https://client.farcaster.xyz/deeplinks/signed-key-request',
'https://farcaster.xyz/~/connect'
)
);
} else {
@ -481,21 +524,14 @@ export function NeynarAuthButton() {
setSignersLoading(false);
setShowDialog(false);
setSignerApprovalUrl(null);
} finally {
setIsSignerFlowRunning(false);
}
};
handleSignerFlow();
}
}, [
message,
signature,
fetchAllSigners,
createSigner,
generateSignedKeyRequest,
startPolling,
context,
useBackendFlow,
]);
}, [message, signature]); // Simplified dependencies
// Backend flow using NextAuth
const handleBackendSignIn = useCallback(async () => {
@ -568,6 +604,9 @@ export function NeynarAuthButton() {
clearInterval(pollingInterval);
setPollingInterval(null);
}
// Reset signer flow flag
signerFlowStartedRef.current = false;
} catch (error) {
console.error('❌ Error during sign out:', error);
// Optionally handle error state