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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st

setLoading(true);
setErrorMessage(undefined);
const res = await getConnectService().completeAppend(attestationOptions);
const res = await getConnectService().completeAppend(attestationOptions, 'manual');
if (res.err) {
if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) {
return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val);
Expand Down
63 changes: 53 additions & 10 deletions packages/connect-react/src/components/append/AppendInitScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ConnectError } from '@corbado/web-core';
import { ConnectErrorType } from '@corbado/web-core';
import type { AppendCompletionType } from '@corbado/web-core/dist/models/connect/append';
import log from 'loglevel';
import React, { useCallback, useEffect, useRef, useState } from 'react';

Expand Down Expand Up @@ -132,11 +133,23 @@ const AppendInitScreen = () => {
}

setAttestationOptions(startAppendRes.val.attestationOptions);
statefulLoader.current.finish();

log.debug('startAppendRes', startAppendRes, flags);

if (startAppendRes.val.conditionalAppend) {
log.debug('starting conditional create');
const handledByConditionalCreate = await handleConditionalCreate(startAppendRes.val.attestationOptions);
log.debug('handledByConditionalCreate', handledByConditionalCreate);

if (handledByConditionalCreate) {
statefulLoader.current.finish();
return;
}
}

statefulLoader.current.finish();
if (startAppendRes.val.autoAppend || flags.hasSupportForAutomaticAppend()) {
await handleSubmit(startAppendRes.val.attestationOptions, false);
console.log('starting auto-append');
await handleSubmit(startAppendRes.val.attestationOptions, 'auto');
}
};

Expand All @@ -153,25 +166,25 @@ const AppendInitScreen = () => {
}, []);

const handleSubmit = useCallback(
async (attestationOptions: string, showErrorIfCancelled: boolean) => {
async (attestationOptions: string, completionType: AppendCompletionType) => {
if (appendLoading || skipping) {
return;
}

setAppendLoading(true);
setErrorMessage(undefined);

const res = await getConnectService().completeAppend(attestationOptions);
const res = await getConnectService().completeAppend(attestationOptions, completionType);
if (res.err) {
if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) {
return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val);
}

if (res.val.type === ConnectErrorType.Cancel) {
if (showErrorIfCancelled) {
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val);
} else {
if (completionType === 'auto') {
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelledSilent, res.val);
} else {
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val);
}
}

Expand All @@ -184,7 +197,26 @@ const AppendInitScreen = () => {
aaguidIcon: res.val.passkeyOperation.aaguidDetails?.iconLight,
});
},
[config, getConnectService, appendLoading, skipping],
[getConnectService, appendLoading, skipping],
);

const handleConditionalCreate = useCallback(
async (attestationOptions: string) => {
const res = await getConnectService().completeAppend(attestationOptions, 'conditional');
if (res.err) {
await handleSituation(AppendSituationCode.ClientPasskeyOperationErrorSilent, res.val);

return res.val.type === ConnectErrorType.RaceTimeout;
}

navigateToScreen(AppendScreenType.Success, {
aaguidName: res.val.passkeyOperation.aaguidDetails?.name,
aaguidIcon: res.val.passkeyOperation.aaguidDetails?.iconLight,
});

return true;
},
[getConnectService],
);

const handleSituation = async (situationCode: AppendSituationCode, error?: ConnectError) => {
Expand Down Expand Up @@ -222,6 +254,10 @@ const AppendInitScreen = () => {
case AppendSituationCode.ExplicitSkipByUser:
await handleSkip(situationCode, true);
break;
case AppendSituationCode.ClientPasskeyOperationErrorSilent:
void handleErrorSoft(situationCode, false, false, error);
setAppendLoading(false);
break;
}
};

Expand Down Expand Up @@ -250,7 +286,14 @@ const AppendInitScreen = () => {
void onReadMoreClick();
setAppendInitState(AppendInitState.ShowBenefits);
}}
handleSubmit={() => void handleSubmit(attestationOptions, true)}
handleSubmit={() => {
let completionType: AppendCompletionType = 'manual';
if (errorMessage) {
completionType = 'manual-retry';
}

void handleSubmit(attestationOptions, completionType);
}}
handleSkip={() => onSkip()}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const PasskeyListScreen = () => {
return handleSituation(PasskeyListSituationCode.CboApiPasskeysNotSupported);
}

