Skip to content

Commit

Permalink
Refactor flowHandler update method and added flowHandlerConfig class
Browse files Browse the repository at this point in the history
  • Loading branch information
Aby-JS committed Dec 21, 2023
1 parent bac14a6 commit 4f0c453
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 103 deletions.
11 changes: 1 addition & 10 deletions packages/react/src/contexts/FlowHandlerContext.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import type {
FlowHandlerEventOptions,
FlowHandlerEvents,
FlowNames,
FlowType,
ScreenNames,
UserState,
} from '@corbado/shared-ui';
import type { FlowHandlerEventOptions, FlowHandlerEvents, FlowNames, ScreenNames, UserState } from '@corbado/shared-ui';
import { CommonScreens, LoginFlowNames } from '@corbado/shared-ui';
import { createContext } from 'react';

Expand All @@ -15,7 +8,6 @@ export interface FlowHandlerContextProps {
currentUserState: UserState;
initialized: boolean;
navigateBack: () => ScreenNames;
changeFlow: (flowType: FlowType) => void;
emitEvent: (event?: FlowHandlerEvents, eventOptions?: FlowHandlerEventOptions) => Promise<void> | undefined;
}

Expand All @@ -25,7 +17,6 @@ export const initialContext: FlowHandlerContextProps = {
currentUserState: {},
initialized: false,
navigateBack: () => CommonScreens.Start,
changeFlow: () => void 0,
emitEvent: () => Promise.reject(),
};

Expand Down
12 changes: 2 additions & 10 deletions packages/react/src/contexts/FlowHandlerProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,13 @@ export const FlowHandlerProvider: FC<PropsWithChildren<Props>> = ({ children, ..
return;
}

flowHandler?.updateUser(user);
flowHandler?.update(user);
}, [initialized, user]);

const navigateBack = useCallback(() => {
return flowHandler?.navigateBack() ?? CommonScreens.Start;
}, [flowHandler]);

const changeFlow = useCallback(
(flowType: FlowType) => {
flowHandler?.changeFlow(flowType);
},
[flowHandler],
);

