11import {
22 Dialog ,
3+ DialogContent ,
4+ DialogFooter ,
35 DialogHeader ,
46 DialogTitle ,
5- DialogFooter ,
6- DialogContent ,
77} from "@/components/ui/dialog" ;
8+ import { OnSignatureFunc , OnTransactionFunc } from "@frames.js/render" ;
89import type {
910 ComposerActionFormResponse ,
1011 ComposerActionState ,
1112} from "frames.js/types" ;
12- import { useEffect , useRef } from "react" ;
13+ import { useCallback , useEffect , useRef } from "react" ;
14+ import { Abi , TypedDataDomain } from "viem" ;
1315import { z } from "zod" ;
1416
1517const composerFormCreateCastMessageSchema = z . object ( {
@@ -23,38 +25,172 @@ const composerFormCreateCastMessageSchema = z.object({
2325 } ) ,
2426} ) ;
2527
28+ const ethSendTransactionActionSchema = z . object ( {
29+ chainId : z . string ( ) ,
30+ method : z . literal ( "eth_sendTransaction" ) ,
31+ attribution : z . boolean ( ) . optional ( ) ,
32+ params : z . object ( {
33+ abi : z . custom < Abi > ( ) ,
34+ to : z . custom < `0x${string } `> (
35+ ( val ) : val is `0x${string } ` =>
36+ typeof val === "string" && val . startsWith ( "0x" )
37+ ) ,
38+ value : z . string ( ) . optional ( ) ,
39+ data : z
40+ . custom < `0x${string } `> (
41+ ( val ) : val is `0x${string } ` =>
42+ typeof val === "string" && val . startsWith ( "0x" )
43+ )
44+ . optional ( ) ,
45+ } ) ,
46+ } ) ;
47+
48+ const ethSignTypedDataV4ActionSchema = z . object ( {
49+ chainId : z . string ( ) ,
50+ method : z . literal ( "eth_signTypedData_v4" ) ,
51+ params : z . object ( {
52+ domain : z . custom < TypedDataDomain > ( ) ,
53+ types : z . unknown ( ) ,
54+ primaryType : z . string ( ) ,
55+ message : z . record ( z . unknown ( ) ) ,
56+ } ) ,
57+ } ) ;
58+
59+ const transactionRequestBodySchema = z . object ( {
60+ requestId : z . string ( ) ,
61+ tx : z . union ( [ ethSendTransactionActionSchema , ethSignTypedDataV4ActionSchema ] ) ,
62+ } ) ;
63+
64+ const composerActionMessageSchema = z . discriminatedUnion ( "type" , [
65+ composerFormCreateCastMessageSchema ,
66+ z . object ( {
67+ type : z . literal ( "requestTransaction" ) ,
68+ data : transactionRequestBodySchema ,
69+ } ) ,
70+ ] ) ;
71+
2672type ComposerFormActionDialogProps = {
2773 composerActionForm : ComposerActionFormResponse ;
2874 onClose : ( ) => void ;
2975 onSave : ( arg : { composerState : ComposerActionState } ) => void ;
76+ onTransaction ?: OnTransactionFunc ;
77+ onSignature ?: OnSignatureFunc ;
78+ // TODO: Consider moving this into return value of onTransaction
79+ connectedAddress ?: `0x${string } `;
3080} ;
3181
3282export function ComposerFormActionDialog ( {
3383 composerActionForm,
3484 onClose,
3585 onSave,
86+ onTransaction,
87+ onSignature,
88+ connectedAddress,
3689} : ComposerFormActionDialogProps ) {
3790 const onSaveRef = useRef ( onSave ) ;
3891 onSaveRef . current = onSave ;
3992
93+ const iframeRef = useRef < HTMLIFrameElement > ( null ) ;
94+
95+ const postMessageToIframe = useCallback (
96+ ( message : any ) => {
97+ if ( iframeRef . current && iframeRef . current . contentWindow ) {
98+ iframeRef . current . contentWindow . postMessage (
99+ message ,
100+ new URL ( composerActionForm . url ) . origin
101+ ) ;
102+ }
103+ } ,
104+ [ composerActionForm . url ]
105+ ) ;
106+
40107 useEffect ( ( ) => {
41108 const handleMessage = ( event : MessageEvent ) => {
42- const result = composerFormCreateCastMessageSchema . safeParse ( event . data ) ;
109+ const result = composerActionMessageSchema . safeParse ( event . data ) ;
43110
44111 // on error is not called here because there can be different messages that don't have anything to do with composer form actions
45112 // instead we are just waiting for the correct message
46113 if ( ! result . success ) {
47- console . warn ( "Invalid message received" , event . data ) ;
114+ console . warn ( "Invalid message received" , event . data , result . error ) ;
48115 return ;
49116 }
50117
51- if ( result . data . data . cast . embeds . length > 2 ) {
52- console . warn ( "Only first 2 embeds are shown in the cast" ) ;
53- }
118+ const message = result . data ;
54119
55- onSaveRef . current ( {
56- composerState : result . data . data . cast ,
57- } ) ;
120+ if ( message . type === "requestTransaction" ) {
121+ if ( message . data . tx . method === "eth_sendTransaction" ) {
122+ onTransaction ?.( {
123+ transactionData : message . data . tx ,
124+ } ) . then ( ( txHash ) => {
125+ if ( txHash ) {
126+ postMessageToIframe ( {
127+ type : "transactionResponse" ,
128+ data : {
129+ requestId : message . data . requestId ,
130+ success : true ,
131+ receipt : {
132+ address : connectedAddress ,
133+ transactionId : txHash ,
134+ } ,
135+ } ,
136+ } ) ;
137+ } else {
138+ postMessageToIframe ( {
139+ type : "transactionResponse" ,
140+ data : {
141+ requestId : message . data . requestId ,
142+ success : false ,
143+ message : "User rejected the request" ,
144+ } ,
145+ } ) ;
146+ }
147+ } ) ;
148+ } else if ( message . data . tx . method === "eth_signTypedData_v4" ) {
149+ onSignature ?.( {
150+ signatureData : {
151+ chainId : message . data . tx . chainId ,
152+ method : "eth_signTypedData_v4" ,
153+ params : {
154+ domain : message . data . tx . params . domain ,
155+ types : message . data . tx . params . types as any ,
156+ primaryType : message . data . tx . params . primaryType ,
157+ message : message . data . tx . params . message ,
158+ } ,
159+ } ,
160+ } ) . then ( ( signature ) => {
161+ if ( signature ) {
162+ postMessageToIframe ( {
163+ type : "signatureResponse" ,
164+ data : {
165+ requestId : message . data . requestId ,
166+ success : true ,
167+ receipt : {
168+ address : connectedAddress ,
169+ transactionId : signature ,
170+ } ,
171+ } ,
172+ } ) ;
173+ } else {
174+ postMessageToIframe ( {
175+ type : "signatureResponse" ,
176+ data : {
177+ requestId : message . data . requestId ,
178+ success : false ,
179+ message : "User rejected the request" ,
180+ } ,
181+ } ) ;
182+ }
183+ } ) ;
184+ }
185+ } else if ( message . type === "createCast" ) {
186+ if ( message . data . cast . embeds . length > 2 ) {
187+ console . warn ( "Only first 2 embeds are shown in the cast" ) ;
188+ }
189+
190+ onSaveRef . current ( {
191+ composerState : message . data . cast ,
192+ } ) ;
193+ }
58194 } ;
59195
60196 window . addEventListener ( "message" , handleMessage ) ;
@@ -80,6 +216,7 @@ export function ComposerFormActionDialog({
80216 < div >
81217 < iframe
82218 className = "h-[600px] w-full opacity-100 transition-opacity duration-300"
219+ ref = { iframeRef }
83220 src = { composerActionForm . url }
84221 sandbox = "allow-forms allow-scripts allow-same-origin"
85222 > </ iframe >
0 commit comments