diff --git a/package.json b/package.json index 835205cd2..913a8519f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@dialectlabs/blinks": "^0.15.1", "@dimensiondev/holoflows-kit": "0.9.0-20240322092738-f9180f3", "@farcaster/core": "^0.15.6", + "@farcaster/frame-host": "^0.0.19", "@giphy/js-fetch-api": "^5.6.0", "@giphy/react-components": "^9.6.0", "@headlessui/react": "2.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cd725b6b..a5d44dc98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: '@farcaster/core': specifier: ^0.15.6 version: 0.15.6(typescript@5.6.3)(zod@3.23.8) + '@farcaster/frame-host': + specifier: ^0.0.19 + version: 0.0.19(typescript@5.6.3)(zod@3.23.8) '@giphy/js-fetch-api': specifier: ^5.6.0 version: 5.6.0 @@ -8615,6 +8618,25 @@ packages: - zod dev: false + /@farcaster/frame-core@0.0.19(typescript@5.6.3): + resolution: {integrity: sha512-/0XjVZa/rUuUR16GVhhKnAQI33SXI61bB24jNA1DD0L0ytcKsM14wBptW9CF4RDiYIXxtt4mXmnQ+rMhO38RcA==} + dependencies: + ox: 0.4.2(typescript@5.6.3)(zod@3.23.8) + zod: 3.23.8 + transitivePeerDependencies: + - typescript + dev: false + + /@farcaster/frame-host@0.0.19(typescript@5.6.3)(zod@3.23.8): + resolution: {integrity: sha512-4Svhipw24PAcux4vCHlXA6L3sVi248Ad4bDuXVgqvOEgQ3Ue1Ic4iDQ/bgF+LG3/hLh5UvYJ4csz0xOEwRNT0Q==} + dependencies: + '@farcaster/frame-core': 0.0.19(typescript@5.6.3) + ox: 0.4.2(typescript@5.6.3)(zod@3.23.8) + transitivePeerDependencies: + - typescript + - zod + dev: false + /@fastify/deepmerge@1.3.0: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: true @@ -10210,7 +10232,7 @@ packages: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.1.0 '@noble/hashes': 1.6.1 - '@scure/base': 1.1.9 + '@scure/base': 1.2.1 '@types/debug': 4.1.12 debug: 4.3.7 pony-cause: 2.1.11 @@ -10227,7 +10249,7 @@ packages: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.1.0 '@noble/hashes': 1.6.1 - '@scure/base': 1.1.9 + '@scure/base': 1.2.1 '@types/debug': 4.1.12 debug: 4.3.7 pony-cause: 2.1.11 @@ -15040,7 +15062,7 @@ packages: resolution: {integrity: sha512-dPObl4ntmfOc0VAGGyyFvrqhL8UkHXmVsgbj0K9RcznKV4KB3MgjGwzo8CTSX5El5lkb0rDeEzFqvToJXRz3dw==} engines: {node: '>=16'} dependencies: - '@noble/curves': 1.6.0 + '@noble/curves': 1.7.0 '@solana/wallet-standard-chains': 1.1.0 '@solana/wallet-standard-features': 1.2.0 dev: false @@ -21631,7 +21653,7 @@ packages: dependencies: '@ecies/ciphers': 0.2.1(@noble/ciphers@1.0.0) '@noble/ciphers': 1.0.0 - '@noble/curves': 1.6.0 + '@noble/curves': 1.7.0 '@noble/hashes': 1.6.1 dev: false @@ -28566,6 +28588,26 @@ packages: - zod dev: false + /ox@0.4.2(typescript@5.6.3)(zod@3.23.8): + resolution: {integrity: sha512-X3Ho21mTtJiCU2rWmfaheh2b0CG70Adre7Da/XQ0ECy+QppI6pLqdbGAJHiu/cTjumVXfwDGfv48APqePCU+ow==} + peerDependencies: + typescript: '>=5.4.0 || 5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 + '@scure/bip32': 1.6.0 + '@scure/bip39': 1.5.0 + abitype: 1.0.7(typescript@5.6.3)(zod@3.23.8) + eventemitter3: 5.0.1 + typescript: 5.6.3 + transitivePeerDependencies: + - zod + dev: false + /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -36370,6 +36412,7 @@ time: /@dialectlabs/blinks@0.15.1: '2024-11-06T17:16:01.791Z' /@emotion/styled@11.11.0: '2023-05-06T08:49:15.982Z' /@farcaster/core@0.15.6: '2024-10-28T17:27:01.168Z' + /@farcaster/frame-host@0.0.19: '2024-12-18T17:31:23.555Z' /@giphy/js-fetch-api@5.6.0: '2024-05-28T16:39:28.633Z' /@giphy/react-components@9.6.0: '2024-08-05T17:59:38.612Z' /@headlessui/react@2.1.10: '2024-10-10T19:00:29.034Z' diff --git a/src/components/Frame/V2/Card.tsx b/src/components/Frame/V2/Card.tsx index 7a7b7bbd5..2a9f69fae 100644 --- a/src/components/Frame/V2/Card.tsx +++ b/src/components/Frame/V2/Card.tsx @@ -1,18 +1,51 @@ -import { memo } from 'react'; +import type { SetPrimaryButton } from '@farcaster/frame-host'; +import { memo, useState } from 'react'; import { ClickableButton } from '@/components/ClickableButton.js'; import { Image } from '@/components/Image.js'; -import { FrameViewerModalRef } from '@/modals/controls.js'; +import { Source } from '@/constants/enum.js'; +import { getCurrentProfile } from '@/helpers/getCurrentProfile.js'; +import { FrameViewerModalRef, LoginModalRef } from '@/modals/controls.js'; +import { FarcasterFrameHost } from '@/providers/frame/Host.js'; +import type { Post } from '@/providers/types/SocialMedia.js'; import type { FrameV2 } from '@/types/frame.js'; interface CardProps { + post: Post; frame: FrameV2; } -export const Card = memo(function Card({ frame }) { +export const Card = memo(function Card({ post, frame }) { + const [primaryButton, setPrimaryButton] = useState[0] | null>(null); + + const [frameHost] = useState( + () => + new FarcasterFrameHost(frame, post, { + ready: (options) => { + FrameViewerModalRef.open({ + ready: true, + frame, + frameHost, + }); + }, + close: () => FrameViewerModalRef.close(), + setPrimaryButton, + }), + ); + const onClick = () => { + const profile = getCurrentProfile(Source.Farcaster); + if (!profile) { + LoginModalRef.open({ + source: Source.Farcaster, + }); + return; + } + FrameViewerModalRef.open({ + ready: false, frame, + frameHost, }); }; @@ -26,9 +59,15 @@ export const Card = memo(function Card({ frame }) { src={frame.imageUrl} alt={frame.x_url} /> - - {frame.button.action.name} - + {primaryButton?.hidden ? null : ( + + {primaryButton?.text ?? frame.button.action.name} + + )} ); }); diff --git a/src/components/Frame/V2/Layout.tsx b/src/components/Frame/V2/Layout.tsx index 854c53852..3b7384cfa 100644 --- a/src/components/Frame/V2/Layout.tsx +++ b/src/components/Frame/V2/Layout.tsx @@ -10,10 +10,10 @@ interface FrameLayoutProps { children?: ReactNode; } -export const FrameLayout = memo(function FrameLayout({ frame }) { +export const FrameLayout = memo(function FrameLayout({ post, frame }) { return (
- +
); }); diff --git a/src/helpers/createEIP1193Provider.ts b/src/helpers/createEIP1193Provider.ts new file mode 100644 index 000000000..4caf52f69 --- /dev/null +++ b/src/helpers/createEIP1193Provider.ts @@ -0,0 +1,17 @@ +import { noop } from 'lodash-es'; +import { getClient } from 'wagmi/actions'; + +import { config } from '@/configs/wagmiClient.js'; + +export function createEIP1193Provider() { + return { + async request(parameters: unknown): Promise { + const client = await getClient(config); + if (!client) throw new Error('Client not found'); + + return client.request(parameters as Parameters[0]); + }, + on: noop, + removeListener: noop, + }; +} diff --git a/src/modals/FrameViewerModal/MoreActionMenu.tsx b/src/modals/FrameViewerModal/MoreActionMenu.tsx index bbbe0bca5..d9116b89c 100644 --- a/src/modals/FrameViewerModal/MoreActionMenu.tsx +++ b/src/modals/FrameViewerModal/MoreActionMenu.tsx @@ -9,16 +9,18 @@ import { MenuGroup } from '@/components/MenuGroup.js'; import { MoreActionMenu } from '@/components/MoreActionMenu.js'; interface MoreActionProps { + disabled?: boolean; onReload?: () => void; } -export const MoreAction = memo(function MoreAction({ onReload }: MoreActionProps) { +export const MoreAction = memo(function MoreAction({ disabled = false, onReload }: MoreActionProps) { return ( }> {({ close }) => ( { close(); onReload?.(); diff --git a/src/modals/FrameViewerModal/index.tsx b/src/modals/FrameViewerModal/index.tsx index 567fdb2dd..30d5f88c5 100644 --- a/src/modals/FrameViewerModal/index.tsx +++ b/src/modals/FrameViewerModal/index.tsx @@ -1,8 +1,13 @@ -import { forwardRef, useState } from 'react'; +import { exposeToIframe, type FrameHost } from '@farcaster/frame-host'; +import { delay } from '@masknet/kit'; +import { forwardRef, useEffect, useRef, useState } from 'react'; +import { useAsyncFn } from 'react-use'; +import FireflyLogo from '@/assets/firefly.logo.svg'; import { CloseButton } from '@/components/IconButton.js'; import { Modal } from '@/components/Modal.js'; -import { NotImplementedError } from '@/constants/error.js'; +import { IS_DEVELOPMENT } from '@/constants/index.js'; +import { createEIP1193Provider } from '@/helpers/createEIP1193Provider.js'; import { parseUrl } from '@/helpers/parseUrl.js'; import { useSingletonModal } from '@/hooks/useSingletonModal.js'; import type { SingletonModalRefCreator } from '@/libs/SingletonModal.js'; @@ -10,12 +15,15 @@ import { MoreAction } from '@/modals/FrameViewerModal/MoreActionMenu.js'; import type { FrameV2 } from '@/types/frame.js'; export type FrameViewerModalOpenProps = { + ready: boolean; frame: FrameV2; + frameHost: Omit; }; export type FrameViewerModalCloseProps = void; export const FrameViewerModal = forwardRef>( function FrameViewerModal(_, ref) { + const frameRef = useRef(null); const [props, setProps] = useState(null); const [open, dispatch] = useSingletonModal(ref, { @@ -27,6 +35,38 @@ export const FrameViewerModal = forwardRef { + if (!frameRef.current) return; + + // frame host is required + if (!props?.frameHost) return; + + const result = exposeToIframe({ + debug: IS_DEVELOPMENT, + iframe: frameRef.current, + sdk: props.frameHost, + ethProvider: createEIP1193Provider(), + frameOrigin: '*', + }); + + return () => { + result?.cleanup(); + }; + }, [props]); + + const [{ loading }, onReload] = useAsyncFn(async () => { + if (!props) return; + + const modalProps = props; + + setProps(null); + await delay(1000); + setProps({ + ...modalProps, + ready: false, + }); + }, [props]); + if (!open || !props) return null; const { frame } = props; @@ -44,15 +84,12 @@ export const FrameViewerModal = forwardRef{u.host} : null}
- { - throw new NotImplementedError(); - }} - /> +