const res = await getConnectService().completeAppend(startAppendRes.val.attestationOptions);
const res = await getConnectService().completeAppend(startAppendRes.val.attestationOptions, 'manual');
if (res.err) {
if (res.val.type === ConnectErrorType.Cancel) {
return handleSituation(PasskeyListSituationCode.ClientPasskeyOperationCancelled, res.val);
Expand Down
1 change: 1 addition & 0 deletions packages/connect-react/src/types/situations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum AppendSituationCode {
DeniedByPasskeyIntel,
ExplicitSkipByUser,
ClientPasskeyOperationCancelledSilent,
ClientPasskeyOperationErrorSilent,
}

export enum PasskeyListSituationCode {
Expand Down
10 changes: 10 additions & 0 deletions packages/web-core/openapi/spec_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,7 @@ components:
- variant
- isRestrictedBrowser
- autoAppend
- conditionalAppend
properties:
attestationOptions:
type: string
Expand All @@ -1499,14 +1500,19 @@ components:
type: boolean
autoAppend:
type: boolean
conditionalAppend:
type: boolean

connectAppendFinishReq:
type: object
required:
- attestationResponse
- completionType
properties:
attestationResponse:
type: string
completionType:
$ref: "#/components/schemas/appendCompletionType"

connectAppendFinishRsp:
type: object
Expand Down Expand Up @@ -2373,6 +2379,10 @@ components:
error:
$ref: "#/components/schemas/requestError"

appendCompletionType:
type: string
enum: ["auto", "conditional", "manual", "manual-retry"]

responses:
"200":
description: Operation succeeded
Expand Down
30 changes: 30 additions & 0 deletions packages/web-core/src/api/v2/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ export interface AaguidDetails {
*/
'iconDark': string;
}
/**
*
* @export
* @enum {string}
*/

export const AppendCompletionType = {
Auto: 'auto',
Conditional: 'conditional',
Manual: 'manual',
ManualRetry: 'manual-retry'
} as const;

export type AppendCompletionType = typeof AppendCompletionType[keyof typeof AppendCompletionType];


/**
*
* @export
Expand Down Expand Up @@ -299,7 +315,15 @@ export interface ConnectAppendFinishReq {
* @memberof ConnectAppendFinishReq
*/
'attestationResponse': string;
/**
*
* @type {AppendCompletionType}
* @memberof ConnectAppendFinishReq
*/
'completionType': AppendCompletionType;
}


/**
*
* @export
Expand Down Expand Up @@ -442,6 +466,12 @@ export interface ConnectAppendStartRsp {
* @memberof ConnectAppendStartRsp
*/
'autoAppend': boolean;
/**
*
* @type {boolean}
* @memberof ConnectAppendStartRsp
*/
'conditionalAppend': boolean;
}

export const ConnectAppendStartRspVariantEnum = {
Expand Down
1 change: 1 addition & 0 deletions packages/web-core/src/models/connect/append.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type AppendCompletionType = 'manual' | 'manual-retry' | 'auto' | 'conditional';
51 changes: 23 additions & 28 deletions packages/web-core/src/services/ConnectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
ConnectManageListRsp,
} from '../api/v2';
import { CorbadoConnectApi, PasskeyEventType } from '../api/v2';
import type { AppendCompletionType } from '../models/connect/append';
import { ConnectFlags } from '../models/connect/connectFlags';
import { ConnectInvitation } from '../models/connect/connectInvitation';
import { ConnectProcess } from '../models/connect/connectProcess';
Expand Down Expand Up @@ -413,27 +414,6 @@ export class ConnectService {
return Ok(appendData);
}

async append(appendTokenValue: string, loadedMs: number): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController()));
if (!existingProcess) {
return Err(new ConnectError(ConnectErrorType.MissingInit));
}

const resStart = await this.wrapWithErr(() =>
this.#connectApi.connectAppendStart({ appendTokenValue: appendTokenValue, loadedMs }),
);
if (resStart.err) {
return resStart;
}

const platformRes = await this.#webAuthnCreatePasskey(resStart.val.attestationOptions);
if (platformRes.err) {
return platformRes;
}

return this.wrapWithErr(() => this.#connectApi.connectAppendFinish({ attestationResponse: platformRes.val }));
}

async startAppend(
appendTokenValue: string,
loadedMs: number,
Expand All @@ -453,19 +433,23 @@ export class ConnectService {
);
}

async completeAppend(attestationOptions: string): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
async completeAppend(
attestationOptions: string,
completionType: AppendCompletionType,
): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController()));
if (!existingProcess) {
return Err(new ConnectError(ConnectErrorType.MissingInit));
}

const res = await this.#webAuthnCreatePasskey(attestationOptions);
const conditional = completionType === 'conditional';
const res = await this.#webAuthnCreatePasskey(attestationOptions, conditional);
if (res.err) {
return res;
}

const finishRes = await this.wrapWithErr(() =>
this.#connectApi.connectAppendFinish({ attestationResponse: res.val }),
this.#connectApi.connectAppendFinish({ attestationResponse: res.val, completionType }),
);
if (finishRes.ok) {
const latestLogin = finishRes.val.passkeyOperation as LastLogin;
Expand Down Expand Up @@ -771,18 +755,29 @@ export class ConnectService {
const started = Date.now();
try {
const res = await this.#webAuthnService.loginRaw(serializedChallenge, isConditional, onConditionalLoginStart);
return Ok(res);
if (res.message) {
void this.recordEventLoginErrorUnexpected(res.message);
}

return Ok(res.response);
} catch (e) {
const runtime = Date.now() - started;
return Err(ConnectError.fromFrontendError(e, runtime));
}
}

async #webAuthnCreatePasskey(serializedChallenge: string): Promise<Result<string, ConnectError>> {
async #webAuthnCreatePasskey(
serializedChallenge: string,
conditional: boolean,
): Promise<Result<string, ConnectError>> {
const started = Date.now();
try {
const res = await this.#webAuthnService.createPasskeyRaw(serializedChallenge);
return Ok(res);
const res = await this.#webAuthnService.createPasskeyRaw(serializedChallenge, conditional);
if (res.message) {
void this.recordEventAppendErrorUnexpected(res.message);
}

return Ok(res.response);
} catch (e) {
const runtime = Date.now() - started;
return Err(ConnectError.fromFrontendError(e, runtime));
Expand Down
Loading
Loading