Skip to content

Commit

Permalink
feat: SessionClient#logout method. React useAccount, useAccountsAvail…
Browse files Browse the repository at this point in the history
…able and, useLogout hooks
  • Loading branch information
cesarenaldi committed Jan 9, 2025
1 parent 82cb52b commit 3b801c7
Show file tree
Hide file tree
Showing 17 changed files with 268 additions and 76 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"build": "turbo build",
"dev": "turbo watch build",
"clean": "rimraf packages/*/dist",
"clean": "rimraf .turbo packages/*/dist",
"lint": "biome check",
"lint:fix": "biome check --write",
"new:package": "NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
Expand Down
10 changes: 5 additions & 5 deletions packages/client/src/AuthenticatedUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const SPONSORED_CLAIM = 'tag:lens.dev,2024:sponsored';
export type AuthenticatedUser = {
address: EvmAddress;
app: EvmAddress;
authentication_id: UUID;
authenticationId: UUID;
role: Role;
signer: EvmAddress;
sponsored: boolean;
Expand All @@ -26,7 +26,7 @@ export function authenticatedUser(
return ok({
address: claims.act?.sub ?? never('Account Manager must have an Actor Claim'),
app: claims.aud,
authentication_id: claims.sid,
authenticationId: claims.sid,
role: Role.AccountManager,
signer: claims.sub,
sponsored: claims[SPONSORED_CLAIM],
Expand All @@ -36,7 +36,7 @@ export function authenticatedUser(
return ok({
address: claims.act?.sub ?? never('Account Owner must have an Actor Claim'),
app: claims.aud,
authentication_id: claims.sid,
authenticationId: claims.sid,
role: Role.AccountOwner,
signer: claims.sub,
sponsored: claims[SPONSORED_CLAIM],
Expand All @@ -46,7 +46,7 @@ export function authenticatedUser(
return ok({
address: claims.sub,
app: claims.aud,
authentication_id: claims.sid,
authenticationId: claims.sid,
role: Role.OnboardingUser,
signer: claims.sub,
sponsored: claims[SPONSORED_CLAIM],
Expand All @@ -56,7 +56,7 @@ export function authenticatedUser(
return ok({
address: claims.sub,
app: claims.aud,
authentication_id: claims.sid,
authenticationId: claims.sid,
role: Role.Builder,
signer: claims.sub,
sponsored: claims[SPONSORED_CLAIM],
Expand Down
21 changes: 20 additions & 1 deletion packages/client/src/clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe(`Given an instance of the ${PublicClient.name}`, () => {
});
});

describe('When authenticating via the `login` convenience method', () => {
describe(`When authenticating via the '${PublicClient.prototype.login.name}' convenience method`, () => {
it('Then it should return an Err<never, SigningError> with any error thrown by the provided `SignMessage` function', async () => {
const authenticated = await client.login({
accountOwner: {
Expand Down Expand Up @@ -108,6 +108,25 @@ describe(`Given an instance of the ${PublicClient.name}`, () => {
});

describe('And a SessionClient created from it', () => {
describe(`When invoking the 'logout' method`, () => {
it('Then it should revoke the current authenticated session and clear the credentials from the storage', async () => {
const authenticated = await client.login({
accountOwner: {
account,
owner: signer,
app,
},
signMessage: signMessageWith(wallet),
});
assertOk(authenticated);

const result = await authenticated.value.logout();
assertOk(result);
assertErr(await currentSession(authenticated.value));
assertErr(await authenticated.value.getAuthenticatedUser());
});
});

describe('When a request fails with UNAUTHENTICATED extension code', () => {
const server = setupServer(
graphql.query(
Expand Down
13 changes: 12 additions & 1 deletion packages/client/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { type Logger, getLogger } from 'loglevel';
import type { SwitchAccountRequest } from '@lens-protocol/graphql';
import { type AuthConfig, authExchange } from '@urql/exchange-auth';
import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser';
import { switchAccount, transactionStatus } from './actions';
import { revokeAuthentication, switchAccount, transactionStatus } from './actions';
import type { ClientConfig } from './config';
import { type Context, configureContext } from './context';
import {
Expand Down Expand Up @@ -331,6 +331,17 @@ class SessionClient<TContext extends Context = Context> extends AbstractClient<
});
}

/**
* Log out the current session.
*/
logout(): ResultAsync<void, UnauthenticatedError | UnexpectedError> {
return this.getAuthenticatedUser()
.andThen(({ authenticationId }) => revokeAuthentication(this, { authenticationId }))
.andTee(() =>
ResultAsync.fromPromise(this.credentials.reset(), (err) => UnexpectedError.from(err)),
);
}

/**
* {@inheritDoc AbstractClient.isPublicClient}
*/
Expand Down
8 changes: 4 additions & 4 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
"require": "./dist/index.cjs"
},
"./ethers": {
"types": "./dist/ethers.d.cts",
"import": "./dist/ethers.js",
"require": "./dist/ethers.cjs"
"require": "./dist/ethers.cjs",
"types": "./dist/ethers.d.cts"
},
"./viem": {
"types": "./dist/viem.d.cts",
"import": "./dist/viem.js",
"require": "./dist/viem.cjs"
"require": "./dist/viem.cjs",
"types": "./dist/viem.d.cts"
}
},
"typesVersions": {
Expand Down
7 changes: 1 addition & 6 deletions packages/react/src/LensProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PublicClient } from '@lens-protocol/client';
import React from 'react';
import type { ReactNode } from 'react';
import { Provider as UrqlProvider } from 'urql';

import { LensContextProvider } from './context';

Expand Down Expand Up @@ -39,9 +38,5 @@ export type LensProviderProps = {
* ```
*/
export function LensProvider({ children, client }: LensProviderProps) {
return (
<UrqlProvider value={client.urql}>
<LensContextProvider client={client}>{children}</LensContextProvider>
</UrqlProvider>
);
return <LensContextProvider client={client}>{children}</LensContextProvider>;
}
1 change: 1 addition & 0 deletions packages/react/src/account/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useAccount';
41 changes: 41 additions & 0 deletions packages/react/src/account/useAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type Account, AccountQuery, type AccountRequest } from '@lens-protocol/graphql';
import {
type ReadResult,
type Suspendable,
type SuspendableResult,
type SuspenseResult,
useSuspendableQuery,
} from '../helpers';

export type UseAccountArgs = AccountRequest;

/**
* Fetch a single Account.
*
* This signature supports React Suspense:
*
* ```tsx
* const { data } = useAccount({ managedBy: evmAddress('0x…'), suspense: true });
* ```
*/
export function useAccount(args: UseAccountArgs & Suspendable): SuspenseResult<Account | null>;

/**
* Fetch a single Account.
*
* ```tsx
* const { data } = useAccount({ managedBy: evmAddress('0x…') });
* ```
*/
export function useAccount(args: UseAccountArgs): ReadResult<Account | null>;

export function useAccount({
suspense = false,
...request
}: UseAccountArgs & { suspense?: boolean }): SuspendableResult<Account | null> {
return useSuspendableQuery({
document: AccountQuery,
variables: { request },
suspense,
});
}
1 change: 1 addition & 0 deletions packages/react/src/authentication/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './useAccountsAvailable';
export * from './useAuthenticatedUser';
export * from './useLogin';
export * from './useLogout';
export * from './useSessionClient';
50 changes: 45 additions & 5 deletions packages/react/src/authentication/useAccountsAvailable.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import { AccountsAvailableQuery, type AccountsAvailableRequest } from '@lens-protocol/graphql';
import { useQuery } from 'urql';
import {
type AccountAvailable,
AccountsAvailableQuery,
type AccountsAvailableRequest,
type Paginated,
} from '@lens-protocol/graphql';
import {
type ReadResult,
type Suspendable,
type SuspendableResult,
type SuspenseResult,
useSuspendableQuery,
} from '../helpers';

export type UseAccountsAvailableArgs = AccountsAvailableRequest;

/**
* Fetch the accounts available for a given address.
*
* This signature supports React Suspense:
*
* ```tsx
* const { data } = useAccountsAvailable({ managedBy: evmAddress('0x…'), suspense: true });
* ```
*/
export function useAccountsAvailable(
args: UseAccountsAvailableArgs & Suspendable,
): SuspenseResult<Paginated<AccountAvailable>>;

/**
* Fetch the accounts available for a given address.
*
* ```tsx
* const { data } = useAccountsAvailable({ managedBy: evmAddress('0x…') });
* ```
*/
export function useAccountsAvailable(request: AccountsAvailableRequest) {
return useQuery({
query: AccountsAvailableQuery,
export function useAccountsAvailable(
args: UseAccountsAvailableArgs,
): ReadResult<Paginated<AccountAvailable>>;

export function useAccountsAvailable({
suspense = false,
...request
}: UseAccountsAvailableArgs & { suspense?: boolean }): SuspendableResult<
Paginated<AccountAvailable>
> {
return useSuspendableQuery({
document: AccountsAvailableQuery,
variables: { request },
suspense: suspense,
});
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type SessionClient, never } from '@lens-protocol/client';
import type { SessionClient } from '@lens-protocol/client';
import type { AuthenticatedUser } from '@lens-protocol/client';
import { account, app, createPublicClient, signer, wallet } from '@lens-protocol/client/test-utils';
import { signMessageWith } from '@lens-protocol/client/viem';
import { beforeAll, describe, expect, it, vi } from 'vitest';

import { renderHookWithContext } from '../test-utils';
import { useAuthenticatedUser } from './useAuthenticatedUser';

Expand Down
27 changes: 27 additions & 0 deletions packages/react/src/authentication/useLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { UnauthenticatedError, UnexpectedError } from '@lens-protocol/client';
import { invariant } from '@lens-protocol/types';

import { type UseAsyncTask, useAsyncTask } from '../helpers';
import { useSessionClient } from './useSessionClient';

export type LogoutError = UnauthenticatedError | UnexpectedError;

/**
* Log out of Lens.
*
* ```tsx
* const { execute, error } = useLogout();
* ```
*/
export function useLogout(): UseAsyncTask<void, void, LogoutError> {
const { data: sessionClient } = useSessionClient();

return useAsyncTask(() => {
invariant(
sessionClient,
'It appears that you are not logged in. Please log in before attempting to log out.',
);

return sessionClient.logout();
});
}
10 changes: 8 additions & 2 deletions packages/react/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { PublicClient, SessionClient } from '@lens-protocol/client';
import type { AuthenticatedUser } from '@lens-protocol/client';
import { invariant } from '@lens-protocol/types';
import React, { type ReactNode, useContext, useEffect, useState } from 'react';
import { Provider as UrqlProvider } from 'urql';

import { ReadResult, type SuspenseResult } from './helpers';

/**
Expand Down Expand Up @@ -94,7 +96,11 @@ type LensContextProviderProps = {
export function LensContextProvider({ children, client }: LensContextProviderProps) {
const value = useLensContextValue(client);

return <LensContext.Provider value={value}>{children}</LensContext.Provider>;
return (
<LensContext.Provider value={value}>
<UrqlProvider value={value.session.client.urql}>{children}</UrqlProvider>
</LensContext.Provider>
);
}

/**
Expand All @@ -105,7 +111,7 @@ export function useLensContext(): LensContextValue {

invariant(
context,
'Could not find Lens SDK context, ensure your code is wrapped in a Lens <Provider>',
'Could not find Lens SDK context, ensure your code is wrapped in a <LensProvider>',
);

return context;
Expand Down
52 changes: 2 additions & 50 deletions packages/react/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,3 @@
export * from './reads';
export * from './results';
export * from './tasks';

/**
* A read hook result.
*
* It's a discriminated union of the possible results of a read operation:
* - Rely on the `loading` value to determine if the `data` or `error` can be evaluated.
* - If `error` is `undefined`, then `data` value will be available.
*/
export type ReadResult<T, E = never> =
| {
data: undefined;
error: undefined;
loading: true;
}
| {
data: T;
error: undefined;
loading: false;
}
| {
data: undefined;
error: E;
loading: false;
};

/**
* @internal
*/
export const ReadResult = {
Initial: <T, E = never>(): ReadResult<T, E> => ({
data: undefined,
error: undefined,
loading: true,
}),
Success: <T, E = never>(data: T): ReadResult<T, E> => ({
data,
error: undefined,
loading: false,
}),
Failure: <T, E = never>(error: E): ReadResult<T, E> => ({
data: undefined,
error,
loading: false,
}),
};

/**
* A read hook result that supports React Suspense
*/
export type SuspenseResult<T> = { data: T };
Loading

0 comments on commit 3b801c7

Please sign in to comment.