Skip to content

Commit 06f3457

Browse files
authored
Add first version of conditional create (#545)
* Add first version of conditional create * Connect conditional-create to FAPI * Stop conditional creation after 5s * Prettier * Redirects for already logged-in users in connect-next playground * Replace console.log * Fix old browser compat
1 parent a13c64e commit 06f3457

File tree

13 files changed

+229
-79
lines changed

13 files changed

+229
-79
lines changed

packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st
2424

2525
setLoading(true);
2626
setErrorMessage(undefined);
27-
const res = await getConnectService().completeAppend(attestationOptions);
27+
const res = await getConnectService().completeAppend(attestationOptions, 'manual');
2828
if (res.err) {
2929
if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) {
3030
return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val);

packages/connect-react/src/components/append/AppendInitScreen.tsx

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ConnectError } from '@corbado/web-core';
22
import { ConnectErrorType } from '@corbado/web-core';
3+
import type { AppendCompletionType } from '@corbado/web-core/dist/models/connect/append';
34
import log from 'loglevel';
45
import React, { useCallback, useEffect, useRef, useState } from 'react';
56

@@ -132,11 +133,23 @@ const AppendInitScreen = () => {
132133
}
133134

134135
setAttestationOptions(startAppendRes.val.attestationOptions);
135-
statefulLoader.current.finish();
136-
137136
log.debug('startAppendRes', startAppendRes, flags);
137+
138+
if (startAppendRes.val.conditionalAppend) {
139+
log.debug('starting conditional create');
140+
const handledByConditionalCreate = await handleConditionalCreate(startAppendRes.val.attestationOptions);
141+
log.debug('handledByConditionalCreate', handledByConditionalCreate);
142+
143+
if (handledByConditionalCreate) {
144+
statefulLoader.current.finish();
145+
return;
146+
}
147+
}
148+
149+
statefulLoader.current.finish();
138150
if (startAppendRes.val.autoAppend || flags.hasSupportForAutomaticAppend()) {
139-
await handleSubmit(startAppendRes.val.attestationOptions, false);
151+
console.log('starting auto-append');
152+
await handleSubmit(startAppendRes.val.attestationOptions, 'auto');
140153
}
141154
};
142155

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

155168
const handleSubmit = useCallback(
156-
async (attestationOptions: string, showErrorIfCancelled: boolean) => {
169+
async (attestationOptions: string, completionType: AppendCompletionType) => {
157170
if (appendLoading || skipping) {
158171
return;
159172
}
160173

161174
setAppendLoading(true);
162175
setErrorMessage(undefined);
163176

164-
const res = await getConnectService().completeAppend(attestationOptions);
177+
const res = await getConnectService().completeAppend(attestationOptions, completionType);
165178
if (res.err) {
166179
if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) {
167180
return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val);
168181
}
169182

170183
if (res.val.type === ConnectErrorType.Cancel) {
171-
if (showErrorIfCancelled) {
172-
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val);
173-
} else {
184+
if (completionType === 'auto') {
174185
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelledSilent, res.val);
186+
} else {
187+
return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val);
175188
}
176189
}
177190

@@ -184,7 +197,26 @@ const AppendInitScreen = () => {
184197
aaguidIcon: res.val.passkeyOperation.aaguidDetails?.iconLight,
185198
});
186199
},
187-
[config, getConnectService, appendLoading, skipping],
200+
[getConnectService, appendLoading, skipping],
201+
);
202+
203+
const handleConditionalCreate = useCallback(
204+
async (attestationOptions: string) => {
205+
const res = await getConnectService().completeAppend(attestationOptions, 'conditional');
206+
if (res.err) {
207+
await handleSituation(AppendSituationCode.ClientPasskeyOperationErrorSilent, res.val);
208+
209+
return res.val.type === ConnectErrorType.RaceTimeout;
210+
}
211+
212+
navigateToScreen(AppendScreenType.Success, {
213+
aaguidName: res.val.passkeyOperation.aaguidDetails?.name,
214+
aaguidIcon: res.val.passkeyOperation.aaguidDetails?.iconLight,
215+
});
216+
217+
return true;
218+
},
219+
[getConnectService],
188220
);
189221

