mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
fix: auth kit url update and seed phrase input
This commit is contained in:
parent
36d2b5d0f7
commit
5fa624a063
38
bin/init.js
38
bin/init.js
@ -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}"`);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neynar/create-farcaster-mini-app",
|
||||
"version": "1.5.7",
|
||||
"version": "1.5.8",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"access": "public",
|
||||
|
||||
@ -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'm using my phone →
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user