From 9f668cbdaf4021983c129d0bc538b6fcc68d3c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 12:14:23 +0100 Subject: [PATCH 1/9] feat: update dependencies, add new methods --- .../app/components/frame-app-debugger.tsx | 16 ++++ packages/debugger/package.json | 4 +- packages/frames.js/package.json | 6 +- packages/render/package.json | 10 +- packages/render/src/frame-app/types.ts | 33 +++++-- packages/render/src/use-frame-app.ts | 82 ++++++++++++----- yarn.lock | 91 +++++++++++-------- 7 files changed, 161 insertions(+), 81 deletions(-) diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index e86b19402..7149fb445 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -23,6 +23,8 @@ import type { FramePrimaryButton, ResolveClientFunction, } from "@frames.js/render/frame-app/types"; +import { useConfig } from "wagmi"; +import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; type TabValues = "events" | "console" | "notifications"; @@ -52,6 +54,7 @@ export function FrameAppDebugger({ farcasterSigner, onClose, }: FrameAppDebuggerProps) { + const config = useConfig(); const farcasterSignerRef = useRef(farcasterSigner); farcasterSignerRef.current = farcasterSigner; const frameAppNotificationManager = useFrameAppNotificationsManager({ @@ -122,6 +125,7 @@ export function FrameAppDebugger({ } : { type: "cast_embed", + embed: "", cast: fallbackFrameContext.castId, }, farcasterSigner, @@ -233,6 +237,18 @@ export function FrameAppDebugger({ throw e; } }, + onEIP6963RequestProviderRequested({ endpoint }) { + if (!config._internal.mipd) { + return; + } + + config._internal.mipd.getProviders().map((providerInfo) => { + endpoint.emit({ + event: "eip6963:announceProvider", + info: providerInfo.info as EIP6963ProviderInfo, + }); + }); + }, }); return ( diff --git a/packages/debugger/package.json b/packages/debugger/package.json index ccfe419e9..335699eba 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -18,14 +18,14 @@ "dependencies": { "@upstash/redis": "^1.34.3", "@lens-protocol/client": "^2.3.2", - "@farcaster/frame-sdk": "^0.0.20", + "@farcaster/frame-sdk": "^0.0.26", "@xmtp/xmtp-js": "^12.0.0", "is-port-reachable": "^4.0.0", "next": "14.1.4", "open": "^10.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "zod": "^3.23.8", + "zod": "^3.24.1", "yargs": "^17.7.2" }, "engines": { diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index ec0ad2139..57e62fe6e 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -419,13 +419,13 @@ "react-dom": "^18.2.0" }, "dependencies": { - "@farcaster/frame-core": "^0.0.19", - "@farcaster/frame-node": "^0.0.8", + "@farcaster/frame-core": "^0.0.24", + "@farcaster/frame-node": "^0.0.13", "@vercel/og": "^0.6.3", "cheerio": "^1.0.0-rc.12", "protobufjs": "^7.2.6", "viem": "^2.7.8", "type-fest": "^4.28.1", - "zod": "^3.23.8" + "zod": "^3.24.1" } } diff --git a/packages/render/package.json b/packages/render/package.json index 472eb5718..86b07aad4 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -336,7 +336,7 @@ "src" ], "devDependencies": { - "@farcaster/frame-host-react-native": "^0.0.14", + "@farcaster/frame-host-react-native": "^0.0.19", "@lens-protocol/client": "^2.3.2", "@rainbow-me/rainbowkit": "^2.1.2", "@remix-run/node": "^2.8.1", @@ -352,7 +352,7 @@ }, "license": "MIT", "peerDependencies": { - "@farcaster/frame-host-react-native": "^0.0.14", + "@farcaster/frame-host-react-native": "^0.0.19", "@lens-protocol/client": "^2.0.0", "@rainbow-me/rainbowkit": "^2.1.2", "@types/react": "^18.2.0", @@ -368,10 +368,10 @@ }, "dependencies": { "@farcaster/core": "^0.15.6", - "@farcaster/frame-host": "^0.0.19", + "@farcaster/frame-host": "^0.0.24", "@noble/ed25519": "^2.0.0", "frames.js": "^0.21.0", - "ox": "^0.4.0", - "zod": "^3.23.8" + "ox": "^0.4.4", + "zod": "^3.24.1" } } diff --git a/packages/render/src/frame-app/types.ts b/packages/render/src/frame-app/types.ts index 500f0b585..73a00211f 100644 --- a/packages/render/src/frame-app/types.ts +++ b/packages/render/src/frame-app/types.ts @@ -1,14 +1,15 @@ -import type { HostEndpoint } from "@farcaster/frame-host"; import type { - AddFrameResult, - FrameContext, - SetPrimaryButton, -} from "@farcaster/frame-sdk"; + HostEndpoint, + Context, + SetPrimaryButtonOptions, + AddFrame, + FrameHost, +} from "@farcaster/frame-host"; import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; import type { Provider } from "ox/Provider"; import type { Default as DefaultRpcSchema, ExtractRequest } from "ox/RpcSchema"; -export type FrameClientConfig = FrameContext["client"]; +export type FrameClientConfig = Context.ClientContext; export type SendTransactionRpcRequest = ExtractRequest< DefaultRpcSchema, @@ -62,16 +63,32 @@ export type SharedEthProviderEventHandlers = { onSignTypedDataRequest: OnSignTypedDataRequestFunction; }; -export type FramePrimaryButton = Parameters[0]; +export type FramePrimaryButton = SetPrimaryButtonOptions; export type OnPrimaryButtonSetFunction = ( options: FramePrimaryButton, pressedCallback: () => void ) => void; +/** + * Returns false if user rejected the request, otherwise it returns the notification details + */ export type OnAddFrameRequestedFunction = ( frame: ParseFramesV2ResultWithFrameworkDetails -) => Promise>; +) => Promise>; + +export type OnEIP6963RequestProviderRequestedFunctionOptions = { + endpoint: HostEndpoint; +}; + +/** + * Function that must emit eip6963:announceProvider event on endpoint to announce available providers + */ +export type OnEIP6963RequestProviderRequestedFunction = ( + options: OnEIP6963RequestProviderRequestedFunctionOptions +) => unknown; + +export type OnViewProfileFunction = FrameHost["viewProfile"]; /** * Function called when the frame app is being loaded and we need to resolve the client that renders the frame app diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index 90efcbd96..a892d2075 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -1,10 +1,6 @@ -import type { - FrameHost, - FrameLocationContext, - FrameLocationContextLauncher, -} from "@farcaster/frame-sdk"; import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; -import type { HostEndpoint } from "@farcaster/frame-host"; +import type { FrameHost, HostEndpoint, Context } from "@farcaster/frame-host"; +import { AddFrame } from "@farcaster/frame-host"; import { useMemo } from "react"; import { useFreshRef } from "./hooks/use-fresh-ref"; import type { FarcasterSignerState } from "./farcaster"; @@ -14,10 +10,12 @@ import type { FrameClientConfig, HostEndpointEmitter, OnAddFrameRequestedFunction, + OnEIP6963RequestProviderRequestedFunction, OnPrimaryButtonSetFunction, OnSendTransactionRequestFunction, OnSignMessageRequestFunction, OnSignTypedDataRequestFunction, + OnViewProfileFunction, ResolveClientFunction, } from "./frame-app/types"; import { assertNever } from "./assert-never"; @@ -69,6 +67,25 @@ const defaultOnSignTypedDataRequest: OnSignTypedDataRequestFunction = () => { return Promise.resolve(true); }; +const defaultViewProfile: OnViewProfileFunction = () => { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + "@frames.js/render/use-frame-app", + "onViewProfile not implemented" + ); + + return Promise.reject(new Error("onViewProfile not implemented")); +}; + +const defaultEIP6963RequestProviderRequested: OnEIP6963RequestProviderRequestedFunction = + () => { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + "@frames.js/render/use-frame-app", + "onEIP6963RequestProviderRequested not implemented" + ); + }; + export type UseFrameAppOptions = { /** * @example @@ -100,7 +117,7 @@ export type UseFrameAppOptions = { * * @defaultValue launcher context */ - location?: FrameLocationContext; + location?: Context.LocationContext; /** * Either: * @@ -164,6 +181,16 @@ export type UseFrameAppOptions = { * If the method has been called during the session more than once it immediatelly rejects */ onAddFrameRequested?: OnAddFrameRequestedFunction; + /** + * Called when app calls `viewProfile` method. + */ + onViewProfile?: OnViewProfileFunction; + /** + * Called when app calls `eip6963RequestProvider` method. + * + * It will announce the provider to the frame app once this function returns the info + */ + onEIP6963RequestProviderRequested?: OnEIP6963RequestProviderRequestedFunction; /** * Enabled debugging * @@ -201,7 +228,7 @@ export type UseFrameAppReturn = status: "error"; }; -const defaultLocation: FrameLocationContextLauncher = { +const defaultLocation: Context.LauncherLocationContext = { type: "launcher", }; @@ -226,6 +253,8 @@ export function useFrameApp({ onSendTransactionRequest = defaultOnSendTransactionRequest, onSignMessageRequest = defaultOnSignMessageRequest, onSignTypedDataRequest = defaultOnSignTypedDataRequest, + onViewProfile = defaultViewProfile, + onEIP6963RequestProviderRequested = defaultEIP6963RequestProviderRequested, }: UseFrameAppOptions): UseFrameAppReturn { const providerRef = useFreshRef(provider); const debugRef = useFreshRef(debug); @@ -234,6 +263,10 @@ export function useFrameApp({ const closeRef = useFreshRef(onClose); const onOpenUrlRef = useFreshRef(onOpenUrl); const onPrimaryButtonSetRef = useFreshRef(onPrimaryButtonSet); + const onViewProfileRef = useFreshRef(onViewProfile); + const onEIP6963RequestProviderRequestedRef = useFreshRef( + onEIP6963RequestProviderRequested + ); const farcasterSignerRef = useFreshRef(farcasterSigner); const onAddFrameRequestedRef = useFreshRef(onAddFrameRequested); const addFrameRequestsCacheRef = useFreshRef(addFrameRequestsCache); @@ -307,10 +340,7 @@ export function useFrameApp({ reason: "invalid_domain_manifest", }); - return { - added: false, - reason: "invalid_domain_manifest", - }; + throw new AddFrame.InvalidDomainManifest(); } if ( @@ -325,21 +355,18 @@ export function useFrameApp({ reason: "rejected_by_user", }); - return { - added: false, - reason: "rejected_by_user", - }; + throw new AddFrame.RejectedByUser(); } - const added = await onAddFrameRequestedRef.current(frame); + const result = await onAddFrameRequestedRef.current(frame); - logDebug("onAddFrameRequested() called", added); + logDebug("onAddFrameRequested() called", result); addFrameRequestsCacheRef.current.add( frame.frame.button.action.url ); - if (!added) { + if (!result) { logDebug("Frame add request rejected by user"); endpoint.emit({ @@ -347,18 +374,15 @@ export function useFrameApp({ reason: "rejected_by_user", }); - return { - added: false, - reason: "rejected_by_user", - }; + throw new AddFrame.RejectedByUser(); } endpoint.emit({ event: "frame_added", - notificationDetails: added.notificationDetails, + notificationDetails: result.notificationDetails, }); - return added; + return result; }, close() { logDebug("sdk.close() called"); @@ -373,6 +397,9 @@ export function useFrameApp({ // @ts-expect-error -- type mismatch return providerRef.current.request(parameters); }, + eip6963RequestProvider() { + onEIP6963RequestProviderRequestedRef.current({ endpoint }); + }, openUrl(url) { logDebug("sdk.openUrl() called", url); @@ -398,6 +425,9 @@ export function useFrameApp({ // @todo implement throw new Error("not implemented"); }, + viewProfile(options) { + return onViewProfileRef.current(options); + }, }), status: "success", frame: frameResolutionState.frame, @@ -431,5 +461,7 @@ export function useFrameApp({ onOpenUrlRef, readyRef, onPrimaryButtonSetRef, + onViewProfileRef, + onEIP6963RequestProviderRequestedRef, ]); } diff --git a/yarn.lock b/yarn.lock index afa990d10..a493b2dfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2403,47 +2403,50 @@ neverthrow "^6.0.0" viem "^2.17.4" -"@farcaster/frame-core@^0.0.19": - version "0.0.19" - resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.19.tgz#76621fe6e207c3a5c78a22aa679bbbd7beafdd11" - integrity sha512-/0XjVZa/rUuUR16GVhhKnAQI33SXI61bB24jNA1DD0L0ytcKsM14wBptW9CF4RDiYIXxtt4mXmnQ+rMhO38RcA== +"@farcaster/frame-core@0.0.24", "@farcaster/frame-core@^0.0.24": + version "0.0.24" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.24.tgz#35c2403f561a74b6551e3032d6d57a730c3dc413" + integrity sha512-iO/Jxz6mZBVUoLIY753Id5Yhn6DHBakQkIBXf0mreAcnjPGCMvKx/0xKEM3ns3M801PqoX7VLYO4q+kKxYzQ0A== dependencies: - ox "^0.4.0" - zod "^3.23.8" + ox "^0.4.4" + zod "^3.24.1" -"@farcaster/frame-host-react-native@^0.0.14": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@farcaster/frame-host-react-native/-/frame-host-react-native-0.0.14.tgz#0919eb1466dfd497a29d0965a5e5c6e7e25336b1" - integrity sha512-iPjysnQ5H8aoWJWIdJIWPaBP9wo7s9fLNPy56RZXMsrqrkWDskOYfgND5CRZJzA9JDvvgLjqJ+XYdGPVXlKyGQ== +"@farcaster/frame-host-react-native@^0.0.19": + version "0.0.19" + resolved "https://registry.yarnpkg.com/@farcaster/frame-host-react-native/-/frame-host-react-native-0.0.19.tgz#7e809d5cd9f6a264aac165360cadc90bf650c79b" + integrity sha512-WH5nQSGPiu99lE0L64tIa6BcfVelbT34XzYo89eEgsAqPmlGRnnHJwQAxlWSAXTHYhaXWpIQMC3u1a6KsQBQCw== dependencies: - "@farcaster/frame-host" "^0.0.19" - ox "^0.4.0" + "@farcaster/frame-core" "0.0.24" + "@farcaster/frame-host" "0.0.24" + ox "^0.4.4" -"@farcaster/frame-host@^0.0.19": - version "0.0.19" - resolved "https://registry.yarnpkg.com/@farcaster/frame-host/-/frame-host-0.0.19.tgz#7e44bc6df2d86bb6f210b51189676558ffe414ef" - integrity sha512-4Svhipw24PAcux4vCHlXA6L3sVi248Ad4bDuXVgqvOEgQ3Ue1Ic4iDQ/bgF+LG3/hLh5UvYJ4csz0xOEwRNT0Q== +"@farcaster/frame-host@0.0.24", "@farcaster/frame-host@^0.0.24": + version "0.0.24" + resolved "https://registry.yarnpkg.com/@farcaster/frame-host/-/frame-host-0.0.24.tgz#8e97b3476443c25b33e8f4cd1948ad912e4deb21" + integrity sha512-4l9FS7LjEYRjOmNnGFvYXnnQnsW8VGrQ6ECIYZjxdAglP63B9nCBmH0UhhkeKrQA+z/HmwIqOneh8LRjj4J5Bg== dependencies: - "@farcaster/frame-core" "^0.0.19" - ox "^0.4.0" + "@farcaster/frame-core" "0.0.24" + ox "^0.4.4" -"@farcaster/frame-node@^0.0.8": - version "0.0.8" - resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.8.tgz#14c5c8c527a793eefbffac5b03570ed3925c95fd" - integrity sha512-AE3xFVj7Tu2nfVASka0wagR9plaZ2UA1ezzh9aHA4f6GU/gVl+SdfZzAn4uOqgEPl3D2g7W4p0tPRYZVwMJfdg== +"@farcaster/frame-node@^0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.13.tgz#ba9d37358589b263aa9566cf1763b1eb528672e6" + integrity sha512-99rhLSpyhKKnWFh8eWx8N5tTYEToFkIzg9r5lRm2bvfsZaT0zXr/cA+G9pWDFZyodfoyS/cLHS46Qsm8P52uTg== dependencies: - "@farcaster/frame-core" "^0.0.19" - ox "^0.4.0" + "@farcaster/frame-core" "0.0.24" + "@noble/curves" "^1.7.0" + ox "^0.4.4" + zod "^3.24.1" -"@farcaster/frame-sdk@^0.0.20": - version "0.0.20" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.20.tgz#253dba9a1aba23c63e01617720835fe6b2b0880b" - integrity sha512-9EbB3A9V1ZSH4NvhHVbylcDVNevJqpuGkwz4I+0bKF2936qxvMlcWclDZVEmfO/woYH0ZQgy47YYRIiS3YpIQw== +"@farcaster/frame-sdk@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.26.tgz#2cf5c5e9e8ecdbdbc244e55f41129fc1caa9b88c" + integrity sha512-tOoJcJLXXezjmP0gg/MOU/mCjNdWyL+j99XMylYCovel1etDQjLovGamVex8A6eELryLZ4LElV9AIbxP/i9IfA== dependencies: - "@farcaster/frame-core" "^0.0.19" + "@farcaster/frame-core" "0.0.24" comlink "^4.4.2" eventemitter3 "^5.0.1" - ox "^0.4.0" + ox "^0.4.4" "@fastify/busboy@^2.0.0": version "2.1.1" @@ -3549,6 +3552,13 @@ dependencies: "@noble/hashes" "1.6.0" +"@noble/curves@^1.7.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.0.tgz#fe035a23959e6aeadf695851b51a87465b5ba8f7" + integrity sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ== + dependencies: + "@noble/hashes" "1.7.0" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -3586,6 +3596,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== +"@noble/hashes@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" + integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== + "@noble/secp256k1@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -14854,10 +14869,10 @@ ox@0.1.2: abitype "^1.0.6" eventemitter3 "5.0.1" -ox@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.4.0.tgz#6c73b27a9f45912888917304d7a81c894856a980" - integrity sha512-F+Q8R/7SZ8AvBcejIV6QUcACLjRuFtSShCkwTuCFWLAN5DoS8dSwiFsDBltvQplEXXNGmAEZCV4HDe7orEDSxA== +ox@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.4.4.tgz#9d1757c026406e60097680d98ffedf9e3bc1fa0b" + integrity sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw== dependencies: "@adraffy/ens-normalize" "^1.10.1" "@noble/curves" "^1.6.0" @@ -19063,10 +19078,10 @@ zod@^3.20.6, zod@^3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== -zod@^3.23.8: - version "3.23.8" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" - integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== +zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== zustand@4.4.1: version "4.4.1" From 44306f75d39111a41a1c8add82106f1a2feecb56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 12:54:57 +0100 Subject: [PATCH 2/9] fix: allow localhost as domain in account association --- ...ster-domain-account-association-dialog.tsx | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/debugger/app/components/farcaster-domain-account-association-dialog.tsx b/packages/debugger/app/components/farcaster-domain-account-association-dialog.tsx index b65c1403c..1036c9890 100644 --- a/packages/debugger/app/components/farcaster-domain-account-association-dialog.tsx +++ b/packages/debugger/app/components/farcaster-domain-account-association-dialog.tsx @@ -12,8 +12,9 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useAccount, useSignMessage, useSwitchChain } from "wagmi"; -import { FormEvent, useCallback, useState } from "react"; +import { FormEvent, useCallback, useRef, useState } from "react"; import { CopyIcon, CopyCheckIcon, CopyXIcon, Loader2Icon } from "lucide-react"; +import { z } from "zod"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; @@ -28,6 +29,7 @@ type FarcasterDomainAccountAssociationDialogProps = { export function FarcasterDomainAccountAssociationDialog({ onClose, }: FarcasterDomainAccountAssociationDialogProps) { + const domainInputRef = useRef(null); const copyCompact = useCopyToClipboard(); const copyJSON = useCopyToClipboard(); const account = useAccount(); @@ -44,7 +46,7 @@ export function FarcasterDomainAccountAssociationDialog({ async (event: FormEvent) => { event.preventDefault(); - const data = new FormData(event.currentTarget); + const data = Object.fromEntries(new FormData(event.currentTarget)); try { if (farcasterSigner.signer?.status !== "approved") { @@ -55,12 +57,36 @@ export function FarcasterDomainAccountAssociationDialog({ throw new Error("Account address is not available"); } - const domain = data.get("domain"); + const parser = z.object({ + domain: z + .preprocess((val) => { + if (typeof val === "string") { + // prepend with prefix because normally it is the domain but we want to validate + // it is in valid format + return `http://${val}`; + } - if (typeof domain !== "string" || !domain) { - throw new Error("Domain is required"); + return val; + }, z.string().url("Invalid domain")) + // remove the protocol prefix + .transform((val) => val.substring(7)), + }); + + const parseResult = parser.safeParse(data); + + if (!parseResult.success) { + parseResult.error.errors.map((error) => { + domainInputRef.current?.setCustomValidity(error.message); + }); + + event.currentTarget.reportValidity(); + + return; } + domainInputRef.current?.setCustomValidity(""); + event.currentTarget.reportValidity(); + setIsGenerating(true); await switchChainAsync({ @@ -69,8 +95,9 @@ export function FarcasterDomainAccountAssociationDialog({ const result = await sign({ fid: farcasterSigner.signer.fid, - payload: - constructJSONFarcasterSignatureAccountAssociationPaylod(domain), + payload: constructJSONFarcasterSignatureAccountAssociationPaylod( + parseResult.data.domain + ), signer: { type: "custody", custodyAddress: account.address, @@ -117,15 +144,25 @@ export function FarcasterDomainAccountAssociationDialog({ Domain Account Association {!associationResult && ( -
+ + + A domain of your frame, e.g. for https://framesjs.org the domain + is framesjs.org, for http://localhost:3000 the domain is + localhost. +
)} {associationResult && ( From 67aebb0dabdba7aa1e81c381ea322595453783a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 13:03:56 +0100 Subject: [PATCH 3/9] fix: typo --- packages/frames.js/src/frame-parsers/farcasterV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index ed8d7bde3..3e8f7aee6 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -57,7 +57,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .transform((val) => { if (!val.startsWith("https://")) { reporter.error( - "fc:manigest.frame.imageUrl", + "fc:manifest.frame.imageUrl", "Must be an https url" ); } From b0fe321adb691a43ad61c0fe576d504d41098116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 13:04:16 +0100 Subject: [PATCH 4/9] fix: load dev env variables from backend --- packages/debugger/app/client-info/route.ts | 17 ++++++++++++ .../app/components/frame-app-debugger.tsx | 27 ++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 packages/debugger/app/client-info/route.ts diff --git a/packages/debugger/app/client-info/route.ts b/packages/debugger/app/client-info/route.ts new file mode 100644 index 000000000..c47278274 --- /dev/null +++ b/packages/debugger/app/client-info/route.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export function GET() { + return Response.json( + { + fid: z.coerce + .number() + .int() + .parse(process.env.FARCASTER_DEVELOPER_FID || "-1"), + }, + { + headers: { + "Cache-Control": "no-store", + }, + } + ); +} diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 7149fb445..a3ed8a212 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -25,6 +25,7 @@ import type { } from "@frames.js/render/frame-app/types"; import { useConfig } from "wagmi"; import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; +import { z } from "zod"; type TabValues = "events" | "console" | "notifications"; @@ -82,8 +83,20 @@ export function FrameAppDebugger({ ); const resolveClient: ResolveClientFunction = useCallback(async () => { try { + const clientInfoResponse = await fetch("/client-info"); + + if (!clientInfoResponse.ok) { + throw new Error("Failed to fetch client info"); + } + + const parseClientInfo = z.object({ + fid: z.number().int(), + }); + + const clientInfo = parseClientInfo.parse(await clientInfoResponse.json()); + const { manager } = await frameAppNotificationManagerPromiseRef.current; - const clientFid = parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"); + const clientFid = clientInfo.fid; if (!manager.state || manager.state.frame.status === "removed") { return { @@ -93,7 +106,7 @@ export function FrameAppDebugger({ } return { - clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), + clientFid, added: true, notificationDetails: manager.state.frame.notificationDetails ?? undefined, @@ -107,12 +120,12 @@ export function FrameAppDebugger({ "Failed to load notifications settings. Check the console for more details.", variant: "destructive", }); - } - return { - clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), - added: false, - }; + return { + clientFid: -1, + added: false, + }; + } }, [toast]); const frameApp = useFrameAppInIframe({ debug: true, From 0b0d29926c76ad299ccde228232c8161a40a13e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 13:16:15 +0100 Subject: [PATCH 5/9] fix: in non strict mode only warn instead of error on non https urls --- .../src/frame-parsers/farcasterV2.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index 3e8f7aee6..f149f4e4f 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -14,10 +14,6 @@ import type { } from "./types"; import { createReporter } from "./reporter"; -// @todo find out how to report that url is not secure but still keep it valid -// maybe do this by using our own issue code which we will filter out before parsing partial data -// this will make sure that manifest/frame status is failure but data is there - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -- hard to type and we can infer because this is internal function, not exported function createDomainManifestParser(strict: boolean, reporter: Reporter) { if (!strict) { @@ -30,7 +26,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:manifest.frame.iconUrl", "Must be an https url" ); @@ -43,7 +39,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:manifest.frame.homeUrl", "Must be an https url" ); @@ -56,7 +52,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:manifest.frame.imageUrl", "Must be an https url" ); @@ -70,7 +66,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:manifest.frame.splashImageUrl", "Must be an https url" ); @@ -84,7 +80,7 @@ function createDomainManifestParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:manifest.frame.webhookUrl", "Must be an https url" ); @@ -112,7 +108,7 @@ function createFrameEmbedParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error("fc:frame.imageUrl", "Must be an https url"); + reporter.warn("fc:frame.imageUrl", "Must be an https url"); } return val; @@ -125,7 +121,7 @@ function createFrameEmbedParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:frame.button.action.url", "Must be an https url" ); @@ -138,7 +134,7 @@ function createFrameEmbedParser(strict: boolean, reporter: Reporter) { .url() .transform((val) => { if (!val.startsWith("https://")) { - reporter.error( + reporter.warn( "fc:frame.button.action.splashImageUrl", "Must be an https url" ); From ecdade4e15c3368bda997ba21578a46f7bf42627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 7 Jan 2025 13:26:36 +0100 Subject: [PATCH 6/9] test: correctly test non strict parser --- .../src/frame-parsers/farcasterV2.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.test.ts b/packages/frames.js/src/frame-parsers/farcasterV2.test.ts index 9dd3e0e6c..7c9793a32 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.test.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.test.ts @@ -1228,7 +1228,7 @@ describe("farcaster frame v2 parser", () => { await expect( parseFarcasterFrameV2(document, { frameUrl, reporter, strict: false }) ).resolves.toMatchObject({ - status: "failure", + status: "success", specification: "farcaster_v2", frame: { ...validFrame, @@ -1238,7 +1238,7 @@ describe("farcaster frame v2 parser", () => { "fc:frame.imageUrl": [ { source: "farcaster_v2", - level: "error", + level: "warning", message: "Must be an https url", }, ], @@ -1269,7 +1269,7 @@ describe("farcaster frame v2 parser", () => { strict: false, }) ).resolves.toMatchObject({ - status: "failure", + status: "success", specification: "farcaster_v2", frame: { ...validFrame, @@ -1285,7 +1285,7 @@ describe("farcaster frame v2 parser", () => { "fc:frame.button.action.url": [ { source: "farcaster_v2", - level: "error", + level: "warning", message: "Must be an https url", }, ], @@ -1315,7 +1315,7 @@ describe("farcaster frame v2 parser", () => { strict: false, }) ).resolves.toMatchObject({ - status: "failure", + status: "success", specification: "farcaster_v2", frame: { ...validFrame, @@ -1331,7 +1331,7 @@ describe("farcaster frame v2 parser", () => { "fc:frame.button.action.splashImageUrl": [ { source: "farcaster_v2", - level: "error", + level: "warning", message: "Must be an https url", }, ], @@ -1382,7 +1382,7 @@ describe("farcaster frame v2 parser", () => { ).resolves.toMatchObject({ status: "success", manifest: { - status: "failure", + status: "success", manifest: { accountAssociation: { header: @@ -1401,7 +1401,7 @@ describe("farcaster frame v2 parser", () => { reports: { "fc:manifest.frame.homeUrl": [ { - level: "error", + level: "warning", message: "Must be an https url", source: "farcaster_v2", }, @@ -1450,7 +1450,7 @@ describe("farcaster frame v2 parser", () => { ).resolves.toMatchObject({ status: "success", manifest: { - status: "failure", + status: "success", manifest: { accountAssociation: { header: @@ -1469,7 +1469,7 @@ describe("farcaster frame v2 parser", () => { reports: { "fc:manifest.frame.iconUrl": [ { - level: "error", + level: "warning", message: "Must be an https url", source: "farcaster_v2", }, @@ -1519,7 +1519,7 @@ describe("farcaster frame v2 parser", () => { ).resolves.toMatchObject({ status: "success", manifest: { - status: "failure", + status: "success", manifest: { accountAssociation: { header: @@ -1539,7 +1539,7 @@ describe("farcaster frame v2 parser", () => { reports: { "fc:manifest.frame.splashImageUrl": [ { - level: "error", + level: "warning", message: "Must be an https url", source: "farcaster_v2", }, From 14aa3ef32dd77ff4e9d207b51fadf677aff04299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 8 Jan 2025 08:50:28 +0100 Subject: [PATCH 7/9] feat: farcaster v2 sign in --- .../app/components/frame-app-debugger.tsx | 397 ++++++++++++------ packages/debugger/package.json | 1 + packages/render/src/frame-app/types.ts | 9 + packages/render/src/use-frame-app.ts | 23 +- yarn.lock | 107 +++++ 5 files changed, 402 insertions(+), 135 deletions(-) diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index a3ed8a212..cf0cbe2ce 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -1,3 +1,5 @@ +import "@farcaster/auth-kit/styles.css"; +import { createAppClient, viemConnector, QRCode } from "@farcaster/auth-kit"; import { Button } from "@/components/ui/button"; import type { FrameLaunchedInContext } from "./frame-debugger"; import { WithTooltip } from "./with-tooltip"; @@ -26,6 +28,8 @@ import type { import { useConfig } from "wagmi"; import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; import { z } from "zod"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { useCopyToClipboard } from "../hooks/useCopyToClipboad"; type TabValues = "events" | "console" | "notifications"; @@ -50,11 +54,20 @@ const addFrameRequestsCache = new (class extends Set { } })(); +const appClient = createAppClient({ + ethereum: viemConnector(), +}); + export function FrameAppDebugger({ context, farcasterSigner, onClose, }: FrameAppDebuggerProps) { + const copyFarcasterSignInLink = useCopyToClipboard(); + const [ + farcasterSignInAbortControllerAndURL, + setFarcasterSignInAbortControllerURL, + ] = useState<{ controller: AbortController; url: URL } | null>(null); const config = useConfig(); const farcasterSignerRef = useRef(farcasterSigner); farcasterSignerRef.current = farcasterSigner; @@ -262,150 +275,270 @@ export function FrameAppDebugger({ }); }); }, + async onSignIn({ nonce, notBefore, expirationTime, frame }) { + let abortTimeout: NodeJS.Timeout | undefined; + + try { + const frameUrl = frame.frame.button?.action?.url; + + if (!frameUrl) { + throw new Error("Frame is malformed, action url is missing"); + } + + const createChannelResult = await appClient.createChannel({ + nonce, + notBefore, + expirationTime, + siweUri: frameUrl, + domain: new URL(frameUrl).hostname, + }); + + if (createChannelResult.isError) { + throw ( + createChannelResult.error || + new Error("Failed to create sign in channel") + ); + } + + const abortController = new AbortController(); + + setFarcasterSignInAbortControllerURL({ + controller: abortController, + url: new URL(createChannelResult.data.url), + }); + + const signInTimeoutReason = "Sign in timed out"; + + // abort controller after 30 seconds + abortTimeout = setTimeout(() => { + abortController.abort(signInTimeoutReason); + }, 30000); + + let status: Awaited>; + + while (true) { + if (abortController.signal.aborted) { + if (abortTimeout) { + clearTimeout(abortTimeout); + } + + if (abortController.signal.reason === signInTimeoutReason) { + toast({ + title: "Sign in timed out", + variant: "destructive", + }); + } + + throw new Error(abortController.signal.reason); + } + + status = await appClient.status({ + channelToken: createChannelResult.data.channelToken, + }); + + if (!status.isError && status.data.state === "completed") { + break; + } + + await new Promise((r) => setTimeout(r, 1000)); + } + + clearTimeout(abortTimeout); + + const { message, signature } = status.data; + + if (!(signature && message)) { + throw new Error("Signature or message is missing"); + } + + return { + signature, + message, + }; + } finally { + clearTimeout(abortTimeout); + setFarcasterSignInAbortControllerURL(null); + } + }, }); return ( -
-
-
- Reload frame app

}> - -
-
-
-
-
+ {!!farcasterSignInAbortControllerAndURL && ( + { + farcasterSignInAbortControllerAndURL.controller.abort( + "User closed sign in dialog" + ); + }} > - {frameApp.status === "pending" || - (!isAppReady && ( -
+
+

Sign in with Farcaster

+ + or +
+ )} +
+
+
+ Reload frame app

}> + +
+
+
+
+
+ {frameApp.status === "pending" || + (!isAppReady && ( +
+ {context.frame.button.action.splashImageUrl && ( +
+ {`${name} +
+ +
+ )} +
+ ))} + {frameApp.status === "success" && ( + <> +