Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multi-actor config #916

Merged
merged 4 commits into from
Sep 10, 2024
Merged
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- feat: allow creation of multiple Actors in `useAuthClient` by passing a record to `actorOptions` with the actor name as the key, and `CreateActorOptions` as the value
- feat: sync_call support in HttpAgent and Actor
- Skips polling if the sync call succeeds and provides a certificate
- Falls back to v2 api if the v3 endpoint 404's
Expand Down
22 changes: 22 additions & 0 deletions packages/use-auth-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ const App = () => {
}
```

## Multiple Actors

If you have multiple actors, you can pass a record of `actorOptions` to the `useAuthClient` hook. The keys of the record will be the names of the actors, and the values will be the `actorOptions` for that actor. It will look something like this:

```ts
const { isAuthenticated, login, logout, actors } = useAuthClient({
actors: {
actor1: {
canisterId: canisterId1,
idlFactory: idlFactory1,
},
actor2: {
canisterId: canisterId2,
idlFactory: idlFactory2,
},
},
});

const { actor1, actor2 } = actors;

```

There is a live demo at https://5ibdo-haaaa-aaaab-qajia-cai.icp0.io/

Additional generated documentaion is available at https://agent-js.icp.xyz/use-auth-client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
service : {
whoami: () -> (principal) query;
}
8 changes: 8 additions & 0 deletions packages/use-auth-client/examples/auth-demo/deps/init.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"canisters": {
"ivcos-eqaaa-aaaab-qablq-cai": {
"arg_str": null,
"arg_raw": null
}
}
}
13 changes: 13 additions & 0 deletions packages/use-auth-client/examples/auth-demo/deps/pulled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"canisters": {
"ivcos-eqaaa-aaaab-qablq-cai": {
"name": "whoami",
"wasm_hash": "a5af74d01aec228c5a717dfb43f773917e1a9138e512431aafcd225ad0001a8b",
"wasm_hash_download": "88f0162fa446ad32e08e7ee513a85a82f145d16a9ebb6d2adfa9bc5ff5605f74",
"init_guide": "null",
"init_arg": null,
"candid_args": "()",
"gzip": false
}
}
}
6 changes: 5 additions & 1 deletion packages/use-auth-client/examples/auth-demo/dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"type": "assets",
"workspace": "auth-demo-frontend"
},
"whoami": {
"type": "pull",
"id": "ivcos-eqaaa-aaaab-qablq-cai"
},
"internet_identity": {
"candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did",
"frontend": {},
Expand All @@ -34,4 +38,4 @@
},
"output_env_file": ".env",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { useState } from 'react';
import { idlFactory, canisterId } from 'declarations/auth-demo-backend';
import { FormEvent, useState } from 'react';
import {
idlFactory as helloFactory,
canisterId as helloId,
} from '../../declarations/auth-demo-backend';
import { _SERVICE as HELLO_SERVICE } from '../../declarations/auth-demo-backend/auth-demo-backend.did';
import {
idlFactory as whoamiFactory,
canisterId as whoamiId,
} from '../../declarations/auth-demo-backend';
import { _SERVICE as WHOAMI_SERVICE } from '../../declarations/auth-demo-backend/auth-demo-backend.did';
import { useAuthClient } from '../../../../../src/index';
import type { ActorSubclass } from '@dfinity/agent';
import IILogo from './IILogo.svg';

export interface ProcessEnv {
[key: string]: string | undefined;
}

declare var process: {
env: ProcessEnv;
};

/**
*
*
* @returns app
*/
function App() {
Expand All @@ -15,23 +33,34 @@ function App() {
`http://${process.env.CANISTER_ID_INTERNET_IDENTITY}.localhost:4943`
: 'https://identity.ic0.app';