190222
const handleSituation = async (situationCode: AppendSituationCode, error?: ConnectError) => {
@@ -222,6 +254,10 @@ const AppendInitScreen = () => {
222254
case AppendSituationCode.ExplicitSkipByUser:
223255
await handleSkip(situationCode, true);
224256
break;
257+
case AppendSituationCode.ClientPasskeyOperationErrorSilent:
258+
void handleErrorSoft(situationCode, false, false, error);
259+
setAppendLoading(false);
260+
break;
225261
}
226262
};
227263

@@ -250,7 +286,14 @@ const AppendInitScreen = () => {
250286
void onReadMoreClick();
251287
setAppendInitState(AppendInitState.ShowBenefits);
252288
}}
253-
handleSubmit={() => void handleSubmit(attestationOptions, true)}
289+
handleSubmit={() => {
290+
let completionType: AppendCompletionType = 'manual';
291+
if (errorMessage) {
292+
completionType = 'manual-retry';
293+
}
294+
295+
void handleSubmit(attestationOptions, completionType);
296+
}}
254297
handleSkip={() => onSkip()}
255298
/>
256299
);

packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const PasskeyListScreen = () => {
130130
return handleSituation(PasskeyListSituationCode.CboApiPasskeysNotSupported);
131131
}
132132

133-
const res = await getConnectService().completeAppend(startAppendRes.val.attestationOptions);
133+
const res = await getConnectService().completeAppend(startAppendRes.val.attestationOptions, 'manual');
134134
if (res.err) {
135135
if (res.val.type === ConnectErrorType.Cancel) {
136136
return handleSituation(PasskeyListSituationCode.ClientPasskeyOperationCancelled, res.val);

packages/connect-react/src/types/situations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum AppendSituationCode {
2727
DeniedByPasskeyIntel,
2828
ExplicitSkipByUser,
2929
ClientPasskeyOperationCancelledSilent,
30+
ClientPasskeyOperationErrorSilent,
3031
}
3132

3233
export enum PasskeyListSituationCode {

packages/web-core/openapi/spec_v2.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,7 @@ components:
14861486
- variant
14871487
- isRestrictedBrowser
14881488
- autoAppend
1489+
- conditionalAppend
14891490
properties:
14901491
attestationOptions:
14911492
type: string
@@ -1499,14 +1500,19 @@ components:
14991500
type: boolean
15001501
autoAppend:
15011502
type: boolean
1503+
conditionalAppend:
1504+
type: boolean
15021505

15031506
connectAppendFinishReq:
15041507
type: object
15051508
required:
15061509
- attestationResponse
1510+
- completionType
15071511
properties:
15081512
attestationResponse:
15091513
type: string
1514+
completionType:
1515+
$ref: "#/components/schemas/appendCompletionType"
15101516

15111517
connectAppendFinishRsp:
15121518
type: object
@@ -2373,6 +2379,10 @@ components:
23732379
error:
23742380
$ref: "#/components/schemas/requestError"
23752381

2382+
appendCompletionType:
2383+
type: string
2384+
enum: ["auto", "conditional", "manual", "manual-retry"]
2385+
23762386
responses:
23772387
"200":
23782388
description: Operation succeeded

packages/web-core/src/api/v2/api.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ export interface AaguidDetails {
4848
*/
4949
'iconDark': string;
5050
}
51+
/**
52+
*
53+
* @export
54+
* @enum {string}
55+
*/
56+
57+
export const AppendCompletionType = {
58+
Auto: 'auto',
59+
Conditional: 'conditional',
60+
Manual: 'manual',
61+
ManualRetry: 'manual-retry'
62+
} as const;
63+
64+
export type AppendCompletionType = typeof AppendCompletionType[keyof typeof AppendCompletionType];
65+
66+
5167
/**
5268
*
5369
* @export
@@ -299,7 +315,15 @@ export interface ConnectAppendFinishReq {
299315
* @memberof ConnectAppendFinishReq
300316
*/
301317
'attestationResponse': string;
318+
/**
319+
*
320+
* @type {AppendCompletionType}
321+
* @memberof ConnectAppendFinishReq
322+
*/
323+
'completionType': AppendCompletionType;
302324
}
325+
326+
303327
/**
304328
*
305329
* @export
@@ -442,6 +466,12 @@ export interface ConnectAppendStartRsp {
442466
* @memberof ConnectAppendStartRsp
443467
*/
444468
'autoAppend': boolean;
469+
/**
470+
*
471+
* @type {boolean}
472+
* @memberof ConnectAppendStartRsp
473+
*/
474+
'conditionalAppend': boolean;
445475
}
446476

447477
export const ConnectAppendStartRspVariantEnum = {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type AppendCompletionType = 'manual' | 'manual-retry' | 'auto' | 'conditional';

packages/web-core/src/services/ConnectService.ts

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
ConnectManageListRsp,
3030
} from '../api/v2';
3131
import { CorbadoConnectApi, PasskeyEventType } from '../api/v2';
32+
import type { AppendCompletionType } from '../models/connect/append';
3233
import { ConnectFlags } from '../models/connect/connectFlags';
3334
import { ConnectInvitation } from '../models/connect/connectInvitation';
3435
import { ConnectProcess } from '../models/connect/connectProcess';
@@ -413,27 +414,6 @@ export class ConnectService {
413414
return Ok(appendData);
414415
}
415416

416-
async append(appendTokenValue: string, loadedMs: number): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
417-
const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController()));
418-
if (!existingProcess) {
419-
return Err(new ConnectError(ConnectErrorType.MissingInit));
420-
}
421-
422-
const resStart = await this.wrapWithErr(() =>
423-
this.#connectApi.connectAppendStart({ appendTokenValue: appendTokenValue, loadedMs }),
424-
);
425-
if (resStart.err) {
426-
return resStart;
427-
}
428-
429-
const platformRes = await this.#webAuthnCreatePasskey(resStart.val.attestationOptions);
430-
if (platformRes.err) {
431-
return platformRes;
432-
}
433-
434-
return this.wrapWithErr(() => this.#connectApi.connectAppendFinish({ attestationResponse: platformRes.val }));
435-
}
436-
437417
async startAppend(
438418
appendTokenValue: string,
439419
loadedMs: number,
@@ -453,19 +433,23 @@ export class ConnectService {
453433
);
454434
}
455435

