From 1e9a263b3c5fc1e01355420491002994cbf29d45 Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 9 Apr 2025 15:03:52 +0200 Subject: [PATCH 1/3] Improve connect errors --- .../append/AppendAfterErrorScreen.tsx | 21 +-- .../components/append/AppendInitScreen.tsx | 32 ++--- .../login-second-factor/InitScreen.tsx | 6 +- .../components/login/LoginErrorScreenHard.tsx | 19 +-- .../components/login/LoginErrorScreenSoft.tsx | 20 +-- .../components/login/LoginHybridScreen.tsx | 13 +- .../src/components/login/LoginInitScreen.tsx | 35 +++-- .../login/LoginPasskeyReLoginScreen.tsx | 17 +-- .../passkeyList/PasskeyListScreen.tsx | 17 +-- .../src/contexts/AppendProcessContext.ts | 12 +- .../src/contexts/AppendProcessProvider.tsx | 22 +-- .../web-core/src/services/ConnectService.ts | 133 +++++++++++++----- .../web-core/src/services/WebAuthnService.ts | 60 ++++---- .../src/utils/errors/connectErrors.ts | 84 +++++++++++ packages/web-core/src/utils/errors/errors.ts | 22 --- packages/web-core/src/utils/errors/index.ts | 1 + 16 files changed, 334 insertions(+), 180 deletions(-) create mode 100644 packages/web-core/src/utils/errors/connectErrors.ts diff --git a/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx b/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx index 4df7a866c..0cc328ca1 100644 --- a/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx +++ b/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx @@ -1,4 +1,5 @@ -import { ExcludeCredentialsMatchError, PasskeyChallengeCancelledError } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType } from '@corbado/web-core'; import log from 'loglevel'; import React, { useCallback, useState } from 'react'; @@ -25,15 +26,15 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st setErrorMessage(undefined); const res = await getConnectService().completeAppend(attestationOptions); if (res.err) { - if (res.val instanceof ExcludeCredentialsMatchError) { - return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch); + if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) { + return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val); } - if (res.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled); + if (res.val.type === ConnectErrorType.Cancel) { + return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val); } - return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator, res.val); } setLoading(false); @@ -43,7 +44,7 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st }); }; - const handleSituation = (situationCode: AppendSituationCode) => { + const handleSituation = (situationCode: AppendSituationCode, error?: ConnectError) => { log.debug(`situation: ${situationCode}`); const message = getAppendErrorMessage(situationCode); @@ -55,14 +56,14 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st case AppendSituationCode.CtApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePostAuthenticator: - void handleErrorHard(situationCode, false); + void handleErrorHard(situationCode, false, error); break; case AppendSituationCode.ClientPasskeyOperationCancelled: setLoading(false); - void handleErrorSoft(situationCode, true, true); + void handleErrorSoft(situationCode, true, true, error); break; case AppendSituationCode.ClientExcludeCredentialsMatch: - void handleCredentialExistsError(); + void handleCredentialExistsError(error); break; case AppendSituationCode.ExplicitSkipByUser: void handleSkip(situationCode, true); diff --git a/packages/connect-react/src/components/append/AppendInitScreen.tsx b/packages/connect-react/src/components/append/AppendInitScreen.tsx index a382872b6..da587ceb4 100644 --- a/packages/connect-react/src/components/append/AppendInitScreen.tsx +++ b/packages/connect-react/src/components/append/AppendInitScreen.tsx @@ -1,4 +1,4 @@ -import { ExcludeCredentialsMatchError, PasskeyChallengeCancelledError } from '@corbado/web-core'; +import { ConnectError, ConnectErrorType } from '@corbado/web-core'; import log from 'loglevel'; import React, { useCallback, useEffect, useRef, useState } from 'react'; @@ -80,11 +80,11 @@ const AppendInitScreen = () => { const res = await getConnectService().appendInit(ac); if (res.err) { - if (res.val.ignore) { + if (res.val.type === ConnectErrorType.Cancel) { return; } - return handleSituation(AppendSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(AppendSituationCode.CboApiNotAvailablePreAuthenticator, res.val); } // we load flags from backend first, then we override them with the ones that are specified in the component's config @@ -107,11 +107,11 @@ const AppendInitScreen = () => { const startAppendRes = await getConnectService().startAppend(appendToken, loadedMs, ac); if (startAppendRes.err) { - if (startAppendRes.val.ignore) { + if (startAppendRes.val.type === ConnectErrorType.Cancel) { return; } - return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator, startAppendRes.val); } if (startAppendRes.val.attestationOptions === '') { @@ -162,19 +162,19 @@ const AppendInitScreen = () => { const res = await getConnectService().completeAppend(attestationOptions); if (res.err) { - if (res.val instanceof ExcludeCredentialsMatchError) { - return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch); + if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) { + return handleSituation(AppendSituationCode.ClientExcludeCredentialsMatch, res.val); } - if (res.val instanceof PasskeyChallengeCancelledError) { + if (res.val.type === ConnectErrorType.Cancel) { if (showErrorIfCancelled) { - return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled); + return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelled, res.val); } else { - return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelledSilent); + return handleSituation(AppendSituationCode.ClientPasskeyOperationCancelledSilent, res.val); } } - return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(AppendSituationCode.CboApiNotAvailablePostAuthenticator, res.val); } setAppendLoading(false); @@ -186,7 +186,7 @@ const AppendInitScreen = () => { [config, getConnectService, appendLoading, skipping], ); - const handleSituation = async (situationCode: AppendSituationCode) => { + const handleSituation = async (situationCode: AppendSituationCode, error?: ConnectError) => { log.debug(`situation: ${situationCode}`); const message = getAppendErrorMessage(situationCode); @@ -198,20 +198,20 @@ const AppendInitScreen = () => { case AppendSituationCode.CtApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePostAuthenticator: - void handleErrorHard(situationCode, false); + void handleErrorHard(situationCode, false, error); statefulLoader.current.finishWithError(); break; case AppendSituationCode.ClientPasskeyOperationCancelled: - void handleErrorSoft(situationCode, true, true); + void handleErrorSoft(situationCode, true, true, error); setAppendLoading(false); break; case AppendSituationCode.ClientPasskeyOperationCancelledSilent: - void handleErrorSoft(situationCode, true, false); + void handleErrorSoft(situationCode, true, false, error); setAppendLoading(false); break; case AppendSituationCode.ClientExcludeCredentialsMatch: - void handleCredentialExistsError(); + void handleCredentialExistsError(error); setAppendLoading(false); break; case AppendSituationCode.DeniedByPartialRollout: diff --git a/packages/connect-react/src/components/login-second-factor/InitScreen.tsx b/packages/connect-react/src/components/login-second-factor/InitScreen.tsx index 2b98df47b..334a0f74f 100644 --- a/packages/connect-react/src/components/login-second-factor/InitScreen.tsx +++ b/packages/connect-react/src/components/login-second-factor/InitScreen.tsx @@ -1,4 +1,4 @@ -import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; +import { ConnectErrorType, PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; import type { ConnectLoginStartRsp } from '@corbado/web-core/dist/api/v2'; import log from 'loglevel'; import React, { useEffect, useRef, useState } from 'react'; @@ -41,7 +41,7 @@ const InitScreen = () => { statefulLoader.current.start(); const res = await getConnectService().loginInit(ac); if (res.err) { - if (res.val.ignore) { + if (res.val.type === ConnectErrorType.Cancel) { return; } @@ -75,7 +75,7 @@ const InitScreen = () => { ac, ); if (resStart.err) { - if (resStart.val.ignore) { + if (resStart.val.type === ConnectErrorType.Cancel) { return; } diff --git a/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx b/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx index 1d73757ee..7d9e0d8be 100644 --- a/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx +++ b/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx @@ -1,4 +1,5 @@ -import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; import log from 'loglevel'; import React, { useState } from 'react'; @@ -29,7 +30,7 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => { setLoading(true); const resStart = await getConnectService().loginStart(currentIdentifier, PasskeyLoginSource.ErrorHard, loadedMs); if (resStart.err) { - return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator, resStart.val); } if (resStart.val.assertionOptions.length === 0) { @@ -39,22 +40,22 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => { message: resStart.val.fallbackOperationError.error?.message ?? null, }; - return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data); + return handleSituation(LoginSituationCode.CboApiFallbackOperationError, undefined, data); } setAssertionOptions(resStart.val.assertionOptions); const resFinish = await getConnectService().loginContinue(resStart.val); if (resFinish.err) { - if (resFinish.val instanceof PasskeyChallengeCancelledError) { + if (resFinish.val.type === ConnectErrorType.Cancel) { if (hardErrorCount >= 3) { - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelledTooManyTimes); + return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelledTooManyTimes, resFinish.val); } - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled); + return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled, resFinish.val); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator, resFinish.val); } setLoading(false); @@ -66,8 +67,8 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => { } }; - const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => { - const messageCode = `situation: ${situationCode}`; + const handleSituation = (situationCode: LoginSituationCode, error?: ConnectError, data?: unknown) => { + const messageCode = `situation: ${situationCode} ${error?.track()}`; log.debug(messageCode); const identifier = currentIdentifier; diff --git a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx index 0164a241a..f4a86f494 100644 --- a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx +++ b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx @@ -1,4 +1,4 @@ -import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; +import { ConnectError, ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; import log from 'loglevel'; import React, { useState } from 'react'; @@ -28,7 +28,7 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => { setLoading(true); const resStart = await getConnectService().loginStart(currentIdentifier, PasskeyLoginSource.ErrorSoft, loadedMs); if (resStart.err) { - return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator, resStart.val); } if (resStart.val.assertionOptions.length === 0) { @@ -38,16 +38,20 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => { message: resStart.val.fallbackOperationError.error?.message ?? null, }; - return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data); + return handleSituation(LoginSituationCode.CboApiFallbackOperationError, undefined, data); } const resFinish = await getConnectService().loginContinue(resStart.val); if (resFinish.err) { - if (resFinish.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled, resStart.val.assertionOptions); + if (resFinish.val.type === ConnectErrorType.Cancel) { + return handleSituation( + LoginSituationCode.ClientPasskeyOperationCancelled, + resFinish.val, + resStart.val.assertionOptions, + ); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator, resFinish.val); } try { @@ -58,8 +62,8 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => { } }; - const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => { - const messageCode = `situation: ${situationCode}`; + const handleSituation = (situationCode: LoginSituationCode, error?: ConnectError, data?: unknown) => { + const messageCode = `situation: ${situationCode} ${error?.track()}`; log.debug(messageCode); const identifier = currentIdentifier; diff --git a/packages/connect-react/src/components/login/LoginHybridScreen.tsx b/packages/connect-react/src/components/login/LoginHybridScreen.tsx index 9962ff7b8..f8b56084e 100644 --- a/packages/connect-react/src/components/login/LoginHybridScreen.tsx +++ b/packages/connect-react/src/components/login/LoginHybridScreen.tsx @@ -1,4 +1,5 @@ -import { PasskeyChallengeCancelledError } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType } from '@corbado/web-core'; import type { ConnectLoginStartRsp } from '@corbado/web-core/dist/api/v2'; import log from 'loglevel'; import React, { useCallback, useState } from 'react'; @@ -23,11 +24,11 @@ const LoginHybridScreen = (resStart: ConnectLoginStartRsp) => { setLoading(true); const res = await getConnectService().loginContinue(resStart); if (res.err) { - if (res.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled); + if (res.val.type === ConnectErrorType.Cancel) { + return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled, res.val); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator, res.val); } try { @@ -37,8 +38,8 @@ const LoginHybridScreen = (resStart: ConnectLoginStartRsp) => { } }, [getConnectService, config, navigateToScreen, currentIdentifier, loading]); - const handleSituation = (situationCode: LoginSituationCode) => { - const messageCode = `situation: ${situationCode}`; + const handleSituation = (situationCode: LoginSituationCode, error?: ConnectError) => { + const messageCode = `situation: ${situationCode} ${error?.track()}`; log.debug(messageCode); const identifier = currentIdentifier; diff --git a/packages/connect-react/src/components/login/LoginInitScreen.tsx b/packages/connect-react/src/components/login/LoginInitScreen.tsx index 26ddebce6..64f610192 100644 --- a/packages/connect-react/src/components/login/LoginInitScreen.tsx +++ b/packages/connect-react/src/components/login/LoginInitScreen.tsx @@ -1,4 +1,5 @@ -import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; import type { ConnectLoginFinishRsp } from '@corbado/web-core/dist/api/v2'; import log from 'loglevel'; import type { FC } from 'react'; @@ -94,12 +95,12 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { const res = await getConnectService().loginInit(ac); if (res.err) { - if (res.val.ignore) { + if (res.val.type === ConnectErrorType.Cancel) { return; } statefulLoader.current.finishWithError(); - return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator, res.val); } // we load flags from backend first, then we override them with the ones that are specified in the component's config @@ -154,16 +155,16 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { if (res.err) { // if a user cancel during CUI, she can try again - if (res.val.ignore || res.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(LoginSituationCode.ClientPasskeyConditionalOperationCancelled); + if (res.val.type === ConnectErrorType.Cancel) { + return handleSituation(LoginSituationCode.ClientPasskeyConditionalOperationCancelled, res.val); } // cuiStarted === true indicates that user has passed the authenticator if (cuiStarted) { - return handleSituation(LoginSituationCode.CboApiNotAvailablePostConditionalAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostConditionalAuthenticator, res.val); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePreConditionalAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreConditionalAuthenticator, res.val); } if (res.val.fallbackOperationError) { @@ -173,7 +174,7 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { message: res.val.fallbackOperationError.error?.message ?? null, }; - return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data); + return handleSituation(LoginSituationCode.CboApiFallbackOperationError, undefined, data); } try { @@ -194,7 +195,7 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { const resStart = await getConnectService().loginStart(identifier, PasskeyLoginSource.TextField, loadedMs); if (resStart.err) { - return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator, resStart.val); } if (resStart.val.isCDA) { @@ -209,17 +210,21 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { message: resStart.val.fallbackOperationError.error?.message ?? null, }; - return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data); + return handleSituation(LoginSituationCode.CboApiFallbackOperationError, undefined, data); } const res = await getConnectService().loginContinue(resStart.val); if (res.err) { setIdentifierBasedLoading(false); - if (res.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled, resStart.val.assertionOptions); + if (res.val.type === ConnectErrorType.Cancel) { + return handleSituation( + LoginSituationCode.ClientPasskeyOperationCancelled, + res.val, + resStart.val.assertionOptions, + ); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator, res.val); } try { @@ -241,8 +246,8 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { fallback(identifier, null); }; - const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => { - const messageCode = `situation: ${situationCode}`; + const handleSituation = (situationCode: LoginSituationCode, error?: ConnectError, data?: unknown) => { + const messageCode = `situation: ${situationCode} ${error?.track()}`; log.debug(messageCode); const message = getLoginErrorMessage(situationCode); diff --git a/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx b/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx index 38c5e1b01..ad60a610d 100644 --- a/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx +++ b/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx @@ -1,4 +1,5 @@ -import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; import log from 'loglevel'; import React, { useEffect, useState } from 'react'; @@ -29,7 +30,7 @@ export const LoginPasskeyReLoginScreen = () => { config.onLoginStart?.(); const resStart = await getConnectService().loginStart(currentIdentifier, PasskeyLoginSource.OneTap, loadedMs); if (resStart.err) { - return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator, resStart.val); } if (resStart.val.assertionOptions.length === 0) { @@ -39,16 +40,16 @@ export const LoginPasskeyReLoginScreen = () => { message: resStart.val.fallbackOperationError.error?.message ?? null, }; - return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data); + return handleSituation(LoginSituationCode.CboApiFallbackOperationError, undefined, data); } const resFinish = await getConnectService().loginContinue(resStart.val); if (resFinish.err) { - if (resFinish.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled); + if (resFinish.val.type === ConnectErrorType.Cancel) { + return handleSituation(LoginSituationCode.ClientPasskeyOperationCancelled, resFinish.val); } - return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(LoginSituationCode.CboApiNotAvailablePostAuthenticator, resFinish.val); } try { @@ -63,8 +64,8 @@ export const LoginPasskeyReLoginScreen = () => { navigateToScreen(LoginScreenType.Init, { prefilledIdentifier: identifier }); }; - const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => { - const messageCode = `situation: ${situationCode}`; + const handleSituation = (situationCode: LoginSituationCode, error?: ConnectError, data?: unknown) => { + const messageCode = `situation: ${situationCode} ${error?.track()}`; log.debug(messageCode); const identifier = currentIdentifier; diff --git a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx index 3ce754dcf..01f2e9e4e 100644 --- a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx +++ b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx @@ -1,5 +1,6 @@ import type { CorbadoConnectPasskeyListConfig } from '@corbado/types'; -import type { Passkey } from '@corbado/web-core'; +import type { ConnectError, Passkey } from '@corbado/web-core'; +import { ConnectErrorType } from '@corbado/web-core'; import { ExcludeCredentialsMatchError, PasskeyChallengeCancelledError } from '@corbado/web-core'; import log from 'loglevel'; import React, { useEffect, useRef, useState } from 'react'; @@ -60,11 +61,11 @@ const PasskeyListScreen = () => { const res = await getConnectService().manageInit(ac); log.debug(res.val); if (res.err) { - if (res.val.ignore) { + if (res.val.type === ConnectErrorType.Cancel) { return; } - return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringInitialLoad); + return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringInitialLoad, res.val); } // we use the manageAllowed flag to determine if appending a passkey is allowed @@ -172,7 +173,7 @@ const PasskeyListScreen = () => { statefulLoader.current.finish(); }; - const handleSituation = (situationCode: PasskeyListSituationCode) => { + const handleSituation = (situationCode: PasskeyListSituationCode, error?: ConnectError) => { const messageCode = `situation: ${situationCode}`; log.debug(messageCode); @@ -180,7 +181,7 @@ const PasskeyListScreen = () => { switch (situationCode) { case PasskeyListSituationCode.ClientExcludeCredentialsMatch: setAppendLoading(false); - void getConnectService().recordEventAppendCredentialExistsError(); + void getConnectService().recordEventAppendCredentialExistsError(`${messageCode} ${error?.track()}`); show(); break; case PasskeyListSituationCode.CboApiPasskeysNotSupportedLight: @@ -199,7 +200,7 @@ const PasskeyListScreen = () => { setErrorMessage(message); } - void getConnectService().recordEventManageErrorUnexpected(messageCode); + void getConnectService().recordEventManageErrorUnexpected(`${messageCode} ${error?.track()}`); break; case PasskeyListSituationCode.CtApiNotAvailablePreDelete: case PasskeyListSituationCode.CboApiNotAvailableDuringDelete: @@ -208,7 +209,7 @@ const PasskeyListScreen = () => { setErrorMessage(message); } - void getConnectService().recordEventManageErrorUnexpected(messageCode); + void getConnectService().recordEventManageErrorUnexpected(`${messageCode} ${error?.track()}`); break; case PasskeyListSituationCode.CtApiNotAvailablePreAuthenticator: case PasskeyListSituationCode.CboApiNotAvailablePreAuthenticator: @@ -220,7 +221,7 @@ const PasskeyListScreen = () => { setErrorMessage(message); } - void getConnectService().recordEventManageErrorUnexpected(messageCode); + void getConnectService().recordEventManageErrorUnexpected(`${messageCode} ${error?.track()}`); } }; diff --git a/packages/connect-react/src/contexts/AppendProcessContext.ts b/packages/connect-react/src/contexts/AppendProcessContext.ts index 2b24cdbc4..6bf77b093 100644 --- a/packages/connect-react/src/contexts/AppendProcessContext.ts +++ b/packages/connect-react/src/contexts/AppendProcessContext.ts @@ -4,6 +4,7 @@ import { createContext } from 'react'; import type { Flags } from '../types/flags'; import { AppendScreenType } from '../types/screenTypes'; import type { AppendSituationCode } from '../types/situations'; +import { ConnectError } from '@corbado/web-core'; const missingImplementation = (): never => { throw new Error('Please make sure that your components are wrapped inside '); @@ -14,9 +15,14 @@ export interface AppendProcessContextProps { currentScreenOptions: any; config: CorbadoConnectAppendConfig; navigateToScreen: (s: AppendScreenType, options?: any) => void; - handleErrorSoft: (situation: AppendSituationCode, expected: boolean, showError: boolean) => Promise; - handleErrorHard: (situation: AppendSituationCode, expected: boolean) => Promise; - handleCredentialExistsError: () => Promise; + handleErrorSoft: ( + situation: AppendSituationCode, + expected: boolean, + showError: boolean, + error?: ConnectError, + ) => Promise; + handleErrorHard: (situation: AppendSituationCode, expected: boolean, error?: ConnectError) => Promise; + handleCredentialExistsError: (error?: ConnectError) => Promise; handleSkip: (situation: AppendSituationCode, explicit?: boolean) => Promise; onReadMoreClick: () => Promise; flags: Flags | undefined; diff --git a/packages/connect-react/src/contexts/AppendProcessProvider.tsx b/packages/connect-react/src/contexts/AppendProcessProvider.tsx index 1fbb66b2c..d3a945112 100644 --- a/packages/connect-react/src/contexts/AppendProcessProvider.tsx +++ b/packages/connect-react/src/contexts/AppendProcessProvider.tsx @@ -9,6 +9,7 @@ import type { AppendScreenType } from '../types/screenTypes'; import type { AppendSituationCode } from '../types/situations'; import type { AppendProcessContextProps } from './AppendProcessContext'; import AppendProcessContext from './AppendProcessContext'; +import type { ConnectError } from '@corbado/web-core'; type Props = { config: CorbadoConnectAppendConfig; @@ -27,11 +28,11 @@ export const AppendProcessProvider: FC> = ({ children, }, []); const handleErrorSoft = useCallback( - async (situationCode: AppendSituationCode, expected: boolean, showError: boolean) => { + async (situationCode: AppendSituationCode, expected: boolean, showError: boolean, error?: ConnectError) => { if (expected) { await getConnectService().recordEventAppendError(); } else { - await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode}`); + await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode} ${error?.track()}`); } if (showError) { @@ -42,11 +43,11 @@ export const AppendProcessProvider: FC> = ({ children, ); const handleErrorHard = useCallback( - async (situationCode: AppendSituationCode, expected: boolean) => { + async (situationCode: AppendSituationCode, expected: boolean, error?: ConnectError) => { if (expected) { await getConnectService().recordEventAppendError(); } else { - await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode}`); + await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode} ${error?.track()}`); } config.onError?.(situationCode.toString()); @@ -72,12 +73,15 @@ export const AppendProcessProvider: FC> = ({ children, await getConnectService().recordEventAppendLearnMore(); }, [getConnectService, config]); - const handleCredentialExistsError = useCallback(async () => { - log.debug('error (credential-exists)'); + const handleCredentialExistsError = useCallback( + async (error?: ConnectError) => { + log.debug('error (credential-exists)'); - await getConnectService().recordEventAppendCredentialExistsError(); - void config.onComplete('complete-noop', getConnectService().encodeClientState()); - }, [getConnectService, config]); + await getConnectService().recordEventAppendCredentialExistsError(error?.track() ?? ''); + void config.onComplete('complete-noop', getConnectService().encodeClientState()); + }, + [getConnectService, config], + ); const contextValue = useMemo( () => ({ diff --git a/packages/web-core/src/services/ConnectService.ts b/packages/web-core/src/services/ConnectService.ts index ac8343d1a..930d07e4b 100644 --- a/packages/web-core/src/services/ConnectService.ts +++ b/packages/web-core/src/services/ConnectService.ts @@ -1,5 +1,11 @@ import FingerprintJS from '@fingerprintjs/fingerprintjs'; -import type { AxiosHeaders, AxiosInstance, HeadersDefaults, RawAxiosRequestHeaders } from 'axios'; +import type { + AxiosHeaders, + AxiosInstance, + HeadersDefaults, + InternalAxiosRequestConfig, + RawAxiosRequestHeaders, +} from 'axios'; import axios, { type AxiosError, type AxiosResponse } from 'axios'; import log from 'loglevel'; import type { Result } from 'ts-results'; @@ -28,13 +34,19 @@ import { ConnectInvitation } from '../models/connect/connectInvitation'; import { ConnectProcess } from '../models/connect/connectProcess'; import type { ConnectAppendInitData, ConnectLoginInitData, ConnectManageInitData } from '../models/connect/login'; import type { PasskeyLoginSource } from '../utils'; -import { CorbadoError } from '../utils'; +import { ConnectError, ConnectErrorType } from '../utils'; import type { LastLogin } from './ClientStateService'; import { ClientStateService } from './ClientStateService'; import { WebAuthnService } from './WebAuthnService'; const packageVersion = process.env.FE_LIBRARY_VERSION; +interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { + metadata?: { + startTime?: number; + }; +} + export class ConnectService { #connectApi: CorbadoConnectApi = new CorbadoConnectApi(); #webAuthnService: WebAuthnService; @@ -81,11 +93,32 @@ export class ConnectService { headers: processId ? { ...headers, 'x-corbado-process-id': processId } : headers, }); - // We transform AxiosErrors into CorbadoErrors using axios interceptors. + out.interceptors.request.use( + (config: CustomAxiosRequestConfig): CustomAxiosRequestConfig | Promise => { + config.metadata = config.metadata || {}; + config.metadata.startTime = Date.now(); + return config; + }, + error => { + log.debug('axios request config error', error); + return Promise.reject(error); + }, + ); + + // We transform AxiosErrors into ConnectErrors using axios interceptors. out.interceptors.response.use( response => response, (error: AxiosError) => { - const e = CorbadoError.fromConnectAxiosError(error); + const endTime = Date.now(); // Or performance.now() + let durationMs = 0; + const config = error.config as CustomAxiosRequestConfig | undefined; // Cast config + const startTime = config?.metadata?.startTime; + + if (startTime) { + durationMs = endTime - startTime; + } + + const e = ConnectError.fromConnectAxiosError(error, durationMs); log.debug('axios error', error, e); return Promise.reject(e); }, @@ -109,21 +142,19 @@ export class ConnectService { this.#connectApi = new CorbadoConnectApi(config, frontendApiUrl, axiosInstance); } - async wrapWithErr(callback: () => Promise>): Promise> { + async wrapWithErr(callback: () => Promise>): Promise> { + const started = Date.now(); try { const r = await callback(); return Ok(r.data); } catch (e) { - if (e instanceof CorbadoError) { - return Err(e); - } - - return Err(CorbadoError.fromUnknownFrontendError(e)); + const runtime = Date.now() - started; + return Err(ConnectError.fromFrontendError(e, runtime)); } } - async loginInit(abortController: AbortController): Promise> { + async loginInit(abortController: AbortController): Promise> { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); const maybeLoginData = existingProcess?.getValidLoginData(); if ( @@ -203,7 +234,7 @@ export class ConnectService { return Ok(loginData); } - async #getExistingProcess(generator: () => Promise>): Promise { + async #getExistingProcess(generator: () => Promise>): Promise { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); if (existingProcess) { log.debug('process found'); @@ -226,10 +257,10 @@ export class ConnectService { loadedMs: number, connectToken?: string, ac?: AbortController, - ): Promise> { + ): Promise> { const existingProcess = await this.loginInit(ac ?? new AbortController()); if (existingProcess.err) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } let identifierHintAvailable = false; @@ -271,8 +302,8 @@ export class ConnectService { return res; } - async loginContinue(start: ConnectLoginStartRsp): Promise> { - const res = await this.#webAuthnService.login(start.assertionOptions, false); + async loginContinue(start: ConnectLoginStartRsp): Promise> { + const res = await this.#webAuthnLogin(start.assertionOptions, false); if (res.err) { this.clearLastLogin(); return res; @@ -286,19 +317,19 @@ export class ConnectService { postWebAuthn: () => void, onLoginEnd: () => void, loadedMs: number, - ): Promise> { + ): Promise> { const existingProcess = await this.#getExistingProcess(() => this.loginInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } if (!existingProcess.loginData || existingProcess.loginData?.conditionalUIChallenge === null) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } const challenge = existingProcess.loginData?.conditionalUIChallenge; - const res = await this.#webAuthnService.login(challenge, true, preWebAuthn); + const res = await this.#webAuthnLogin(challenge, true, preWebAuthn); if (res.err) { return res; } @@ -310,7 +341,7 @@ export class ConnectService { return loginFinishResp; } - async appendInit(abortController: AbortController): Promise> { + async appendInit(abortController: AbortController): Promise> { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); if (existingProcess) { log.debug('process exists, preparing api clients'); @@ -370,10 +401,10 @@ export class ConnectService { return Ok(appendData); } - async append(appendTokenValue: string, loadedMs: number): Promise> { + async append(appendTokenValue: string, loadedMs: number): Promise> { const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } const resStart = await this.wrapWithErr(() => @@ -383,7 +414,7 @@ export class ConnectService { return resStart; } - const platformRes = await this.#webAuthnService.createPasskey(resStart.val.attestationOptions); + const platformRes = await this.#webAuthnCreatePasskey(resStart.val.attestationOptions); if (platformRes.err) { return platformRes; } @@ -396,10 +427,10 @@ export class ConnectService { loadedMs: number, abortController?: AbortController, initiatedByUser?: boolean, - ): Promise> { + ): Promise> { const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } return this.wrapWithErr(() => @@ -410,13 +441,13 @@ export class ConnectService { ); } - async completeAppend(attestationOptions: string): Promise> { + async completeAppend(attestationOptions: string): Promise> { const existingProcess = await this.#getExistingProcess(() => this.appendInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } - const res = await this.#webAuthnService.createPasskey(attestationOptions); + const res = await this.#webAuthnCreatePasskey(attestationOptions); if (res.err) { return res; } @@ -440,10 +471,10 @@ export class ConnectService { assertionResponse: string, isConditionalUI: boolean, loadedMs?: number, - ): Promise> { + ): Promise> { const existingProcess = await this.#getExistingProcess(() => this.loginInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } const res = await this.wrapWithErr(() => @@ -462,7 +493,7 @@ export class ConnectService { return res; } - async manageInit(abortController: AbortController): Promise> { + async manageInit(abortController: AbortController): Promise> { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); if (existingProcess) { log.debug('process exists, preparing api clients'); @@ -523,10 +554,10 @@ export class ConnectService { async manageList( passkeyListToken: string, triggerSignalAllAccepted: boolean, - ): Promise> { + ): Promise> { const existingProcess = await this.#getExistingProcess(() => this.manageInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } const req: ConnectManageListReq = { @@ -554,10 +585,10 @@ export class ConnectService { async manageDelete( passkeyDeleteToken: string, credentialID: string, - ): Promise> { + ): Promise> { const existingProcess = await this.#getExistingProcess(() => this.manageInit(new AbortController())); if (!existingProcess) { - return Err(CorbadoError.missingInit()); + return Err(new ConnectError(ConnectErrorType.MissingInit)); } const req: ConnectManageDeleteReq = { @@ -617,8 +648,8 @@ export class ConnectService { return this.#recordEvent(PasskeyEventType.UserAppendAfterLoginErrorBlacklisted); } - recordEventAppendCredentialExistsError() { - return this.#recordEvent(PasskeyEventType.AppendCredentialExists); + recordEventAppendCredentialExistsError(messageCode: string) { + return this.#recordEvent(PasskeyEventType.AppendCredentialExists, messageCode); } recordEventAppendError() { @@ -719,4 +750,30 @@ export class ConnectService { return { req, flags }; } + + async #webAuthnLogin( + serializedChallenge: string, + isConditional: boolean, + onConditionalLoginStart?: (ac: AbortController) => void, + ): Promise> { + const started = Date.now(); + try { + const res = await this.#webAuthnService.loginRaw(serializedChallenge, isConditional, onConditionalLoginStart); + return Ok(res); + } catch (e) { + const runtime = Date.now() - started; + return Err(ConnectError.fromFrontendError(e, runtime)); + } + } + + async #webAuthnCreatePasskey(serializedChallenge: string): Promise> { + const started = Date.now(); + try { + const res = await this.#webAuthnService.createPasskeyRaw(serializedChallenge); + return Ok(res); + } catch (e) { + const runtime = Date.now() - started; + return Err(ConnectError.fromFrontendError(e, runtime)); + } + } } diff --git a/packages/web-core/src/services/WebAuthnService.ts b/packages/web-core/src/services/WebAuthnService.ts index 14acdd57d..1af0909e3 100644 --- a/packages/web-core/src/services/WebAuthnService.ts +++ b/packages/web-core/src/services/WebAuthnService.ts @@ -25,15 +25,8 @@ export class WebAuthnService { async createPasskey(serializedChallenge: string): Promise> { try { - const abortController = this.abortOngoingOperation(); - const challenge = JSON.parse(serializedChallenge); - challenge.signal = abortController.signal; - this.#abortController = abortController; - - const signedChallenge = await create(challenge); - const serializedResponse = JSON.stringify(signedChallenge); - - return Ok(serializedResponse); + const res = await this.createPasskeyRaw(serializedChallenge); + return Ok(res); } catch (e) { if (e instanceof DOMException) { return Err(CorbadoError.fromDOMException(e)); @@ -43,28 +36,24 @@ export class WebAuthnService { } } + async createPasskeyRaw(serializedChallenge: string): Promise { + const abortController = this.abortOngoingOperation(); + const challenge = JSON.parse(serializedChallenge); + challenge.signal = abortController.signal; + this.#abortController = abortController; + + const signedChallenge = await create(challenge); + return JSON.stringify(signedChallenge); + } + async login( serializedChallenge: string, conditional: boolean, onConditionalLoginStart?: (ac: AbortController) => void, ): Promise> { try { - const abortController = this.abortOngoingOperation(); - - const challenge: CredentialRequestOptionsJSON = JSON.parse(serializedChallenge); - - challenge.signal = abortController.signal; - this.#abortController = abortController; - onConditionalLoginStart?.(abortController); - - if (conditional) { - challenge.mediation = 'conditional'; - } - - const signedChallenge = await get(challenge); - const serializedResponse = JSON.stringify(signedChallenge); - - return Ok(serializedResponse); + const res = await this.loginRaw(serializedChallenge, conditional, onConditionalLoginStart); + return Ok(res); } catch (e) { if (e instanceof DOMException) { return Err(CorbadoError.fromDOMException(e)); @@ -74,6 +63,27 @@ export class WebAuthnService { } } + async loginRaw( + serializedChallenge: string, + conditional: boolean, + onConditionalLoginStart?: (ac: AbortController) => void, + ): Promise { + const abortController = this.abortOngoingOperation(); + + const challenge: CredentialRequestOptionsJSON = JSON.parse(serializedChallenge); + + challenge.signal = abortController.signal; + this.#abortController = abortController; + onConditionalLoginStart?.(abortController); + + if (conditional) { + challenge.mediation = 'conditional'; + } + + const signedChallenge = await get(challenge); + return JSON.stringify(signedChallenge); + } + async getClientInformation(maybeClientHandle: ClientStateEntry | undefined): Promise { const bluetoothAvailable = await WebAuthnService.canUseBluetooth(); const isUserVerifyingPlatformAuthenticatorAvailable = await WebAuthnService.doesBrowserSupportPasskeys(); diff --git a/packages/web-core/src/utils/errors/connectErrors.ts b/packages/web-core/src/utils/errors/connectErrors.ts new file mode 100644 index 000000000..d8b8d40d0 --- /dev/null +++ b/packages/web-core/src/utils/errors/connectErrors.ts @@ -0,0 +1,84 @@ +import type { AxiosError } from 'axios'; +import log from 'loglevel'; + +export enum ConnectErrorType { + MissingInit, + RequestTimeout, + Cancel, + InvalidState, + SecurityError, + ExcludeCredentialsMatch, +} + +export class ConnectError { + type: ConnectErrorType; + message?: string; + runtime?: number; + + constructor(type: ConnectErrorType, message?: string, runtime?: number) { + this.type = type; + this.message = message; + this.runtime = runtime; + } + + track(): string { + let out = `type: ${this.type}`; + if (this.message) { + out += ` message: ${this.message}`; + } + + if (this.runtime) { + out += ` runtime: ${this.runtime}`; + } + + return out; + } + + static fromConnectAxiosError(error: AxiosError, durationMs: number): ConnectError { + log.debug('axios error', error); + + if (error.code === 'ECONNABORTED' || error.code === 'ERR_NETWORK') { + return new ConnectError(ConnectErrorType.RequestTimeout, error.message, durationMs); + } + + if (error.name === 'CanceledError') { + return new ConnectError(ConnectErrorType.Cancel, error.message, durationMs); + } + + if (!error.response || !error.response.data) { + return new ConnectError(ConnectErrorType.InvalidState, error.message, durationMs); + } + + return new ConnectError(ConnectErrorType.InvalidState, error.response.data as string, durationMs); + } + + static fromFrontendError(e: unknown, runtime?: number): ConnectError { + if (e instanceof ConnectError) { + return e; + } + + if (e instanceof DOMException) { + switch (e.name) { + case 'NotAllowedError': + case 'AbortError': + return new ConnectError(ConnectErrorType.Cancel, e.message, runtime); + case 'SecurityError': + return new ConnectError(ConnectErrorType.SecurityError, e.message, runtime); + case 'InvalidStateError': + return new ConnectError(ConnectErrorType.ExcludeCredentialsMatch, e.message, runtime); + default: + return new ConnectError(ConnectErrorType.InvalidState, e.message, runtime); + } + } + + if (e instanceof Error) { + if (e.name === 'CanceledError') { + return new ConnectError(ConnectErrorType.Cancel, e.message); + } + + return new ConnectError(ConnectErrorType.InvalidState, e.message); + } + + return new ConnectError(ConnectErrorType.InvalidState, `unknown ${e}`); + } +} diff --git a/packages/web-core/src/utils/errors/errors.ts b/packages/web-core/src/utils/errors/errors.ts index 8b57146f6..1bfdef134 100644 --- a/packages/web-core/src/utils/errors/errors.ts +++ b/packages/web-core/src/utils/errors/errors.ts @@ -83,28 +83,6 @@ export class CorbadoError extends Error { return NonRecoverableError.unhandledBackendError(errorResp.type); } - static fromConnectAxiosError(error: AxiosError): RecoverableError | NonRecoverableError { - log.debug('axios error', error); - - if (error.code === 'ECONNABORTED' || error.code === 'ERR_NETWORK') { - return new ConnectRequestTimedOut(); - } - - if (error.name === 'CanceledError') { - return CorbadoError.ignore(); - } - - if (!error.response || !error.response.data) { - return NonRecoverableError.unhandledBackendError('no_data_in_response'); - } - - const errorRespRaw = error.response.data as ErrorRsp; - log.debug('errorRespRaw', errorRespRaw.error.type); - const errorResp = errorRespRaw.error; - - return NonRecoverableError.unhandledBackendError(errorResp.type); - } - static fromDOMException(e: DOMException): CorbadoError { log.debug('e', e.name, e.message); switch (e.name) { diff --git a/packages/web-core/src/utils/errors/index.ts b/packages/web-core/src/utils/errors/index.ts index f72bc43e2..2b57f1ff2 100644 --- a/packages/web-core/src/utils/errors/index.ts +++ b/packages/web-core/src/utils/errors/index.ts @@ -1 +1,2 @@ export * from './errors'; +export * from './connectErrors'; From 047a34e1f6ceaf0bf951fb4101b738203591aee2 Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 9 Apr 2025 15:05:40 +0200 Subject: [PATCH 2/3] Linter --- .../connect-react/src/components/append/AppendInitScreen.tsx | 3 ++- .../src/components/login/LoginErrorScreenSoft.tsx | 3 ++- packages/connect-react/src/contexts/AppendProcessContext.ts | 2 +- packages/connect-react/src/contexts/AppendProcessProvider.tsx | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/connect-react/src/components/append/AppendInitScreen.tsx b/packages/connect-react/src/components/append/AppendInitScreen.tsx index da587ceb4..f94b2d66e 100644 --- a/packages/connect-react/src/components/append/AppendInitScreen.tsx +++ b/packages/connect-react/src/components/append/AppendInitScreen.tsx @@ -1,4 +1,5 @@ -import { ConnectError, ConnectErrorType } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType } from '@corbado/web-core'; import log from 'loglevel'; import React, { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx index f4a86f494..5c6b5d506 100644 --- a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx +++ b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx @@ -1,4 +1,5 @@ -import { ConnectError, ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; +import type { ConnectError } from '@corbado/web-core'; +import { ConnectErrorType, PasskeyLoginSource } from '@corbado/web-core'; import log from 'loglevel'; import React, { useState } from 'react'; diff --git a/packages/connect-react/src/contexts/AppendProcessContext.ts b/packages/connect-react/src/contexts/AppendProcessContext.ts index 6bf77b093..6513b5f3a 100644 --- a/packages/connect-react/src/contexts/AppendProcessContext.ts +++ b/packages/connect-react/src/contexts/AppendProcessContext.ts @@ -1,10 +1,10 @@ import type { CorbadoConnectAppendConfig } from '@corbado/types'; +import type { ConnectError } from '@corbado/web-core'; import { createContext } from 'react'; import type { Flags } from '../types/flags'; import { AppendScreenType } from '../types/screenTypes'; import type { AppendSituationCode } from '../types/situations'; -import { ConnectError } from '@corbado/web-core'; const missingImplementation = (): never => { throw new Error('Please make sure that your components are wrapped inside '); diff --git a/packages/connect-react/src/contexts/AppendProcessProvider.tsx b/packages/connect-react/src/contexts/AppendProcessProvider.tsx index d3a945112..30cf0d347 100644 --- a/packages/connect-react/src/contexts/AppendProcessProvider.tsx +++ b/packages/connect-react/src/contexts/AppendProcessProvider.tsx @@ -1,4 +1,5 @@ import type { AppendStatus, CorbadoConnectAppendConfig } from '@corbado/types'; +import type { ConnectError } from '@corbado/web-core'; import log from 'loglevel'; import type { FC, PropsWithChildren } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; @@ -9,7 +10,6 @@ import type { AppendScreenType } from '../types/screenTypes'; import type { AppendSituationCode } from '../types/situations'; import type { AppendProcessContextProps } from './AppendProcessContext'; import AppendProcessContext from './AppendProcessContext'; -import type { ConnectError } from '@corbado/web-core'; type Props = { config: CorbadoConnectAppendConfig; From ccff17c577735c5ad871bdda1ca904a0b532ac3a Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 9 Apr 2025 15:16:51 +0200 Subject: [PATCH 3/3] Fix new error handling in passkey-list --- .../passkeyList/PasskeyListScreen.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx index 01f2e9e4e..72750265c 100644 --- a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx +++ b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx @@ -1,7 +1,6 @@ import type { CorbadoConnectPasskeyListConfig } from '@corbado/types'; import type { ConnectError, Passkey } from '@corbado/web-core'; import { ConnectErrorType } from '@corbado/web-core'; -import { ExcludeCredentialsMatchError, PasskeyChallengeCancelledError } from '@corbado/web-core'; import log from 'loglevel'; import React, { useEffect, useRef, useState } from 'react'; @@ -96,7 +95,7 @@ const PasskeyListScreen = () => { const deletePasskeyRes = await getConnectService().manageDelete(deleteToken, credentialsId); if (deletePasskeyRes.err) { - return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringDelete); + return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringDelete, deletePasskeyRes.val); } await getPasskeyList(config, true); @@ -120,7 +119,7 @@ const PasskeyListScreen = () => { const loadedMs = Date.now(); const startAppendRes = await getConnectService().startAppend(appendToken, loadedMs, undefined, true); if (startAppendRes.err) { - return handleSituation(PasskeyListSituationCode.CboApiNotAvailablePreAuthenticator); + return handleSituation(PasskeyListSituationCode.CboApiNotAvailablePreAuthenticator, startAppendRes.val); } if (!startAppendRes.val.attestationOptions) { @@ -133,15 +132,15 @@ const PasskeyListScreen = () => { const res = await getConnectService().completeAppend(startAppendRes.val.attestationOptions); if (res.err) { - if (res.val instanceof PasskeyChallengeCancelledError) { - return handleSituation(PasskeyListSituationCode.ClientPasskeyOperationCancelled); + if (res.val.type === ConnectErrorType.Cancel) { + return handleSituation(PasskeyListSituationCode.ClientPasskeyOperationCancelled, res.val); } - if (res.val instanceof ExcludeCredentialsMatchError) { - return handleSituation(PasskeyListSituationCode.ClientExcludeCredentialsMatch); + if (res.val.type === ConnectErrorType.ExcludeCredentialsMatch) { + return handleSituation(PasskeyListSituationCode.ClientExcludeCredentialsMatch, res.val); } - return handleSituation(PasskeyListSituationCode.CboApiNotAvailablePostAuthenticator); + return handleSituation(PasskeyListSituationCode.CboApiNotAvailablePostAuthenticator, res.val); } log.debug('get passkey list'); @@ -164,7 +163,7 @@ const PasskeyListScreen = () => { const passkeyList = await getConnectService().manageList(listTokenRes, triggerSignalAllAccepted); if (passkeyList.err) { - return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringInitialLoad); + return handleSituation(PasskeyListSituationCode.CboApiNotAvailableDuringInitialLoad, passkeyList.val); } console.log('passkeyList', passkeyList.val.passkeys);