Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/light-mice-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@onflow/react-sdk": minor
---

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.
1 change: 1 addition & 0 deletions packages/react-sdk/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {useFlowCurrentUser} from "./useFlowCurrentUser"
export {useFlowAuthz} from "./useFlowAuthz"
export {useFlowAccount} from "./useFlowAccount"
export {useFlowBlock} from "./useFlowBlock"
export {useFlowChainId} from "./useFlowChainId"
Expand Down
193 changes: 193 additions & 0 deletions packages/react-sdk/src/hooks/useFlowAuthz.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as fcl from "@onflow/fcl"
import {InteractionAccount} from "@onflow/typedefs"
import {act, renderHook} from "@testing-library/react"
import {createMockFclInstance, MockFclInstance} from "../__mocks__/flow-client"
import {FlowProvider} from "../provider"
import {useFlowAuthz} from "./useFlowAuthz"

jest.mock("@onflow/fcl", () => require("../__mocks__/fcl").default)

const createMockAccount = (): Partial<InteractionAccount> => ({
tempId: "MOCK_TEMP_ID",
resolve: null,
})

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

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

afterEach(() => {
jest.clearAllMocks()
})

test("returns authorization function from current user", () => {
const {result} = renderHook(() => useFlowAuthz(), {
wrapper: FlowProvider,
})

expect(result.current).toBeDefined()
expect(typeof result.current).toBe("function")
expect(result.current).toBe(
mockFcl.mockFclInstance.currentUser.authorization
)
})

test("authorization function can be called", async () => {
const mockAuthzFn = jest.fn().mockResolvedValue({
tempId: "CURRENT_USER",
resolve: jest.fn(),
})

mockFcl.mockFclInstance.currentUser.authorization = mockAuthzFn

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

const mockAccount = createMockAccount()

await act(async () => {
await result.current(mockAccount)
})

expect(mockAuthzFn).toHaveBeenCalledWith(mockAccount)
})

test("returns stable authorization reference", () => {
const {result, rerender} = renderHook(() => useFlowAuthz(), {
wrapper: FlowProvider,
})

const firstAuth = result.current
expect(firstAuth).toBeDefined()

// Rerender should return the same authorization function
rerender()

expect(result.current).toBe(firstAuth)
})

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

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

expect(result.current).toBe(customFlowClient.currentUser.authorization)
})

test("creates custom authorization with authorization function", () => {
const customAuthz = (account: Partial<InteractionAccount>) => ({
...account,
addr: "0xBACKEND",
keyId: 0,
signingFunction: jest.fn(),
})

const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
wrapper: FlowProvider,
})

expect(result.current).toBeDefined()
expect(typeof result.current).toBe("function")
expect(result.current).toBe(customAuthz)
})

test("custom authorization returns correct account data", () => {
const customAddress = "0xBACKEND"
const customKeyId = 5
const mockSigningFunction = jest.fn()

const customAuthz = (account: Partial<InteractionAccount>) => ({
...account,
addr: customAddress,
keyId: customKeyId,
signingFunction: mockSigningFunction,
})

const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
wrapper: FlowProvider,
})

const mockAccount = createMockAccount()
const authResult = result.current(
mockAccount
) as Partial<InteractionAccount>

expect(authResult.addr).toBe(customAddress)
expect(authResult.keyId).toBe(customKeyId)
expect(authResult.signingFunction).toBe(mockSigningFunction)
})

test("custom authorization signing function can be called", async () => {
const mockSigningFunction = jest.fn().mockResolvedValue({
addr: "0xBACKEND",
keyId: 0,
signature: "mock_signature_123",
})

const customAuthz = (account: Partial<InteractionAccount>) => ({
...account,
addr: "0xBACKEND",
keyId: 0,
signingFunction: mockSigningFunction,
})

const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
wrapper: FlowProvider,
})

const mockAccount = createMockAccount()
const authResult = result.current(
mockAccount
) as Partial<InteractionAccount>

const mockSignable = {
message: "test_message",
addr: "0xBACKEND",
keyId: 0,
roles: {proposer: false, authorizer: true, payer: false},
voucher: {},
}

const signatureResult = await authResult.signingFunction!(mockSignable)

expect(mockSigningFunction).toHaveBeenCalledWith(mockSignable)
expect(signatureResult).toEqual({
addr: "0xBACKEND",
keyId: 0,
signature: "mock_signature_123",
})
})

test("custom authorization works even when user is not logged in", () => {
const customAuthz = (account: Partial<InteractionAccount>) => ({
...account,
addr: "0xBACKEND",
keyId: 0,
signingFunction: jest.fn(),
})

const {result} = renderHook(() => useFlowAuthz({authz: customAuthz}), {
wrapper: FlowProvider,
})

// User is not logged in (defaultUser.loggedIn === false)
// But custom auth should still work
expect(result.current).toBeDefined()
expect(typeof result.current).toBe("function")
expect(result.current).toBe(customAuthz)
})
})
70 changes: 70 additions & 0 deletions packages/react-sdk/src/hooks/useFlowAuthz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {InteractionAccount} from "@onflow/typedefs"
import {useFlowClient} from "./useFlowClient"

export type AuthorizationFunction = (
account: Partial<InteractionAccount>
) => Partial<InteractionAccount> | Promise<Partial<InteractionAccount>>

interface UseFlowAuthzArgs {
/** Custom authorization function. If not provided, uses current user's wallet authorization. */
authz?: AuthorizationFunction
/** Optional FlowClient instance to use instead of the default */
flowClient?: ReturnType<typeof useFlowClient>
}

/**
* @description A React hook that returns an authorization function for Flow transactions.
* If no custom authorization is provided, it returns the current user's wallet authorization.
*
* @param options Optional configuration object
* @param options.authz Optional custom authorization function
* @param options.flowClient Optional FlowClient instance to use instead of the default
*
* @returns The authorization function compatible with Flow transactions authorizations parameter
*
* @example
* // Current user authorization
* import { useFlowAuthz } from "@onflow/react-sdk"
* import * as fcl from "@onflow/fcl"
*
* function MyComponent() {
* const authorization = useFlowAuthz()
*
* const sendTransaction = async () => {
* await fcl.mutate({
* cadence: `transaction { prepare(signer: auth(Storage) &Account) {} }`,
* authorizations: [authorization],
* })
* }
* }
*
* @example
* // Custom authorization function
* import { useFlowAuthz } from "@onflow/react-sdk"
* import * as fcl from "@onflow/fcl"
*
* function MyComponent() {
* const customAuthz = (account) => ({
* ...account,
* addr: "0xCUSTOM",
* keyId: 0,
* signingFunction: async (signable) => ({ signature: "0x..." })
* })
*
* const authorization = useFlowAuthz({ authz: customAuthz })
*
* const sendTransaction = async () => {
* await fcl.mutate({
* cadence: `transaction { prepare(signer: auth(Storage) &Account) {} }`,
* authorizations: [authorization],
* })
* }
* }
*/
export function useFlowAuthz({
authz,
flowClient,
}: UseFlowAuthzArgs = {}): AuthorizationFunction {
const fcl = useFlowClient({flowClient})
return authz || (fcl.currentUser.authorization as any)
}