Skip to content

Commit

Permalink
Add generic fallback handling (#520)
Browse files Browse the repository at this point in the history
* Add generic fallback handling

* Fix error message

* Stop wrong login error collection

* Manage cui loading state
  • Loading branch information
incorbador authored Feb 25, 2025
1 parent 9b0f9a6 commit c19fc4b
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConnectUserNotFound, PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core';
import { 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';
Expand Down Expand Up @@ -79,10 +79,6 @@ const InitScreen = () => {
return;
}

if (resStart.val instanceof ConnectUserNotFound) {
return handleSituation(LoginSituationCode.PreAuthenticatorUserNotFound);
}

return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useShared from '../../hooks/useShared';
import { LoginScreenType } from '../../types/screenTypes';
import { getLoginErrorMessage, LoginSituationCode } from '../../types/situations';
import LoginErrorHard from './base/LoginErrorHard';
import { connectLoginFinishToComplete } from './LoginInitScreen';
import { type CboApiFallbackOperationError, connectLoginFinishToComplete } from './LoginInitScreen';

type Props = {
previousAssertionOptions: string;
Expand All @@ -32,6 +32,16 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => {
return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator);
}

if (resStart.val.assertionOptions.length === 0) {
const data: CboApiFallbackOperationError = {
initFallback: resStart.val.fallbackOperationError.initFallback,
identifierFallback: resStart.val.fallbackOperationError.identifier ?? '',
message: resStart.val.fallbackOperationError.error?.message ?? null,
};

return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data);
}

setAssertionOptions(resStart.val.assertionOptions);

const resFinish = await getConnectService().loginContinue(resStart.val);
Expand All @@ -56,7 +66,7 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => {
}
};