const emitEvent = useCallback(
(event?: FlowHandlerEvents, eventOptions?: FlowHandlerEventOptions) => {
return flowHandler?.handleStateUpdate(event, eventOptions);
Expand All @@ -98,10 +91,9 @@ export const FlowHandlerProvider: FC<PropsWithChildren<Props>> = ({ children, ..
currentUserState,
initialized,
navigateBack,
changeFlow,
emitEvent,
}),
[currentFlow, currentScreen, currentUserState, initialized, navigateBack, changeFlow],
[currentFlow, currentScreen, currentUserState, initialized, navigateBack],
);

return <FlowHandlerContext.Provider value={contextValue}>{children}</FlowHandlerContext.Provider>;
Expand Down
127 changes: 65 additions & 62 deletions packages/shared-ui/src/flowHandler/flowHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,70 @@ import type { CorbadoApp } from '@corbado/web-core';
import type { i18n } from 'i18next';

import { canUsePasskeys } from '../utils';
import type { FlowHandlerEvents } from './constants';
import { CommonScreens, FlowNameByFlowStyle, FlowType, LoginFlowNames, SignUpFlowNames } from './constants';
import type { FlowHandlerEvents, FlowType } from './constants';
import { CommonScreens } from './constants';
import { FlowHandlerConfig } from './flowHandlerConfig';
import { FlowHandlerState } from './flowHandlerState';
import { flows } from './flows';
import type {
Flow,
FlowHandlerConfig,
FlowHandlerEventOptions,
FlowHandlerStateUpdate,
FlowNames,
FlowOptions,
ScreenNames,
UserState,
} from './types';
import type { FlowHandlerEventOptions, FlowHandlerStateUpdate, FlowNames, ScreenNames, UserState } from './types';

/**
* FlowHandler is a class that manages the navigation flow of the application.
* It keeps track of the current flow, the current screen, and the screen history.
* It also provides methods for navigating to the next screen, navigating back, and changing the flow.
*/
export class FlowHandler {
#currentFlow!: Flow;
#currentScreen: ScreenNames;
#screenHistory: ScreenNames[];
#flowName!: FlowNames;
#i18next: i18n;
#flowHandlerConfig: FlowHandlerConfig;
#projectConfig: ProjectConfig;
#signUpFlowName: SignUpFlowNames = SignUpFlowNames.PasskeySignupWithEmailOTPFallback;
#loginFlowName: LoginFlowNames = LoginFlowNames.PasskeyLoginWithEmailOTPFallback;
#config: FlowHandlerConfig;
#state: FlowHandlerState | undefined;

#onScreenUpdateCallbacks: Array<(screen: ScreenNames) => void> = [];
#onFlowUpdateCallbacks: Array<(flow: FlowNames) => void> = [];
#onUserStateChangeCallbacks: Array<(v: UserState) => void> = [];

#state!: FlowHandlerState;

/**
* The constructor initializes the FlowHandler with a flow name, a project configuration, and a flow handler configuration.
* It sets the current flow to the specified flow, the current screen to the Start screen, and initializes the screen history as an empty array.
*/
constructor(projectConfig: ProjectConfig, flowHandlerConfig: FlowHandlerConfig, i18next: i18n) {
this.#flowHandlerConfig = flowHandlerConfig;
constructor(projectConfig: ProjectConfig, onLoggedIn: () => void) {
this.#config = new FlowHandlerConfig(onLoggedIn, projectConfig);
this.#screenHistory = [];
this.#currentScreen = CommonScreens.Start;
this.#i18next = i18next;
this.#projectConfig = projectConfig;
this.#signUpFlowName =
FlowNameByFlowStyle[projectConfig.signupFlow].SignUp ?? SignUpFlowNames.PasskeySignupWithEmailOTPFallback;
this.#loginFlowName =
FlowNameByFlowStyle[projectConfig.loginFlow].Login ?? LoginFlowNames.PasskeyLoginWithEmailOTPFallback;
}

/**
* Initializes the FlowHandler.
* Call this function after registering all callbacks.
*/
async init(corbadoApp: CorbadoApp | undefined) {
async init(corbadoApp: CorbadoApp | undefined, i18next: i18n) {
if (!corbadoApp) {
throw new Error('corbadoApp is undefined. This should not happen.');
}

const passkeysSupported = await canUsePasskeys();

this.#state = new FlowHandlerState(
this.#getFlowOptions(this.#flowHandlerConfig.initialFlowType),
this.#config.flowOptions,
{
email: undefined,
fullName: undefined,
emailError: undefined,
},
passkeysSupported,
corbadoApp,
this.#i18next,
i18next,
);

this.changeFlow(this.#flowHandlerConfig.initialFlowType);
this.#changeFlow();
}

get currentScreenName() {
return this.#currentScreen;
}

get currentFlowName() {
return this.#flowName;
return this.#config.flowName;
}

/**
Expand Down Expand Up @@ -146,34 +125,53 @@ export class FlowHandler {
this.#onFlowUpdateCallbacks[cbId] = cb;
}

/**
* Method to add a callback function to be called when the user state changes.
* @param cb The callback function to be called when the user state changes.
* @returns The callback id.
*/
onUserStateChange(cb: (v: UserState) => void) {
const cbId = this.#onUserStateChangeCallbacks.push(cb) - 1;

return cbId;
}

/**
* Method to remove a callback function that was registered with onUserStateChange.
* @param cbId The callback id returned by onUserStateChange.
*/
removeOnUserStateChange(cbId: number) {
this.#onUserStateChangeCallbacks.splice(cbId, 1);
}

/**
* Method to handle state updates. It calls the current flow's state updater with the current state, event, and event options.
* If the state updater returns a flow update, it changes the flow, updates the state, and changes the screen as specified by the flow update.
* @param event The event that triggered the state update.
* @param eventOptions The options for the event.
*/
async handleStateUpdate(event?: FlowHandlerEvents, eventOptions?: FlowHandlerEventOptions) {
const stateUpdater = this.#currentFlow[this.#currentScreen];
if (!this.#state) {
throw new Error('FlowHandler is not initialized');
}

const stateUpdater = flows[this.#config.flowName][this.#currentScreen];
if (!stateUpdater) {
throw new Error('Invalid screen');
}

const flowUpdate = await stateUpdater(this.#state, event, eventOptions);
if (flowUpdate && flowUpdate?.nextFlow !== null) {
this.changeFlow(flowUpdate.nextFlow);
this.#changeFlow(flowUpdate.nextFlow);
}

if (flowUpdate?.stateUpdate) {
this.#changeUserState({ userState: flowUpdate.stateUpdate });
this.#changeState({ userState: flowUpdate.stateUpdate });
}

if (flowUpdate?.nextScreen) {
if (flowUpdate.nextScreen === CommonScreens.End) {
return void this.#flowHandlerConfig.onLoggedIn();
return void this.#config.onLoggedIn();
}

this.#screenHistory.push(this.#currentScreen);
Expand All @@ -185,6 +183,7 @@ export class FlowHandler {
}
}

//TODO: Remove navigateBack method and make it part as a state update as FlowHandlerEvents.Back
/**
* Method to navigate back to the previous screen.
* If there is no previous screen, it navigates to the Start screen.
Expand All @@ -205,51 +204,55 @@ export class FlowHandler {
return this.#currentScreen;
}

//TODO: Remove update method and make it part as a state update by adding a subscriber on corbadoApp.authService.userChanges in FlowHandlerState
/**
* Method to update the user state with a new user.
* @param user The new user.
*/
update(user: SessionUser) {
this.#changeState({ user: user });
}

/**
* Method to change the current flow.
* It sets the current flow to the specified flow, resets the current screen to the Start screen, and clears the screen history.
* It calls any registered onFlowUpdate callbacks with the new flow, and any registered onScreenUpdate callbacks with the new current screen.
* @param flowType - The new flow.
* @param screen - The new current screen.
* @returns The new current screen.
* @param flowType
*/
changeFlow(flowType: FlowType) {
let flowName: FlowNames;

if (flowType === FlowType.SignUp) {
flowName = this.#signUpFlowName;
} else {
flowName = this.#loginFlowName;
#changeFlow(flowType?: FlowType, screen: CommonScreens = CommonScreens.Start) {
if (flowType) {
this.#config.update(flowType);
}

this.#state.updateFlowOptions(this.#getFlowOptions(flowType));
const flowName = this.#config.flowName;
const flowOptions = this.#config.flowOptions;

this.#currentFlow = flows[flowName];
this.#flowName = flowName;
this.#currentScreen = CommonScreens.Start;
this.#changeState({ flowOptions });

this.#currentScreen = screen;
this.#screenHistory = [];

if (this.#onFlowUpdateCallbacks.length) {
this.#onFlowUpdateCallbacks.forEach(cb => cb(this.#flowName));
this.#onFlowUpdateCallbacks.forEach(cb => cb(flowName));
}

if (this.#onScreenUpdateCallbacks.length) {
this.#onScreenUpdateCallbacks.forEach(cb => cb(this.#currentScreen));
}

return this.#currentScreen;
}

updateUser(user: SessionUser) {
this.#changeUserState({ user: user });
return screen;
}

#getFlowOptions(flowType: FlowType): Partial<FlowOptions> {
return flowType === FlowType.SignUp ? this.#projectConfig.signupFlowOptions : this.#projectConfig.loginFlowOptions;
}
#changeState(update: FlowHandlerStateUpdate) {
if (!this.#state) {
throw new Error('FlowHandler is not initialized');
}

#changeUserState(update: FlowHandlerStateUpdate) {
this.#state.updateUser(update);
this.#state.update(update);

this.#onUserStateChangeCallbacks.forEach(cb => cb(this.#state.userState));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.#onUserStateChangeCallbacks.forEach(cb => cb(this.#state!.userState));
}
}
15 changes: 5 additions & 10 deletions packages/shared-ui/src/flowHandler/flowHandlerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,23 @@ export class FlowHandlerState {
* Allows to update the internal state of the FlowHandler User.
* @param update
*/
updateUser(update: FlowHandlerStateUpdate) {
const newState = update.userState || defaultErrors || this.#userState;
this.#userState = this.withTranslation(newState);
update(update: FlowHandlerStateUpdate) {
const newState = update.userState || defaultErrors;
this.#userState = this.#withTranslation(newState);
this.#user = update.user || this.#user;
}

/**
* Allows to update the internal state of the FlowHandler FlowOptions.
*/
updateFlowOptions(updateFlowOptions: Partial<FlowOptions>) {
//TODO: Remove defaultOptions once BE has added support for flow options
this.#flowOptions = {
...defaultFlowOptions,
...updateFlowOptions,
...update.flowOptions,
};
}

/**
* Here we translate the error messages. This is a very simple implementation, but it should be enough for now.
* @param userState
*/
withTranslation(userState: UserState): UserState {
#withTranslation(userState: UserState): UserState {
if (userState.emailError) {
userState.emailError.translatedMessage = this.#i18next.t(userState.emailError.name);
}
Expand Down
12 changes: 1 addition & 11 deletions packages/shared-ui/src/flowHandler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
CommonScreens,
EmailOtpSignupScreens,
FlowHandlerEvents,
FlowType,
LoginFlowNames,
PasskeyLoginWithEmailOtpFallbackScreens,
PasskeySignupWithEmailOtpFallbackScreens,
Expand All @@ -14,16 +13,6 @@ import type {
import type { FlowHandlerState } from './flowHandlerState';
import type { FlowUpdate } from './flowUpdate';

/**
* Configuration settings for handling different authentication flows.
*/
export interface FlowHandlerConfig {
// callback that will be executed when a flow reached its end
onLoggedIn: () => void;
// initial flow to start with
initialFlowType: FlowType;
}

/**
* Configuration options for the passkey sign-up with email OTP fallback flow.
*/
Expand Down Expand Up @@ -107,6 +96,7 @@ export type UserState = {
export type FlowHandlerStateUpdate = {
userState?: UserState;
user?: SessionUser;
flowOptions?: Partial<FlowOptions>;
};

export type EmailOTPState = {
Expand Down

0 comments on commit 4f0c453

Please sign in to comment.