@heymarco/next-auth
Version:
A complete authentication solution for web applications.
1,163 lines • 50.8 kB
JavaScript
'use client';
// react:
import {
// react:
default as React,
// contexts:
createContext,
// hooks:
useContext, useRef, useState, useEffect, useMemo, } from 'react';
// next-js:
import {
// navigations:
useRouter, usePathname, useSearchParams, } from 'next/navigation';
// next-auth:
import {
// apis:
signIn, } from 'next-auth/react';
// reusable-ui core:
import {
// react helper hooks:
useIsomorphicLayoutEffect, useEvent, useMergeEvents, useScheduleTriggerEvent, useMountedFlag,
// an accessibility management system:
AccessibilityProvider,
// a validation management system:
ValidationProvider, } from '@reusable-ui/core'; // a set of reusable-ui packages which are responsible for building any component
// reusable-ui components:
import {
// layout-components:
CardBody,
// status-components:
Busy, ModalCard,
// utility-components:
paragraphify, useDialogMessage, } from '@reusable-ui/components'; // a set of official Reusable-UI components
import {
// utilities:
invalidSelector, getAuthErrorDescription, resolveProviderName as defaultResolveProviderName, isClientError, } from '../utilities.js';
import { useFieldState, } from '../hooks.js';
import { passwordResetPath as defaultPasswordResetPath, usernameValidationPath as defaultUsernameValidationPath, emailValidationPath as defaultEmailValidationPath, passwordValidationPath as defaultPasswordValidationPath, signUpPath as defaultSignUpPath, emailConfirmationPath as defaultEmailConfirmationPath, } from '../../api-paths.js';
const noopHandler = { onChange: () => { } };
const SignInStateContext = createContext({
// constraints:
nameMinLength: 0,
nameMaxLength: 0,
emailMinLength: 0,
emailMaxLength: 0,
emailFormat: /./,
emailFormatHint: null,
usernameMinLength: 0,
usernameMaxLength: 0,
usernameFormat: /./,
usernameFormatHint: null,
usernameProhibitedHint: null,
passwordMinLength: 0,
passwordMaxLength: 0,
passwordHasUppercase: false,
passwordHasLowercase: false,
passwordProhibitedHint: null,
// data:
callbackUrl: null,
passwordResetToken: null,
emailConfirmationToken: null,
// states:
section: 'signIn',
isSignUpSection: false,
isSignInSection: false,
isRecoverSection: false,
isResetSection: false,
tokenVerified: null,
emailVerified: null,
isSignUpApplied: false,
isRecoverApplied: false,
isResetApplied: false,
isBusy: false,
// fields & validations:
userInteracted: false,
formRef: { current: null },
nameRef: { current: null },
name: '',
nameHandlers: noopHandler,
nameFocused: false,
nameValid: false,
nameValidLength: false,
emailRef: { current: null },
email: '',
emailHandlers: noopHandler,
emailFocused: false,
emailValid: 'unknown',
emailValidLength: false,
emailValidFormat: false,
emailValidAvailable: 'unknown',
usernameRef: { current: null },
username: '',
usernameHandlers: noopHandler,
usernameFocused: false,
usernameValid: 'unknown',
usernameValidLength: false,
usernameValidFormat: false,
usernameValidAvailable: 'unknown',
usernameValidNotProhibited: 'unknown',
usernameOrEmailRef: { current: null },
usernameOrEmail: '',
usernameOrEmailHandlers: noopHandler,
usernameOrEmailFocused: false,
usernameOrEmailValid: false,
passwordRef: { current: null },
password: '',
passwordHandlers: noopHandler,
passwordFocused: false,
passwordValid: false,
passwordValidLength: false,
passwordValidUppercase: false,
passwordValidLowercase: false,
passwordValidNotProhibited: 'unknown',
password2Ref: { current: null },
password2: '',
password2Handlers: noopHandler,
password2Focused: false,
password2Valid: false,
password2ValidLength: false,
password2ValidUppercase: false,
password2ValidLowercase: false,
password2ValidMatch: false,
// navigations:
gotoSignUp: () => { },
gotoSignIn: () => { },
gotoRecover: () => { },
gotoHome: () => { },
// actions:
doSignUp: async () => { },
doSignIn: async () => { },
doSignInWith: async () => { },
doRecover: async () => { },
doReset: async () => { },
// utilities:
resolveProviderName: () => '',
});
SignInStateContext.displayName = 'SignInState';
export const useSignInState = () => {
return useContext(SignInStateContext);
};
const SignInStateProvider = (props) => {
// rest props:
const {
// configs:
credentialsConfigClient,
// auths:
resolveProviderName: resolveProviderNameUnstable, basePath = '/api/auth',
// pages:
homepagePath = '/', defaultCallbackUrl = null,
// tabs:
defaultSection: defaultUncontrollableSection = 'signIn', section: controllableSection, onSectionChange: onControllableSectionChange,
// components:
signInWithDialogComponent = React.createElement(ModalCard, { theme: 'primary', backdropStyle: 'static', inheritEnabled: false }),
// children:
children, } = props;
const resolveProviderName = useEvent((oAuthProvider) => {
return (resolveProviderNameUnstable ?? defaultResolveProviderName)(oAuthProvider);
});
const passwordResetPath = `${basePath}/${defaultPasswordResetPath}`;
const usernameValidationPath = `${basePath}/${defaultUsernameValidationPath}`;
const emailValidationPath = `${basePath}/${defaultEmailValidationPath}`;
const passwordValidationPath = `${basePath}/${defaultPasswordValidationPath}`;
const signUpPath = `${basePath}/${defaultSignUpPath}`;
const emailConfirmationPath = `${basePath}/${defaultEmailConfirmationPath}`;
// navigations:
const router = useRouter();
const pathName = usePathname();
const searchParams = useSearchParams();
// data:
const callbackUrlRef = useRef(searchParams?.get('callbackUrl') || defaultCallbackUrl);
const callbackUrl = callbackUrlRef.current;
const passwordResetTokenRef = useRef(searchParams?.get('passwordResetToken') || null);
const passwordResetToken = passwordResetTokenRef.current;
const emailConfirmationTokenRef = useRef(searchParams?.get('emailConfirmationToken') || null);
const emailConfirmationToken = emailConfirmationTokenRef.current;
// states:
const isControllableSection = (controllableSection !== undefined);
const [uncontrollableSection, setUncontrollableSection] = useState(!!passwordResetToken
? 'reset' // special_uncontrollable
: defaultUncontrollableSection);
const section = ((uncontrollableSection === 'reset')
? 'reset' // special_uncontrollable
: (controllableSection /*controllable*/ ?? uncontrollableSection /*uncontrollable*/));
const handleUncontrollableSectionChange = useEvent((newSection) => {
// update state if uncontrollable -or- controllable with initially 'reset'_special_uncontrollable:
if (!isControllableSection || (uncontrollableSection === 'reset'))
setUncontrollableSection(newSection);
});
const handleSectionChange = useMergeEvents(
// preserves the original `onControllableSectionChange` from `props`:
onControllableSectionChange, /*controllable*/
// actions:
handleUncontrollableSectionChange);
const scheduleTriggerEvent = useScheduleTriggerEvent();
const triggerSectionChange = useEvent((newSection) => {
if (handleSectionChange)
scheduleTriggerEvent(() => {
// fire `on(Controllable|Uncontrollable)SectionChange` react event:
handleSectionChange(newSection);
});
});
const isSignUpSection = (section === 'signUp');
const isSignInSection = (section === 'signIn');
const isRecoverSection = (section === 'recover');
const isResetSection = (section === 'reset');
const [tokenVerified, setTokenVerified] = useState(!passwordResetToken ? false : null);
const [emailVerified, setEmailVerified] = useState(!emailConfirmationToken ? false : null);
const [isSignUpApplied, setIsSignUpApplied] = useState(false);
const [isRecoverApplied, setIsRecoverApplied] = useState(false);
const [isResetApplied, setIsResetApplied] = useState(false);
const [isBusy, setIsBusyInternal] = useState(false);
const isMounted = useMountedFlag();
// fields:
const formRef = useRef(null);
const nameRef = useRef(null);
const emailRef = useRef(null);
const usernameRef = useRef(null);
const usernameOrEmailRef = useRef(null);
const passwordRef = useRef(null);
const password2Ref = useRef(null);
const [enableValidation, setEnableValidation] = useState(false);
const [userInteracted, setUserInteracted] = useState(false);
const ignoreFocusRef = useRef(false);
const fieldsHandleChange = useEvent((event) => {
// actions:
setUserInteracted(true);
});
const fieldsHandleFocus = useEvent((event) => {
// conditions:
if (userInteracted)
return; // already marked as userInteracted => ignore)
if (ignoreFocusRef.current)
return; // only interested focusing by user (ignore by system)
// actions:
setTimeout(() => {
setTimeout(() => {
// conditions:
if (!event.target.matches(':focus'))
return; // focus on very brief moment => ignore
// actions:
setUserInteracted(true);
}, 0);
}, 0);
});
const [name, setName, nameFocused, nameHandlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
const [email, setEmail, emailFocused, emailHandlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
const [username, setUsername, usernameFocused, usernameHandlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
const [usernameOrEmail, setUsernameOrEmail, usernameOrEmailFocused, usernameOrEmailHandlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
const [password, setPassword, passwordFocused, passwordHandlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
const [password2, setPassword2, password2Focused, password2Handlers] = useFieldState({ onChange: fieldsHandleChange, onFocus: fieldsHandleFocus });
// utilities:
const internalSetFocus = (element) => {
if (!element)
return;
ignoreFocusRef.current = true;
element.focus();
setTimeout(() => {
setTimeout(() => {
ignoreFocusRef.current = false;
}, 0);
}, 0);
};
// constraints:
const { name: { minLength: nameMinLength, maxLength: nameMaxLength, }, email: { minLength: emailMinLength, maxLength: emailMaxLength, format: emailFormat, formatHint: emailFormatHint, }, username: { minLength: usernameMinLength, maxLength: usernameMaxLength, format: usernameFormat, formatHint: usernameFormatHint, prohibitedHint: usernameProhibitedHint, }, password: { minLength: passwordMinLength, maxLength: passwordMaxLength, hasUppercase: passwordHasUppercase, hasLowercase: passwordHasLowercase, prohibitedHint: passwordProhibitedHint, }, } = credentialsConfigClient;
// validations:
const isDataEntry = ((section === 'signUp') || (section === 'reset'));
const nameValidLength = !isDataEntry ? (name.length >= 1) : ((name.length >= nameMinLength) && (name.length <= nameMaxLength));
const nameValid = nameValidLength;
const emailValidLength = !isDataEntry ? (email.length >= 5) : ((email.length >= emailMinLength) && (email.length <= emailMaxLength));
const emailValidFormat = !!email.match(emailFormat);
const [emailValidAvailableRaw, setEmailValidAvailable] = useState('unknown');
const emailValidAvailable = !isDataEntry ? true : emailValidAvailableRaw;
const emailValid = emailValidLength && emailValidFormat && emailValidAvailable;
const usernameValidLength = !isDataEntry ? (username.length >= 1) : ((username.length >= usernameMinLength) && (username.length <= usernameMaxLength));
const usernameValidFormat = !isDataEntry ? true : !!username.match(usernameFormat);
const [usernameValidAvailableRaw, setUsernameValidAvailable] = useState('unknown');
const usernameValidAvailable = !isDataEntry ? true : usernameValidAvailableRaw;
const [usernameValidNotProhibitedRaw, setUsernameValidNotProhibited] = useState('unknown');
const usernameValidNotProhibited = !isDataEntry ? true : usernameValidNotProhibitedRaw;
const usernameValid = usernameValidLength && usernameValidFormat && usernameValidAvailable && usernameValidNotProhibited;
const usernameOrEmailValid = (usernameOrEmail.length >= 1);
const passwordValidLength = !isDataEntry ? (password.length >= 1) : ((password.length >= passwordMinLength) && (password.length <= passwordMaxLength));
const passwordValidUppercase = !isDataEntry ? true : (!passwordHasUppercase || !!password.match(/[A-Z]/));
const passwordValidLowercase = !isDataEntry ? true : (!passwordHasLowercase || !!password.match(/[a-z]/));
const [passwordValidNotProhibitedRaw, setPasswordValidNotProhibited] = useState('unknown');
const passwordValidNotProhibited = !isDataEntry ? true : passwordValidNotProhibitedRaw;
const passwordValid = passwordValidLength && passwordValidUppercase && passwordValidLowercase && passwordValidNotProhibited;
const password2ValidLength = !isDataEntry ? (password2.length >= 1) : ((password2.length >= passwordMinLength) && (password2.length <= passwordMaxLength));
const password2ValidUppercase = !isDataEntry ? true : (!passwordHasUppercase || !!password2.match(/[A-Z]/));
const password2ValidLowercase = !isDataEntry ? true : (!passwordHasLowercase || !!password2.match(/[a-z]/));
const password2ValidMatch = !isDataEntry ? true : (!!password && (password2 === password));
const password2Valid = password2ValidLength && password2ValidUppercase && password2ValidLowercase && password2ValidMatch;
// dialogs:
const { showDialog, showMessageError, showMessageFieldError, showMessageFetchError, showMessageSuccess, } = useDialogMessage();
// effects:
// displays an error passed by `next-auth`:
useEffect(() => {
// conditions:
const error = searchParams?.get('error');
if (!error)
return; // no error passed => ignore
// report the failure:
showMessageError(getAuthErrorDescription(error));
}, []);
// remove passed queryString(s):
useEffect(() => {
// conditions:
if (!pathName)
return; // the router is not ready => ignore
if (!searchParams?.get('error')
&&
!searchParams?.get('callbackUrl')
&&
!searchParams?.get('passwordResetToken')
&&
!searchParams?.get('emailConfirmationToken'))
return; // no queryString(s) passed => nothing to remove => ignore
try {
// get current browser's queryString:
const newSearchParams = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
// remove `?error=***` on browser's url:
newSearchParams.delete('error');
// remove `?callbackUrl=***` on browser's url:
newSearchParams.delete('callbackUrl');
// remove `?passwordResetToken=***` on browser's url:
newSearchParams.delete('passwordResetToken');
// remove `?emailConfirmationToken=***` on browser's url:
newSearchParams.delete('emailConfirmationToken');
// update browser's url:
router.replace(`${pathName}${!!newSearchParams.size ? `?${newSearchParams}` : ''}`, { scroll: false });
}
catch {
// ignore any error
} // if
}, [pathName]);
// validate password reset token at startup:
const hasPasswordResetTokenInitialized = useRef(false); // make sure the validation is never performed twice
useEffect(() => {
// conditions:
if (!passwordResetToken)
return; // no token => nothing to reset => ignore
if (tokenVerified !== null)
return; // already verified with success/failed result => ignore
if (hasPasswordResetTokenInitialized.current)
return; // already performed => ignore
hasPasswordResetTokenInitialized.current = true; // mark as performed
// actions:
(async () => {
// attempts validate password reset token:
try {
const response = await fetch(`${passwordResetPath}?passwordResetToken=${encodeURIComponent(passwordResetToken)}`, {
method: 'GET',
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
const data = await response.json();
if (!isMounted.current)
return; // unmounted => abort
// success
// passwordResetTokenValidation succeeded => save the success:
setTokenVerified(data);
// now the user can fill the passwordResetToken form
}
catch (error) { // error
// save the failure:
setTokenVerified(false);
// report the failure:
await showMessageFetchError(error);
if (!isMounted.current)
return; // unmounted => abort
// passwordResetTokenValidation failed due to network|client|server error => redirect to signIn tab:
gotoSignIn();
} // try
})();
}, [passwordResetToken, tokenVerified]);
// validate email confirmation token at startup:
const hasEmailConfirmationTokenInitialized = useRef(false); // make sure the validation is never performed twice
useEffect(() => {
// conditions:
if (!emailConfirmationToken)
return; // no token => nothing to confirm => ignore
if (emailVerified !== null)
return; // already verified with success/failed result => ignore
if (hasEmailConfirmationTokenInitialized.current)
return; // already performed => ignore
hasEmailConfirmationTokenInitialized.current = true; // mark as performed
// actions:
(async () => {
// attempts validate email confirmation token:
try {
const response = await fetch(emailConfirmationPath, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailConfirmationToken }),
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
const data = await response.json();
if (!isMounted.current)
return; // unmounted => abort
// success
// emailConfirmationTokenValidation succeeded => save the success:
setEmailVerified(true);
// report the success:
showMessageSuccess(data.message
? paragraphify(data.message)
: (React.createElement("p", null, "Your email has been successfully confirmed. Now you can sign in with your username (or email) and password.")));
}
catch (error) { // error
// save the failure:
setEmailVerified(false);
// report the failure:
await showMessageFetchError(error);
// if (!isMounted.current) return; // unmounted => abort
// emailConfirmationTokenValidation failed due to network|client|server error => no need to redirect to another tab, because signIn tab is the default tab
// stays on signIn tab
} // try
})();
}, [emailConfirmationToken, emailVerified]);
// validate email availability:
useEffect(() => {
// conditions:
if (!isSignUpSection
||
!email
||
!emailValidLength
||
!emailValidFormat) {
setEmailValidAvailable('unknown');
return;
} // if
// actions:
const abortController = new AbortController();
(async () => {
// attempts validate email availability:
try {
// delay a brief moment, waiting for the user typing:
setEmailValidAvailable('unknown');
await new Promise((resolved) => {
setTimeout(() => {
resolved();
}, 500);
});
if (abortController.signal.aborted)
return;
setEmailValidAvailable('loading');
const response = await fetch(`${emailValidationPath}?email=${encodeURIComponent(email)}`, {
method: 'GET',
signal: abortController.signal,
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
if (!isMounted.current)
return; // unmounted => abort
// success
// save the success:
if (!abortController.signal.aborted)
setEmailValidAvailable(true);
}
catch (error) {
// save the failure:
if (!abortController.signal.aborted)
setEmailValidAvailable(isClientError(error) ? false : 'error');
} // try
})();
// cleanups:
return () => {
abortController.abort();
};
}, [isSignUpSection, email, emailValidLength, emailValidFormat]);
// validate username availability:
useEffect(() => {
// conditions:
if (!isSignUpSection
||
!username
||
!usernameValidLength
||
!usernameValidFormat) {
setUsernameValidAvailable('unknown');
return;
} // if
// actions:
const abortController = new AbortController();
(async () => {
// attempts validate username availability:
try {
// delay a brief moment, waiting for the user typing:
setUsernameValidAvailable('unknown');
await new Promise((resolved) => {
setTimeout(() => {
resolved();
}, 500);
});
if (abortController.signal.aborted)
return;
setUsernameValidAvailable('loading');
const response = await fetch(`${usernameValidationPath}?username=${encodeURIComponent(username)}`, {
method: 'GET',
signal: abortController.signal,
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
if (!isMounted.current)
return; // unmounted => abort
// success
// save the success:
if (!abortController.signal.aborted)
setUsernameValidAvailable(true);
}
catch (error) {
// save the failure:
if (!abortController.signal.aborted)
setUsernameValidAvailable(isClientError(error) ? false : 'error');
} // try
})();
// cleanups:
return () => {
abortController.abort();
};
}, [isSignUpSection, username, usernameValidLength, usernameValidFormat]);
// validate username not_prohibited:
useEffect(() => {
// conditions:
if (!isSignUpSection
||
!username
||
!usernameValidLength
||
!usernameValidFormat) {
setUsernameValidNotProhibited('unknown');
return;
} // if
// actions:
const abortController = new AbortController();
(async () => {
// attempts validate username not_prohibited:
try {
// delay a brief moment, waiting for the user typing:
setUsernameValidNotProhibited('unknown');
await new Promise((resolved) => {
setTimeout(() => {
resolved();
}, 500);
});
if (abortController.signal.aborted)
return;
setUsernameValidNotProhibited('loading');
const response = await fetch(`${usernameValidationPath}?username=${encodeURIComponent(username)}`, {
method: 'PUT',
signal: abortController.signal,
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
if (!isMounted.current)
return; // unmounted => abort
// success
// save the success:
if (!abortController.signal.aborted)
setUsernameValidNotProhibited(true);
}
catch (error) {
// save the failure:
if (!abortController.signal.aborted)
setUsernameValidNotProhibited(isClientError(error) ? false : 'error');
} // try
})();
// cleanups:
return () => {
abortController.abort();
};
}, [isSignUpSection, username, usernameValidLength, usernameValidFormat]);
// validate password not_prohibited:
useEffect(() => {
// conditions:
if ((!isSignUpSection && !isResetSection)
||
!password
||
!passwordValidLength) {
setPasswordValidNotProhibited('unknown');
return;
} // if
// actions:
const abortController = new AbortController();
(async () => {
// attempts validate password not_prohibited:
try {
// delay a brief moment, waiting for the user typing:
setPasswordValidNotProhibited('unknown');
await new Promise((resolved) => {
setTimeout(() => {
resolved();
}, 500);
});
if (abortController.signal.aborted)
return;
setPasswordValidNotProhibited('loading');
const response = await fetch(`${passwordValidationPath}?password=${encodeURIComponent(password)}`, {
method: 'PUT',
signal: abortController.signal,
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
if (!isMounted.current)
return; // unmounted => abort
// success
// save the success:
if (!abortController.signal.aborted)
setPasswordValidNotProhibited(true);
}
catch (error) {
// save the failure:
if (!abortController.signal.aborted)
setPasswordValidNotProhibited(isClientError(error) ? false : 'error');
} // try
})();
// cleanups:
return () => {
abortController.abort();
};
}, [isSignUpSection, password, passwordValidLength]);
// focus on email field when the section is 'signUp':
useEffect(() => {
// conditions:
if (section !== 'signUp')
return; // other than 'signUp' => ignore
// actions:
internalSetFocus(nameRef.current);
}, [section]);
// focus on usernameOrEmail field when the section is 'signIn' or 'recover':
useEffect(() => {
// conditions:
if ((section !== 'signIn') && (section !== 'recover'))
return; // other than 'signIn' or 'recover' => ignore
// actions:
internalSetFocus(usernameOrEmailRef.current);
}, [section]);
// focus on password field after successfully verified the password reset token:
useEffect(() => {
// conditions:
if (!tokenVerified)
return; // NOT verified with success result => ignore
// actions:
internalSetFocus(passwordRef.current);
}, [tokenVerified]);
// focus on usernameOrEmail field after successfully verified the email confirmation token:
useEffect(() => {
// conditions:
if (!emailVerified)
return; // NOT verified with success result => ignore
// actions:
internalSetFocus(usernameOrEmailRef.current);
}, [emailVerified]);
// resets input states when the `section` changes:
const prevSection = useRef(section);
useIsomorphicLayoutEffect(() => {
// conditions:
if (prevSection.current === section)
return; // no change => ignore
prevSection.current = section; // sync
// reset request states:
setIsSignUpApplied(false);
setIsRecoverApplied(false);
setIsResetApplied(false);
// reset fields & validations:
setEnableValidation(false);
setUserInteracted(false);
setName('');
setEmail('');
setUsername('');
setUsernameOrEmail('');
setPassword('');
setPassword2('');
}, [section]);
// stable callbacks:
const setIsBusy = useEvent((isBusy) => {
signInState.isBusy = isBusy; /* instant update without waiting for (slow|delayed) re-render */
setIsBusyInternal(isBusy);
});
const gotoSignUp = useEvent(() => {
triggerSectionChange('signUp');
});
const gotoSignIn = useEvent(() => {
triggerSectionChange('signIn');
});
const gotoRecover = useEvent(() => {
triggerSectionChange('recover');
});
const gotoHome = useEvent(() => {
router.push(homepagePath);
});
const doSignUp = useEvent(async () => {
// conditions:
if (signInState.isBusy)
return; // ignore when busy /* instant update without waiting for (slow|delayed) re-render */
// validate:
// enable validation and *wait* until the next re-render of validation_enabled before we're going to `querySelectorAll()`:
setEnableValidation(true);
await new Promise((resolve) => {
setTimeout(() => {
setTimeout(() => {
resolve();
}, 0);
}, 0);
});
if (!isMounted.current)
return; // unmounted => abort
const fieldErrors = formRef?.current?.querySelectorAll?.(invalidSelector);
if (fieldErrors?.length) { // there is an/some invalid field
showMessageFieldError(fieldErrors);
return;
} // if
// attempts apply signUp:
setIsBusy('signUp'); // mark as busy
try {
const response = await fetch(signUpPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, email, username, password }),
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
const data = await response.json();
if (!isMounted.current)
return; // unmounted => abort
// success
setIsSignUpApplied(true); // mark signUp as applied
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
setPassword('');
setPassword2('');
// report the success:
await showMessageSuccess(data.message
? paragraphify(data.message)
: ((response.status === 201)
? (React.createElement(React.Fragment, null,
React.createElement("p", null, "Your account has been successfully created."),
React.createElement("p", null, "We have sent a confirmation link to your email to activate your account. Please check your inbox in a moment.")))
: (React.createElement(React.Fragment, null,
React.createElement("p", null, "Your account has been successfully created."),
React.createElement("p", null, "Now you can sign in with the new username and password.")))));
if (!isMounted.current)
return; // unmounted => abort
// signUp succeeded => redirect to signIn tab:
gotoSignIn();
}
catch (error) { // error
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
// report the failure:
await showMessageFetchError(error);
if (!isMounted.current)
return; // unmounted => abort
// signUp failed due to network|client|server error => user can retry signUp again:
// focus to name field:
nameRef.current?.setSelectionRange(0, name.length);
nameRef.current?.focus();
} // try
});
const doSignIn = useEvent(async () => {
// conditions:
if (signInState.isBusy)
return; // ignore when busy /* instant update without waiting for (slow|delayed) re-render */
// validate:
// enable validation and *wait* until the next re-render of validation_enabled before we're going to `querySelectorAll()`:
setEnableValidation(true);
await new Promise((resolve) => {
setTimeout(() => {
setTimeout(() => {
resolve();
}, 0);
}, 0);
});
if (!isMounted.current)
return; // unmounted => abort
const fieldErrors = formRef?.current?.querySelectorAll?.(invalidSelector);
if (fieldErrors?.length) { // there is an/some invalid field
showMessageFieldError(fieldErrors);
return;
} // if
// attempts sign in using credentials:
setIsBusy('credentials'); // mark as busy
const result = await signIn('credentials', { username: usernameOrEmail, password, redirect: false });
if (!isMounted.current)
return; // unmounted => abort
// verify the sign in status:
if (!!result?.error) { // error
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
setPassword('');
// report the failure:
await showMessageError(getAuthErrorDescription(result?.error ?? 'CredentialsSignin'));
// signIn failed due to network|client|server error => user can retry signIn again:
// focus to password field:
passwordRef.current?.setSelectionRange(0, password.length);
passwordRef.current?.focus();
}
else { // success
// resets:
setUsernameOrEmail('');
setPassword('');
// signIn succeeded => redirect to origin page:
if (callbackUrl) {
// redirect to `callbackUrl` (if supplied)
router.replace(callbackUrl);
// in case of redirect to current page => just hide the busy indicator after redirect finished
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
// setUsernameOrEmail(''); // already reseted above
// setPassword(''); // already reseted above
}
else {
// stays on signIn tab
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
// setUsernameOrEmail(''); // already reseted above
// setPassword(''); // already reseted above
} // if
} // if
});
const doSignInWith = useEvent(async (providerType) => {
// conditions:
if (signInState.isBusy)
return; // ignore when busy /* instant update without waiting for (slow|delayed) re-render */
// attempts sign in using OAuth:
setIsBusy(providerType); // mark as busy
const result = await signIn(providerType, {
callbackUrl: callbackUrl // signInWith succeeded => redirect to `callbackUrl` (if supplied)
|| // -or-
undefined // signInWith succeeded => stays on signIn tab
});
if (!isMounted.current)
return; // unmounted => abort
// verify the sign in status:
if (!!result?.error) { // error
setIsBusy(false); // unmark as busy
// report the failure:
showMessageError(getAuthErrorDescription(result?.error ?? 'OAuthSignin'));
// signInWith failed due to network|client|server error => user can retry signIn again:
// stays on signIn tab
}
else { // success
// signInWith succeeded => report the success:
if (signInWithDialogComponent) {
showDialog(React.cloneElement(signInWithDialogComponent,
// props:
{
// global stackable:
viewport: signInWithDialogComponent.props.viewport ?? formRef,
},
// children:
(signInWithDialogComponent.props.children ?? React.createElement(CardBody, null,
React.createElement("p", null,
React.createElement(Busy, null),
"\u00A0Authenticating using ",
providerType,
"...")))));
} // if
} // if
});
const doRecover = useEvent(async () => {
// conditions:
if (signInState.isBusy)
return; // ignore when busy /* instant update without waiting for (slow|delayed) re-render */
// validate:
// enable validation and *wait* until the next re-render of validation_enabled before we're going to `querySelectorAll()`:
setEnableValidation(true);
await new Promise((resolve) => {
setTimeout(() => {
setTimeout(() => {
resolve();
}, 0);
}, 0);
});
if (!isMounted.current)
return; // unmounted => abort
const fieldErrors = formRef?.current?.querySelectorAll?.(invalidSelector);
if (fieldErrors?.length) { // there is an/some invalid field
showMessageFieldError(fieldErrors);
return;
} // if
// attempts request recover password:
setIsBusy('recover'); // mark as busy
try {
const response = await fetch(passwordResetPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: usernameOrEmail }),
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
const data = await response.json();
if (!isMounted.current)
return; // unmounted => abort
// success
setIsRecoverApplied(true); // mark recoverRequest as sent
setIsBusy(false); // unmark as busy
// report the success:
await showMessageSuccess(data.message
? paragraphify(data.message)
: (React.createElement("p", null, "A password reset link sent to your email. Please check your inbox in a moment.")));
if (!isMounted.current)
return; // unmounted => abort
// requestRecoverPassword succeeded => redirect to signIn tab:
gotoSignIn();
}
catch (error) { // error
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
// report the failure:
await showMessageFetchError(error);
// requestRecoverPassword failed due to network|client|server error => user can retry requestRecoverPassword again:
// focus to usernameOrEmail field:
usernameOrEmailRef.current?.setSelectionRange(0, usernameOrEmail.length);
usernameOrEmailRef.current?.focus();
} // try
});
const doReset = useEvent(async () => {
// conditions:
if (signInState.isBusy)
return; // ignore when busy /* instant update without waiting for (slow|delayed) re-render */
// validate:
// enable validation and *wait* until the next re-render of validation_enabled before we're going to `querySelectorAll()`:
setEnableValidation(true);
await new Promise((resolve) => {
setTimeout(() => {
setTimeout(() => {
resolve();
}, 0);
}, 0);
});
if (!isMounted.current)
return; // unmounted => abort
const fieldErrors = formRef?.current?.querySelectorAll?.(invalidSelector);
if (fieldErrors?.length) { // there is an/some invalid field
showMessageFieldError(fieldErrors);
return;
} // if
// attempts apply password reset:
setIsBusy('reset'); // mark as busy
try {
const response = await fetch(passwordResetPath, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ passwordResetToken, password }),
});
if (!response.ok)
throw Error(response.statusText, { cause: response });
const data = await response.json();
if (!isMounted.current)
return; // unmounted => abort
// success
setIsResetApplied(true); // mark passwordReset as applied
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
setPassword('');
setPassword2('');
// report the success:
await showMessageSuccess(data.message
? paragraphify(data.message)
: (React.createElement("p", null, "The password has been successfully changed. Now you can sign in with the new password.")));
if (!isMounted.current)
return; // unmounted => abort
// resetPassword succeeded => redirect to signIn tab:
gotoSignIn();
}
catch (error) { // error
setIsBusy(false); // unmark as busy
// resets:
setEnableValidation(false);
// report the failure:
await showMessageFetchError(error);
if (!isMounted.current)
return; // unmounted => abort
// resetPassword failed due to network|client|server error => user can retry resetPassword again:
// focus to password field:
passwordRef.current?.setSelectionRange(0, password.length);
passwordRef.current?.focus();
} // try
});
// apis:
const signInState = useMemo(() => ({
// constraints:
nameMinLength, // stable value
nameMaxLength, // stable value
emailMinLength, // stable value
emailMaxLength, // stable value
emailFormat, // stable value
emailFormatHint, // stable value
usernameMinLength, // stable value
usernameMaxLength, // stable value
usernameFormat, // stable value
usernameFormatHint, // stable value
usernameProhibitedHint, // stable value
passwordMinLength, // stable value
passwordMaxLength, // stable value
passwordHasUppercase, // stable value
passwordHasLowercase, // stable value
passwordProhibitedHint, // stable value
// data:
callbackUrl, // mutable value
passwordResetToken, // mutable value
emailConfirmationToken, // mutable value
// states:
section, // mutable value
isSignUpSection, // mutable value
isSignInSection, // mutable value
isRecoverSection, // mutable value
isResetSection, // mutable value
tokenVerified, // mutable value
emailVerified, // mutable value
isSignUpApplied, // mutable value
isRecoverApplied, // mutable value
isResetApplied, // mutable value
isBusy, // mutable value
// fields & validations:
userInteracted, // mutable value
formRef, // stable ref
nameRef, // stable ref
name, // mutable value
nameHandlers, // stable ref
nameFocused, // mutable value
nameValid, // mutable value
nameValidLength, // mutable value
emailRef, // stable ref
email: (isResetSection
? (tokenVerified === false) ? '' : (tokenVerified?.email ?? '')
: email), // mutable value
emailHandlers, // stable ref
emailFocused, // mutable value
emailValid, // mutable value
emailValidLength, // mutable value
emailValidFormat, // mutable value
emailValidAvailable, // mutable value
usernameRef, // stable ref
username, // mutable value
usernameHandlers, // stable ref
usernameFocused, // mutable value
usernameValid, // mutable value
usernameValidLength, // mutable value
usernameValidFormat, // mutable value
usernameValidAvailable, // mutable value
usernameValidNotProhibited, // mutable value
usernameOrEmailRef, // stable ref
usernameOrEmail, // mutable value
usernameOrEmailHandlers, // stable ref
usernameOrEmailFocused, // mutable value
usernameOrEmailValid, // mutable value
passwordRef, // stable ref
password, // mutable value
passwordHandlers, // stable ref
passwordFocused, // mutable value
passwordValid, // mutable value
passwordValidLength, // mutable value
passwordValidUppercase, // mutable value
passwordValidLowercase, // mutable value
passwordValidNotProhibited, // mutable value
password2Ref, // stable ref
password2, // mutable value
password2Handlers, // stable ref
password2Focused, // mutable value
password2Valid, // mutable value
password2ValidLength, // mutable value
password2ValidUppercase, // mutable value
password2ValidLowercase, // mutable value
password2ValidMatch, // mutable value
// navigations:
gotoSignUp, // stable ref
gotoSignIn, // stable ref
gotoRecover, // stable ref
gotoHome, // stable ref
// actions:
doSignUp, // stable ref
doSignIn, // stable ref
doSignInWith, // stable ref
doRecover, // stable ref
doReset, // stable ref
// utilities:
resolveProviderName, // stable ref
}), [
// data:
callbackUrl,
passwordResetToken,
emailConfirmationToken,
// states:
section,
isSignUpSection,
isSignInSection,
isRecoverSection,
isResetSection,
tokenVerified,
emailVerified,
isSignUpApplied,
isRecoverApplied,
isResetApplied,
isBusy,
// fields & validations:
userInteracted,
name,
nameFocused,
nameValid,
nameValidLength,
email,
emailFocused,
emailValid,
emailValidLength,
emailValidFormat,
emailValidAvailable,
username,
usernameFocused,
usernameValid,
usernameValidLength,
usernameValidFormat,
usernameValidAvailable,
usernameValidNotProhibited,
usernameOrEmail,
usernameOrEmailFocused,
usernameOrEmailValid,
password,
passwordFocused,
passwordValid,
passwordValidLength,
passwordValidUppercase,
passwordValidLowercase,
passwordValidNotProhibited,
password2,
password2Focused,
password2Valid,
password2ValidLength,
password2ValidUppercase,
password2ValidLowercase,
password2ValidMatch,
]);
// jsx:
return (React.createElement(SignInStateContext.Provider, { value: signInState },
React.createElement(AccessibilityProvider
// accessibilities:
, {
// accessibilities:
enabled: !isBusy // disabled if busy
&&
((isSignUpSection && !isSignUpApplied) // on 'signUp' section => enabled if registration___was_NOT_sent
||
(isSignInSection && (!emailConfirmationToken || (emailVerified !== null))) // on 'signIn' section => enabled if no_emailConfirmationToken -or- email_has_verified