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: SessionClient#switchAccount to support onboarding flow #1024

Merged
merged 12 commits into from
Dec 17, 2024
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_modules
# build output
**/dist
/docs
*/graphql-cache.d.ts

# misc
.DS_Store
Expand Down
10 changes: 10 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
"enabled": false
}
},
{
"include": ["./packages/**/*.test.ts"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
}
}
}
},
{
"include": ["./packages/graphql/*.graphql"],
"formatter": {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"lint": "biome check",
"lint:fix": "biome check --write",
"new:lib": "NODE_OPTIONS='--import tsx' plop --plopfile=plopfile.ts",
"postinstall": "pnpm run build",
"prepublish": "pnpm run build",
"test:client": "vitest --project @lens-protocol/client",
"test:react": "vitest --project @lens-protocol/react",
Expand Down
4 changes: 3 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@
},
"devDependencies": {
"@lens-network/sdk": "canary",
"@lens-protocol/metadata": "next",
"tsup": "^8.3.5",
"typescript": "^5.6.3",
"viem": "^2.21.53"
"viem": "^2.21.53",
"zod": "^3.23.8"
},
"license": "MIT"
}
4 changes: 2 additions & 2 deletions packages/client/src/actions/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function legacyRolloverRefresh(
* Switch to account managed.
*
* ```ts
* const result = await switchAccount(sessionClient{
* const result = await switchAccount(sessionClient, {
* account: evmAddress('0x90c8c68d0Abfb40D4fCD72316A65e42161520BC3'),
* });
* ```
Expand All @@ -140,5 +140,5 @@ export function switchAccount(
client: SessionClient,
request: SwitchAccountRequest,
): ResultAsync<SwitchAccountResult, UnauthenticatedError | UnexpectedError> {
return client.query(SwitchAccountMutation, { request });
return client.mutation(SwitchAccountMutation, { request });
}
67 changes: 67 additions & 0 deletions packages/client/src/actions/onboarding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { account } from '@lens-protocol/metadata';
import { assertOk, never } from '@lens-protocol/types';
import { describe, expect, it } from 'vitest';

import { type Account, Role } from '@lens-protocol/graphql';
import { uri } from '@lens-protocol/types';
import { loginAsOnboardingUser, signer, signerWallet } from '../../testing-utils';
import { handleWith } from '../viem';
import { createAccountWithUsername, fetchAccount } from './account';

const walletClient = signerWallet();
const metadata = account({
name: 'John Doe',
bio: 'A test account',
});

describe('Given an onboarding user', () => {
describe('When switching to the newly created account', () => {
it.skip('Then it should be authenticated', { timeout: 60000 }, async () => {
let newAccount: Account | null = null;

// Login as onboarding user
const sessionClient = await loginAsOnboardingUser()
.andThen((sessionClient) =>
// Create an account with username
createAccountWithUsername(sessionClient, {
username: { localName: `testname${Date.now()}` },
metadataUri: uri(`data:application/json,${JSON.stringify(metadata)}`),
})
// Sign if necessary
.andThen(handleWith(walletClient))

// Wait for the transaction to be mined
.andThen(sessionClient.waitForTransaction)

// Fetch the account
.andThen((txHash) => fetchAccount(sessionClient, { txHash }))

.andTee((account) => {
newAccount = account ?? never('Account not found');
})

// Switch to the newly created account
.andThen((account) =>
sessionClient.switchAccount({
account: account?.address ?? never('Account not found'),
}),
),
)
.match(
(value) => value,
(error) => {
throw error;
},
);

const user = await sessionClient.getAuthenticatedUser();
assertOk(user);

expect(user.value).toMatchObject({
role: Role.AccountOwner,
account: newAccount!.address.toLowerCase(),
owner: signer.toLowerCase(),
});
});
});
});
2 changes: 2 additions & 0 deletions packages/client/src/clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ describe(`Given an instance of the ${PublicClient.name}`, () => {
environment: {
backend: url('http://127.0.0.1'),
name: 'broken',
indexingTimeout: 1000,
pollingInterval: 1000,
},
origin: 'http://example.com',
});
Expand Down
30 changes: 29 additions & 1 deletion packages/client/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import {
} from '@urql/core';
import { type Logger, getLogger } from 'loglevel';

import type { SwitchAccountRequest } from '@lens-protocol/graphql';
import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser';
import { transactionStatus } from './actions';
import { switchAccount, transactionStatus } from './actions';
import type { ClientConfig } from './config';
import { type Context, configureContext } from './context';
import {
Expand Down Expand Up @@ -345,6 +346,33 @@ class SessionClient<TContext extends Context = Context> extends AbstractClient<
return true;
}

/**
* Switch authenticated account to a new account.
*
* You MUST be authenticated as Onboarding User, Account Owner, or Account Manager to be able to switch.
* The signer associated with the current session MUST be the owner or a manager of the account.
*
* @returns The updated SessionClient if the switch was successful.
*/
switchAccount(
request: SwitchAccountRequest,
): ResultAsync<
SessionClient<TContext>,
AuthenticationError | UnauthenticatedError | UnexpectedError
> {
return switchAccount(this, request)
.andThen((result) => {
if (result.__typename === 'ForbiddenError') {
return AuthenticationError.from(result.reason).asResultAsync();
}
return okAsync(result);
})
.map(async (tokens) => {
await this.credentials.set(tokens);
return this;
});
}

/**
* Execute a GraphQL query operation.
*
Expand Down
32 changes: 14 additions & 18 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,20 @@ export type ErrorResponse<T extends string> = {
reason: string;
};

export type DelegableOperationResult<
O extends string,
E extends string,
OR extends OperationResponse<string> = OperationResponse<O>,
ER extends ErrorResponse<string> = ErrorResponse<E>,
> = OR | SponsoredTransactionRequest | SelfFundedTransactionRequest | ER;

export type RestrictedOperationResult<
E extends string,
ER extends ErrorResponse<string> = ErrorResponse<E>,
> = SponsoredTransactionRequest | SelfFundedTransactionRequest | ER;

export type OperationResult<
O extends string,
E extends string,
OR extends OperationResponse<string> = OperationResponse<O>,
ER extends ErrorResponse<string> = ErrorResponse<E>,
> = DelegableOperationResult<O, E, OR, ER> | RestrictedOperationResult<E, ER>;
export type DelegableOperationResult<O extends string, E extends string> =
| OperationResponse<O>
| SponsoredTransactionRequest
| SelfFundedTransactionRequest
| ErrorResponse<E>;

export type RestrictedOperationResult<E extends string> =
| SponsoredTransactionRequest
| SelfFundedTransactionRequest
| ErrorResponse<E>;

export type OperationResult<O extends string, E extends string> =
| DelegableOperationResult<O, E>
| RestrictedOperationResult<E>;

export type RestrictedOperationHandler<E extends string> = (
result: RestrictedOperationResult<E>,
Expand Down
34 changes: 30 additions & 4 deletions packages/client/testing-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { chains } from '@lens-network/sdk/viem';
import { evmAddress } from '@lens-protocol/types';
import { http, type Account, type Transport, type WalletClient, createWalletClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { PublicClient, testnet } from './src';

const signer = privateKeyToAccount(import.meta.env.PRIVATE_KEY);
const owner = evmAddress(signer.address);
const pk = privateKeyToAccount(import.meta.env.PRIVATE_KEY);
const account = evmAddress(import.meta.env.TEST_ACCOUNT);
const app = evmAddress(import.meta.env.TEST_APP);

export const signer = evmAddress(pk.address);

export function loginAsAccountOwner() {
const client = PublicClient.create({
environment: testnet,
Expand All @@ -16,9 +19,32 @@ export function loginAsAccountOwner() {
return client.login({
accountOwner: {
account,
owner,
owner: signer,
app,
},
signMessage: (message) => signer.signMessage({ message }),
signMessage: (message) => pk.signMessage({ message }),
});
}

export function loginAsOnboardingUser() {
const client = PublicClient.create({
environment: testnet,
origin: 'http://example.com',
});

return client.login({
onboardingUser: {
wallet: signer,
app,
},
signMessage: (message) => pk.signMessage({ message }),
});
}

export function signerWallet(): WalletClient<Transport, chains.LensNetworkChain, Account> {
return createWalletClient({
account: privateKeyToAccount(import.meta.env.PRIVATE_KEY),
chain: chains.testnet,
transport: http(),
});
}
2 changes: 1 addition & 1 deletion packages/env/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ export const staging: EnvironmentConfig = {
export const local: EnvironmentConfig = {
name: 'local',
backend: url('http://localhost:3000/graphql'),
indexingTimeout: 5000,
indexingTimeout: 60000,
pollingInterval: 500,
};
1 change: 1 addition & 0 deletions packages/graphql/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/graphql-cache.d.ts
4 changes: 3 additions & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"gql:genereate:cache": "gql.tada generate turbo",
"gql:download:local": "gql-tada generate schema 'http://localhost:3000/graphql' --output './schema.graphql'",
"gql:download:staging": "gql-tada generate schema 'https://api.staging.lens.dev/graphql' --output './schema.graphql'",
"gql:generate": "gql-tada generate output"
"gql:generate": "gql-tada generate output",
"gql:turbo": "gql-tada turbo",
"prebuild": "pnpm run gql:turbo"
},
"dependencies": {
"@lens-protocol/types": "workspace:*",
Expand Down
Loading
Loading