Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4afcbd0
First implementation still preview
mfbz Sep 18, 2025
f1e376b
Minor fixes
mfbz Sep 18, 2025
808282f
Added improved implementation of useFlowSchedule hook
mfbz Sep 23, 2025
0374705
Cleaned up hook
mfbz Oct 1, 2025
77295d3
Added tests
mfbz Oct 1, 2025
d62421d
Added changeset
mfbz Oct 1, 2025
f431cab
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 1, 2025
cbc1d78
Improved changeset description
mfbz Oct 2, 2025
6950c0b
Changed transactionId to txId for consistency
mfbz Oct 2, 2025
f0517cb
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 2, 2025
70440a3
Added useFlowSchedule card to demo and improved naming
mfbz Oct 2, 2025
c6bbe8a
Renamed txId to scheduledTxId for better clarity
mfbz Oct 2, 2025
3862072
Converted txid from bigint to string for consistency
mfbz Oct 3, 2025
c2023b3
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 3, 2025
c20706e
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 14, 2025
b296b19
Minor ordering update
mfbz Oct 14, 2025
0fedcd8
Updated card id
mfbz Oct 14, 2025
27b072e
Refactored single hook into multiple ones
mfbz Oct 14, 2025
dfedfc2
Improved hooks and added card implementation in demo
mfbz Oct 15, 2025
96bf4ba
Made setup transaction idempodent
mfbz Oct 15, 2025
db9c4bc
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 15, 2025
567e80a
Update packages/demo/src/components/content-sidebar.tsx
mfbz Oct 15, 2025
8a761cf
Update packages/demo/src/components/content-sidebar.tsx
mfbz Oct 15, 2025
66f39dd
Improved scheduled transactions demo card
mfbz Oct 15, 2025
7fee026
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 16, 2025
ee91c3f
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 17, 2025
b22ea28
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 18, 2025
ffd246a
Merge branch 'master' into mfbz/use-flow-schedule-hook
mfbz Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/upset-cities-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@onflow/react-sdk": minor
---

Added useFlowSchedule hook
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this more descriptive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved 👍

7 changes: 7 additions & 0 deletions packages/react-sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const CONTRACT_ADDRESSES = {
FlowEVMBridgeUtils: "0xdfc20aee650fcbdf",
FlowEVMBridgeConfig: "0xdfc20aee650fcbdf",
FungibleTokenMetadataViews: "0x9a0766d93b6608b7",
FlowTransactionScheduler: "0x8c5303eaa26202d6",
FlowTransactionSchedulerUtils: "0x8c5303eaa26202d6",
},
mainnet: {
EVM: "0xe467b9dd11fa00df",
Expand All @@ -24,6 +26,9 @@ export const CONTRACT_ADDRESSES = {
FlowEVMBridgeUtils: "0x1e4aa0b87d10b141",
FlowEVMBridgeConfig: "0x1e4aa0b87d10b141",
FungibleTokenMetadataViews: "0xf233dcee88fe0abe",
// TODO: Update with mainnet addresses
FlowTransactionScheduler: "0x0000000000000000",
FlowTransactionSchedulerUtils: "0x0000000000000000",
Comment on lines +30 to +31
Copy link

Copilot AI Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder addresses for mainnet (0x0000000000000000) could cause runtime errors if used before being updated. Consider adding runtime validation to throw a descriptive error when these placeholder addresses are detected, helping developers identify the issue quickly.

Copilot uses AI. Check for mistakes.

},
local: {
EVM: "0xf8d6e0586b0a20c7",
Expand All @@ -37,6 +42,8 @@ export const CONTRACT_ADDRESSES = {
FlowEVMBridgeUtils: "0xf8d6e0586b0a20c7",
FlowEVMBridgeConfig: "0xf8d6e0586b0a20c7",
FungibleTokenMetadataViews: "0xee82856bf20e2aa6",
FlowTransactionScheduler: "0xf8d6e0586b0a20c7",
FlowTransactionSchedulerUtils: "0xf8d6e0586b0a20c7",
},
}

Expand Down
1 change: 1 addition & 0 deletions packages/react-sdk/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export {useFlowTransactionStatus} from "./useFlowTransactionStatus"
export {useCrossVmSpendNft} from "./useCrossVmSpendNft"
export {useCrossVmSpendToken} from "./useCrossVmSpendToken"
export {useCrossVmTransactionStatus} from "./useCrossVmTransactionStatus"
export {useFlowSchedule} from "./useFlowSchedule"
310 changes: 310 additions & 0 deletions packages/react-sdk/src/hooks/useFlowSchedule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import * as fcl from "@onflow/fcl"
import {renderHook} from "@testing-library/react"
import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client"
import {FlowProvider} from "../provider"
import {useFlowChainId} from "./useFlowChainId"
import {
TransactionInfoWithHandler,
TransactionPriority,
TransactionStatus,
useFlowSchedule,
} from "./useFlowSchedule"

jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default)
jest.mock("./useFlowChainId", () => ({
useFlowChainId: jest.fn(),
}))

