From 21ccdf3623a25ab1bf28201b7d2397c7e268882a Mon Sep 17 00:00:00 2001 From: veganbeef Date: Fri, 6 Jun 2025 09:38:57 -0700 Subject: [PATCH] feat: share button --- bin/init.js | 2 +- package.json | 2 +- src/app/api/best-friends/route.ts | 44 ++++++++++++ src/components/Demo.tsx | 18 ++--- src/components/ui/Share.tsx | 107 ++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 src/app/api/best-friends/route.ts create mode 100644 src/components/ui/Share.tsx diff --git a/bin/init.js b/bin/init.js index 6f1b11a..0f80586 100644 --- a/bin/init.js +++ b/bin/init.js @@ -328,7 +328,7 @@ export async function init() { "@farcaster/frame-node": ">=0.0.18 <1.0.0", "@farcaster/frame-sdk": ">=0.0.31 <1.0.0", "@farcaster/frame-wagmi-connector": ">=0.0.19 <1.0.0", - "@farcaster/mini-app-solana": "^0.0.5", + "@farcaster/mini-app-solana": ">=0.0.17 <1.0.0", "@neynar/react": "^1.2.2", "@radix-ui/react-label": "^2.1.1", "@solana/wallet-adapter-react": "^0.15.38", diff --git a/package.json b/package.json index 968e0dd..146f279 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.3.1", + "version": "1.3.2", "type": "module", "private": false, "access": "public", diff --git a/src/app/api/best-friends/route.ts b/src/app/api/best-friends/route.ts new file mode 100644 index 0000000..b223684 --- /dev/null +++ b/src/app/api/best-friends/route.ts @@ -0,0 +1,44 @@ +import { NeynarAPIClient } from '@neynar/nodejs-sdk'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const apiKey = process.env.NEYNAR_API_KEY; + const { searchParams } = new URL(request.url); + const fid = searchParams.get('fid'); + + if (!apiKey) { + return NextResponse.json( + { error: 'Neynar API key is not configured. Please add NEYNAR_API_KEY to your environment variables.' }, + { status: 500 } + ); + } + + if (!fid) { + return NextResponse.json( + { error: 'FID parameter is required' }, + { status: 400 } + ); + } + + try { + const neynar = new NeynarAPIClient({ apiKey }); + const { users } = await neynar.fetchUserFollowers({ + fid: parseInt(fid), + limit: 3, + viewerFid: parseInt(fid), + }); + + const bestFriends = users.map(user => ({ + fid: user.user?.fid, + username: user.user?.username, + })); + + return NextResponse.json({ bestFriends }); + } catch (error) { + console.error('Failed to fetch best friends:', error); + return NextResponse.json( + { error: 'Failed to fetch best friends. Please check your Neynar API key and try again.' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/components/Demo.tsx b/src/components/Demo.tsx index 4c46d81..5b1d0e6 100644 --- a/src/components/Demo.tsx +++ b/src/components/Demo.tsx @@ -42,10 +42,7 @@ export default function Demo( added, notificationDetails, lastEvent, - addFrame, - addFrameResult, - openUrl, - close, + actions, } = useMiniApp(); const [isContextOpen, setIsContextOpen] = useState(false); const [txHash, setTxHash] = useState(null); @@ -244,7 +241,7 @@ export default function Demo( sdk.actions.openUrl - +
@@ -262,7 +259,7 @@ export default function Demo( sdk.actions.close
- + @@ -290,15 +287,10 @@ export default function Demo(
-                sdk.actions.addFrame
+                sdk.actions.addMiniApp
               
- {addFrameResult && ( -
- Add frame result: {addFrameResult} -
- )} -
diff --git a/src/components/ui/Share.tsx b/src/components/ui/Share.tsx new file mode 100644 index 0000000..625c166 --- /dev/null +++ b/src/components/ui/Share.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { useCallback, useState, useEffect } from 'react'; +import { Button } from './Button'; +import { useMiniApp } from '@neynar/react'; +import { type ComposeCast } from "@farcaster/frame-sdk"; + +interface CastConfig extends ComposeCast.Options { + bestFriends?: boolean; +} + +interface ShareButtonProps { + buttonText: string; + cast: CastConfig; + className?: string; + isLoading?: boolean; +} + +export function ShareButton({ buttonText, cast, className = '', isLoading = false }: ShareButtonProps) { + const [isProcessing, setIsProcessing] = useState(false); + const [bestFriends, setBestFriends] = useState<{ fid: number; username: string; }[] | null>(null); + const { context, actions } = useMiniApp(); + + // Fetch best friends if needed + useEffect(() => { + if (cast.bestFriends && context?.user?.fid) { + setIsProcessing(true); + fetch(`/api/best-friends?fid=${context.user.fid}`) + .then(res => res.json()) + .then(data => setBestFriends(data.bestFriends)) + .catch(err => console.error('Failed to fetch best friends:', err)) + .finally(() => setIsProcessing(false)); + } + }, [cast.bestFriends, context?.user?.fid]); + + const handleShare = useCallback(async () => { + try { + setIsProcessing(true); + + let finalText = cast.text || ''; + + // Process best friends if enabled and data is loaded + if (cast.bestFriends && bestFriends) { + // Replace @N with usernames + finalText = finalText.replace(/@\d+/g, (match) => { + const friendIndex = parseInt(match.slice(1)) - 1; + const friend = bestFriends[friendIndex]; + if (friend) { + return `@${friend.username}`; + } + return match; + }); + } + + // Process embeds + const processedEmbeds = await Promise.all( + (cast.embeds || []).map(async (embed) => { + if (typeof embed === 'string') { + return embed; + } + if (embed.path) { + const baseUrl = process.env.NEXT_PUBLIC_URL || window.location.origin; + const url = new URL(`${baseUrl}${embed.path}`); + + // Add UTM parameters + url.searchParams.set('utm_source', `share-cast-${context?.user?.fid || 'unknown'}`); + + // If custom image generator is provided, use it + if (embed.imageUrl) { + const imageUrl = await embed.imageUrl(); + url.searchParams.set('share_image_url', imageUrl); + } + + return url.toString(); + } + return embed.url || ''; + }) + ); + + // Open cast composer with all supported intents + await actions.composeCast({ + text: finalText, + embeds: processedEmbeds as [string] | [string, string] | undefined, + parent: cast.parent, + channel: cast.channelKey, + close: cast.close, + }, 'share-button'); + } catch (error) { + console.error('Failed to share:', error); + } finally { + setIsProcessing(false); + } + }, [cast, bestFriends, context?.user?.fid, actions]); + + const isButtonDisabled = cast.bestFriends && !bestFriends; + + return ( + + ); +}