diff --git a/resources/js/login/error_handling.js b/resources/js/login/error_handling.js
new file mode 100644
index 00000000..e1141605
--- /dev/null
+++ b/resources/js/login/error_handling.js
@@ -0,0 +1,32 @@
+export const SESSION_EXPIRED_MESSAGE =
+ 'Your session has expired or your browser is blocking cookies. Please reload the page and try again.';
+export const GENERIC_RETRY_MESSAGE = 'Something went wrong. Please try again.';
+export const TOO_MANY_REQUESTS_MESSAGE = 'Too many requests. Try it later.';
+
+export const ACCOUNT_NOT_FOUND = 'account_not_found';
+export const SESSION_EXPIRED = 'session_expired';
+export const TOO_MANY_REQUESTS = 'too_many_requests';
+export const GENERIC_ERROR = 'generic_error';
+
+/**
+ * Classifies a failed POST /auth/login/account-verify.
+ * Only a real 404 means the account does not exist; anything else must never
+ * switch the page to the sign-up state.
+ * @param {number|undefined} status HTTP status (undefined on network errors)
+ * @returns {{type: string, message: string}}
+ */
+export const classifyAccountVerifyError = (status) => {
+ switch (Number(status)) {
+ case 404:
+ // non-empty on purpose: errors.email only has to be truthy for this branch
+ return {type: ACCOUNT_NOT_FOUND, message: ' '};
+ case 419:
+ return {type: SESSION_EXPIRED, message: SESSION_EXPIRED_MESSAGE};
+ case 429:
+ return {type: TOO_MANY_REQUESTS, message: TOO_MANY_REQUESTS_MESSAGE};
+ default:
+ return {type: GENERIC_ERROR, message: GENERIC_RETRY_MESSAGE};
+ }
+};
+
+export const isSessionExpired = (error) => Number(error?.status) === 419;
diff --git a/resources/js/login/error_handling.test.js b/resources/js/login/error_handling.test.js
new file mode 100644
index 00000000..e8314e24
--- /dev/null
+++ b/resources/js/login/error_handling.test.js
@@ -0,0 +1,42 @@
+import {
+ classifyAccountVerifyError,
+ isSessionExpired,
+ ACCOUNT_NOT_FOUND,
+ SESSION_EXPIRED,
+ TOO_MANY_REQUESTS,
+ GENERIC_ERROR,
+ SESSION_EXPIRED_MESSAGE,
+ GENERIC_RETRY_MESSAGE,
+ TOO_MANY_REQUESTS_MESSAGE,
+} from './error_handling';
+
+describe('classifyAccountVerifyError', () => {
+ test('404 is the only status that means account not found', () => {
+ const r = classifyAccountVerifyError(404);
+ expect(r.type).toBe(ACCOUNT_NOT_FOUND);
+ expect(r.message.trim()).toBe('');
+ expect(r.message).toBeTruthy();
+ });
+
+ test('419 is a session/cookies problem, not account not found', () => {
+ expect(classifyAccountVerifyError(419)).toEqual({type: SESSION_EXPIRED, message: SESSION_EXPIRED_MESSAGE});
+ });
+
+ test('429 keeps the too many requests message', () => {
+ expect(classifyAccountVerifyError(429)).toEqual({type: TOO_MANY_REQUESTS, message: TOO_MANY_REQUESTS_MESSAGE});
+ expect(TOO_MANY_REQUESTS_MESSAGE).toBe('Too many requests. Try it later.');
+ });
+
+ test.each([500, 502, 503, 400, undefined, null])('status %p is a generic error', (status) => {
+ expect(classifyAccountVerifyError(status)).toEqual({type: GENERIC_ERROR, message: GENERIC_RETRY_MESSAGE});
+ });
+});
+
+describe('isSessionExpired', () => {
+ test('detects 419 only', () => {
+ expect(isSessionExpired({status: 419})).toBe(true);
+ expect(isSessionExpired({status: 404})).toBe(false);
+ expect(isSessionExpired({})).toBe(false);
+ expect(isSessionExpired(undefined)).toBe(false);
+ });
+});
diff --git a/resources/js/login/login.js b/resources/js/login/login.js
index ee061b9a..1b184b5e 100644
--- a/resources/js/login/login.js
+++ b/resources/js/login/login.js
@@ -25,6 +25,13 @@ import CustomSnackbar from "../components/custom_snackbar";
import Banner from '../components/banner/banner';
import OtpInput from 'react-otp-input';
import {handleErrorResponse, handleThirdPartyProvidersVerbiage} from '../utils';
+import {
+ classifyAccountVerifyError,
+ isSessionExpired,
+ ACCOUNT_NOT_FOUND,
+ SESSION_EXPIRED,
+ SESSION_EXPIRED_MESSAGE
+} from './error_handling';
import styles from './login.module.scss'
import "./third_party_identity_providers.scss";
@@ -453,6 +460,8 @@ class LoginPage extends React.Component {
user_verified: props.hasOwnProperty('user_verified') ? props.user_verified : false,
user_active: props.hasOwnProperty('user_active') ? props.user_active : null,
email_verified: props.hasOwnProperty('email_verified') ? props.email_verified : null,
+ account_not_found: false,
+ account_verify_error: null,
errors: {
email: '',
otp: props.authError != '' ? props.authError : '',
@@ -530,6 +539,10 @@ class LoginPage extends React.Component {
this.showAlert(errors[0], 'error');
return;
}
+ if (isSessionExpired(error)) {
+ this.showAlert(SESSION_EXPIRED_MESSAGE, 'error');
+ return;
+ }
this.showAlert('Oops... Something went wrong!', 'error');
});
return false;
@@ -628,6 +641,8 @@ class LoginPage extends React.Component {
user_pic: response.pic,
user_fullname: response.full_name,
user_verified: true,
+ account_not_found: false,
+ account_verify_error: null,
user_active: response.is_active,
email_verified: response.is_verified,
authFlow: response.has_password_set ? password_flow : otp_flow,
@@ -648,20 +663,20 @@ class LoginPage extends React.Component {
let { response, status, message } = error;
+ const {type, message: errorMessage} = classifyAccountVerifyError(status);
+
let newErrors = {};
newErrors['password'] = '';
- newErrors['email'] = " ";
-
- if (status == 429) {
- newErrors['email'] = "Too many requests. Try it later.";
- }
+ newErrors['email'] = errorMessage;
this.setState({
...this.state,
user_pic: null,
user_fullname: null,
user_verified: false,
+ account_not_found: type === ACCOUNT_NOT_FOUND,
+ account_verify_error: type === ACCOUNT_NOT_FOUND ? null : type,
errors: newErrors,
disableInput: false
});
@@ -686,6 +701,10 @@ class LoginPage extends React.Component {
'We\'ve sent you a verification email. Please check your inbox and click the link to verify your account.',
'success');
}, (error) => {
+ if (isSessionExpired(error)) {
+ this.showAlert(SESSION_EXPIRED_MESSAGE, 'error');
+ return;
+ }
handleErrorResponse(error, (title, messageLines, type) => {
const message = (messageLines ?? []).join(', ')
this.showAlert(`${title}: ${message}`, type);
@@ -701,6 +720,8 @@ class LoginPage extends React.Component {
user_pic: null,
user_fullname: null,
user_verified: false,
+ account_not_found: false,
+ account_verify_error: null,
user_active: null,
email_verified: null,
authFlow: "password",
@@ -728,9 +749,9 @@ class LoginPage extends React.Component {
}
getSignUpSignInTitle() {
- const { errors, user_active } = this.state;
+ const { account_not_found } = this.state;
- if (errors.email && this.existingUserCanContinue()) {
+ if (account_not_found && this.existingUserCanContinue()) {
return 'Create an account for:';
}
return 'Sign in';
@@ -801,7 +822,28 @@ class LoginPage extends React.Component {
// we already had an interaction and got an user error...
this.state.errors.email !== '' &&
<>
- {this.existingUserCanContinue() &&
+ {this.state.account_verify_error === SESSION_EXPIRED &&
+
+ }
+ {this.state.account_verify_error !== null &&
+ this.state.account_verify_error !== SESSION_EXPIRED &&
+ this.state.account_verify_error !== 'too_many_requests' &&
+
+ }
+ {this.state.account_not_found && this.existingUserCanContinue() &&