diff --git a/packages/wallet/wdk/src/sequence/index.ts b/packages/wallet/wdk/src/sequence/index.ts index d2afe15a3..b3081a2a9 100644 --- a/packages/wallet/wdk/src/sequence/index.ts +++ b/packages/wallet/wdk/src/sequence/index.ts @@ -15,6 +15,8 @@ export type { MnemonicSignupArgs, EmailOtpSignupArgs, CompleteRedirectArgs, + CompleteRedirectWithMetadataArgs, + CompleteRedirectMetadataResult, SignupArgs, AddLoginSignerArgs, RemoveLoginSignerArgs, diff --git a/packages/wallet/wdk/src/sequence/wallets.ts b/packages/wallet/wdk/src/sequence/wallets.ts index 5746f7a83..2d3af321b 100644 --- a/packages/wallet/wdk/src/sequence/wallets.ts +++ b/packages/wallet/wdk/src/sequence/wallets.ts @@ -101,6 +101,23 @@ export type IdTokenSignupArgs = CommonSignupArgs & { export type CompleteRedirectArgs = CommonSignupArgs & { state: string code: string + includeMetadata?: false +} + +export type CompleteRedirectWithMetadataArgs = Omit & { + includeMetadata: true +} + +export type CompleteRedirectMetadataResult = { + target: string + addedLoginSigner?: { + wallet: Address.Address + signer: { + address: Address.Address + kind: string + email?: string + } + } } export type AuthCodeSignupArgs = CommonSignupArgs & { @@ -278,6 +295,7 @@ export interface WalletsInterface { * @param args The arguments containing the `state` and `code` from the redirect, along with original sign-up options. * @returns A promise that resolves to target path that should be redirected to. */ + completeRedirect(args: CompleteRedirectWithMetadataArgs): Promise completeRedirect(args: CompleteRedirectArgs): Promise /** @@ -923,12 +941,18 @@ export class Wallets implements WalletsInterface { return handler.commitAuth(args.target, { type: 'add-signer', wallet: args.wallet }) } - async completeRedirect(args: CompleteRedirectArgs): Promise { + async completeRedirect(args: CompleteRedirectWithMetadataArgs): Promise + async completeRedirect(args: CompleteRedirectArgs): Promise + async completeRedirect( + args: CompleteRedirectArgs | CompleteRedirectWithMetadataArgs, + ): Promise { const commitment = await this.shared.databases.authCommitments.get(args.state) if (!commitment) { throw new Error('invalid-state') } + let addedLoginSigner: CompleteRedirectMetadataResult['addedLoginSigner'] + switch (commitment.type) { case 'add-signer': { const handlerKind = getSignupHandlerKey(commitment.kind) @@ -949,13 +973,31 @@ export class Wallets implements WalletsInterface { throw new Error('wallet-not-ready') } - const [signer] = await handler.completeAuth(commitment, args.code) + const [signer, metadata] = await handler.completeAuth(commitment, args.code) const signerKind = getSignerKindForSignup(commitment.kind) + const signerAddress = await signer.address await this.addLoginSignerFromPrepared(walletAddress, { signer, extra: { signerKind }, }) + + const addedSigner: { + address: Address.Address + kind: string + email?: string + } = { + address: signerAddress, + kind: signerKind, + } + if (metadata?.email !== undefined) { + addedSigner.email = metadata.email + } + + addedLoginSigner = { + wallet: walletAddress, + signer: addedSigner, + } break } @@ -991,6 +1033,16 @@ export class Wallets implements WalletsInterface { throw new Error('invalid-state') } + if (args.includeMetadata) { + const result: CompleteRedirectMetadataResult = { + target: commitment.target, + } + if (addedLoginSigner) { + result.addedLoginSigner = addedLoginSigner + } + return result + } + return commitment.target } diff --git a/packages/wallet/wdk/test/wallets.test.ts b/packages/wallet/wdk/test/wallets.test.ts index d686ac257..d2d1a6ee8 100644 --- a/packages/wallet/wdk/test/wallets.test.ts +++ b/packages/wallet/wdk/test/wallets.test.ts @@ -163,6 +163,109 @@ describe('Wallets', () => { expect(commitAuthSpy).toHaveBeenCalledWith('/auth/return', { type: 'auth' }) }) + it('Should expose added login signer metadata from redirect when requested', async () => { + manager = newManager({ + identity: { + google: { + enabled: true, + clientId: 'test-google-client-id', + }, + }, + }) + + const wallet = await manager.wallets.signUp({ + mnemonic: Mnemonic.random(Mnemonic.english), + kind: 'mnemonic', + noGuard: true, + }) + expect(wallet).toBeDefined() + + const handler = (manager as any).shared.handlers.get(Kinds.LoginGoogle) as AuthCodePkceHandler + const addedSigner = MnemonicHandler.toSigner(Mnemonic.random(Mnemonic.english)) + if (!addedSigner) { + throw new Error('Failed to create added login signer for test') + } + + const completeAuthSpy = vi + .spyOn(handler, 'completeAuth') + .mockResolvedValue([addedSigner as unknown as IdentitySigner, { email: 'secondary-google-user@example.com' }]) + + const state = 'add-signer-state-with-metadata' + await (manager as any).shared.databases.authCommitments.set({ + id: state, + kind: 'google-pkce', + metadata: {}, + target: '/account/signers', + type: 'add-signer', + wallet: wallet!, + }) + + const result = await manager.wallets.completeRedirect({ + state, + code: 'auth-code', + includeMetadata: true, + }) + + expect(completeAuthSpy).toHaveBeenCalledWith(expect.objectContaining({ id: state }), 'auth-code') + expect(result).toEqual({ + target: '/account/signers', + addedLoginSigner: { + wallet, + signer: { + address: await addedSigner.address, + kind: Kinds.LoginGoogle, + email: 'secondary-google-user@example.com', + }, + }, + }) + }) + + it('Should keep returning the redirect target string when metadata is not requested', async () => { + manager = newManager({ + identity: { + google: { + enabled: true, + clientId: 'test-google-client-id', + }, + }, + }) + + const wallet = await manager.wallets.signUp({ + mnemonic: Mnemonic.random(Mnemonic.english), + kind: 'mnemonic', + noGuard: true, + }) + expect(wallet).toBeDefined() + + const handler = (manager as any).shared.handlers.get(Kinds.LoginGoogle) as AuthCodePkceHandler + const addedSigner = MnemonicHandler.toSigner(Mnemonic.random(Mnemonic.english)) + if (!addedSigner) { + throw new Error('Failed to create added login signer for test') + } + + vi.spyOn(handler, 'completeAuth').mockResolvedValue([ + addedSigner as unknown as IdentitySigner, + { email: 'secondary-google-user@example.com' }, + ]) + + const state = 'add-signer-state-without-metadata' + await (manager as any).shared.databases.authCommitments.set({ + id: state, + kind: 'google-pkce', + metadata: {}, + target: '/account/signers', + type: 'add-signer', + wallet: wallet!, + }) + + const result = await manager.wallets.completeRedirect({ + state, + code: 'auth-code', + }) + + expect(result).toBe('/account/signers') + }) + it('Should reject google-id-token signup when Google is configured for redirect auth', async () => { manager = newManager({ identity: {