const handleSituation = (situationCode: LoginSituationCode) => {
const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => {
const messageCode = `situation: ${situationCode}`;
log.debug(messageCode);

Expand Down Expand Up @@ -92,6 +102,17 @@ const LoginErrorScreenHard = ({ previousAssertionOptions }: Props) => {

void getConnectService().recordEventLoginExplicitAbort(assertionOptions);
break;
case LoginSituationCode.CboApiFallbackOperationError: {
const { initFallback, identifierFallback, message } = data as CboApiFallbackOperationError;
if (initFallback) {
navigateToScreen(LoginScreenType.Invisible);
fallback(identifierFallback, message);
}
void getConnectService().recordEventLoginError(messageCode);

setLoading(false);
break;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useShared from '../../hooks/useShared';
import { LoginScreenType } from '../../types/screenTypes';
import { getLoginErrorMessage, LoginSituationCode } from '../../types/situations';
import LoginErrorSoft from './base/LoginErrorSoft';
import type { CboApiFallbackOperationError } from './LoginInitScreen';
import { connectLoginFinishToComplete } from './LoginInitScreen';

type Props = {
Expand All @@ -30,6 +31,16 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => {
return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator);
}

if (resStart.val.assertionOptions.length === 0) {
const data: CboApiFallbackOperationError = {
initFallback: resStart.val.fallbackOperationError.initFallback,
identifierFallback: resStart.val.fallbackOperationError.identifier ?? '',
message: resStart.val.fallbackOperationError.error?.message ?? null,
};

return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data);
}

const resFinish = await getConnectService().loginContinue(resStart.val);
if (resFinish.err) {
if (resFinish.val instanceof PasskeyChallengeCancelledError) {
Expand All @@ -55,6 +66,7 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => {
const message = getLoginErrorMessage(situationCode);

switch (situationCode) {
case LoginSituationCode.CboApiNotAvailablePreAuthenticator:
case LoginSituationCode.CtApiNotAvailablePostAuthenticator:
case LoginSituationCode.CboApiNotAvailablePostAuthenticator:
navigateToScreen(LoginScreenType.Invisible);
Expand All @@ -79,6 +91,17 @@ const LoginErrorScreenSoft = ({ previousAssertionOptions }: Props) => {
void getConnectService().recordEventLoginExplicitAbort(previousAssertionOptions);
break;
}
case LoginSituationCode.CboApiFallbackOperationError: {
const { initFallback, identifierFallback, message } = data as CboApiFallbackOperationError;
if (initFallback) {
navigateToScreen(LoginScreenType.Invisible);
fallback(identifierFallback, message);
}
void getConnectService().recordEventLoginError(messageCode);

setLoading(false);
break;
}
}
};

Expand Down
83 changes: 37 additions & 46 deletions packages/connect-react/src/components/login/LoginInitScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
ConnectConditionalUIPasskeyDeleted,
ConnectCustomError,
ConnectExistingPasskeysNotAvailable,
ConnectNoPasskeyAvailableError,
ConnectUserNotFound,
PasskeyChallengeCancelledError,
PasskeyLoginSource,
} from '@corbado/web-core';
import { PasskeyChallengeCancelledError, PasskeyLoginSource } from '@corbado/web-core';
import type { ConnectLoginFinishRsp } from '@corbado/web-core/dist/api/v2';
import log from 'loglevel';
import type { FC } from 'react';
Expand All @@ -16,12 +8,17 @@ import useLoginProcess from '../../hooks/useLoginProcess';
import useShared from '../../hooks/useShared';
import { Flags } from '../../types/flags';
import { LoginScreenType } from '../../types/screenTypes';
import type { PreAuthenticatorCustomErrorData } from '../../types/situations';
import { getLoginErrorMessage, LoginSituationCode } from '../../types/situations';
import { StatefulLoader } from '../../utils/statefulLoader';
import LoginInitLoaded from './base/LoginInitLoaded';
import LoginInitLoading from './base/LoginInitLoading';

export type CboApiFallbackOperationError = {
initFallback: boolean;
identifierFallback: string;
message: string | null;
};

export enum LoginInitState {
SilentLoading,
Loading,
Expand All @@ -42,8 +39,7 @@ export const connectLoginFinishToComplete = (v: ConnectLoginFinishRsp): string =
};

const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
const { config, navigateToScreen, setCurrentIdentifier, setFlags, flags, loadedMs, fallback, fallbackCustom } =
useLoginProcess();
const { config, navigateToScreen, setCurrentIdentifier, setFlags, flags, loadedMs, fallback } = useLoginProcess();
const { sharedConfig, getConnectService } = useShared();
const [cuiBasedLoading, setCuiBasedLoading] = useState(false);
const [identifierBasedLoading, setIdentifierBasedLoading] = useState(false);
Expand Down Expand Up @@ -160,11 +156,6 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
return handleSituation(LoginSituationCode.ClientPasskeyConditionalOperationCancelled);
}

// if a passkey has been deleted, CUI will fail => fallback with message
if (res.val instanceof ConnectConditionalUIPasskeyDeleted) {
return handleSituation(LoginSituationCode.PasskeyNotAvailablePostConditionalAuthenticator);
}

// cuiStarted === true indicates that user has passed the authenticator
if (cuiStarted) {
return handleSituation(LoginSituationCode.CboApiNotAvailablePostConditionalAuthenticator);
Expand All @@ -173,6 +164,16 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
return handleSituation(LoginSituationCode.CboApiNotAvailablePreConditionalAuthenticator);
}

if (res.val.fallbackOperationError) {
const data: CboApiFallbackOperationError = {
initFallback: res.val.fallbackOperationError.initFallback,
identifierFallback: res.val.fallbackOperationError.identifier ?? '',
message: res.val.fallbackOperationError.error?.message ?? null,
};

return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data);
}

try {
await config.onComplete(connectLoginFinishToComplete(res.val));
} catch {
Expand All @@ -191,19 +192,6 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {

const resStart = await getConnectService().loginStart(identifier, PasskeyLoginSource.TextField, loadedMs);
if (resStart.err) {
if (resStart.val instanceof ConnectUserNotFound) {
return handleSituation(LoginSituationCode.PreAuthenticatorUserNotFound);
}
if (resStart.val instanceof ConnectCustomError) {
return handleSituation(LoginSituationCode.PreAuthenticatorCustomError, resStart.val);
}
if (resStart.val instanceof ConnectExistingPasskeysNotAvailable) {
return handleSituation(LoginSituationCode.PreAuthenticatorExistingPasskeysNotAvailable);
}
if (resStart.val instanceof ConnectNoPasskeyAvailableError) {
return handleSituation(LoginSituationCode.PreAuthenticatorNoPasskeyAvailable);
}

return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator);
}

Expand All @@ -212,6 +200,16 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
return;
}

if (resStart.val.assertionOptions.length === 0) {
const data: CboApiFallbackOperationError = {
initFallback: resStart.val.fallbackOperationError.initFallback,
identifierFallback: resStart.val.fallbackOperationError.identifier ?? '',
message: resStart.val.fallbackOperationError.error?.message ?? null,
};

return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data);
}

const res = await getConnectService().loginContinue(resStart.val);
if (res.err) {
setIdentifierBasedLoading(false);
Expand Down Expand Up @@ -255,13 +253,10 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
statefulLoader.current.finish();
break;
case LoginSituationCode.DeniedByPartialRollout:
case LoginSituationCode.PreAuthenticatorExistingPasskeysNotAvailable:
case LoginSituationCode.PreAuthenticatorNoPasskeyAvailable:
automaticFallback(identifier, message);

statefulLoader.current.finish();
break;
case LoginSituationCode.PasskeyNotAvailablePostConditionalAuthenticator:
case LoginSituationCode.CboApiNotAvailablePostConditionalAuthenticator:
case LoginSituationCode.CboApiNotAvailablePreConditionalAuthenticator:
case LoginSituationCode.CtApiNotAvailablePostAuthenticator:
Expand All @@ -280,26 +275,22 @@ const LoginInitScreen: FC<Props> = ({ showFallback = false }) => {
setIdentifierBasedLoading(false);
break;
}
case LoginSituationCode.PreAuthenticatorUserNotFound:
setError(message ?? '');
void getConnectService().recordEventLoginErrorUnexpected(messageCode);

setIdentifierBasedLoading(false);
break;
case LoginSituationCode.ExplicitFallbackByUser:
explicitFallback();

void getConnectService().recordEventLoginExplicitAbort();
break;
case LoginSituationCode.PreAuthenticatorCustomError: {
navigateToScreen(LoginScreenType.Invisible);
void getConnectService().recordEventLoginErrorUnexpected(messageCode);
if (!data) {
return fallback(identifier, null);
case LoginSituationCode.CboApiFallbackOperationError: {
const typed = data as CboApiFallbackOperationError;

if (typed.initFallback) {
return automaticFallback(typed.identifierFallback, typed.message);
}

const typed = data as PreAuthenticatorCustomErrorData;
fallbackCustom(identifier, typed.code, typed.message);
setError(typed.message ?? '');
setCuiBasedLoading(false);
setIdentifierBasedLoading(false);
break;
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useShared from '../../hooks/useShared';
import { LoginScreenType } from '../../types/screenTypes';
import { getLoginErrorMessage, LoginSituationCode } from '../../types/situations';
import LoginOneTap from './base/LoginOneTap';
import { connectLoginFinishToComplete } from './LoginInitScreen';
import { type CboApiFallbackOperationError, connectLoginFinishToComplete } from './LoginInitScreen';

export const LoginPasskeyReLoginScreen = () => {
const { config, navigateToScreen, setCurrentIdentifier, currentIdentifier, loadedMs, fallback } = useLoginProcess();
Expand All @@ -32,6 +32,16 @@ export const LoginPasskeyReLoginScreen = () => {
return handleSituation(LoginSituationCode.CboApiNotAvailablePreAuthenticator);
}

if (resStart.val.assertionOptions.length === 0) {
const data = {
initFallback: resStart.val.fallbackOperationError.initFallback,
identifierFallback: resStart.val.fallbackOperationError.identifier ?? '',
message: resStart.val.fallbackOperationError.error?.message ?? null,
};

return handleSituation(LoginSituationCode.CboApiFallbackOperationError, data);
}

const resFinish = await getConnectService().loginContinue(resStart.val);
if (resFinish.err) {
if (resFinish.val instanceof PasskeyChallengeCancelledError) {
Expand All @@ -53,7 +63,7 @@ export const LoginPasskeyReLoginScreen = () => {
navigateToScreen(LoginScreenType.Init, { prefilledIdentifier: identifier });
};

const handleSituation = (situationCode: LoginSituationCode) => {
const handleSituation = (situationCode: LoginSituationCode, data?: unknown) => {
const messageCode = `situation: ${situationCode}`;
log.debug(messageCode);

Expand All @@ -77,6 +87,17 @@ export const LoginPasskeyReLoginScreen = () => {

setLoading(false);
break;
case LoginSituationCode.CboApiFallbackOperationError: {
const { initFallback, identifierFallback, message } = data as CboApiFallbackOperationError;
if (initFallback) {
navigateToScreen(LoginScreenType.Invisible);
fallback(identifierFallback, message);
}
void getConnectService().recordEventLoginError(messageCode);

setLoading(false);
break;
}
}
};

Expand Down
12 changes: 1 addition & 11 deletions packages/connect-react/src/types/situations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum LoginSituationCode {
PreAuthenticatorCustomError,
PreAuthenticatorExistingPasskeysNotAvailable,
PreAuthenticatorNoPasskeyAvailable,
CboApiFallbackOperationError,
}

export enum AppendSituationCode {
Expand Down Expand Up @@ -41,11 +42,6 @@ export enum PasskeyListSituationCode {
CboApiPasskeysNotSupportedLight,
}

export type PreAuthenticatorCustomErrorData = {
code: string;
message: string;
};

export const getLoginErrorMessage = (code: LoginSituationCode): string | null => {
switch (code) {
case LoginSituationCode.CboApiNotAvailablePostAuthenticator:
Expand All @@ -54,12 +50,6 @@ export const getLoginErrorMessage = (code: LoginSituationCode): string | null =>
case LoginSituationCode.ClientPasskeyOperationCancelledTooManyTimes:
return "We couldn't log you in with your passkey due to a system error. Use your password to log in instead.";

case LoginSituationCode.PasskeyNotAvailablePostConditionalAuthenticator:
return 'You previously deleted this passkey. Use your password to log in instead.';

case LoginSituationCode.PreAuthenticatorUserNotFound:
return 'There is no account registered to that email address.';

default:
return null;
}
Expand Down
Loading

0 comments on commit c19fc4b

Please sign in to comment.