diff --git a/apps/client/src/common/utils/__tests__/urlPresets.test.ts b/apps/client/src/common/utils/__tests__/urlPresets.test.ts index 45635e6c9..7b127d703 100644 --- a/apps/client/src/common/utils/__tests__/urlPresets.test.ts +++ b/apps/client/src/common/utils/__tests__/urlPresets.test.ts @@ -1,6 +1,6 @@ import { resolvePath } from 'react-router-dom'; -import { generatePathFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets'; +import { arePathsEquivalent, generatePathFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets'; describe('validateUrlPresetPaths()', () => { test.each([ @@ -60,6 +60,18 @@ describe('getRouteFromPreset()', () => { const location = resolvePath('/unknown'); expect(getRouteFromPreset(location, presets)).toEqual(null); }); + + describe('handle url sharing edge cases', () => { + it('finds the correct preset when the url contains extra arguments', () => { + const location = resolvePath('/demopage?locked=true&token=123'); + expect(getRouteFromPreset(location, presets)?.startsWith('timer?user=guest&alias=demopage')).toBeTruthy() + }) + + it('appends the feature params to the alias', () => { + const location = resolvePath('/demopage?locked=true&token=123'); + expect(getRouteFromPreset(location, presets)).toBe('timer?user=guest&alias=demopage&locked=true&token=123') + }) + }); }); describe('generatePathFromPreset()', () => { @@ -67,6 +79,29 @@ describe('generatePathFromPreset()', () => { ['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'], ['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'], ])('generates a path from a preset: %s', (path, alias, expected) => { - expect(generatePathFromPreset(path, alias)).toEqual(expected); + expect(generatePathFromPreset(path, alias, null, null)).toEqual(expected); + }); + + test('appends the feature params to the alias', () => { + expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe('timer?user=guest&alias=demopage&locked=true&token=123'); }); }); + +describe('arePathsEquivalent()', () => { + it("checks whether the paths match", () => { + expect(arePathsEquivalent('demopage', 'timer')).toBeFalsy(); + expect(arePathsEquivalent('timer', 'timer')).toBeTruthy(); + expect(arePathsEquivalent('timer?user=guest', 'timer?user=guest')).toBeTruthy(); + }) + + it("checks whether the params match", () => { + expect(arePathsEquivalent('timer?test=a', 'timer?test=b')).toBeFalsy(); + expect(arePathsEquivalent('timer?test=a', 'timer?test=a')).toBeTruthy(); + }) + + it("considers edge cases for the url sharing feature", () => { + expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=b')).toBeFalsy(); + expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy(); + }) +}); + diff --git a/apps/client/src/common/utils/urlPresets.ts b/apps/client/src/common/utils/urlPresets.ts index c4ef93c3b..19795af2d 100644 --- a/apps/client/src/common/utils/urlPresets.ts +++ b/apps/client/src/common/utils/urlPresets.ts @@ -36,31 +36,35 @@ function removeTrailingSlash(text: string): string { /** * Checks whether the current location corresponds to a preset and returns the new path if necessary */ -export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]) { +export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): string | null { // current url is the pathname without the leading slash const currentURL = location.pathname.substring(1); + const searchParams = new URLSearchParams(location.search); + + // check if we have token or locked in the search params + const locked = searchParams.get('locked'); + const token = searchParams.get('token'); // we need to check if the whole url is an alias const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled); if (foundPreset) { // if so, we can redirect to the preset path - return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias); + return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token); } // if the current url is not an alias, we check if the alias is in the search parameters - const searchParams = new URLSearchParams(location.search); - const presetOnPage = searchParams.get('alias'); if (!presetOnPage) { return null; } + const currentPath = `${location.pathname}${location.search}`.substring(1); + for (const preset of urlPresets) { // if the page has a known enabled alias, we check if we need to redirect if (preset.alias === presetOnPage && preset.enabled) { - const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias); - const currentPath = `${location.pathname}${location.search}`.substring(1); - if (currentPath !== newPath) { + const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token); + if (!arePathsEquivalent(currentPath, newPath)) { // if current path is out of date // return new path so we can redirect return newPath; @@ -73,13 +77,50 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]) { /** * Handles generating a path and search parameters from a preset */ -export function generatePathFromPreset(pathAndParams: string, alias: string): string { +export function generatePathFromPreset(pathAndParams: string, alias: string, locked: string | null, token: string | null ): string { const path = resolvePath(pathAndParams); const searchParams = new URLSearchParams(path.search); // save the alias so we have a reference to it being a preset and can update if necessary searchParams.set('alias', alias); + // maintain params from the URL search feature + if (locked) { + searchParams.set('locked', locked); + } + + if (token) { + searchParams.set('token', token); + } + // return path concatenated without the leading slash return `${path.pathname}?${searchParams}`.substring(1); } + +/** + * Utility checks if two paths are equivalent + * Considers the edge cases for url sharing where a path may contain extra arguments from the alias + * - token + * - locked + */ +export function arePathsEquivalent(currentPath: string, newPath: string): boolean { + const currentUrl = new URL(currentPath, document.location.origin); + const newUrl = new URL(newPath, document.location.origin); + + // check path + if (currentUrl.pathname !== newUrl.pathname) { + return false + } + + // check search params + // if the params match, we dont need further checks + if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) { + return true + } + + // if there is no match, we check the edge cases for the url sharing feature + currentUrl.searchParams.delete('token'); + currentUrl.searchParams.delete('locked'); + + return currentUrl.searchParams.toString() === newUrl.searchParams.toString(); +}