Skip to content

FUI - Reset password #2530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/apim.runtime.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import { SignInRuntimeModule } from "./components/users/signin/signin.runtime.mo
import { SignInSocialRuntimeModule } from "./components/users/signin-social/signinSocial.runtime.module";
import { SignUpRuntimeModule } from "./components/users/signup/signup.runtime.module";
import { ProfileRuntimeModule } from "./components/users/profile/profile.runtime.module";
import { ResetPasswordRuntimeModule } from "./components/users/reset-password/resetPassword.runtime.module";
import { SubscriptionsRuntimeModule } from "./components/users/subscriptions/subscriptions.runtime.module";
import { ValidationSummaryRuntimeModule } from "./components/users/validation-summary/validationSummary.runtime.module";

Expand Down Expand Up @@ -196,6 +197,7 @@ export class ApimRuntimeModule implements IInjectorModule {
injector.bindModule(new SignInSocialRuntimeModule());
injector.bindModule(new SignUpRuntimeModule());
injector.bindModule(new ProfileRuntimeModule());
injector.bindModule(new ResetPasswordRuntimeModule());
injector.bindModule(new SubscriptionsRuntimeModule());
injector.bindModule(new ValidationSummaryRuntimeModule());

Expand Down
6 changes: 6 additions & 0 deletions src/components/users/reset-password/ko/resetPassword.html
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
<!-- ko if: isRedesignEnabled -->
<fui-reset-password data-bind="attr: { props: runtimeConfig }"></fui-reset-password>
<!-- /ko -->

<!-- ko ifnot: isRedesignEnabled -->
<reset-password-runtime data-bind="attr: { params: runtimeConfig }"></reset-password-runtime>
<!-- /ko -->
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { Component } from "@paperbits/common/ko/decorators";
})
export class ResetPasswordViewModel {
public readonly runtimeConfig: ko.Observable<string>;
public readonly isRedesignEnabled: ko.Observable<boolean>;

constructor() {
this.runtimeConfig = ko.observable();
this.isRedesignEnabled = ko.observable<boolean>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import { ViewModelBinder, WidgetState } from "@paperbits/common/widgets";
import { ResetPasswordViewModel } from "./resetPasswordViewModel";
import { ResetPasswordModel } from "../resetPasswordModel";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { ISiteService } from "@paperbits/common/sites/ISiteService";
import { isRedesignEnabledSetting } from "../../../../constants";

export class ResetPasswordViewModelBinder implements ViewModelBinder<ResetPasswordModel, ResetPasswordViewModel> {
constructor(private readonly settingsProvider: ISettingsProvider) {}
constructor(
private readonly settingsProvider: ISettingsProvider,
private readonly siteService: ISiteService,
) { }

public stateToInstance(state: WidgetState, componentInstance: ResetPasswordViewModel): void {
componentInstance.runtimeConfig(JSON.stringify({
requireHipCaptcha: state.useHipCaptcha === undefined ? true : state.useHipCaptcha
}));

componentInstance.isRedesignEnabled(state.isRedesignEnabled);
}

public async modelToState(model: ResetPasswordModel, state: WidgetState): Promise<void> {
const useHipCaptcha = await this.settingsProvider.getSetting<boolean>("useHipCaptcha");
state.requireHipCaptcha = useHipCaptcha;

state.isRedesignEnabled = !!(await this.siteService.getSetting(isRedesignEnabledSetting));
}
}
83 changes: 83 additions & 0 deletions src/components/users/reset-password/react/ResetPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from "react";
import { useCallback } from "react";
import { Stack } from "@fluentui/react";
import { Body1, Body1Strong, Input, Label } from "@fluentui/react-components";
import { HipCaptcha } from "../../runtime/hip-captcha/react";
import { TCaptchaObj, TOnInitComplete } from "../../runtime/hip-captcha/react/LegacyCaptcha";
import { BtnSpinner } from "../../../utils/react/BtnSpinner";
import { BackendService } from "../../../../services/backendService";

export type TSubmit = (
email: string,
captchaObj: TCaptchaObj,
) => Promise<boolean>;

type ResetPasswordFormProps = {
requireHipCaptcha: boolean
backendService: BackendService
submit: TSubmit
}

export const ResetPasswordForm = ({
requireHipCaptcha,
backendService,
submit,
}: ResetPasswordFormProps) => {
const [email, setEmail] = React.useState("");
const [captchaObj, setCaptchaObj] = React.useState<TCaptchaObj>();
const [isSubmitted, setIsSubmitted] = React.useState(false);

const handleSubmit = async () => submit(email, captchaObj).then(setIsSubmitted);

const onInitComplete: TOnInitComplete = useCallback((captchaValid, refreshCaptcha, captchaData) => {
setCaptchaObj({ captchaValid, refreshCaptcha, captchaData });
}, []);

if (isSubmitted) return (
<>
<Body1Strong>Your password reset request was successfully processed</Body1Strong>
<br />
<Body1>
Change password confirmation email is on the way to {email}. Please follow the instructions within the email to continue your password change process.
</Body1>
</>
);

return (
<Stack tokens={{ childrenGap: 20, maxWidth: 435 }}>
<Stack.Item>
<Stack>
<Label required htmlFor="email">
Email address
</Label>
<Input
id="email"
placeholder="Enter email address"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
</Stack>
</Stack.Item>

{requireHipCaptcha && (
<Stack.Item>
<Label required>Captcha</Label>
<HipCaptcha
backendService={backendService}
onInitComplete={onInitComplete}
/>
</Stack.Item>
)}

<Stack.Item>
<BtnSpinner
appearance="primary"
onClick={handleSubmit}
>
Request reset
</BtnSpinner>
</Stack.Item>
</Stack>
)
}
107 changes: 107 additions & 0 deletions src/components/users/reset-password/react/ResetPasswordRuntime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from "react";
import { FluentProvider } from "@fluentui/react-components";
import { Resolve } from "@paperbits/react/decorators";
import { EventManager } from "@paperbits/common/events";
import { Logger } from "@paperbits/common/logging";
import * as Constants from "../../../../constants";
import { UsersService } from "../../../../services";
import { BackendService } from "../../../../services/backendService";
import { ResetPasswordForm, TSubmit } from "./ResetPasswordForm";
import { ValidationMessages } from "../../validationMessages";
import { dispatchErrors, parseAndDispatchError } from "../../validation-summary/utils";
import { ErrorSources } from "../../validation-summary/constants";
import { validateBasic } from "../../../utils/react/validateBasic";
import { ResetRequest } from "../../../../contracts/resetRequest";

type ResetPasswordRuntimeProps = {
requireHipCaptcha: boolean
};

export class ResetPasswordRuntime extends React.Component<ResetPasswordRuntimeProps> {
@Resolve("usersService")
public usersService: UsersService;

@Resolve("eventManager")
public eventManager: EventManager;

@Resolve("backendService")
public backendService: BackendService;

@Resolve("logger")
public logger: Logger;

async componentDidMount() {
const isUserSignedIn = await this.usersService.isUserSignedIn();

if (isUserSignedIn) {
this.usersService.navigateToHome();
return;
}
}

submit: TSubmit = async (
email,
{ captchaValid, refreshCaptcha, captchaData } = ({} as any),
) => {
const isCaptchaRequired = this.props.requireHipCaptcha;

const validationGroup = {
email: ValidationMessages.emailRequired,
}

if (isCaptchaRequired) {
if (!refreshCaptcha) {
this.logger.trackEvent("CaptchaValidation", { message: "Captcha failed to initialize." });
dispatchErrors(this.eventManager, ErrorSources.resetpassword, [ValidationMessages.captchaNotInitialized]);
return false;
}

validationGroup["captchaValid"] = ValidationMessages.captchaRequired;
}

const values = { email, captchaValid };
const clientErrors = validateBasic(values, validationGroup);

if (clientErrors.length > 0) {
dispatchErrors(this.eventManager, ErrorSources.resetpassword, clientErrors);
return false;
}

try {
dispatchErrors(this.eventManager, ErrorSources.resetpassword, []);

if (isCaptchaRequired) {
const resetRequest: ResetRequest = {
challenge: captchaData.challenge,
solution: captchaData.solution?.solution,
flowId: captchaData.solution?.flowId,
token: captchaData.solution?.token,
type: captchaData.solution?.type,
email,
};
await this.backendService.sendResetRequest(resetRequest);
} else {
await this.usersService.createResetPasswordRequest(email);
}

return true;
} catch (error) {
if (isCaptchaRequired) await refreshCaptcha();

parseAndDispatchError(this.eventManager, ErrorSources.resetpassword, error, this.logger, undefined, detail => `${detail.target}: ${detail.message} \n`);
return false;
}
}

render() {
return (
<FluentProvider theme={Constants.fuiTheme}>
<ResetPasswordForm
{...this.props}
backendService={this.backendService}
submit={this.submit.bind(this)}
/>
</FluentProvider>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IInjector, IInjectorModule } from "@paperbits/common/injection";
import { registerCustomElement } from "@paperbits/react/customElements";
import { ResetPasswordRuntime } from "./react/ResetPasswordRuntime";

export class ResetPasswordRuntimeModule implements IInjectorModule {
public register(injector: IInjector): void {
injector.bind("ResetPasswordRuntimeModule", ResetPasswordRuntime);
registerCustomElement(ResetPasswordRuntime, "fui-reset-password", injector);
}
}
17 changes: 9 additions & 8 deletions src/components/users/signin/react/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,15 @@ export const SignInForm = ({
</Stack>
</Stack.Item>

<BtnSpinner
style={{ maxWidth: "7em" }}
appearance="primary"
onClick={submit}
disabled={!email || !password}
>
Sign in
</BtnSpinner>
<Stack.Item>
<BtnSpinner
appearance="primary"
onClick={submit}
disabled={!email || !password}
>
Sign in
</BtnSpinner>
</Stack.Item>
</Stack>
);
};
15 changes: 8 additions & 7 deletions src/components/users/signup/react/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,14 @@ export const SignUpForm = ({
</Stack.Item>
)}

<BtnSpinner
style={{ maxWidth: "7em" }}
appearance="primary"
onClick={submit}
>
Create
</BtnSpinner>
<Stack.Item>
<BtnSpinner
appearance="primary"
onClick={submit}
>
Create
</BtnSpinner>
</Stack.Item>
</Stack>
);
};
8 changes: 4 additions & 4 deletions src/components/users/signup/react/SignUpRuntime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class SignUpRuntime extends React.Component<SignUpRuntimeProps> {
consented,
{ captchaValid, refreshCaptcha, captchaData } = ({} as any),
) => {
const captchaIsRequired = this.props.requireHipCaptcha;
const isCaptchaRequired = this.props.requireHipCaptcha;

const validationGroup = {
email: ValidationMessages.emailRequired,
Expand All @@ -121,7 +121,7 @@ export class SignUpRuntime extends React.Component<SignUpRuntimeProps> {
lastName: ValidationMessages.lastNameRequired,
}

if (captchaIsRequired) {
if (isCaptchaRequired) {
if (!refreshCaptcha) {
this.logger.trackEvent("CaptchaValidation", { message: "Captcha failed to initialize." });
dispatchErrors(this.eventManager, ErrorSources.resetpassword, [ValidationMessages.captchaNotInitialized]);
Expand Down Expand Up @@ -155,7 +155,7 @@ export class SignUpRuntime extends React.Component<SignUpRuntimeProps> {
try {
dispatchErrors(this.eventManager, ErrorSources.signup, []);

if (captchaIsRequired) {
if (isCaptchaRequired) {
const createSignupRequest: SignupRequest = {
challenge: captchaData.challenge,
solution: captchaData.solution?.solution,
Expand All @@ -172,7 +172,7 @@ export class SignUpRuntime extends React.Component<SignUpRuntimeProps> {

return true;
} catch (error) {
if (captchaIsRequired) await refreshCaptcha();
if (isCaptchaRequired) await refreshCaptcha();

parseAndDispatchError(this.eventManager, ErrorSources.signup, error, this.logger, Constants.genericHttpRequestError);
return false;
Expand Down
1 change: 1 addition & 0 deletions src/themes/website/styles/widgets/fui/fluentui.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fui-product-subscribe-runtime,
fui-product-subscriptions-runtime,
fui-signin-runtime,
fui-signup-runtime,
fui-reset-password,
fui-profile-runtime,
fui-subscriptions-runtime,
fui-validation-summary {
Expand Down