describe("useFlowSchedule", () => {
let mockFcl: MockFclInstance

beforeEach(() => {
jest.clearAllMocks()
jest.mocked(useFlowChainId).mockReturnValue({
data: "testnet",
isLoading: false,
} as any)

mockFcl = createMockFclInstance()
jest.mocked(fcl.createFlowClient).mockReturnValue(mockFcl.mockFclInstance)
})

describe("list", () => {
test("lists transactions for an account", async () => {
const mockTransactions = [
{
id: "1",
priority: 1,
executionEffort: "100",
status: 0,
fees: "0.001",
scheduledTimestamp: "1234567890.0",
handlerTypeIdentifier: "A.123.Handler",
handlerAddress: "0x123",
},
]

jest
.mocked(mockFcl.mockFclInstance.query)
.mockResolvedValueOnce(mockTransactions)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transactions = await result.current.list("0xACCOUNT")

expect(transactions).toHaveLength(1)
expect(transactions[0].id).toBe(1n)
expect(transactions[0].priority).toBe(TransactionPriority.Medium)
expect(transactions[0].status).toBe(TransactionStatus.Pending)
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("lists transactions with handler data", async () => {
const mockTransactionsWithHandler = [
{
id: "1",
priority: 2,
executionEffort: "200",
status: 1,
fees: "0.002",
scheduledTimestamp: "1234567890.0",
handlerTypeIdentifier: "A.456.Handler",
handlerAddress: "0x456",
handlerUUID: "9999",
handlerResolvedViews: {display: {name: "Test"}},
},
]

jest
.mocked(mockFcl.mockFclInstance.query)
.mockResolvedValueOnce(mockTransactionsWithHandler)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transactions = (await result.current.list("0xACCOUNT", {
includeHandlerData: true,
})) as TransactionInfoWithHandler[]

expect(transactions).toHaveLength(1)
expect(transactions[0].id).toBe(1n)
expect(transactions[0].handlerUUID).toBe(9999n)
expect(transactions[0].handlerResolvedViews).toEqual({
display: {name: "Test"},
})
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("returns empty array when no transactions", async () => {
jest.mocked(mockFcl.mockFclInstance.query).mockResolvedValueOnce([])

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transactions = await result.current.list("0xACCOUNT")

expect(transactions).toEqual([])
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("throws error when chain ID not detected", async () => {
jest.mocked(useFlowChainId).mockReturnValue({
data: null,
isLoading: false,
} as any)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

await expect(result.current.list("0xACCOUNT")).rejects.toThrow(
"Chain ID not detected"
)
})

test("handles query errors", async () => {
const error = new Error("Query failed")
jest.mocked(mockFcl.mockFclInstance.query).mockRejectedValueOnce(error)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

await expect(result.current.list("0xACCOUNT")).rejects.toThrow(
"Failed to list transactions: Query failed"
)
})
})

describe("get", () => {
test("gets a transaction by ID", async () => {
const mockTransaction = {
id: "42",
priority: 0,
executionEffort: "50",
status: 2,
fees: "0.0005",
scheduledTimestamp: "1234567890.0",
handlerTypeIdentifier: "A.789.Handler",
handlerAddress: "0x789",
}

jest
.mocked(mockFcl.mockFclInstance.query)
.mockResolvedValueOnce(mockTransaction)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transaction = await result.current.get(42n)

expect(transaction).toBeDefined()
expect(transaction?.id).toBe(42n)
expect(transaction?.priority).toBe(TransactionPriority.Low)
expect(transaction?.status).toBe(TransactionStatus.Completed)
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("gets a transaction with handler data", async () => {
const mockTransactionWithHandler = {
id: "42",
priority: 1,
executionEffort: "100",
status: 0,
fees: "0.001",
scheduledTimestamp: "1234567890.0",
handlerTypeIdentifier: "A.789.Handler",
handlerAddress: "0x789",
handlerUUID: "5555",
handlerResolvedViews: {metadata: {description: "Test handler"}},
}

jest
.mocked(mockFcl.mockFclInstance.query)
.mockResolvedValueOnce(mockTransactionWithHandler)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transaction = (await result.current.get(42n, {
includeHandlerData: true,
})) as TransactionInfoWithHandler

expect(transaction).toBeDefined()
expect(transaction.id).toBe(42n)
expect(transaction.handlerUUID).toBe(5555n)
expect(transaction.handlerResolvedViews).toEqual({
metadata: {description: "Test handler"},
})
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("returns null when transaction not found", async () => {
jest.mocked(mockFcl.mockFclInstance.query).mockResolvedValueOnce(null)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const transaction = await result.current.get(999n)

expect(transaction).toBeNull()
expect(mockFcl.mockFclInstance.query).toHaveBeenCalled()
})

test("handles query errors", async () => {
const error = new Error("Query failed")
jest.mocked(mockFcl.mockFclInstance.query).mockRejectedValueOnce(error)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

await expect(result.current.get(42n)).rejects.toThrow(
"Failed to get transaction: Query failed"
)
})
})

describe("setup", () => {
test("sets up manager successfully", async () => {
const txId = "setup-tx-id-123"
jest.mocked(mockFcl.mockFclInstance.mutate).mockResolvedValueOnce(txId)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const returnedTxId = await result.current.setup()

expect(returnedTxId).toBe(txId)
expect(mockFcl.mockFclInstance.mutate).toHaveBeenCalled()
})

test("handles setup errors", async () => {
const error = new Error("Setup failed")
jest.mocked(mockFcl.mockFclInstance.mutate).mockRejectedValueOnce(error)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

await expect(result.current.setup()).rejects.toThrow(
"Failed to setup manager: Setup failed"
)
})
})

describe("cancel", () => {
test("cancels a transaction successfully", async () => {
const txId = "cancel-tx-id-456"
jest.mocked(mockFcl.mockFclInstance.mutate).mockResolvedValueOnce(txId)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

const returnedTxId = await result.current.cancel(42n)

expect(returnedTxId).toBe(txId)
expect(mockFcl.mockFclInstance.mutate).toHaveBeenCalled()
})

test("handles cancel errors", async () => {
const error = new Error("Cancel failed")
jest.mocked(mockFcl.mockFclInstance.mutate).mockRejectedValueOnce(error)

const {result} = renderHook(() => useFlowSchedule(), {
wrapper: FlowProvider,
})

await expect(result.current.cancel(42n)).rejects.toThrow(
"Failed to cancel transaction: Cancel failed"
)
})
})

test("uses custom flowClient when provided", async () => {
const customMockFcl = createMockFclInstance()
const customFlowClient = customMockFcl.mockFclInstance as any

jest.mocked(customFlowClient.query).mockResolvedValueOnce([])

const {result} = renderHook(
() => useFlowSchedule({flowClient: customFlowClient}),
{
wrapper: FlowProvider,
}
)

await result.current.list("0xACCOUNT")

expect(customFlowClient.query).toHaveBeenCalled()
})
})
Loading