const { isAuthenticated, login, logout, actor } = useAuthClient({
const { isAuthenticated, login, logout, actors } = useAuthClient({
loginOptions: {
identityProvider,
},
actorOptions: {
canisterId,
idlFactory,
whoami_canister: {
canisterId: whoamiId,
idlFactory: whoamiFactory,
},
greet_canister: {
canisterId: helloId,
idlFactory: helloFactory,
},
},
});

const { whoami_canister, greet_canister } = actors as unknown as {
whoami_canister: ActorSubclass<WHOAMI_SERVICE>;
greet_canister: ActorSubclass<HELLO_SERVICE>;
};

const [greeting, setGreeting] = useState('');
const [whoamiText, setWhoamiText] = useState('');

function handleSubmit(event) {
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const name = event.target.elements.name.value;
actor.greet(name).then(greeting => {
const name = (event.target as HTMLFormElement).querySelector('input')?.value || '';
greet_canister.greet(name).then(greeting => {
setGreeting(greeting);
});
return false;
Expand Down Expand Up @@ -61,8 +90,8 @@ function App() {
<p>{isAuthenticated ? 'You are logged in' : 'You are not logged in'}</p>
<button
onClick={async () => {
const whoami = await actor.whoami();
setWhoamiText(whoami);
const whoami = await whoami_canister.whoami();
setWhoamiText(whoami.toString());
}}
>
Whoami
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vite/client"]
"types": ["vite/client", "./src/vite-env.d.ts"]
},
"include": ["src"]
}
63 changes: 54 additions & 9 deletions packages/use-auth-client/src/use-auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ActorConfig,
HttpAgent,
Actor,
ActorSubclass,
} from '@dfinity/agent';
import type { IDL } from '@dfinity/candid';
import { Principal } from '@dfinity/principal';
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface CreateActorOptions {
* Options for the useAuthClient hook
*/
export type UseAuthClientOptions = {
createSync?: boolean;
/**
* Options passed during the creation of the auth client
*/
Expand All @@ -50,7 +52,7 @@ export type UseAuthClientOptions = {
/**
* Options to create an actor using the auth client identity
*/
actorOptions?: CreateActorOptions;
actorOptions?: CreateActorOptions | Record<string, CreateActorOptions>;
};

/**
Expand All @@ -63,7 +65,8 @@ export type UseAuthClientOptions = {
export function useAuthClient(options?: UseAuthClientOptions) {
const [authClient, setAuthClient] = React.useState<AuthClient | null>(null);
const [identity, setIdentity] = React.useState<Identity | null>(null);
const [actor, setActor] = React.useState<Actor | null>(null);
const [actor, setActor] = React.useState<ActorSubclass | null>();
const [actors, setActors] = React.useState<Record<string, ActorSubclass>>({});
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(false);

// load the auth client on mount
Expand All @@ -87,12 +90,33 @@ export function useAuthClient(options?: UseAuthClientOptions) {

React.useEffect(() => {
if (identity && options?.actorOptions) {
createActor({
...options.actorOptions,
agentOptions: { ...options?.actorOptions?.agentOptions, identity },
}).then(actor => {
setActor(actor);
});
// if the options is for a single actor, it will have a canisterId
if ('canisterId' in options.actorOptions) {
const createActorOptions = options.actorOptions as CreateActorOptions;
createActor({
...createActorOptions,
agentOptions: { ...createActorOptions?.agentOptions, identity },
}).then(actor => {
// set the actor service

setActor(actor);
});
} else {
// if the options is for multiple actors, it will have a key value pair of an identifier and CreateActorOptions
const actorOptions = options.actorOptions as Record<string, CreateActorOptions>;
const actorPromises = Object.entries(actorOptions).map(
async ([canisterId, createActorOptions]) => {
const actor = await createActor({
...createActorOptions,
agentOptions: { ...createActorOptions?.agentOptions, identity },
});
return [canisterId, actor];
},
);
Promise.all(actorPromises).then(actors => {
setActors(Object.fromEntries(actors));
});
}
}
}, [identity]);

Expand Down Expand Up @@ -132,12 +156,32 @@ export function useAuthClient(options?: UseAuthClientOptions) {
setIsAuthenticated(false);
setIdentity(null);
await authClient.logout();
setActor(await createActor(options?.actorOptions));
if (options?.actorOptions) {
if ('canisterId' in options.actorOptions) {
setActor(await createActor(options.actorOptions as CreateActorOptions));
} else {
const actorOptions = options.actorOptions as Record<string, CreateActorOptions>;
const actorPromises = Object.entries(actorOptions).map(
async ([canisterId, createActorOptions]) => {
// Initialize with anonymous identity
const actor = await createActor(createActorOptions);
return [canisterId, actor];
},
);
Promise.all(actorPromises).then(actors => {
setActors(Object.fromEntries(actors));
});
}
} else {
setActor(null);
setActors({});
}
}
}

return {
actor,
actors,
authClient,
identity,
isAuthenticated,
Expand All @@ -147,6 +191,7 @@ export function useAuthClient(options?: UseAuthClientOptions) {
}

const createActor = async (options: CreateActorOptions) => {
options;
const agent = options.agent || (await HttpAgent.create({ ...options.agentOptions }));

if (options.agent && options.agentOptions) {
Expand Down
59 changes: 56 additions & 3 deletions packages/use-auth-client/test/use-auth-client.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, cleanup } from '@testing-library/react';
import { useAuthClient } from '../src/use-auth-client';
import { describe, it } from '@jest/globals';
import { describe, it, jest, afterEach } from '@jest/globals';

afterEach(cleanup);

describe('useAuthClient', () => {
it('should return an authClient object with the expected properties', async () => {
Expand All @@ -18,7 +20,6 @@ describe('useAuthClient', () => {
</div>
);
};
// disable typescript
render(<Component />);

(
Expand All @@ -27,4 +28,56 @@ describe('useAuthClient', () => {
}
).toHaveTextContent('false');
});
it('should support configs for multiple actors', async () => {
interface Props {
children?: React.ReactNode;
}

const idlFactory = ({ IDL }) => {
return IDL.Service({ whoami: IDL.Func([], [IDL.Principal], ['query']) });
};
const Component: React.FC<Props> = () => {
const { actors } = useAuthClient({
actorOptions: {
actor1: {
agentOptions: {
fetch: jest.fn() as typeof globalThis.fetch,
},
canisterId: 'rrkah-fqaaa-aaaaa-aaaaq-cai',
idlFactory,
},
actor2: {
agentOptions: {
fetch: jest.fn() as typeof globalThis.fetch,
},
canisterId: 'rrkah-fqaaa-aaaaa-aaaaq-cai',
idlFactory,
},
},
});
return (
<div>
<div data-testid="actors">{JSON.stringify(actors)}</div>
</div>
);
};
render(<Component />);

(
expect(screen.getByTestId('actors')) as unknown as {
toHaveTextContent: (str: string) => boolean;
}
).toHaveTextContent('{}');

jest.useRealTimers();

// wait for the actors to be set
await new Promise(resolve => setTimeout(resolve, 1000));

(
expect(screen.getByTestId('actors')) as unknown as {
toHaveTextContent: (str: string) => boolean;
}
).toHaveTextContent('{"actor1":{},"actor2":{}}');
});
});
Loading