|
| 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