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
224 changes: 224 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/payments/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
FundingSession,
FundingProvider,
FundingProviderFactory,
ProviderCapability,
} from "./types"
import type {createFlowClientCore} from "@onflow/fcl-core"
import {ADDRESS_PATTERN} from "./constants"
Expand All @@ -18,6 +19,11 @@ export interface PaymentsClient {
* @returns Promise resolving to a funding session with instructions
*/
createSession(intent: FundingIntent): Promise<FundingSession>
/**
* Get capabilities from all configured providers
* @returns Promise resolving to an array of provider capabilities
*/
getCapabilities(): Promise<ProviderCapability[]>
}

/**
Expand Down Expand Up @@ -134,5 +140,13 @@ export function createPaymentsClient(
`Failed to create session: no provider could handle the request. Errors: ${errorDetails}`
)
},
async getCapabilities() {
const allCapabilities: ProviderCapability[] = []
for (const provider of providers) {
const capabilities = await provider.getCapabilities()
allCapabilities.push(...capabilities)
}
return allCapabilities
},
}
}
4 changes: 2 additions & 2 deletions packages/payments/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"extends": "../../tsconfig",
"include": ["src/**/*", "src/**/*.d.ts", "cadence/**/*", "flow.json"],
"include": ["src/**/*", "src/**/*.d.ts"],
"compilerOptions": {
"declarationDir": "types",
"rootDir": ".",
"rootDir": "src",
"resolveJsonModule": true
}
}
1 change: 1 addition & 0 deletions packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"dependencies": {
"@onflow/fcl-core": "^1.30.1",
"@onflow/payments": "0.0.1",
"@onflow/typedefs": "^1.8.0",
"@babel/runtime": "^7.25.7",
"@tanstack/react-query": "^5.67.3"
Expand Down
5 changes: 5 additions & 0 deletions packages/react-core/src/core/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {createContext} from "react"
import type {FlowClientCore} from "@onflow/fcl-core"
import type {FlowNetwork} from "./types"
import type {PaymentsClient} from "@onflow/payments"

// FlowConfig based on @onflow/fcl-core's FlowClientCoreConfig
// This matches the config structure used by both @onflow/fcl and @onflow/fcl-react-native
Expand Down Expand Up @@ -38,3 +39,7 @@ export type FlowConfig = {
export const FlowConfigContext = createContext<FlowConfig>({})

export const FlowClientContext = createContext<FlowClientCore | null>(null)

export const PaymentsClientContext = createContext<PaymentsClient | undefined>(
undefined
)
3 changes: 3 additions & 0 deletions packages/react-core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ export * from "./useFlowScheduledTransactionList"
export * from "./useFlowScheduledTransaction"
export * from "./useFlowScheduledTransactionSetup"
export * from "./useFlowScheduledTransactionCancel"
export * from "./useFund"
export * from "./useFundingCapabilities"
export * from "./usePaymentsClient"
95 changes: 95 additions & 0 deletions packages/react-core/src/hooks/useFund.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {renderHook, waitFor} from "@testing-library/react"
import {useFund} from "./useFund"
import {FundingIntent, FundingSession, PaymentsClient} from "@onflow/payments"
import {TestProvider} from "../__mocks__/TestProvider"

describe("useFund", () => {
const mockSession: FundingSession = {
provider: "test-provider",
instructions: {
kind: "crypto",
address: "0xTestDepositAddress",
chain: "eip155:1",
currency: "0xUSDC",
},
}

const mockIntent: FundingIntent = {
kind: "crypto",
destination: "eip155:747:0xRecipient",
currency: "0xUSDC",
amount: "100",
sourceChain: "eip155:1",
sourceCurrency: "0xUSDC",
}

// Create mock payments client
const createMockPaymentsClient = (shouldSucceed: boolean): PaymentsClient => {
return {
createSession: jest
.fn()
.mockResolvedValue(
shouldSucceed
? mockSession
: Promise.reject(new Error("Provider failed"))
),
} as any
}

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

it("should successfully create a funding session", async () => {
const mockClient = createMockPaymentsClient(true)

const {result} = renderHook(() => useFund({paymentsClient: mockClient}), {
wrapper: TestProvider,
})

// Trigger the mutation
result.current.mutateAsync(mockIntent)

await waitFor(() => {
expect(result.current.isSuccess).toBe(true)
})

expect(result.current.data).toEqual(mockSession)
expect(mockClient.createSession).toHaveBeenCalledWith(mockIntent)
})

it("should handle funding session errors", async () => {
const error = new Error("Provider failed")
const mockClient = createMockPaymentsClient(false)

const {result} = renderHook(() => useFund({paymentsClient: mockClient}), {
wrapper: TestProvider,
})

// Trigger the mutation
result.current.mutate(mockIntent)

await waitFor(() => {
expect(result.current.isError).toBe(true)
})

expect(result.current.error?.message).toContain("Provider failed")
})

it("should throw error when no payments client is provided", async () => {
const {result} = renderHook(() => useFund(), {
wrapper: TestProvider,
})

// Trigger the mutation
result.current.mutate(mockIntent)

await waitFor(() => {
expect(result.current.isError).toBe(true)
})

expect(result.current.error?.message).toContain(
"No payments client available"
)
})
})
89 changes: 89 additions & 0 deletions packages/react-core/src/hooks/useFund.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
useMutation,
UseMutationResult,
UseMutationOptions,
} from "@tanstack/react-query"
import {useCallback} from "react"
import {useFlowQueryClient} from "../provider/FlowQueryClient"
import {useFlowClient} from "./useFlowClient"
import {usePaymentsClient} from "./usePaymentsClient"
import {FundingIntent, FundingSession, PaymentsClient} from "@onflow/payments"

/**
* Arguments for the useFund hook.
*/
export interface UseFundArgs {
/** Optional payments client (uses context if not provided) */
paymentsClient?: PaymentsClient
/** Optional React Query mutation settings (e.g., `onSuccess`, `onError`, `retry`) */
mutation?: Omit<
UseMutationOptions<FundingSession, Error, FundingIntent>,
"mutationFn"
>
/** Optional Flow client override */
flowClient?: ReturnType<typeof useFlowClient>
}

/**
* useFund
*
* Creates a funding session via the payments client and returns a React Query mutation.
* Use this hook to initiate crypto or fiat funding flows.
*
* @param args.paymentsClient - Optional payments client (uses context if not provided)
* @param args.mutation - Optional React Query mutation options
* @param args.flowClient - Optional Flow client override
*
* @example
* ```tsx
* import { useFund } from "@onflow/react-sdk"
*
* function FundButton() {
* const { mutateAsync: fund, isPending } = useFund()
*
* const handleFund = async () => {
* const session = await fund({
* kind: "crypto",
* destination: "eip155:747:0xRecipient",
* currency: "0xUSDC",
* amount: "100",
* sourceChain: "eip155:1",
* sourceCurrency: "0xUSDC",
* })
* console.log("Deposit to:", session.instructions.address)
* }
*
* return <button onClick={handleFund} disabled={isPending}>Fund</button>
* }
* ```
*/
export function useFund({
paymentsClient: _paymentsClient,
mutation: mutationOptions = {},
flowClient,
}: UseFundArgs = {}): UseMutationResult<FundingSession, Error, FundingIntent> {
const queryClient = useFlowQueryClient()
const contextPaymentsClient = usePaymentsClient()
const paymentsClient = _paymentsClient || contextPaymentsClient

const mutationFn = useCallback(
async (intent: FundingIntent) => {
if (!paymentsClient) {
throw new Error(
"No payments client available. Configure fundingProviders in FlowProvider or pass paymentsClient to useFund."
)
}
return paymentsClient.createSession(intent)
},
[paymentsClient]
)

return useMutation<FundingSession, Error, FundingIntent>(
{
mutationFn,
retry: false,
...mutationOptions,
},
queryClient
)
}
99 changes: 99 additions & 0 deletions packages/react-core/src/hooks/useFundingCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {useQuery, UseQueryOptions, UseQueryResult} from "@tanstack/react-query"
import {useCallback} from "react"
import {useFlowQueryClient} from "../provider/FlowQueryClient"
import {useFlowClient} from "./useFlowClient"
import {usePaymentsClient} from "./usePaymentsClient"
import {useFlowChainId} from "./useFlowChainId"
import {ProviderCapability, PaymentsClient} from "@onflow/payments"

/**
* Arguments for the useFundingCapabilities hook.
*/
export interface UseFundingCapabilitiesArgs {
/** Optional payments client (uses context if not provided) */
paymentsClient?: PaymentsClient
/** Optional React Query options */
query?: Omit<
UseQueryOptions<ProviderCapability[], Error>,
"queryKey" | "queryFn"
>
/** Optional Flow client override */
flowClient?: ReturnType<typeof useFlowClient>
}

/**
* useFundingCapabilities
*
* Fetches the capabilities (supported chains, currencies, etc.) from funding providers.
* Use this to dynamically populate UI with available funding options.
*
* @param args.paymentsClient - Optional payments client (uses context if not provided)
* @param args.query - Optional React Query options
* @param args.flowClient - Optional Flow client override
*
* @example
* ```tsx
* import { useFundingCapabilities } from "@onflow/react-sdk"
*
* function FundingOptions() {
* const { data: capabilities, isLoading } = useFundingCapabilities()
*
* if (isLoading) return <div>Loading...</div>
*
* const cryptoCapability = capabilities?.find(c => c.type === "crypto")
* const chains = cryptoCapability?.sourceChains || []
* const currencies = cryptoCapability?.currencies || []
*
* return (
* <div>
* <h3>Supported Chains:</h3>
* <ul>{chains.map(chain => <li key={chain}>{chain}</li>)}</ul>
* <h3>Supported Currencies:</h3>
* <ul>{currencies.map(currency => <li key={currency}>{currency}</li>)}</ul>
* </div>
* )
* }
* ```
*/
export function useFundingCapabilities({
paymentsClient: _paymentsClient,
query: queryOptions = {},
flowClient,
}: UseFundingCapabilitiesArgs = {}): UseQueryResult<
ProviderCapability[],
Error
> {
const queryClient = useFlowQueryClient()
const contextPaymentsClient = usePaymentsClient()
const paymentsClient = _paymentsClient || contextPaymentsClient
const {data: chainId} = useFlowChainId()

// Use chainId in query key for proper cache invalidation when network switches
const chainIdForKey = chainId || "unknown"

const fetchCapabilities = useCallback(async () => {
if (!paymentsClient) {
throw new Error(
"No payments client available. Configure fundingProviders in FlowProvider or pass paymentsClient to useFundingCapabilities."
)
}

// Use the getCapabilities method from the payments client
return await paymentsClient.getCapabilities()
}, [paymentsClient])

return useQuery<ProviderCapability[], Error>(
{
queryKey: [
"fundingCapabilities",
paymentsClient ? "configured" : "none",
chainIdForKey,
],
queryFn: fetchCapabilities,
enabled: !!paymentsClient,
staleTime: 5 * 60 * 1000, // 5 minutes - capabilities don't change often
...queryOptions,
},
queryClient
)
}
10 changes: 10 additions & 0 deletions packages/react-core/src/hooks/usePaymentsClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {useContext} from "react"
import {PaymentsClientContext} from "../core/context"

/**
* Hook to access the PaymentsClient from FlowProvider context
* @returns The PaymentsClient instance or undefined if not configured
*/
export function usePaymentsClient() {
return useContext(PaymentsClientContext)
}
1 change: 1 addition & 0 deletions packages/react-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"dependencies": {
"@babel/runtime": "^7.25.7",
"@headlessui/react": "^2.2.2",
"@onflow/payments": "0.0.1",
"@onflow/react-core": "0.8.1",
"@tanstack/react-query": "^5.67.3",
"@testing-library/react": "^16.2.0",
Expand Down
1 change: 1 addition & 0 deletions packages/react-sdk/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type FlowNetwork = "emulator" | "testnet" | "mainnet"
Loading