diff --git a/.env.example b/.env.example deleted file mode 100644 index 9783487..0000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -KV_REST_API_TOKEN -KV_REST_API_URL= -NEXT_PUBLIC_URL= -NEXTAUTH_URL= -NEXTAUTH_SECERT= -NEYNAR_API_KEY= diff --git a/package.json b/package.json index 0c0f78e..7d55e0a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@farcaster/frame-node": "^0.0.7", "@farcaster/frame-sdk": "^0.0.26", "@farcaster/frame-wagmi-connector": "^0.0.6", + "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-switch": "^1.1.2", "@tanstack/react-query": "^5.61.0", "@upstash/redis": "^1.34.3", "class-variance-authority": "^0.7.1", @@ -25,6 +27,7 @@ "ox": "^0.4.2", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", + "react-dropzone": "^14.3.5", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "viem": "^2.21.55", @@ -39,5 +42,6 @@ "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/app/app.tsx b/src/app/app.tsx index f5757df..d3b61fb 100644 --- a/src/app/app.tsx +++ b/src/app/app.tsx @@ -1,6 +1,7 @@ "use client"; import dynamic from "next/dynamic"; +import "./globals.css"; const Demo = dynamic(() => import("~/components/Demo"), { ssr: false, diff --git a/src/app/globals.css b/src/app/globals.css index ccb716b..ac68442 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,35 +2,93 @@ @tailwind components; @tailwind utilities; -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { - color: var(--foreground); - background: var(--background); - font-family: 'Inter', Helvetica, Arial, sans-serif; -} - -* { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE and Edge */ + font-family: Arial, Helvetica, sans-serif; } -*::-webkit-scrollbar { - display: none; +@layer utilities { + .text-balance { + text-wrap: balance; + } } @layer base { :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; } } diff --git a/src/auth.ts b/src/auth.ts index 91695b4..11ba32d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,4 +1,4 @@ -import { AuthOptions, getServerSession } from "next-auth" +import { AuthOptions, getServerSession } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { createAppClient, viemConnector } from "@farcaster/auth-client"; @@ -11,7 +11,10 @@ declare module "next-auth" { } export const authOptions: AuthOptions = { - // Configure one or more authentication providers + // Configure one or more authentication providers + + // Fix: Add secret to Auth.ts for secure NextAuth.js configuration in production + secret: process.env.NEXTAUTH_SECRET, providers: [ CredentialsProvider({ name: "Sign in with Farcaster", @@ -49,7 +52,7 @@ export const authOptions: AuthOptions = { const verifyResponse = await appClient.verifySignInMessage({ message: credentials?.message as string, signature: credentials?.signature as `0x${string}`, - domain: new URL(process.env.NEXTAUTH_URL ?? '').hostname, + domain: new URL(process.env.NEXTAUTH_URL ?? "").hostname, nonce: csrfToken, }); const { success, fid } = verifyResponse; @@ -67,11 +70,11 @@ export const authOptions: AuthOptions = { callbacks: { session: async ({ session, token }) => { if (session?.user) { - session.user.fid = parseInt(token.sub ?? ''); + session.user.fid = parseInt(token.sub ?? ""); } return session; }, - } -} + }, +}; -export const getSession = () => getServerSession(authOptions) +export const getSession = () => getServerSession(authOptions); diff --git a/src/components/Demo.tsx b/src/components/Demo.tsx index fabc346..8ca2b13 100644 --- a/src/components/Demo.tsx +++ b/src/components/Demo.tsx @@ -1,711 +1,13 @@ -"use client"; - -import { useEffect, useCallback, useState, useMemo } from "react"; -import { Input } from "../components/ui/input" -import { signIn, signOut, getCsrfToken } from "next-auth/react"; -import sdk, { - AddFrame, - FrameNotificationDetails, - SignIn as SignInCore, - type Context, -} from "@farcaster/frame-sdk"; -import { - useAccount, - useSendTransaction, - useSignMessage, - useSignTypedData, - useWaitForTransactionReceipt, - useDisconnect, - useConnect, - useSwitchChain, - useChainId, -} from "wagmi"; - -import { config } from "~/components/providers/WagmiProvider"; -import { Button } from "~/components/ui/Button"; -import { truncateAddress } from "~/lib/truncateAddress"; -import { base, optimism } from "wagmi/chains"; -import { BaseError, UserRejectedRequestError } from "viem"; -import { useSession } from "next-auth/react" -import { createStore } from 'mipd' -import { Label } from "~/components/ui/label"; - - -export default function Demo( - { title }: { title?: string } = { title: "Frames v2 Demo" } -) { - const [isSDKLoaded, setIsSDKLoaded] = useState(false); - const [context, setContext] = useState(); - const [isContextOpen, setIsContextOpen] = useState(false); - const [txHash, setTxHash] = useState(null); - - const [added, setAdded] = useState(false); - const [notificationDetails, setNotificationDetails] = - useState(null); - - const [lastEvent, setLastEvent] = useState(""); - - const [addFrameResult, setAddFrameResult] = useState(""); - const [sendNotificationResult, setSendNotificationResult] = useState(""); - - useEffect(() => { - setNotificationDetails(context?.client.notificationDetails ?? null); - }, [context]); - - const { address, isConnected } = useAccount(); - const chainId = useChainId(); - - const { - sendTransaction, - error: sendTxError, - isError: isSendTxError, - isPending: isSendTxPending, - } = useSendTransaction(); - - const { isLoading: isConfirming, isSuccess: isConfirmed } = - useWaitForTransactionReceipt({ - hash: txHash as `0x${string}`, - }); - - const { - signTypedData, - error: signTypedError, - isError: isSignTypedError, - isPending: isSignTypedPending, - } = useSignTypedData(); - - const { disconnect } = useDisconnect(); - const { connect } = useConnect(); - - const { - switchChain, - error: switchChainError, - isError: isSwitchChainError, - isPending: isSwitchChainPending, - } = useSwitchChain(); - - const handleSwitchChain = useCallback(() => { - switchChain({ chainId: chainId === base.id ? optimism.id : base.id }); - }, [switchChain, chainId]); - - useEffect(() => { - const load = async () => { - const context = await sdk.context; - setContext(context); - setAdded(context.client.added); - - sdk.on("frameAdded", ({ notificationDetails }) => { - setLastEvent( - `frameAdded${!!notificationDetails ? ", notifications enabled" : ""}` - ); - - setAdded(true); - if (notificationDetails) { - setNotificationDetails(notificationDetails); - } - }); - - sdk.on("frameAddRejected", ({ reason }) => { - setLastEvent(`frameAddRejected, reason ${reason}`); - }); - - sdk.on("frameRemoved", () => { - setLastEvent("frameRemoved"); - setAdded(false); - setNotificationDetails(null); - }); - - sdk.on("notificationsEnabled", ({ notificationDetails }) => { - setLastEvent("notificationsEnabled"); - setNotificationDetails(notificationDetails); - }); - sdk.on("notificationsDisabled", () => { - setLastEvent("notificationsDisabled"); - setNotificationDetails(null); - }); - - sdk.on("primaryButtonClicked", () => { - console.log("primaryButtonClicked"); - }); - - console.log("Calling ready"); - sdk.actions.ready({}); - -// Set up a MIPD Store, and request Providers. -const store = createStore() - -// Subscribe to the MIPD Store. -store.subscribe(providerDetails => { - console.log("PROVIDER DETAILS", providerDetails) - // => [EIP6963ProviderDetail, EIP6963ProviderDetail, ...] -}) - - }; - if (sdk && !isSDKLoaded) { - console.log("Calling load"); - setIsSDKLoaded(true); - load(); - return () => { - sdk.removeAllListeners(); - }; - } - }, [isSDKLoaded]); - - const openUrl = useCallback(() => { - sdk.actions.openUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); - }, []); - - const openWarpcastUrl = useCallback(() => { - sdk.actions.openUrl("https://warpcast.com/~/compose"); - }, []); - - const close = useCallback(() => { - sdk.actions.close(); - }, []); - - const addFrame = useCallback(async () => { - try { - setNotificationDetails(null); - - const result = await sdk.actions.addFrame(); - - if (result.notificationDetails) { - setNotificationDetails(result.notificationDetails); - } - setAddFrameResult( - result.notificationDetails - ? `Added, got notificaton token ${result.notificationDetails.token} and url ${result.notificationDetails.url}` - : "Added, got no notification details" - ); - } catch (error) { - if (error instanceof AddFrame.RejectedByUser) { - setAddFrameResult(`Not added: ${error.message}`); - } - - if (error instanceof AddFrame.InvalidDomainManifest) { - setAddFrameResult(`Not added: ${error.message}`); - } - - setAddFrameResult(`Error: ${error}`); - } - }, []); - - const sendNotification = useCallback(async () => { - setSendNotificationResult(""); - if (!notificationDetails || !context) { - return; - } - - try { - const response = await fetch("/api/send-notification", { - method: "POST", - mode: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - fid: context.user.fid, - notificationDetails, - }), - }); - - if (response.status === 200) { - setSendNotificationResult("Success"); - return; - } else if (response.status === 429) { - setSendNotificationResult("Rate limited"); - return; - } - - const data = await response.text(); - setSendNotificationResult(`Error: ${data}`); - } catch (error) { - setSendNotificationResult(`Error: ${error}`); - } - }, [context, notificationDetails]); - - const sendTx = useCallback(() => { - sendTransaction( - { - // call yoink() on Yoink contract - to: "0x4bBFD120d9f352A0BEd7a014bd67913a2007a878", - data: "0x9846cd9efc000023c0", - }, - { - onSuccess: (hash) => { - setTxHash(hash); - }, - } - ); - }, [sendTransaction]); - - const signTyped = useCallback(() => { - signTypedData({ - domain: { - name: "Frames v2 Demo", - version: "1", - chainId, - }, - types: { - Message: [{ name: "content", type: "string" }], - }, - message: { - content: "Hello from Frames v2!", - }, - primaryType: "Message", - }); - }, [chainId, signTypedData]); - - const toggleContext = useCallback(() => { - setIsContextOpen((prev) => !prev); - }, []); - - if (!isSDKLoaded) { - return
Loading...
; - } +import React from "react"; +import NFTCampaignStepper from "./nft-campaign-stepper"; +function Demo({ title }: { title?: string } = { title: "Frames v2 Demo" }) { return ( -
-
-

{title}

- -
-

Context

- - - {isContextOpen && ( -
-
-                {JSON.stringify(context, null, 2)}
-              
-
- )} -
- -
-

Actions

- -
-
-
-                sdk.actions.signIn
-              
-
- -
- -
-
-
-                sdk.actions.openUrl
-              
-
- -
- -
-
-
-                sdk.actions.openUrl
-              
-
- -
- -
-
-
-                sdk.actions.viewProfile
-              
-
- -
- -
-
-
-                sdk.actions.close
-              
-
- -
-
- -
-

Last event

- -
-
-              {lastEvent || "none"}
-            
-
-
- -
-

Add to client & notifications

- -
- Client fid {context?.client.clientFid}, - {added ? " frame added to client," : " frame not added to client,"} - {notificationDetails - ? " notifications enabled" - : " notifications disabled"} -
- -
-
-
-                sdk.actions.addFrame
-              
-
- {addFrameResult && ( -
- Add frame result: {addFrameResult} -
- )} - -
- - {sendNotificationResult && ( -
- Send notification result: {sendNotificationResult} -
- )} -
- -
-
- -
-

Wallet

- - {address && ( -
- Address:
{truncateAddress(address)}
-
- )} - - {chainId && ( -
- Chain ID:
{chainId}
-
- )} - -
- -
- -
- -
- - {isConnected && ( - <> -
- -
-
- - {isSendTxError && renderError(sendTxError)} - {txHash && ( -
-
Hash: {truncateAddress(txHash)}
-
- Status:{" "} - {isConfirming - ? "Confirming..." - : isConfirmed - ? "Confirmed!" - : "Pending"} -
-
- )} -
-
- - {isSignTypedError && renderError(signTypedError)} -
-
- - {isSwitchChainError && renderError(switchChainError)} -
- - )} -
-
+
+

{title}

+
); } -function SignMessage() { - const { isConnected } = useAccount(); - const { connectAsync } = useConnect(); - const { - signMessage, - data: signature, - error: signError, - isError: isSignError, - isPending: isSignPending, - } = useSignMessage(); - - const handleSignMessage = useCallback(async () => { - if (!isConnected) { - await connectAsync({ - chainId: base.id, - connector: config.connectors[0], - }); - } - - signMessage({ message: "Hello from Frames v2!" }); - }, [connectAsync, isConnected, signMessage]); - - return ( - <> - - {isSignError && renderError(signError)} - {signature && ( -
-
Signature: {signature}
-
- )} - - ); -} - -function SendEth() { - const { isConnected, chainId } = useAccount(); - const { - sendTransaction, - data, - error: sendTxError, - isError: isSendTxError, - isPending: isSendTxPending, - } = useSendTransaction(); - - const { isLoading: isConfirming, isSuccess: isConfirmed } = - useWaitForTransactionReceipt({ - hash: data, - }); - - const toAddr = useMemo(() => { - // Protocol guild address - return chainId === base.id - ? "0x32e3C7fD24e175701A35c224f2238d18439C7dBC" - : "0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830"; - }, [chainId]); - - const handleSend = useCallback(() => { - sendTransaction({ - to: toAddr, - value: 1n, - }); - }, [toAddr, sendTransaction]); - - return ( - <> - - {isSendTxError && renderError(sendTxError)} - {data && ( -
-
Hash: {truncateAddress(data)}
-
- Status:{" "} - {isConfirming - ? "Confirming..." - : isConfirmed - ? "Confirmed!" - : "Pending"} -
-
- )} - - ); -} - -function SignIn() { - const [signingIn, setSigningIn] = useState(false); - const [signingOut, setSigningOut] = useState(false); - const [signInResult, setSignInResult] = useState(); - const [signInFailure, setSignInFailure] = useState(); - const { data: session, status } = useSession() - - const getNonce = useCallback(async () => { - const nonce = await getCsrfToken(); - if (!nonce) throw new Error("Unable to generate nonce"); - return nonce; - }, []); - - const handleSignIn = useCallback(async () => { - try { - setSigningIn(true); - setSignInFailure(undefined); - const nonce = await getNonce(); - const result = await sdk.actions.signIn({ nonce }); - setSignInResult(result); - - await signIn("credentials", { - message: result.message, - signature: result.signature, - redirect: false, - }); - } catch (e) { - if (e instanceof SignInCore.RejectedByUser) { - setSignInFailure("Rejected by user"); - return; - } - - setSignInFailure("Unknown error"); - } finally { - setSigningIn(false); - } - }, [getNonce]); - - const handleSignOut = useCallback(async () => { - try { - setSigningOut(true); - await signOut({ redirect: false }) - setSignInResult(undefined); - } finally { - setSigningOut(false); - } - }, []); - - return ( - <> - {status !== "authenticated" && - - } - {status === "authenticated" && - - } - {session && -
-
Session
-
{JSON.stringify(session, null, 2)}
-
- } - {signInFailure && !signingIn && ( -
-
SIWF Result
-
{signInFailure}
-
- )} - {signInResult && !signingIn && ( -
-
SIWF Result
-
{JSON.stringify(signInResult, null, 2)}
-
- )} - - ); -} - -function ViewProfile() { - const [fid, setFid] = useState('3'); - - return ( - <> -
- - { - setFid(e.target.value) - }} - step="1" - min="1" - /> -
- - - ); -} - -const renderError = (error: Error | null) => { - if (!error) return null; - if (error instanceof BaseError) { - const isUserRejection = error.walk( - (e) => e instanceof UserRejectedRequestError - ); - - if (isUserRejection) { - return
Rejected by user.
; - } - } - - return
{error.message}
; -}; +export default Demo; diff --git a/src/components/campaign-details.tsx b/src/components/campaign-details.tsx new file mode 100644 index 0000000..099cd12 --- /dev/null +++ b/src/components/campaign-details.tsx @@ -0,0 +1,136 @@ +import { useState } from 'react' +import { Input } from "../components/ui/input" +import { Label } from "../components/ui/label" +import { Textarea } from "../components/ui/textarea" +import { Switch } from "../components/ui/switch" +import ImageUpload from './image-upload' + +interface CampaignDetailsProps { + onUpdate: (details: { + collectionName: string, + title: string, + description: string, + image: File | null, + timeBound: boolean, + startDate: string, + startTime: string, + endDate: string, + endTime: string, + soulbound: boolean, + openEdition: boolean, + }) => void +} + +export default function CampaignDetails({ onUpdate }: CampaignDetailsProps) { + const [details, setDetails] = useState({ + collectionName: '', + title: '', + description: '', + image: null as File | null, + timeBound: false, + startDate: '', + startTime: '', + endDate: '', + endTime: '', + soulbound: false, + openEdition: false, + }) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setDetails(prev => ({ ...prev, [name]: value })) + onUpdate({ ...details, [name]: value }) + } + + const handleToggle = (name: string) => (checked: boolean) => { + setDetails(prev => ({ ...prev, [name]: checked })) + onUpdate({ ...details, [name]: checked }) + } + + const handleImageUpload = (file: File) => { + setDetails(prev => ({ ...prev, image: file })) + onUpdate({ ...details, image: file }) + } + + return ( +
+
+ + +
+
+ + +
+
+ +