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;
|
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
|
// Ask about analytics opt-out
|
||||||
const analyticsAnswer = await inquirer.prompt([
|
const analyticsAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
@ -601,7 +638,6 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
|
|||||||
}
|
}
|
||||||
if (answers.seedPhrase) {
|
if (answers.seedPhrase) {
|
||||||
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
|
fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`);
|
||||||
fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="${answers.sponsorSigner}"`);
|
|
||||||
}
|
}
|
||||||
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neynar/create-farcaster-mini-app",
|
"name": "@neynar/create-farcaster-mini-app",
|
||||||
"version": "1.5.7",
|
"version": "1.5.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": false,
|
"private": false,
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@ -191,7 +191,8 @@ export function AuthDialog({
|
|||||||
|
|
||||||
{content.showOpenButton && content.qrUrl && (
|
{content.showOpenButton && content.qrUrl && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
|
if (content.qrUrl) {
|
||||||
window.open(
|
window.open(
|
||||||
content.qrUrl
|
content.qrUrl
|
||||||
.replace(
|
.replace(
|
||||||
@ -199,12 +200,13 @@ export function AuthDialog({
|
|||||||
'https://client.farcaster.xyz/deeplinks/'
|
'https://client.farcaster.xyz/deeplinks/'
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
'https://client.farcaster.xyz/deeplinks/',
|
'https://client.farcaster.xyz/deeplinks/signed-key-request',
|
||||||
'farcaster://'
|
'https://farcaster.xyz/~/connect'
|
||||||
),
|
),
|
||||||
'_blank'
|
'_blank'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
className="btn btn-outline flex items-center justify-center gap-2 w-full"
|
className="btn btn-outline flex items-center justify-center gap-2 w-full"
|
||||||
>
|
>
|
||||||
I'm using my phone →
|
I'm using my phone →
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import '@farcaster/auth-kit/styles.css';
|
import '@farcaster/auth-kit/styles.css';
|
||||||
import { useSignIn } from '@farcaster/auth-kit';
|
import { useSignIn, UseSignInData } from '@farcaster/auth-kit';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { cn } from '~/lib/utils';
|
import { cn } from '~/lib/utils';
|
||||||
import { Button } from '~/components/ui/Button';
|
import { Button } from '~/components/ui/Button';
|
||||||
import { isMobile } from '~/lib/devices';
|
|
||||||
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
|
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
|
||||||
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
|
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
|
||||||
import { getItem, removeItem, setItem } from '~/lib/localStorage';
|
import { getItem, removeItem, setItem } from '~/lib/localStorage';
|
||||||
@ -113,6 +112,8 @@ export function NeynarAuthButton() {
|
|||||||
);
|
);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [signature, setSignature] = 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
|
// Determine which flow to use based on context
|
||||||
const useBackendFlow = context !== undefined;
|
const useBackendFlow = context !== undefined;
|
||||||
@ -290,14 +291,46 @@ export function NeynarAuthButton() {
|
|||||||
// Helper function to poll signer status
|
// Helper function to poll signer status
|
||||||
const startPolling = useCallback(
|
const startPolling = useCallback(
|
||||||
(signerUuid: string, message: string, signature: string) => {
|
(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 () => {
|
const interval = setInterval(async () => {
|
||||||
|
// Check if we've been polling too long
|
||||||
|
if (Date.now() - startTime > maxPollingTime) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setPollingInterval(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/auth/signer?signerUuid=${signerUuid}`
|
`/api/auth/signer?signerUuid=${signerUuid}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const signerData = await response.json();
|
||||||
@ -319,7 +352,7 @@ export function NeynarAuthButton() {
|
|||||||
|
|
||||||
setPollingInterval(interval);
|
setPollingInterval(interval);
|
||||||
},
|
},
|
||||||
[fetchAllSigners]
|
[fetchAllSigners, pollingInterval]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cleanup polling on unmount
|
// Cleanup polling on unmount
|
||||||
@ -328,6 +361,7 @@ export function NeynarAuthButton() {
|
|||||||
if (pollingInterval) {
|
if (pollingInterval) {
|
||||||
clearInterval(pollingInterval);
|
clearInterval(pollingInterval);
|
||||||
}
|
}
|
||||||
|
signerFlowStartedRef.current = false;
|
||||||
};
|
};
|
||||||
}, [pollingInterval]);
|
}, [pollingInterval]);
|
||||||
|
|
||||||
@ -362,11 +396,11 @@ export function NeynarAuthButton() {
|
|||||||
|
|
||||||
// Success callback - this is critical!
|
// Success callback - this is critical!
|
||||||
const onSuccessCallback = useCallback(
|
const onSuccessCallback = useCallback(
|
||||||
async (res: unknown) => {
|
async (res: UseSignInData) => {
|
||||||
if (!useBackendFlow) {
|
if (!useBackendFlow) {
|
||||||
// Only handle localStorage for frontend flow
|
// Only handle localStorage for frontend flow
|
||||||
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
|
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
|
||||||
const user = await fetchUserData(res.fid);
|
const user = res.fid ? await fetchUserData(res.fid) : null;
|
||||||
const authState: StoredAuthState = {
|
const authState: StoredAuthState = {
|
||||||
...existingAuth,
|
...existingAuth,
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
@ -409,6 +443,11 @@ export function NeynarAuthButton() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMessage(data?.message || null);
|
setMessage(data?.message || null);
|
||||||
setSignature(data?.signature || 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]);
|
}, [data?.message, data?.signature]);
|
||||||
|
|
||||||
// Connect for frontend flow when nonce is available
|
// Connect for frontend flow when nonce is available
|
||||||
@ -420,8 +459,11 @@ export function NeynarAuthButton() {
|
|||||||
|
|
||||||
// Handle fetching signers after successful authentication
|
// Handle fetching signers after successful authentication
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (message && signature) {
|
if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) {
|
||||||
|
signerFlowStartedRef.current = true;
|
||||||
|
|
||||||
const handleSignerFlow = async () => {
|
const handleSignerFlow = async () => {
|
||||||
|
setIsSignerFlowRunning(true);
|
||||||
try {
|
try {
|
||||||
const clientContext = context?.client as Record<string, unknown>;
|
const clientContext = context?.client as Record<string, unknown>;
|
||||||
const isMobileContext =
|
const isMobileContext =
|
||||||
@ -437,6 +479,7 @@ export function NeynarAuthButton() {
|
|||||||
|
|
||||||
// First, fetch existing signers
|
// First, fetch existing signers
|
||||||
const signers = await fetchAllSigners(message, signature);
|
const signers = await fetchAllSigners(message, signature);
|
||||||
|
|
||||||
if (useBackendFlow && isMobileContext) setSignersLoading(true);
|
if (useBackendFlow && isMobileContext) setSignersLoading(true);
|
||||||
|
|
||||||
// Check if no signers exist or if we have empty signers
|
// Check if no signers exist or if we have empty signers
|
||||||
@ -457,8 +500,8 @@ export function NeynarAuthButton() {
|
|||||||
setShowDialog(false);
|
setShowDialog(false);
|
||||||
await sdk.actions.openUrl(
|
await sdk.actions.openUrl(
|
||||||
signedKeyData.signer_approval_url.replace(
|
signedKeyData.signer_approval_url.replace(
|
||||||
'https://client.farcaster.xyz/deeplinks/',
|
'https://client.farcaster.xyz/deeplinks/signed-key-request',
|
||||||
'farcaster://'
|
'https://farcaster.xyz/~/connect'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -481,21 +524,14 @@ export function NeynarAuthButton() {
|
|||||||
setSignersLoading(false);
|
setSignersLoading(false);
|
||||||
setShowDialog(false);
|
setShowDialog(false);
|
||||||
setSignerApprovalUrl(null);
|
setSignerApprovalUrl(null);
|
||||||
|
} finally {
|
||||||
|
setIsSignerFlowRunning(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSignerFlow();
|
handleSignerFlow();
|
||||||
}
|
}
|
||||||
}, [
|
}, [message, signature]); // Simplified dependencies
|
||||||
message,
|
|
||||||
signature,
|
|
||||||
fetchAllSigners,
|
|
||||||
createSigner,
|
|
||||||
generateSignedKeyRequest,
|
|
||||||
startPolling,
|
|
||||||
context,
|
|
||||||
useBackendFlow,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Backend flow using NextAuth
|
// Backend flow using NextAuth
|
||||||
const handleBackendSignIn = useCallback(async () => {
|
const handleBackendSignIn = useCallback(async () => {
|
||||||
@ -568,6 +604,9 @@ export function NeynarAuthButton() {
|
|||||||
clearInterval(pollingInterval);
|
clearInterval(pollingInterval);
|
||||||
setPollingInterval(null);
|
setPollingInterval(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset signer flow flag
|
||||||
|
signerFlowStartedRef.current = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error during sign out:', error);
|
console.error('❌ Error during sign out:', error);
|
||||||
// Optionally handle error state
|
// Optionally handle error state
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user