Skip to content

Commit 8ef9cc6

Browse files
authored
Added ScheduledTransactionList component (#2679)
* Created scheduled transaction list components with demo card * Improved metadata visualization * Added changeset * Added scheduled transactions view to connect modal * Prettier fix * Updated changeset * Fixed scrolling in scheduled transactions demo card * Added demo account usage to show default data * Improved error handling * Fixed show scheduled transactions false as default * Added filter by handler type and sorting * Renamed accountAddress to address * Improved metadataviews.display search * Improved scheduled transaction card * Fixed paddings * Added flowscan link and improve responsivness * Fixed thumbnail showing --------- Co-authored-by: mfbz <mfbz@users.noreply.github.com>
1 parent 3d6d1ab commit 8ef9cc6

File tree

14 files changed

+973
-6
lines changed

14 files changed

+973
-6
lines changed

.changeset/soft-goats-fold.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@onflow/react-sdk": minor
3+
"@onflow/demo": minor
4+
---
5+
6+
Added `ScheduledTransactionList` component, a scrollable list that displays scheduled transactions for a Flow account with support for MetadataViews.Display (thumbnails, names, descriptions), transaction cancellation, automatic refresh, responsive design and dark mode. Each card shows the scheduled execution time, fee, priority, and effort with an optional cancel button for pending transactions.
7+
8+
Enhanced `Connect` component to display scheduled transactions in the profile modal. The modal now shows the user's scheduled transactions below their account info with a configurable `modalConfig` prop to control visibility.

packages/demo/src/components/component-cards/connect-card.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ const PROPS: PropDefinition[] = [
3434
description: "Type of balance to display (from cross-VM token balance)",
3535
defaultValue: '"cadence"',
3636
},
37+
{
38+
name: "modalConfig",
39+
type: "ConnectModalConfig",
40+
required: false,
41+
description:
42+
"Configuration for the profile modal (like show scheduled transactions, filter handler types)",
43+
},
3744
]
3845

