Skip to content

Commit 879ae91

Browse files
authored
Add useFlowAuthz hook (#2652)
* Implemented useFlowAuthz hook with tests * Added changeset * Renamed customAuthz to authz * Simplified implementation following PR review * Improved changeset description --------- Co-authored-by: mfbz <mfbz@users.noreply.github.com>
1 parent 729fda7 commit 879ae91

File tree

4 files changed

+269
-0
lines changed

4 files changed

+269
-0
lines changed

.changeset/light-mice-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@onflow/react-sdk": minor
3+
---
4+
5+
Added `useFlowAuthz` hook for handling Flow transaction authorization. This hook returns an authorization function that can be used when sending a transaction, defaulting to the current user's wallet authorization when no custom authorization is provided.

packages/react-sdk/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export {useFlowCurrentUser} from "./useFlowCurrentUser"
2+
export {useFlowAuthz} from "./useFlowAuthz"
23
export {useFlowAccount} from "./useFlowAccount"
34
export {useFlowBlock} from "./useFlowBlock"
45
export {useFlowChainId} from "./useFlowChainId"
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import * as fcl from "@onflow/fcl"
2+
import {InteractionAccount} from "@onflow/typedefs"
3+
import {act, renderHook} from "@testing-library/react"
4+
import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client"
5+
import {FlowProvider} from "../provider"
6+
import {useFlowAuthz} from "./useFlowAuthz"
7+
8+
jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default)
9+
10+
const createMockAccount = (): Partial<InteractionAccount> => ({
11+
tempId: "MOCK_TEMP_ID",
12+
resolve: null,
13+
})
14+
15+
describe("useFlowAuthz", () => {
16+
let mockFcl: MockFclInstance
17+
18+
beforeEach(() => {
19+
mockFcl = createMockFclInstance()
20+
jest.mocked(fcl.createFlowClient).mockReturnValue(mockFcl.mockFclInstance)
21+
})
22+
23+
afterEach(() => {
24+
jest.clearAllMocks()
25+
})
26+
27+
test("returns authorization function from current user", () => {
28+
const {result} = renderHook(() => useFlowAuthz(), {
29+
wrapper: FlowProvider,
30+
})
31+
32+
expect(result.current).toBeDefined()
33+
expect(typeof result.current).toBe("function")
34+
expect(result.current).toBe(
35+
mockFcl.mockFclInstance.currentUser.authorization
36+
)
37+
})
38+
39+
test("authorization function can be called", async () => {
40+
const mockAuthzFn = jest.fn().mockResolvedValue({
41+
tempId: "CURRENT_USER",
42+
resolve: jest.fn(),
43+
})
44+
45+
mockFcl.mockFclInstance.currentUser.authorization = mockAuthzFn
46+
47+
const {result} = renderHook(() => useFlowAuthz(), {
48+
wrapper: FlowProvider,
49+
})
50+
51+
const mockAccount = createMockAccount()
52+
53+
await act(async () => {
54+
await result.current(mockAccount)
55+
})
56+
57+
expect(mockAuthzFn).toHaveBeenCalledWith(mockAccount)
58+
})
59+
60+
test("returns stable authorization reference", () => {
61+
const {result, rerender} = renderHook(() => useFlowAuthz(), {
62+
wrapper: FlowProvider,
63+
})
64+
65+
const firstAuth = result.current
66+
expect(firstAuth).toBeDefined()
67+
68+
// Rerender should return the same authorization function
69+
rerender()
70+
71+
expect(result.current).toBe(firstAuth)
72+
})
73+
74+
test("uses custom flowClient when provided", () => {
75+
const customMockFcl = createMockFclInstance()
76+
const customFlowClient = customMockFcl.mockFclInstance as any
77+
78+
const {result} = renderHook(
79+
() =>
80+
useFlowAuthz({
81+
flowClient: customFlowClient,
82+
}),
83+
{
84+
wrapper: FlowProvider,
85+
}
86+
)
87+
88+
expect(result.current).toBe(customFlowClient.currentUser.authorization)
89+
})
90+
91+
test("creates custom authorization with authorization function", () => {
92+
const customAuthz = (account: Partial<InteractionAccount>) => ({
93+
...account,
94+
addr: "0xBACKEND",
95+
keyId: 0,
96+
signingFunction: jest.fn(),
97+
})
98+
99+
const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
100+
wrapper: FlowProvider,
101+
})
102+
103+
expect(result.current).toBeDefined()
104+
expect(typeof result.current).toBe("function")
105+
expect(result.current).toBe(customAuthz)
106+
})
107+
108+
test("custom authorization returns correct account data", () => {
109+
const customAddress = "0xBACKEND"
110+
const customKeyId = 5
111+
const mockSigningFunction = jest.fn()
112+
113+
const customAuthz = (account: Partial<InteractionAccount>) => ({
114+
...account,
115+
addr: customAddress,
116+
keyId: customKeyId,
117+
signingFunction: mockSigningFunction,
118+
})
119+
120+
const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
121+
wrapper: FlowProvider,
122+
})
123+
124+
const mockAccount = createMockAccount()
125+
const authResult = result.current(
126+
mockAccount
127+
) as Partial<InteractionAccount>
128+
129+
expect(authResult.addr).toBe(customAddress)
130+
expect(authResult.keyId).toBe(customKeyId)
131+
expect(authResult.signingFunction).toBe(mockSigningFunction)
132+
})
133+
134+
test("custom authorization signing function can be called", async () => {
135+
const mockSigningFunction = jest.fn().mockResolvedValue({
136+
addr: "0xBACKEND",
137+
keyId: 0,
138+
signature: "mock_signature_123",
139+
})
140+
141+
const customAuthz = (account: Partial<InteractionAccount>) => ({
142+
...account,
143+
addr: "0xBACKEND",
144+
keyId: 0,
145+
signingFunction: mockSigningFunction,
146+
})
147+
148+
const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
149+
wrapper: FlowProvider,
150+
})
151+
152+
const mockAccount = createMockAccount()
153+
const authResult = result.current(
154+
mockAccount
155+
) as Partial<InteractionAccount>
156+
157+
const mockSignable = {
158+
message: "test_message",
159+
addr: "0xBACKEND",
160+
keyId: 0,
161+
roles: {proposer: false, authorizer: true, payer: false},
162+
voucher: {},
163+
}
164+
165+
const signatureResult = await authResult.signingFunction!(mockSignable)
166+
167+
expect(mockSigningFunction).toHaveBeenCalledWith(mockSignable)
168+
expect(signatureResult).toEqual({
169+
addr: "0xBACKEND",
170+
keyId: 0,
171+
signature: "mock_signature_123",
172+
})
173+
})
174+
175+
test("custom authorization works even when user is not logged in", () => {
176+
const customAuthz = (account: Partial<InteractionAccount>) => ({
177+
...account,
178+
addr: "0xBACKEND",
179+
keyId: 0,
180+
signingFunction: jest.fn(),
181+
})
182+
183+
const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
184+
wrapper: FlowProvider,
185+
})
186+
187+
// User is not logged in (defaultUser.loggedIn === false)
188+
// But custom auth should still work
189+
expect(result.current).toBeDefined()
190+
expect(typeof result.current).toBe("function")
191+
expect(result.current).toBe(customAuthz)
192+
})
193+
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {InteractionAccount} from "@onflow/typedefs"
2+
import {useFlowClient} from "./useFlowClient"
3+
4+
export type AuthorizationFunction = (
5+
account: Partial<InteractionAccount>
6+
) => Partial<InteractionAccount> | Promise<Partial<InteractionAccount>>
7+
8+
interface UseFlowAuthzArgs {
9+
/** Custom authorization function. If not provided, uses current user's wallet authorization. */
10+
authz?: AuthorizationFunction
11+
/** Optional FlowClient instance to use instead of the default */
12+
flowClient?: ReturnType<typeof useFlowClient>
13+
}
14+
15+
/**
16+
* @description A React hook that returns an authorization function for Flow transactions.
17+
* If no custom authorization is provided, it returns the current user's wallet authorization.
18+
*
19+
* @param options Optional configuration object
20+
* @param options.authz Optional custom authorization function
21+
* @param options.flowClient Optional FlowClient instance to use instead of the default
22+
*
23+
* @returns The authorization function compatible with Flow transactions authorizations parameter
24+
*
25+
* @example
26+
* // Current user authorization
27+
* import { useFlowAuthz } from "@onflow/react-sdk"
28+
* import * as fcl from "@onflow/fcl"
29+
*
30+
* function MyComponent() {
31+
* const authorization = useFlowAuthz()
32+
*
33+
* const sendTransaction = async () => {
34+
* await fcl.mutate({
35+
* cadence: `transaction { prepare(signer: auth(Storage) &Account) {} }`,
36+
* authorizations: [authorization],
37+
* })
38+
* }
39+
* }
40+
*
41+
* @example
42+
* // Custom authorization function
43+
* import { useFlowAuthz } from "@onflow/react-sdk"
44+
* import * as fcl from "@onflow/fcl"
45+
*
46+
* function MyComponent() {
47+
* const customAuthz = (account) => ({
48+
* ...account,
49+
* addr: "0xCUSTOM",
50+
* keyId: 0,
51+
* signingFunction: async (signable) => ({ signature: "0x..." })
52+
* })
53+
*
54+
* const authorization = useFlowAuthz({ authz: customAuthz })
55+
*
56+
* const sendTransaction = async () => {
57+
* await fcl.mutate({
58+
* cadence: `transaction { prepare(signer: auth(Storage) &Account) {} }`,
59+
* authorizations: [authorization],
60+
* })
61+
* }
62+
* }
63+
*/
64+
export function useFlowAuthz({
65+
authz,
66+
flowClient,
67+
}: UseFlowAuthzArgs = {}): AuthorizationFunction {
68+
const fcl = useFlowClient({flowClient})
69+
return authz || (fcl.currentUser.authorization as any)
70+
}

0 commit comments

Comments
 (0)