Skip to content
Open
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
70 changes: 10 additions & 60 deletions client/components/status/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {localStorage} from "../../utils/storage";
import handleSession from "../../utils/session";
import getPlanSelection from "../../utils/get-plan-selection";
import getPlans from "../../utils/get-plans";
import {
storeValue,
resolveStoredValue,
} from "../../utils/captive-portal-storage";

export default class Status extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -153,13 +157,13 @@ export default class Status extends React.Component {
mustLogout: userMustLogout,
repeatLogin,
} = userData;
const mustLogin = this.resolveStoredValue(
const mustLogin = resolveStoredValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogin`,
userMustLogin,
cookies,
);
const mustLogout = this.resolveStoredValue(
const mustLogout = resolveStoredValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogout`,
userMustLogout,
Expand Down Expand Up @@ -227,7 +231,7 @@ export default class Status extends React.Component {
shouldLogin = shouldLogin && settings.payment_requires_internet;
}
if (this.loginFormRef && this.loginFormRef.current && shouldLogin) {
this.storeValue(
storeValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogin`,
false,
Expand Down Expand Up @@ -494,7 +498,7 @@ export default class Status extends React.Component {
// After a successful payment, the user is redirected back to the status page.
// If the user plan was previously exhausted, they need to be logged into the captive portal
// to regain internet access. This ensures seamless browsing after upgrading their plan.
this.storeValue(
storeValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogin`,
true,
Expand Down Expand Up @@ -582,7 +586,7 @@ export default class Status extends React.Component {
this.repeatLogin = true;
}
if (!internetMode) {
this.storeValue(
storeValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogout`,
true,
Expand Down Expand Up @@ -699,7 +703,7 @@ export default class Status extends React.Component {
const userAutoLogin = localStorage.getItem("userAutoLogin") === "true";
if (
loggedOut ||
this.resolveStoredValue(
resolveStoredValue(
captivePortalSyncAuth,
`${orgSlug}_mustLogout`,
false,
Expand Down Expand Up @@ -811,60 +815,6 @@ export default class Status extends React.Component {
}
};

// eslint-disable-next-line class-methods-use-this
storeValue = (captivePortalSyncAuth, key, value, cookies) => {
/**
* Stores a value in both cookies and localStorage if synchronous
* captive portal authentication is enabled.
*
* In synchronous authentication, submitting the captive portal form
* triggers a page reload, which resets the component state.
* Storing the value in cookies ensures it persists across reloads.
*
* The value is also saved in localStorage as a fallback in case the browser does not support cookies.
*
* @param {boolean} captivePortalSyncAuth - Whether synchronous authentication is enabled.
* @param {string} key - The key under which the value is stored.
* @param {boolean} value - The value to store.
* @param {Cookies} cookies - The cookies instance used to set the cookie.
*/
if (!captivePortalSyncAuth) {
return;
}
localStorage.setItem(key, value);
cookies.set(key, value, {path: "/", maxAge: 60});
};

// eslint-disable-next-line class-methods-use-this
resolveStoredValue = (captivePortalSyncAuth, key, fallback, cookies) => {
/**
* Resolves the correct value by checking cookies, then localStorage,
* falling back to a default value if neither is found.
*
* @param {boolean} captivePortalSyncAuth - Whether synchronization is enabled.
* @param {string} cookieKey - The key to look for in cookies and localStorage.
* @param {*} fallback - The fallback value if no valid stored value is found.
* @returns {*} - The selected value based on storage or fallback.
*/
if (!captivePortalSyncAuth) {
return fallback;
}

const cookieValue = cookies.get(key);
if (cookieValue !== undefined) {
localStorage.removeItem(key);
return cookieValue;
}

const localStorageValue = localStorage.getItem(key);
if (localStorageValue !== null) {
localStorage.removeItem(key);
return localStorageValue === "true";
}

return fallback;
};

updateScreenWidth = () => {
this.setStateSafe({screenWidth: window.innerWidth});
};
Expand Down
59 changes: 59 additions & 0 deletions client/utils/captive-portal-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {localStorage} from "./storage";

/**
* Stores a value in both cookies and localStorage if synchronous
* captive portal authentication is enabled.
*
* In synchronous authentication, submitting the captive portal form
* triggers a page reload, which resets the component state.
* Storing the value in cookies ensures it persists across reloads.
*
* The value is also saved in localStorage as a fallback in case the browser does not support cookies.
*
* @param {boolean} captivePortalSyncAuth - Whether synchronous authentication is enabled.
* @param {string} key - The key under which the value is stored.
* @param {boolean} value - The value to store.
* @param {Cookies} cookies - The cookies instance used to set the cookie.
*/
export const storeValue = (captivePortalSyncAuth, key, value, cookies) => {
if (!captivePortalSyncAuth) {
return;
}
localStorage.setItem(key, value);
cookies.set(key, value, {path: "/", maxAge: 60});
};

/**
* Resolves the correct value by checking cookies, then localStorage,
* falling back to a default value if neither is found.
*
* @param {boolean} captivePortalSyncAuth - Whether synchronization is enabled.
* @param {string} key - The key to look for in cookies and localStorage.
* @param {*} fallback - The fallback value if no valid stored value is found.
* @param {Cookies} cookies - The cookies instance used to get the cookie.
* @returns {*} - The selected value based on storage or fallback.
*/
export const resolveStoredValue = (
captivePortalSyncAuth,
key,
fallback,
cookies,
) => {
if (!captivePortalSyncAuth) {
return fallback;
}

const cookieValue = cookies.get(key);
if (cookieValue !== undefined) {
localStorage.removeItem(key);
return cookieValue === true || cookieValue === "true";
}

const localStorageValue = localStorage.getItem(key);
if (localStorageValue !== null) {
localStorage.removeItem(key);
return localStorageValue === "true";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return fallback;
};
Comment thread
kunalverma2512 marked this conversation as resolved.
64 changes: 64 additions & 0 deletions client/utils/captive-portal-storage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {resolveStoredValue} from "./captive-portal-storage";
import {localStorage} from "./storage";

// Mock the internal storage dependency
jest.mock("./storage", () => ({
localStorage: {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
},
}));

describe("captive-portal-storage utility", () => {
let mockCookies;

beforeEach(() => {
// Clear mocks before each test
jest.clearAllMocks();

mockCookies = {
get: jest.fn(),
set: jest.fn(),
};
});

describe("resolveStoredValue", () => {
const key = "testKey";
const fallback = "fallbackValue";

it("returns fallback when captivePortalSyncAuth is false", () => {
const result = resolveStoredValue(false, key, fallback, mockCookies);
expect(result).toBe(fallback);
expect(mockCookies.get).not.toHaveBeenCalled();
});

it("returns true and removes localStorage when cookie exists", () => {
mockCookies.get.mockReturnValue("true"); // Cookie exists

const result = resolveStoredValue(true, key, fallback, mockCookies);

expect(result).toBe(true);
expect(localStorage.removeItem).toHaveBeenCalledWith(key);
});

it("returns true and removes localStorage when cookie is undefined but localStorage exists", () => {
mockCookies.get.mockReturnValue(undefined); // No cookie
localStorage.getItem.mockReturnValue("true"); // Has localStorage

const result = resolveStoredValue(true, key, fallback, mockCookies);

expect(result).toBe(true);
expect(localStorage.removeItem).toHaveBeenCalledWith(key);
});

it("returns fallback when neither cookie nor localStorage is present", () => {
mockCookies.get.mockReturnValue(undefined);
localStorage.getItem.mockReturnValue(null);

const result = resolveStoredValue(true, key, fallback, mockCookies);

expect(result).toBe(fallback);
});
});
});
Loading