Skip to content
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
2 changes: 2 additions & 0 deletions packages/wallet/wdk/src/sequence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type {
MnemonicSignupArgs,
EmailOtpSignupArgs,
CompleteRedirectArgs,
CompleteRedirectWithMetadataArgs,
CompleteRedirectMetadataResult,
SignupArgs,
AddLoginSignerArgs,
RemoveLoginSignerArgs,
Expand Down
56 changes: 54 additions & 2 deletions packages/wallet/wdk/src/sequence/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,23 @@ export type IdTokenSignupArgs = CommonSignupArgs & {
export type CompleteRedirectArgs = CommonSignupArgs & {
state: string
code: string
includeMetadata?: false
}

export type CompleteRedirectWithMetadataArgs = Omit<CompleteRedirectArgs, 'includeMetadata'> & {
includeMetadata: true
}

export type CompleteRedirectMetadataResult = {
target: string
addedLoginSigner?: {
wallet: Address.Address
signer: {
address: Address.Address
kind: string
email?: string
}
}
}

export type AuthCodeSignupArgs = CommonSignupArgs & {
Expand Down Expand Up @@ -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<CompleteRedirectMetadataResult>
completeRedirect(args: CompleteRedirectArgs): Promise<string>

/**
Expand Down Expand Up @@ -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<string> {
async completeRedirect(args: CompleteRedirectWithMetadataArgs): Promise<CompleteRedirectMetadataResult>
async completeRedirect(args: CompleteRedirectArgs): Promise<string>
async completeRedirect(
args: CompleteRedirectArgs | CompleteRedirectWithMetadataArgs,
): Promise<string | CompleteRedirectMetadataResult> {
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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
103 changes: 103 additions & 0 deletions packages/wallet/wdk/test/wallets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading