From ea9d502b2ea1a7f79c5047fc4060f46099c29577 Mon Sep 17 00:00:00 2001 From: b3hr4d Date: Wed, 21 Feb 2024 19:45:31 +0300 Subject: [PATCH] Refactor actor and agent contexts --- .../src/components/CandidViewer.tsx | 25 +- .../candid-react/src/declarations/ledger.ts | 293 ++++++++++++++++++ examples/candid-react/src/index.tsx | 9 +- packages/react/src/helpers/types.ts | 5 +- packages/react/src/hooks/types.ts | 4 +- packages/react/src/hooks/useReactor.ts | 64 ++-- packages/react/src/provider/actor/context.tsx | 177 +---------- packages/react/src/provider/actor/hooks.ts | 154 +++++++++ packages/react/src/provider/actor/index.ts | 9 +- packages/react/src/provider/actor/types.ts | 16 +- packages/react/src/provider/agent/context.tsx | 49 +-- packages/react/src/provider/agent/hooks.ts | 165 ++++++++-- packages/react/src/provider/agent/index.ts | 12 + packages/react/src/provider/agent/types.ts | 19 +- 14 files changed, 726 insertions(+), 275 deletions(-) create mode 100644 examples/candid-react/src/declarations/ledger.ts create mode 100644 packages/react/src/provider/actor/hooks.ts diff --git a/examples/candid-react/src/components/CandidViewer.tsx b/examples/candid-react/src/components/CandidViewer.tsx index 4feaf49254..0ec704ba04 100644 --- a/examples/candid-react/src/components/CandidViewer.tsx +++ b/examples/candid-react/src/components/CandidViewer.tsx @@ -1,16 +1,19 @@ -import { extractActorHooks, useReactor } from "@ic-reactor/react" -import { ActorContextType } from "@ic-reactor/react/dist/types" +import { AgentProvider, extractActorHooks, useReactor } from "@ic-reactor/react" import { createContext } from "react" +import type { ActorHooks } from "@ic-reactor/react/dist/types" +import type { Ledger } from "../declarations/ledger" -const ActorContext = createContext(null) +const ActorContext = createContext | null>(null) -const CandidViewer = () => { - const { hooks, fetching, fetchError } = useReactor({ +const { useQueryCall } = extractActorHooks(ActorContext) + +const Reactor = () => { + const { hooks, fetching, fetchError } = useReactor({ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", }) return ( - +

IC Canister Interaction

{fetching &&

Loading Candid interface...

} {fetchError &&

Error: {fetchError}

} @@ -19,8 +22,6 @@ const CandidViewer = () => { ) } -const { useQueryCall } = extractActorHooks(ActorContext) - const CanisterName = () => { const { data } = useQueryCall({ functionName: "name", @@ -34,4 +35,10 @@ const CanisterName = () => { ) } -export default CandidViewer +const App = () => ( + + + +) + +export default App diff --git a/examples/candid-react/src/declarations/ledger.ts b/examples/candid-react/src/declarations/ledger.ts new file mode 100644 index 0000000000..db9826dd59 --- /dev/null +++ b/examples/candid-react/src/declarations/ledger.ts @@ -0,0 +1,293 @@ +import type { Principal } from "@dfinity/principal" +import type { ActorMethod } from "@dfinity/agent" + +export interface Account { + owner: Principal + subaccount: [] | [Uint8Array | number[]] +} +export interface AccountBalanceArgs { + account: string +} +export interface Allowance { + allowance: bigint + expires_at: [] | [bigint] +} +export interface AllowanceArgs { + account: Account + spender: Account +} +export interface ApproveArgs { + fee: [] | [bigint] + memo: [] | [Uint8Array | number[]] + from_subaccount: [] | [Uint8Array | number[]] + created_at_time: [] | [bigint] + amount: bigint + expected_allowance: [] | [bigint] + expires_at: [] | [bigint] + spender: Account +} +export type ApproveError = + | { + GenericError: { message: string; error_code: bigint } + } + | { TemporarilyUnavailable: null } + | { Duplicate: { duplicate_of: bigint } } + | { BadFee: { expected_fee: bigint } } + | { AllowanceChanged: { current_allowance: bigint } } + | { CreatedInFuture: { ledger_time: bigint } } + | { TooOld: null } + | { Expired: { ledger_time: bigint } } + | { InsufficientFunds: { balance: bigint } } +export interface ArchiveInfo { + canister_id: Principal +} +export interface ArchiveOptions { + num_blocks_to_archive: bigint + max_transactions_per_response: [] | [bigint] + trigger_threshold: bigint + max_message_size_bytes: [] | [bigint] + cycles_for_archive_creation: [] | [bigint] + node_max_memory_size_bytes: [] | [bigint] + controller_id: Principal +} +export interface ArchivedBlocksRange { + callback: [Principal, string] + start: bigint + length: bigint +} +export interface ArchivedEncodedBlocksRange { + callback: [Principal, string] + start: bigint + length: bigint +} +export interface Archives { + archives: Array +} +export interface BinaryAccountBalanceArgs { + account: Uint8Array | number[] +} +export interface BlockRange { + blocks: Array +} +export interface CandidBlock { + transaction: CandidTransaction + timestamp: TimeStamp + parent_hash: [] | [Uint8Array | number[]] +} +export type CandidOperation = + | { + Approve: { + fee: Tokens + from: Uint8Array | number[] + allowance_e8s: bigint + allowance: Tokens + expected_allowance: [] | [Tokens] + expires_at: [] | [TimeStamp] + spender: Uint8Array | number[] + } + } + | { + Burn: { + from: Uint8Array | number[] + amount: Tokens + spender: [] | [Uint8Array | number[]] + } + } + | { Mint: { to: Uint8Array | number[]; amount: Tokens } } + | { + Transfer: { + to: Uint8Array | number[] + fee: Tokens + from: Uint8Array | number[] + amount: Tokens + spender: [] | [Uint8Array | number[]] + } + } +export interface CandidTransaction { + memo: bigint + icrc1_memo: [] | [Uint8Array | number[]] + operation: [] | [CandidOperation] + created_at_time: TimeStamp +} +export interface Decimals { + decimals: number +} +export interface Duration { + secs: bigint + nanos: number +} +export interface FeatureFlags { + icrc2: boolean +} +export interface GetBlocksArgs { + start: bigint + length: bigint +} +export type GetBlocksError = + | { + BadFirstBlockIndex: { + requested_index: bigint + first_valid_index: bigint + } + } + | { Other: { error_message: string; error_code: bigint } } +export interface InitArgs { + send_whitelist: Array + token_symbol: [] | [string] + transfer_fee: [] | [Tokens] + minting_account: string + maximum_number_of_accounts: [] | [bigint] + accounts_overflow_trim_quantity: [] | [bigint] + transaction_window: [] | [Duration] + max_message_size_bytes: [] | [bigint] + icrc1_minting_account: [] | [Account] + archive_options: [] | [ArchiveOptions] + initial_values: Array<[string, Tokens]> + token_name: [] | [string] + feature_flags: [] | [FeatureFlags] +} +export type LedgerCanisterPayload = + | { Upgrade: [] | [UpgradeArgs] } + | { Init: InitArgs } +export type MetadataValue = + | { Int: bigint } + | { Nat: bigint } + | { Blob: Uint8Array | number[] } + | { Text: string } +export interface Name { + name: string +} +export interface QueryBlocksResponse { + certificate: [] | [Uint8Array | number[]] + blocks: Array + chain_length: bigint + first_block_index: bigint + archived_blocks: Array +} +export interface QueryEncodedBlocksResponse { + certificate: [] | [Uint8Array | number[]] + blocks: Array + chain_length: bigint + first_block_index: bigint + archived_blocks: Array +} +export type Result = { Ok: bigint } | { Err: TransferError } +export type Result_1 = { Ok: bigint } | { Err: ApproveError } +export type Result_2 = { Ok: bigint } | { Err: TransferFromError } +export type Result_3 = { Ok: BlockRange } | { Err: GetBlocksError } +export type Result_4 = + | { Ok: Array } + | { Err: GetBlocksError } +export type Result_5 = { Ok: bigint } | { Err: TransferError_1 } +export interface SendArgs { + to: string + fee: Tokens + memo: bigint + from_subaccount: [] | [Uint8Array | number[]] + created_at_time: [] | [TimeStamp] + amount: Tokens +} +export interface StandardRecord { + url: string + name: string +} +export interface Symbol { + symbol: string +} +export interface TimeStamp { + timestamp_nanos: bigint +} +export interface Tokens { + e8s: bigint +} +export interface TransferArg { + to: Account + fee: [] | [bigint] + memo: [] | [Uint8Array | number[]] + from_subaccount: [] | [Uint8Array | number[]] + created_at_time: [] | [bigint] + amount: bigint +} +export interface TransferArgs { + to: Uint8Array | number[] + fee: Tokens + memo: bigint + from_subaccount: [] | [Uint8Array | number[]] + created_at_time: [] | [TimeStamp] + amount: Tokens +} +export type TransferError = + | { + GenericError: { message: string; error_code: bigint } + } + | { TemporarilyUnavailable: null } + | { BadBurn: { min_burn_amount: bigint } } + | { Duplicate: { duplicate_of: bigint } } + | { BadFee: { expected_fee: bigint } } + | { CreatedInFuture: { ledger_time: bigint } } + | { TooOld: null } + | { InsufficientFunds: { balance: bigint } } +export type TransferError_1 = + | { + TxTooOld: { allowed_window_nanos: bigint } + } + | { BadFee: { expected_fee: Tokens } } + | { TxDuplicate: { duplicate_of: bigint } } + | { TxCreatedInFuture: null } + | { InsufficientFunds: { balance: Tokens } } +export interface TransferFee { + transfer_fee: Tokens +} +export interface TransferFromArgs { + to: Account + fee: [] | [bigint] + spender_subaccount: [] | [Uint8Array | number[]] + from: Account + memo: [] | [Uint8Array | number[]] + created_at_time: [] | [bigint] + amount: bigint +} +export type TransferFromError = + | { + GenericError: { message: string; error_code: bigint } + } + | { TemporarilyUnavailable: null } + | { InsufficientAllowance: { allowance: bigint } } + | { BadBurn: { min_burn_amount: bigint } } + | { Duplicate: { duplicate_of: bigint } } + | { BadFee: { expected_fee: bigint } } + | { CreatedInFuture: { ledger_time: bigint } } + | { TooOld: null } + | { InsufficientFunds: { balance: bigint } } +export interface UpgradeArgs { + maximum_number_of_accounts: [] | [bigint] + icrc1_minting_account: [] | [Account] + feature_flags: [] | [FeatureFlags] +} +export interface Ledger { + account_balance: ActorMethod<[BinaryAccountBalanceArgs], Tokens> + account_balance_dfx: ActorMethod<[AccountBalanceArgs], Tokens> + account_identifier: ActorMethod<[Account], Uint8Array | number[]> + archives: ActorMethod<[], Archives> + decimals: ActorMethod<[], Decimals> + icrc1_balance_of: ActorMethod<[Account], bigint> + icrc1_decimals: ActorMethod<[], number> + icrc1_fee: ActorMethod<[], bigint> + icrc1_metadata: ActorMethod<[], Array<[string, MetadataValue]>> + icrc1_minting_account: ActorMethod<[], [] | [Account]> + icrc1_name: ActorMethod<[], string> + icrc1_supported_standards: ActorMethod<[], Array> + icrc1_symbol: ActorMethod<[], string> + icrc1_total_supply: ActorMethod<[], bigint> + icrc1_transfer: ActorMethod<[TransferArg], Result> + icrc2_allowance: ActorMethod<[AllowanceArgs], Allowance> + icrc2_approve: ActorMethod<[ApproveArgs], Result_1> + icrc2_transfer_from: ActorMethod<[TransferFromArgs], Result_2> + name: ActorMethod<[], Name> + query_blocks: ActorMethod<[GetBlocksArgs], QueryBlocksResponse> + query_encoded_blocks: ActorMethod<[GetBlocksArgs], QueryEncodedBlocksResponse> + send_dfx: ActorMethod<[SendArgs], bigint> + symbol: ActorMethod<[], Symbol> + transfer: ActorMethod<[TransferArgs], Result_5> + transfer_fee: ActorMethod<[{}], TransferFee> +} diff --git a/examples/candid-react/src/index.tsx b/examples/candid-react/src/index.tsx index e92a2d9af4..22c404664c 100644 --- a/examples/candid-react/src/index.tsx +++ b/examples/candid-react/src/index.tsx @@ -1,12 +1,7 @@ import ReactDOM from "react-dom/client" import "./index.css" -import CandidViewer from "./components/CandidViewer" -import { AgentProvider } from "@ic-reactor/react" +import App from "./components/CandidViewer" const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) -root.render( - - - -) +root.render() diff --git a/packages/react/src/helpers/types.ts b/packages/react/src/helpers/types.ts index 678bc9cf7f..b80256a1e7 100644 --- a/packages/react/src/helpers/types.ts +++ b/packages/react/src/helpers/types.ts @@ -12,6 +12,7 @@ import type { AuthState, HttpAgent, AgentState, + BaseActor, } from "@ic-reactor/core/dist/types" export interface AgentHooks { @@ -22,10 +23,10 @@ export interface AgentHooks { export interface AuthHooks { useUserPrincipal: () => Principal | undefined useAuthState: () => AuthState - useAuthClient: (events?: UseAuthClientArgs) => UseAuthClientReturn + useAuthClient: (args?: UseAuthClientArgs) => UseAuthClientReturn } -export interface ActorHooks { +export interface ActorHooks { initialize: () => Promise useActorState: () => UseActorState useQueryCall: UseQueryCall diff --git a/packages/react/src/hooks/types.ts b/packages/react/src/hooks/types.ts index 893619aa49..fa3379faa1 100644 --- a/packages/react/src/hooks/types.ts +++ b/packages/react/src/hooks/types.ts @@ -1,6 +1,6 @@ import { IDL } from "@dfinity/candid" import { ActorManagerOptions, BaseActor } from "@ic-reactor/core/dist/types" -import { ActorHooks, AgentContextType } from "../types" +import { ActorHooks, AgentContext } from "../types" export interface UseReactorOptions extends Omit< @@ -9,7 +9,7 @@ export interface UseReactorOptions > { canisterId: string idlFactory?: IDL.InterfaceFactory - agentContext?: AgentContextType + agentContext?: React.Context didjsCanisterId?: string } diff --git a/packages/react/src/hooks/useReactor.ts b/packages/react/src/hooks/useReactor.ts index 414fd9e307..ebc0cadafe 100644 --- a/packages/react/src/hooks/useReactor.ts +++ b/packages/react/src/hooks/useReactor.ts @@ -11,36 +11,60 @@ import { getActorHooks } from "../helpers" * It simplifies the process of interacting with canisters by encapsulating * the logic for Candid retrieval and actor store management. * + * By using this hook, you can create have full control over the lifecycle of + * the actor store and the Candid interface. This hook is useful when you need + * to manage the lifecycle of the actor store and Candid interface yourself. + * * @example * ```tsx - * import React from 'react'; - * import { useReactor } from '@ic-reactor/react'; - * import { IDL } from '@dfinity/candid'; + * import { AgentProvider, extractActorHooks, useReactor } from "@ic-reactor/react" + * import { createContext } from "react" + * import type { ActorHooks } from "@ic-reactor/react/dist/types" + * // With this import, you can have type safety for the actor's interface. + * // You can get it from the `.did.d.ts` file generated by the DFX tool. + * // or from dashboard https://dashboard.internetcomputer.org/canisters/ + * import type { Ledger } from "../declarations/ledger" + * + * const ActorContext = createContext | null>(null) + * + * const { useQueryCall } = extractActorHooks(ActorContext) * - * const App = () => { - * const { actorManager, fetchCandid, candid, fetching, fetchError } = useReactor({ - * canisterId: 'ryjl3-tyaaa-aaaaa-aaaba-cai', - * }); + * const Reactor = () => { + * const { hooks, fetching, fetchError } = useReactor({ + * canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai", + * }) * * return ( - *
+ * *

IC Canister Interaction

* {fetching &&

Loading Candid interface...

} * {fetchError &&

Error: {fetchError}

} - * {candid.idlFactory && ( - *
- *

Candid interface fetched successfully.

- *
{JSON.stringify(candid.idlFactory({ IDL }), null, 2)}
- *
- * )} - * + * {hooks && } + *
+ * ) + * } + * + * const CanisterName = () => { + * const { data } = useQueryCall({ + * functionName: "name", + * }) + * + * return ( + *
+ *

Query Call

+ *

Result: {JSON.stringify(data)}

*
- * ); - * }; + * ) + * } + * + * const App = () => ( + * + * + * + * ) + * + * export default App * - * export default App; * ``` */ export const useReactor =
({ diff --git a/packages/react/src/provider/actor/context.tsx b/packages/react/src/provider/actor/context.tsx index c380d9ef45..19ae625fc6 100644 --- a/packages/react/src/provider/actor/context.tsx +++ b/packages/react/src/provider/actor/context.tsx @@ -1,23 +1,19 @@ -import React, { createContext, useMemo, useContext } from "react" +import React, { createContext, useMemo } from "react" +import { ActorHooks, BaseActor } from "../../types" import { - BaseActor, - FunctionName, - UseMethodCallArg, - UseQueryCallArgs, - UseUpdateCallArgs, -} from "../../types" -import { - CreateActorOptions, - ActorContextType, + CreateActorContextOptions, + CreateActorContextReturn, ActorProviderProps, } from "./types" import { useReactor } from "../../hooks/useReactor" +import { extractActorHooks } from "./hooks" + +export function createActorContext( + reactorOptions: Partial = {} +): CreateActorContextReturn { + const { canisterId: defaultCanisterId, ...defaultConfig } = reactorOptions -export function createReactorContext({ - canisterId: defaultCanisterId, - ...defaultConfig -}: Partial = {}) { - const ActorContext = createContext | null>(null) + const ActorContext = createContext | null>(null) const ActorProvider: React.FC = ({ children, @@ -43,7 +39,7 @@ export function createReactorContext({ }) return ( - }> + {fetching || hooks === null ? fetchError ?? loadingComponent : children} ) @@ -52,154 +48,7 @@ export function createReactorContext({ ActorProvider.displayName = "ActorProvider" return { - ActorProvider, + ActorProvider: ActorProvider, ...extractActorHooks(ActorContext), } } - -export function extractActorHooks( - ActorContext: React.Context | null> -) { - /** - * Hook for accessing the actor context, including the actor manager and state. - * @returns The actor context, including the actor manager and state. - * @example - * ```tsx - * function ActorComponent() { - * const { initialize, useActorState, useQueryCall, useUpdateCall, useMethodCall, useVisitMethod } = useActorContext(); - * const { canisterId } = useActorState(); - * - * return ( - *
- *

Canister ID: {canisterId}

- *
- * ); - * } - * ``` - */ - const useActorContext = () => { - const context = useContext(ActorContext) as ActorContextType
| null - - if (!context) { - throw new Error("useActor must be used within a ActorProvider") - } - - return context - } - - /** - * Initializes the actor manager, setting up the actor's state. - */ - const initialize = () => useActorContext().initialize() - - /** - * Hook for accessing the current state of the actor, including the canister ID. - * - * @returns An object containing the current state of the actor from Zustand's store and the canister ID. - * @example - * ```tsx - * function ActorStateComponent() { - * const { canisterId, initializing, error, initialized } = useActorState(); - * - * return ( - *
- *

Canister ID: {canisterId}

- *

Initializing: {initializing.toString()}

- *

Initialized: {initialized.toString()}

- *

Error: {error?.message}

- *
- * ); - * } - *``` - */ - const useActorState = () => useActorContext().useActorState() - - /** - * Hook for making query calls to actors. It supports automatic refetching on component mount and at specified intervals. - * - * @param options Configuration object for the query call, including refetching options and other configurations passed to useReactorCall. - * @returns An object containing the query call function and the current call state (data, error, loading, call, reset). - * @example - * ```tsx - * function QueryCallComponent() { - * const { call, data, loading } = useQueryCall({ - * functionName: 'getUserProfile', - * args: ['123'], - * refetchOnMount: true, - * refetchInterval: 5000, // refetch every 5 seconds - * }); - * - * if (loading) return

Loading profile...

; - * - * return ( - *
- *

User Profile: {JSON.stringify(data)}

- * - *
- * ); - * } - * ``` - */ - const useQueryCall = >( - args: UseQueryCallArgs - ) => useActorContext().useQueryCall(args) - - /** - * Hook for making update calls to actors, handling loading states, and managing errors. It supports custom event handlers for loading, success, and error events. - * - * @param options Configuration object for the actor method call, including the method name, arguments, and event handlers. - * @returns An object containing the method call function, a reset function to reset the call state to its default, and the current call state (data, error, loading, call, reset). - * @example - * ```tsx - * function UpdateCallComponent() { - * const { call, data, loading } = useUpdateCall({ - * functionName: 'updateUserProfile', - * args: ['123', { name: 'John Doe' }], - * onLoading: (loading) => console.log('Loading:', loading), - * onError: (error) => console.error('Error:', error), - * onSuccess: (data) => console.log('Success:', data), - * }); - * - * if (loading) return

Updating profile...

; - * - * return ( - *
- *

Updated Profile: {JSON.stringify(data)}

- * - *
- * ); - * } - * ``` - */ - const useUpdateCall = >( - args: UseUpdateCallArgs - ) => useActorContext().useUpdateCall(args) - - /** - * Hook that combines useVisitMethod and useReactorCall for calling actor methods. It provides both the visit service for the method and the ability to make actor calls with state management. - * - * @param args Configuration object including the function name and arguments for the actor method call. - * @returns An object containing the visit function for the method and the current call state (data, error, loading). - */ - const useMethodCall = >( - args: UseMethodCallArg - ) => useActorContext().useMethodCall(args) - - /** - * Memoizes and returns a visit service function for a specific actor method. - * - * @param functionName The name of the actor method to visit. - * @returns The visit service function for the specified method. - */ - const useVisitMethod = (functionName: FunctionName
) => - useActorContext().useVisitMethod(functionName) - - return { - useActorState, - useQueryCall, - useUpdateCall, - useMethodCall, - useVisitMethod, - initialize, - } -} diff --git a/packages/react/src/provider/actor/hooks.ts b/packages/react/src/provider/actor/hooks.ts new file mode 100644 index 0000000000..259cacc47d --- /dev/null +++ b/packages/react/src/provider/actor/hooks.ts @@ -0,0 +1,154 @@ +import { useContext } from "react" +import { + ActorHooks, + BaseActor, + CreateActorContextReturn, + FunctionName, + UseMethodCall, + UseQueryCall, + UseUpdateCall, +} from "../../types" + +export function extractActorHooks( + actorContext: React.Context | null> +): Omit, "ActorProvider"> { + /** + * Hook for accessing the actor context, including the actor manager and state. + * @returns The actor context, including the actor manager and state. + * @example + * ```tsx + * function ActorComponent() { + * const { initialize, useActorState, useQueryCall, useUpdateCall, useMethodCall, useVisitMethod } = useActorContext(); + * const { canisterId } = useActorState(); + * + * return ( + *
+ *

Canister ID: {canisterId}

+ *
+ * ); + * } + * ``` + */ + const useActorContext = () => { + const context = useContext(actorContext) + + if (!context) { + throw new Error("useActor must be used within a ActorProvider") + } + + return context + } + + /** + * Initializes the actor manager, setting up the actor's state. + */ + const initialize = () => useActorContext().initialize() + + /** + * Hook for accessing the current state of the actor, including the canister ID. + * + * @returns An object containing the current state of the actor from Zustand's store and the canister ID. + * @example + * ```tsx + * function ActorStateComponent() { + * const { canisterId, initializing, error, initialized } = useActorState(); + * + * return ( + *
+ *

Canister ID: {canisterId}

+ *

Initializing: {initializing.toString()}

+ *

Initialized: {initialized.toString()}

+ *

Error: {error?.message}

+ *
+ * ); + * } + *``` + */ + const useActorState = () => useActorContext().useActorState() + + /** + * Hook for making query calls to actors. It supports automatic refetching on component mount and at specified intervals. + * + * @param options Configuration object for the query call, including refetching options and other configurations passed to useReactorCall. + * @returns An object containing the query call function and the current call state (data, error, loading, call, reset). + * @example + * ```tsx + * function QueryCallComponent() { + * const { call, data, loading } = useQueryCall({ + * functionName: 'getUserProfile', + * args: ['123'], + * refetchOnMount: true, + * refetchInterval: 5000, // refetch every 5 seconds + * }); + * + * if (loading) return

Loading profile...

; + * + * return ( + *
+ *

User Profile: {JSON.stringify(data)}

+ * + *
+ * ); + * } + * ``` + */ + const useQueryCall: UseQueryCall
= (args) => + useActorContext().useQueryCall(args) + + /** + * Hook for making update calls to actors, handling loading states, and managing errors. It supports custom event handlers for loading, success, and error events. + * + * @param options Configuration object for the actor method call, including the method name, arguments, and event handlers. + * @returns An object containing the method call function, a reset function to reset the call state to its default, and the current call state (data, error, loading, call, reset). + * @example + * ```tsx + * function UpdateCallComponent() { + * const { call, data, loading } = useUpdateCall({ + * functionName: 'updateUserProfile', + * args: ['123', { name: 'John Doe' }], + * onLoading: (loading) => console.log('Loading:', loading), + * onError: (error) => console.error('Error:', error), + * onSuccess: (data) => console.log('Success:', data), + * }); + * + * if (loading) return

Updating profile...

; + * + * return ( + *
+ *

Updated Profile: {JSON.stringify(data)}

+ * + *
+ * ); + * } + * ``` + */ + const useUpdateCall: UseUpdateCall
= (args) => + useActorContext().useUpdateCall(args) + + /** + * Hook that combines useVisitMethod and useReactorCall for calling actor methods. It provides both the visit service for the method and the ability to make actor calls with state management. + * + * @param args Configuration object including the function name and arguments for the actor method call. + * @returns An object containing the visit function for the method and the current call state (data, error, loading). + */ + const useMethodCall: UseMethodCall = (args) => + useActorContext().useMethodCall(args) + + /** + * Memoizes and returns a visit service function for a specific actor method. + * + * @param functionName The name of the actor method to visit. + * @returns The visit service function for the specified method. + */ + const useVisitMethod = (functionName: FunctionName) => + useActorContext().useVisitMethod(functionName) + + return { + useActorState, + useQueryCall, + useUpdateCall, + useMethodCall, + useVisitMethod, + initialize, + } +} diff --git a/packages/react/src/provider/actor/index.ts b/packages/react/src/provider/actor/index.ts index 6ebc84ab60..ce12c082b6 100644 --- a/packages/react/src/provider/actor/index.ts +++ b/packages/react/src/provider/actor/index.ts @@ -1,12 +1,13 @@ -import { createReactorContext } from "./context" +import { createActorContext } from "./context" export const { - ActorProvider, + ActorProvider: ActorProvider, useActorState, useQueryCall, useUpdateCall, useMethodCall, useVisitMethod, -} = createReactorContext() +} = createActorContext() -export { createReactorContext, extractActorHooks } from "./context" +export * from "./context" +export * from "./hooks" diff --git a/packages/react/src/provider/actor/types.ts b/packages/react/src/provider/actor/types.ts index 1c9e93685e..ba4d56ef8f 100644 --- a/packages/react/src/provider/actor/types.ts +++ b/packages/react/src/provider/actor/types.ts @@ -1,30 +1,24 @@ import { IDL } from "@dfinity/candid" import { ActorManagerOptions, BaseActor } from "@ic-reactor/core/dist/types" -import { ActorHooks, AgentContextType } from "../../types" +import { ActorHooks, AgentContext } from "../../types" -export interface ActorContextType extends ActorHooks { - ActorContext: React.Context | null> - useActorContext: () => ActorContextType +export interface CreateActorContextReturn extends ActorHooks { ActorProvider: React.FC } -export type CreateReactorContext = ( - options?: Partial -) => ActorContextType - -export interface CreateActorOptions +export interface CreateActorContextOptions extends Omit< ActorManagerOptions, "idlFactory" | "agentManager" | "canisterId" > { didjsId?: string canisterId?: string - agentContext?: AgentContextType + agentContext?: React.Context idlFactory?: IDL.InterfaceFactory loadingComponent?: React.ReactNode } -export interface ActorProviderProps extends CreateActorOptions { +export interface ActorProviderProps extends CreateActorContextOptions { children?: React.ReactNode | undefined loadingComponent?: React.ReactNode } diff --git a/packages/react/src/provider/agent/context.tsx b/packages/react/src/provider/agent/context.tsx index 87a760552d..d570752984 100644 --- a/packages/react/src/provider/agent/context.tsx +++ b/packages/react/src/provider/agent/context.tsx @@ -3,29 +3,40 @@ import { createAgentManager } from "@ic-reactor/core" import { getAgentHooks } from "../../helpers/agent" import { getAuthHooks } from "../../helpers/auth" import type { AgentManagerOptions } from "@ic-reactor/core/dist/types" -import type { AgentContextValue, AgentProviderProps } from "./types" +import type { + CreateAgentContextReturn, + AgentProviderProps, + AgentContext, +} from "./types" +import { extractAgentHooks } from "./hooks" -export const AgentContext = createContext(null) - -const AgentProvider: React.FC = ({ - children, - ...config -}) => { - const value = useMemo(() => createAgentContext(config), [config]) +export const createAgentContext = ( + agentConfig: Partial = {} +): CreateAgentContextReturn => { + const AgentContext = createContext(null) - return {children} -} + const AgentProvider: React.FC = ({ + children, + agentManager: mybeAgentManager, + ...config + }) => { + const hooks = useMemo(() => { + const agentManager = + mybeAgentManager ?? createAgentManager({ ...config, ...agentConfig }) -AgentProvider.displayName = "AgentProvider" + return { + ...getAgentHooks(agentManager), + ...getAuthHooks(agentManager), + agentManager, + } + }, [config]) -export { AgentProvider } + return ( + {children} + ) + } -export const createAgentContext = ( - config: AgentManagerOptions -): AgentContextValue => { - const agentManager = createAgentManager(config) - const agenthooks = getAgentHooks(agentManager) - const authHooks = getAuthHooks(agentManager) + AgentProvider.displayName = "AgentProvider" - return { ...agenthooks, ...authHooks, agentManager } + return { AgentProvider, ...extractAgentHooks(AgentContext) } } diff --git a/packages/react/src/provider/agent/hooks.ts b/packages/react/src/provider/agent/hooks.ts index 9c94999b7a..99e04550bc 100644 --- a/packages/react/src/provider/agent/hooks.ts +++ b/packages/react/src/provider/agent/hooks.ts @@ -1,44 +1,151 @@ import { useContext } from "react" import { AgentManager } from "@ic-reactor/core/dist/agent" -import { AgentContext } from "./context" -import type { AgentContextType } from "./types" +import type { AgentContext, CreateAgentContextReturn } from "./types" import { UseAuthClientArgs } from "../../types" -export const useAgentContext = (agentContext?: AgentContextType) => { - const context = useContext(agentContext || AgentContext) +/** + * This function facilitates the use of contextually provided agent functionalities, + * such as managing the agent's state, authentication state, and user principal. + * + * @param agentContext A React context object of type AgentContext or null, + * typically provided by an AgentProvider at a higher level in the component tree. + * @returns An object containing the following hooks: + * - useAgent: Hook for accessing the current agent instance. + * - useAuthState: Hook for accessing the current authentication state. + * - useAgentState: Hook for accessing the current state of the agent. + * - useAuthClient: Hook for accessing the authentication client, optionally accepting arguments for configuration. + * - useAgentManager: Hook for accessing the AgentManager instance. + * - useUserPrincipal: Hook for accessing the user's principal. + * + * Each hook is designed to be used within components that are descendants of an AgentProvider, + * ensuring access to the necessary agent-related context. + * + * Throws: + * - Error if used outside of an AgentProvider context. + * + * ### Integration + * + * To use these hooks, ensure your components are wrapped in an `AgentProvider` that you've set up to supply the `AgentContext`. + * This context provides the necessary agent functionalities and state management capabilities required by the hooks. + */ +export const extractAgentHooks = ( + agentContext: React.Context +): Omit => { + const useAgentContext = ( + mybeAgentContext?: React.Context + ) => { + const context = useContext(mybeAgentContext || agentContext) - if (!context) { - throw new Error("Agent context must be used within a AgentProvider") + if (!context) { + throw new Error("Agent context must be used within a AgentProvider") + } + + return context } - return context -} + /** + * Accesses the `AgentManager` instance for managing agent configurations and state. + * + * @example + *```jsx + * function AgentManagerComponent() { + * const agentManager = useAgentManager(); + * + * // Use agentManager for managing agent configurations, etc. + * return
Agent Manager ready.
; + * } + *``` + */ + const useAgentManager = ( + agentContext?: React.Context + ): AgentManager => { + const context = useAgentContext(agentContext) -export const useAgentManager = ( - agentContext?: AgentContextType -): AgentManager => { - const context = useAgentContext(agentContext) + return context.agentManager + } - return context.agentManager -} + /** + * Accesses the current agent instance. + * + * @example + *```jsx + * function AgentComponent() { + * const agent = useAgent(); + * + * // Use agent for interacting with the Internet Computer. + * return
Agent ready.
; + * } + *``` + */ + const useAgent = () => useAgentContext().useAgent() -export const useAgent = (agentContext?: AgentContextType) => - useAgentContext(agentContext).useAgent() + /** + * Accesses the current authentication state. + * + * @example + * ```jsx + * function AuthStateComponent() { + * const { isAuthenticated, user } = useAuthState(); + * + * return ( + *
+ * {isAuthenticated ? `User ${user} is authenticated.` : 'User is not authenticated.'} + *
+ * ); + * } + * ``` + */ + const useAuthState = () => useAgentContext().useAuthState() -export const useAuthState = (agentContext?: AgentContextType) => - useAgentContext(agentContext).useAuthState() + /** + * Accesses the current state of the agent. + * + * @example + * ```jsx + * function AgentStateComponent() { + * const { initialized, initializing } = useAgentState(); + * + * return ( + *
+ * {initialized + * ? 'Agent is initialized.' + * : initializing + * ? 'Agent is initializing...' + * : 'Agent is not initialized.'} + *
+ * ); + * } + * ``` + */ + const useAgentState = () => useAgentContext().useAgentState() -export const useAgentState = (agentContext?: AgentContextType) => - useAgentContext(agentContext).useAgentState() + const useAuthClient = (args?: UseAuthClientArgs) => + useAgentContext().useAuthClient(args) -export const useAuthClient = ({ - agentContext, - ...args -}: UseAuthClientArgs & { agentContext?: AgentContextType }) => { - const context = useAgentContext(agentContext) + /** + * Accesses the user's principal. + * + * @example + * ```jsx + * function UserPrincipalComponent() { + * const userPrincipal = useUserPrincipal(); + * + * return ( + *
+ * {userPrincipal ? `User principal: ${userPrincipal}` : 'User principal not found.'} + *
+ * ); + * } + * ``` + */ + const useUserPrincipal = () => useAgentContext().useUserPrincipal() - return context.useAuthClient(args) + return { + useAgent, + useAuthState, + useAgentState, + useAuthClient, + useAgentManager, + useUserPrincipal, + } } - -export const useUserPrincipal = (agentContext?: AgentContextType) => - useAgentContext(agentContext).useUserPrincipal() diff --git a/packages/react/src/provider/agent/index.ts b/packages/react/src/provider/agent/index.ts index 52356ce0d6..03184225d2 100644 --- a/packages/react/src/provider/agent/index.ts +++ b/packages/react/src/provider/agent/index.ts @@ -1,2 +1,14 @@ +import { createAgentContext } from "./context" + export * from "./context" export * from "./hooks" + +export const { + AgentProvider, + useAgent, + useAuthClient, + useAuthState, + useAgentState, + useAgentManager, + useUserPrincipal, +} = createAgentContext() diff --git a/packages/react/src/provider/agent/types.ts b/packages/react/src/provider/agent/types.ts index 213a5ba4c1..72c585b458 100644 --- a/packages/react/src/provider/agent/types.ts +++ b/packages/react/src/provider/agent/types.ts @@ -1,15 +1,18 @@ -import React, { PropsWithChildren } from "react" +import { PropsWithChildren } from "react" import type { AgentManagerOptions } from "@ic-reactor/core/dist/types" import type { AgentManager } from "@ic-reactor/core/dist/agent" -import type { getAuthHooks } from "../../helpers/auth" -import type { getAgentHooks } from "../../helpers/agent" +import { AgentHooks, AuthHooks } from "../../types" -export type AgentContextValue = ReturnType & - ReturnType & { - agentManager: AgentManager - } +export interface AgentContext extends AgentHooks, AuthHooks { + agentManager: AgentManager +} -export type AgentContextType = React.Context +export interface CreateAgentContextReturn extends AgentHooks, AuthHooks { + useAgentManager: ( + agentContext?: React.Context + ) => AgentManager + AgentProvider: React.FC +} export interface AgentProviderProps extends PropsWithChildren,