456-
async completeAppend(attestationOptions: string): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
436+
async completeAppend(
437+
attestationOptions: string,
438+
completionType: AppendCompletionType,
439+
): Promise<Result<ConnectAppendFinishRsp, ConnectError>> {
457440
const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController()));
458441
if (!existingProcess) {
459442
return Err(new ConnectError(ConnectErrorType.MissingInit));
460443
}
461444

462-
const res = await this.#webAuthnCreatePasskey(attestationOptions);
445+
const conditional = completionType === 'conditional';
446+
const res = await this.#webAuthnCreatePasskey(attestationOptions, conditional);
463447
if (res.err) {
464448
return res;
465449
}
466450

467451
const finishRes = await this.wrapWithErr(() =>
468-
this.#connectApi.connectAppendFinish({ attestationResponse: res.val }),
452+
this.#connectApi.connectAppendFinish({ attestationResponse: res.val, completionType }),
469453
);
470454
if (finishRes.ok) {
471455
const latestLogin = finishRes.val.passkeyOperation as LastLogin;
@@ -771,18 +755,29 @@ export class ConnectService {
771755
const started = Date.now();
772756
try {
773757
const res = await this.#webAuthnService.loginRaw(serializedChallenge, isConditional, onConditionalLoginStart);
774-
return Ok(res);
758+
if (res.message) {
759+
void this.recordEventLoginErrorUnexpected(res.message);
760+
}
761+
762+
return Ok(res.response);
775763
} catch (e) {
776764
const runtime = Date.now() - started;
777765
return Err(ConnectError.fromFrontendError(e, runtime));
778766
}
779767
}
780768

781-
async #webAuthnCreatePasskey(serializedChallenge: string): Promise<Result<string, ConnectError>> {
769+
async #webAuthnCreatePasskey(
770+
serializedChallenge: string,
771+
conditional: boolean,
772+
): Promise<Result<string, ConnectError>> {
782773
const started = Date.now();
783774
try {
784-
const res = await this.#webAuthnService.createPasskeyRaw(serializedChallenge);
785-
return Ok(res);
775+
const res = await this.#webAuthnService.createPasskeyRaw(serializedChallenge, conditional);
776+
if (res.message) {
777+
void this.recordEventAppendErrorUnexpected(res.message);
778+
}
779+
780+
return Ok(res.response);
786781
} catch (e) {
787782
const runtime = Date.now() - started;
788783
return Err(ConnectError.fromFrontendError(e, runtime));

0 commit comments

Comments
 (0)