Skip to content

Commit

Permalink
[CA-4293] Pouvoir réinitialiser son mot de passe en fournissant un ph…
Browse files Browse the repository at this point in the history
…one_number (#231)
  • Loading branch information
moust authored Nov 21, 2024
1 parent 4012973 commit 1feeccd
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 86 deletions.
6 changes: 5 additions & 1 deletion src/components/form/buttonComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ export const Button = styled(({ tagname = 'button', className, extendedClasses,
}
`;

export const DefaultButton = withTheme(props => (
interface DefaultButtonProps extends Omit<ButtonProps, 'background' | 'border' | 'color'> {
theme: DefaultTheme
}

export const DefaultButton = withTheme((props: PropsWithChildren<DefaultButtonProps>) => (
<Button {...props} background="#ffffff" border={props.theme.borderColor} color={props.theme.textColor} />
));

Expand Down
9 changes: 8 additions & 1 deletion src/components/form/fields/phoneNumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ function importLocale(locale: string) {
}

export type PhoneNumberOptions = {
/**
* If `withCountryCallingCode` property is explicitly set to true then the "country calling code" part (e.g. "+1" when country is "US") is included in the input field (but still isn't editable).
* @default true
*/
withCountryCallingCode?: boolean
/**
* If `withCountrySelect` property is `true` then the user can select the country for the phone number. Must be a supported {@link https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements country code}.
* @default false
Expand Down Expand Up @@ -76,6 +81,7 @@ const PhoneNumberField = (props: PhoneNumberFieldProps) => {
showLabel,
validation = {},
value,
withCountryCallingCode = true,
withCountrySelect = false,
} = props;

Expand Down Expand Up @@ -128,7 +134,7 @@ const PhoneNumberField = (props: PhoneNumberFieldProps) => {
onChange={handlePhoneChange}
labels={labels}
international={true}
withCountryCallingCode={true}
withCountryCallingCode={withCountryCallingCode}
{...(withCountrySelect
? ({
defaultCountry: country,
Expand Down Expand Up @@ -181,6 +187,7 @@ const phoneNumberField = (
extendedParams: {
country: isValidCountryCode(config.countryCode) ? config.countryCode : undefined,
locale: config.language,
withCountryCallingCode: props.withCountryCallingCode,
}
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/form/fields/simpleField.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Validator } from '../../../core/validation'
import type { FieldCreator, Formatter } from '../fieldCreator'
import { FormContext } from '../formComponent'

interface Mapping<T> {
bind: () => undefined,
Expand All @@ -14,7 +15,7 @@ export interface BaseOptions<T> {
autoComplete?: HTMLInputElement['autocomplete']
placeholder?: string
readOnly?: boolean
validator?: Validator
validator?: Validator<T, FormContext<any>>
format?: Formatter<T>
mapping?: Mapping
}
Expand Down
19 changes: 16 additions & 3 deletions src/components/form/formComponent.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import type { WithI18n } from '../../contexts/i18n'

import type { AppError } from '../../helpers/errors'

import type { FieldCreator } from './fieldCreator'
import type { FieldCreator, FieldValue } from './fieldCreator'

/** @todo to refine */
type FormContext<T> = {
errorMessage?: string
fields: FieldValues<T>
hasErrors?: boolean
isLoading?: boolean
isSubmitted: boolean,
}

type FieldValues<T> = {
[K in keyof T]: FieldValue<T[K]>
}

export interface PasswordPolicy {
minLength: number
Expand All @@ -19,7 +32,7 @@ export interface FormOptions<P = {}> {
fields?: FieldCreator[] | ((props: WithConfig<WithI18n<P>>) => FieldCreator[])
resetAfterSuccess?: boolean
resetAfterError?: boolean
submitLabel?: string
submitLabel?: string | ((props: WithConfig<P>) => string)
supportMultipleSubmits?: boolean
webAuthnButtons?: (disabled: boolean, enablePasswordAuthentication: boolean, clickHandler: (event: MouseEvent) => void) => ReactNode
}
Expand All @@ -30,7 +43,7 @@ export type WithFormProps<T, P = {}> = P & FormOptions<P> & {
handler?: (data: T) => Promise<unknown | void>
initialModel?: T
onError?: (error: Error | AppError) => void
onFieldChange?: (data: Record<string, { value?: string }>) => void
onFieldChange?: (data: FieldValues<T>) => void
onSuccess?: (result) => void
redirect?: (data: T) => void
supportMultipleSubmits?: boolean
Expand Down
8 changes: 4 additions & 4 deletions src/components/form/formComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,8 @@ export function createForm(config) {
}

handleFieldChange = (fieldName, stateUpdate) => {
this.props.onFieldChange(this.state.fields);

this.setState(prevState => {
const currentState = prevState.fields[fieldName];
const { validation, ...currentState } = prevState.fields[fieldName] ?? {};
const newState = {
...currentState,
...(typeof stateUpdate === 'function' ? stateUpdate(currentState) : stateUpdate)
Expand Down Expand Up @@ -138,6 +136,8 @@ export function createForm(config) {
}
};

this.props.onFieldChange(newFields);

return {
hasErrors: !!validation.error || find(newFields, ({ validation = {} }) => validation.error) !== undefined,
fields: newFields
Expand Down Expand Up @@ -234,7 +234,7 @@ export function createForm(config) {
}
{!allowWebAuthnLogin && (
<PrimaryButton disabled={isLoading}>
{i18n(submitLabel)}
{i18n(typeof submitLabel === 'function' ? submitLabel(this.props) : submitLabel)}
</PrimaryButton>
)}
{allowWebAuthnLogin && this.props.webAuthnButtons(isLoading, enablePasswordAuthentication, this.handleClick)}
Expand Down
10 changes: 5 additions & 5 deletions src/components/form/passwordSignupFormComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useLayoutEffect, useState } from 'react';
import { AuthOptions } from '@reachfive/identity-core';
import { SignupParams } from '@reachfive/identity-core/es/main/oAuthClient';

import { createForm } from './formComponent';
import { createForm, FieldValues } from './formComponent';
import { buildFormFields, type Field } from './formFieldFactory';
import { UserAggreementStyle } from './formControlsComponent'
import { type PhoneNumberOptions } from './fields/phoneNumberField';
Expand Down Expand Up @@ -78,10 +78,10 @@ export const PasswordSignupForm = ({
)

const refreshBlacklist = useCallback(
(data: Record<string, { value?: string }>) => {
const email = data['email'] && data['email'].value || '';
const givenName = data['given_name'] && data['given_name'].value || '';
const lastName = data['family_name'] && data['family_name'].value || '';
(data: FieldValues<SignupParams['data']>) => {
const email = data.email?.value || '';
const givenName = data.givenName?.value || '';
const lastName = data.familyName?.value || '';

const list = [
email.split('@'),
Expand Down
2 changes: 1 addition & 1 deletion src/components/reCaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class ReCaptcha {
return await window.grecaptcha.execute(siteKey, {action: action})
}

static handle = async <T extends { captchaToken?: string }>(data: T, conf: ReCaptchaConf, callback: (data: T) => Promise<unknown>, action: string) => {
static handle = async <T extends { captchaToken?: string }, R = {}>(data: T, conf: ReCaptchaConf, callback: (data: T) => Promise<R>, action: string) => {
if (conf.recaptcha_enabled)
{
try {
Expand Down
2 changes: 1 addition & 1 deletion src/components/widget/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function multiViewWidget<P, U>({ initialView, views, initialState = {} as MultiW
};

// _goTo = <View extends keyof typeof views, S extends ComponentProps<(typeof views)[View]>>(view: View, props?: S) => this.setState({
_goTo = <S extends Record<string, unknown>>(view: string, params?: S) => this.setState({
_goTo = <S extends Record<string, unknown>>(view: keyof typeof views, params?: S) => this.setState({
activeView: view as MultiWidgetState['activeView'],
...params
});
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ export function debounce(func: (...args: unknown[]) => void, delay: number, { le
let timerId: NodeJS.Timeout

return (...args: unknown[]) => {
if (!timerId && leading) {
func(...args)
}
clearTimeout(timerId)
if (!timerId && leading) {
func(...args)
}
clearTimeout(timerId)

timerId = setTimeout(() => func(...args), delay)
timerId = setTimeout(() => func(...args), delay)
}
}
}
6 changes: 3 additions & 3 deletions src/widgets/auth/authWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import LoginWithPasswordView, { type LoginWithPasswordViewProps } from './views/
import SignupView, { type SignupViewProps } from './views/signupViewComponent'
import SignupWithPasswordView, { type SignupWithPasswordViewProps } from './views/signupWithPasswordViewComponent'
import SignupWithWebAuthnView, { type SignupWithWebAuthnViewProps} from './views/signupWithWebAuthnViewComponent'
import { ForgotPasswordView, ForgotPasswordSuccessView, type ForgotPasswordViewProps, type ForgotPasswordSuccessViewProps } from './views/forgotPasswordViewComponent'
import { ForgotPasswordView, ForgotPasswordCodeView, ForgotPasswordSuccessView, type ForgotPasswordViewProps, ForgotPasswordPhoneNumberView } from './views/forgotPasswordViewComponent'
import QuickLoginView, { type QuickLoginViewProps } from './views/quickLoginViewComponent'
import ReauthView, { type ReauthViewProps } from './views/reauthViewComponent'
import { FaSelectionView, VerificationCodeView, } from '../stepUp/mfaStepUpWidget'
Expand All @@ -29,7 +29,6 @@ export interface AuthWidgetProps extends
SignupWithPasswordViewProps,
SignupWithWebAuthnViewProps,
ForgotPasswordViewProps,
ForgotPasswordSuccessViewProps,
QuickLoginViewProps,
ReauthViewProps,
Omit<FaSelectionViewProps, keyof FaSelectionViewState>,
Expand Down Expand Up @@ -88,7 +87,9 @@ export default createMultiViewWidget<AuthWidgetProps, PropsWithSession<AuthWidge
'signup-with-password': SignupWithPasswordView,
'signup-with-web-authn': SignupWithWebAuthnView,
'forgot-password': ForgotPasswordView,
'forgot-password-phone-number': ForgotPasswordPhoneNumberView,
'account-recovery': AccountRecoveryView,
'forgot-password-code': ForgotPasswordCodeView,
'forgot-password-success': ForgotPasswordSuccessView,
'account-recovery-success': AccountRecoverySuccessView,
'quick-login': QuickLoginView,
Expand All @@ -100,7 +101,6 @@ export default createMultiViewWidget<AuthWidgetProps, PropsWithSession<AuthWidge
if (!config.passwordPolicy) {
throw new UserError('This feature is not available on your account.');
}

if (!config.webAuthn && options.allowWebAuthnLogin) {
throw new UserError('The WebAuthn feature is not available on your account.');
}
Expand Down
Loading

0 comments on commit 1feeccd

Please sign in to comment.