3946
export function ConnectCard() {
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
import {
2+
ScheduledTransactionList,
3+
useFlowCurrentUser,
4+
useFlowChainId,
5+
} from "@onflow/react-sdk"
6+
import {useState} from "react"
7+
import {useDarkMode} from "../flow-provider-wrapper"
8+
import {DemoCard, type PropDefinition} from "../ui/demo-card"
9+
import {PlusGridIcon} from "../ui/plus-grid"
10+
import {DEMO_ADDRESS_TESTNET} from "../../constants"
11+
12+
const IMPLEMENTATION_CODE = `import { ScheduledTransactionList, useFlowCurrentUser } from "@onflow/react-sdk"
13+
14+
function MyComponent() {
15+
const { user } = useFlowCurrentUser()
16+
17+
return (
18+
<div style={{ height: "600px" }}>
19+
<ScheduledTransactionList
20+
address={user?.addr || ""}
21+
filterHandlerTypes={["A.123.Contract.Handler1"]}
22+
cancelEnabled={true}
23+
/>
24+
</div>
25+
)
26+
}`
27+
28+
const PROPS: PropDefinition[] = [
29+
{
30+
name: "address",
31+
type: "string",
32+
required: true,
33+
description: "The Flow account address to fetch scheduled transactions for",
34+
},
35+
{
36+
name: "filterHandlerTypes",
37+
type: "string[]",
38+
required: false,
39+
description:
40+
"Array of handler type identifiers to filter. Only transactions matching these types will be displayed",
41+
},
42+
{
43+
name: "cancelEnabled",
44+
type: "boolean",
45+
required: false,
46+
description:
47+
"Whether to show cancel buttons on transaction cards. Defaults to true",
48+
},
49+
{
50+
name: "className",
51+
type: "string",
52+
required: false,
53+
description: "Additional CSS classes to apply to the list container",
54+
},
55+
{
56+
name: "style",
57+
type: "React.CSSProperties",
58+
required: false,
59+
description: "Inline styles to apply to the list container",
60+
},
61+
]
62+
63+
export function ScheduledTransactionListDemo() {
64+
const {darkMode} = useDarkMode()
65+
const {user} = useFlowCurrentUser()
66+
const {data: chainId, isLoading} = useFlowChainId()
67+
const isEmulator = chainId === "emulator" || chainId === "local"
68+
const [customAddress, setCustomAddress] = useState("")
69+
const [filterInput, setFilterInput] = useState("")
70+
71+
const normalizeAddress = (address: string): string => {
72+
if (!address) return ""
73+
const trimmed = address.trim()
74+
return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`
75+
}
76+
77+
const displayAddress = customAddress
78+
? normalizeAddress(customAddress)
79+
: chainId === "testnet"
80+
? DEMO_ADDRESS_TESTNET
81+
: user?.addr || ""
82+
83+
const filterHandlerTypes = filterInput
84+
? filterInput
85+
.split(",")
86+
.map(s => s.trim())
87+
.filter(s => s.length > 0)
88+
: undefined
89+
90+
return (
91+
<DemoCard
92+
id="scheduledtransactionlist"
93+
title="<ScheduledTransactionList />"
94+
description="A scrollable list component that displays all scheduled transactions for an account with automatic refresh after cancellation."
95+
code={IMPLEMENTATION_CODE}
96+
props={PROPS}
97+
docsUrl="https://developers.flow.com/build/tools/react-sdk/components#scheduledtransactionlist"
98+
>
99+
<div className="space-y-6">
100+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
101+
<div
102+
className={`relative p-4 rounded-lg border ${
103+
darkMode
104+
? "bg-gray-900/50 border-white/10"
105+
: "bg-gray-50 border-black/5"
106+
}`}
107+
>
108+
<PlusGridIcon placement="top left" className="absolute" />
109+
<h4
110+
className={`text-xs font-medium mb-1 ${darkMode ? "text-gray-500" : "text-gray-500"}`}
111+
>
112+
Auto-fetch
113+
</h4>
114+
<p className={`text-sm ${darkMode ? "text-white" : "text-black"}`}>
115+
Automatic data fetching
116+
</p>
117+
</div>
118+
119+
<div
120+
className={`relative p-4 rounded-lg border ${
121+
darkMode
122+
? "bg-gray-900/50 border-white/10"
123+
: "bg-gray-50 border-black/5"
124+
}`}
125+
>
126+
<PlusGridIcon placement="top right" className="absolute" />
127+
<h4
128+
className={`text-xs font-medium mb-1 ${darkMode ? "text-gray-500" : "text-gray-500"}`}
129+
>
130+
Scrollable
131+
</h4>
132+
<p className={`text-sm ${darkMode ? "text-white" : "text-black"}`}>
133+
Configurable max height
134+
</p>
135+
</div>
136+
137+
<div
138+
className={`relative p-4 rounded-lg border ${
139+
darkMode
140+
? "bg-gray-900/50 border-white/10"
141+
: "bg-gray-50 border-black/5"
142+
}`}
143+
>
144+
<PlusGridIcon placement="bottom left" className="absolute" />
145+
<h4
146+
className={`text-xs font-medium mb-1 ${darkMode ? "text-gray-500" : "text-gray-500"}`}
147+
>
148+
Auto-refresh
149+
</h4>
150+
<p className={`text-sm ${darkMode ? "text-white" : "text-black"}`}>
151+
Refreshes after cancellation
152+
</p>
153+
</div>
154+
</div>
155+
156+
<div className="space-y-4">
157+
<div
158+
className={`p-4 rounded-lg border ${
159+
darkMode
160+
? "bg-gray-900/50 border-white/10"
161+
: "bg-gray-50 border-black/5"
162+
}`}
163+
>
164+
<label
165+
className={`block text-sm font-medium mb-2 ${darkMode ? "text-gray-300" : "text-gray-700"}`}
166+
>
167+
Account Address
168+
</label>
169+
<div className="flex gap-2">
170+
<input
171+
type="text"
172+
value={customAddress}
173+
onChange={e => setCustomAddress(e.target.value)}
174+
placeholder={user?.addr || "Enter Flow address (e.g., 0x...)"}
175+
className={`flex-1 px-3 py-2 rounded-md border text-sm font-mono ${
176+
darkMode
177+
? `bg-gray-800 border-gray-700 text-white placeholder-gray-500
178+
focus:border-blue-500`
179+
: `bg-white border-gray-300 text-gray-900 placeholder-gray-400
180+
focus:border-blue-500`
181+
} focus:outline-none focus:ring-1 focus:ring-blue-500`}
182+
/>
183+
{customAddress && (
184+
<button
185+
onClick={() => setCustomAddress("")}
186+
className={`px-3 py-2 rounded-md border text-sm ${
187+
darkMode
188+
? "bg-gray-800 border-gray-700 text-gray-300 hover:bg-gray-700"
189+
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
190+
}`}
191+
>
192+
Clear
193+
</button>
194+
)}
195+
</div>
196+
{!customAddress && (
197+
<p
198+
className={`text-xs mt-2 ${darkMode ? "text-gray-500" : "text-gray-500"}`}
199+
>
200+
{chainId === "testnet"
201+
? `Using demo account: ${DEMO_ADDRESS_TESTNET}`
202+
: user?.addr
203+
? `Using connected wallet address: ${user.addr}`
204+
: "Connect wallet or enter address to view scheduled transactions"}
205+
</p>
206+
)}
207+
</div>
208+
209+
<div
210+
className={`p-4 rounded-lg border ${
211+
darkMode
212+
? "bg-gray-900/50 border-white/10"
213+
: "bg-gray-50 border-black/5"
214+
}`}
215+
>
216+
<label
217+
className={`block text-sm font-medium mb-2 ${darkMode ? "text-gray-300" : "text-gray-700"}`}
218+
>
219+
Filter by Handler Types (Optional)
220+
</label>
221+
<div className="flex gap-2">
222+
<input
223+
type="text"
224+
value={filterInput}
225+
onChange={e => setFilterInput(e.target.value)}
226+
placeholder="e.g., A.123.Contract.Handler1, A.456.Contract.Handler2"
227+
className={`flex-1 px-3 py-2 rounded-md border text-sm font-mono ${
228+
darkMode
229+
? `bg-gray-800 border-gray-700 text-white placeholder-gray-500
230+
focus:border-blue-500`
231+
: `bg-white border-gray-300 text-gray-900 placeholder-gray-400
232+
focus:border-blue-500`
233+
} focus:outline-none focus:ring-1 focus:ring-blue-500`}
234+
/>
235+
{filterInput && (
236+
<button
237+
onClick={() => setFilterInput("")}
238+
className={`px-3 py-2 rounded-md border text-sm ${
239+
darkMode
240+
? "bg-gray-800 border-gray-700 text-gray-300 hover:bg-gray-700"
241+
: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"
242+
}`}
243+
>
244+
Clear
245+
</button>
246+
)}
247+
</div>
248+
<p
249+
className={`text-xs mt-2 ${darkMode ? "text-gray-500" : "text-gray-500"}`}
250+
>
251+
{filterInput
252+
? `Filtering by ${filterHandlerTypes?.length || 0} handler type(s)`
253+
: "Enter comma-separated handler type identifiers to filter transactions"}
254+
</p>
255+
</div>
256+
</div>
257+
258+
<div
259+
className={`relative rounded-lg border ${
260+
darkMode
261+
? "bg-gray-900/50 border-white/10"
262+
: "bg-gray-50 border-black/5"
263+
}`}
264+
>
265+
{isLoading ? (
266+
<div className="text-center py-8 px-6">
267+
<div
268+
className={`inline-block animate-spin rounded-full h-8 w-8 border-b-2 ${
269+
darkMode ? "border-white" : "border-black" }`}
270+
></div>
271+
<p
272+
className={`mt-4 text-sm ${darkMode ? "text-gray-400" : "text-gray-600"}`}
273+
>
274+
Loading chain information...
275+
</p>
276+
</div>
277+
) : isEmulator ? (
278+
<div className="p-6">
279+
<div
280+
className={`text-center py-8 px-6 rounded-lg border ${
281+
darkMode
282+
? "bg-orange-900/20 border-orange-800/50"
283+
: "bg-orange-50 border-orange-200"
284+
}`}
285+
>
286+
<svg
287+
className="w-8 h-8 mx-auto mb-2 text-orange-500"
288+
fill="none"
289+
viewBox="0 0 24 24"
290+
stroke="currentColor"
291+
>
292+
<path
293+
strokeLinecap="round"
294+
strokeLinejoin="round"
295+
strokeWidth={2}
296+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
297+
/>
298+
</svg>
299+
<p
300+
className={`text-sm font-medium ${darkMode ? "text-orange-400" : "text-orange-600"}`}
301+
>
302+
Emulator Network Detected
303+
</p>
304+
<p
305+
className={`text-xs mt-1 ${darkMode ? "text-orange-400/70" : "text-orange-600/70"}`}
306+
>
307+
Scheduled transactions require testnet or mainnet
308+
</p>
309+
</div>
310+
</div>
311+
) : !displayAddress ? (
312+
<div className="p-6">
313+
<div
314+
className={`text-center py-8 px-6 rounded-lg border ${
315+
darkMode
316+
? "bg-blue-900/20 border-blue-800/50"
317+
: "bg-blue-50 border-blue-200"
318+
}`}
319+
>
320+
<svg
321+
className="w-8 h-8 mx-auto mb-2 text-blue-500"
322+
fill="none"
323+
viewBox="0 0 24 24"
324+
stroke="currentColor"
325+
>
326+
<path
327+
strokeLinecap="round"
328+
strokeLinejoin="round"
329+
strokeWidth={2}
330+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
331+
/>
332+
</svg>
333+
<p
334+
className={`text-sm font-medium ${darkMode ? "text-blue-400" : "text-blue-600"}`}
335+
>
336+
Connect Wallet or Enter Address
337+
</p>
338+
<p
339+
className={`text-xs mt-1 ${darkMode ? "text-blue-400/70" : "text-blue-600/70"}`}
340+
>
341+
Connect your wallet or enter a Flow address above to view
342+
scheduled transactions
343+
</p>
344+
</div>
345+
</div>
346+
) : (
347+
<div
348+
className={"p-3 m-2"}
349+
style={{height: "500px", overflowY: "auto"}}
350+
>
351+
<ScheduledTransactionList
352+
address={displayAddress}
353+
filterHandlerTypes={filterHandlerTypes}
354+
/>
355+
</div>
356+
)}
357+
</div>
358+
</div>
359+
</DemoCard>
360+
)
361+
}

0 commit comments

Comments
 (0)