1 import { type Location, createBrowserHistory } from 'history';
3 import { decodeUtf8Base64, encodeUtf8Base64 } from '@proton/crypto/lib/utils';
4 import { authStore } from '@proton/pass/lib/auth/store';
5 import type { ItemFilters, ItemType, MaybeNull } from '@proton/pass/types';
6 import { partialMerge } from '@proton/pass/utils/object/merge';
7 import { getLocalIDPath, stripLocalBasenameFromPathname } from '@proton/shared/lib/authentication/pathnameHelper';
8 import { APPS } from '@proton/shared/lib/constants';
9 import { getAppUrlFromApiUrl } from '@proton/shared/lib/helpers/url';
11 export type ItemNewRouteParams = { type: ItemType };
12 export type ItemRouteOptions = { trashed?: boolean; prefix?: string };
13 export type AuthRouteState = { error?: string; userInitiatedLock?: boolean };
15 export enum UnauthorizedRoutes {
16 SecureLink = '/secure-link/:token',
19 export const history = createBrowserHistory();
21 /** Appends the localID path to the provided path. If `localID` is not
22 * defined returns the original path (this may be the case in the extension
23 * for user's using legacy AuthSessions which were not persisted with the
24 * session's LocalID */
25 export const getLocalPath = (path: string = '') => {
26 const localID = authStore.getLocalID();
27 return localID !== undefined ? `/${getLocalIDPath(localID)}/${path}` : `/${path}`;
30 export const removeLocalPath = (path: string) => {
31 const re = /\/u\/\d+(?:\/(.+))?\/?$/;
32 if (!re.test(path)) return path;
34 const match = path.match(re);
35 return match?.[1] ?? '';
38 export const subPath = (path: string, sub: string) => `${path}/${sub}`;
39 export const maybeTrash = (path: string, inTrash?: boolean) => (inTrash ? subPath('trash', path) : path);
41 /** Resolves the item route given a shareId and an itemId. */
42 export const getItemRoute = (shareId: string, itemId: string, options?: ItemRouteOptions) => {
43 const basePath = maybeTrash(`share/${shareId}/item/${itemId}`, options?.trashed);
44 const prefixed = options?.prefix ? subPath(options.prefix, basePath) : basePath;
45 return getLocalPath(prefixed);
48 export const getItemHistoryRoute = (shareId: string, itemId: string, options?: ItemRouteOptions) =>
49 `${getItemRoute(shareId, itemId, options)}/history`;
51 /** Resolves the new item route given an item type. */
52 export const getNewItemRoute = (type: ItemType) => getLocalPath(`item/new/${type}`);
53 export const getTrashRoute = () => getLocalPath('trash');
54 export const getOnboardingRoute = () => getLocalPath('onboarding');
56 export const getInitialFilters = (): ItemFilters => ({ search: '', sort: 'recent', type: '*', selectedShareId: null });
58 export const decodeFilters = (encodedFilters: MaybeNull<string>): ItemFilters =>
63 if (!encodedFilters) return {};
64 return JSON.parse(decodeUtf8Base64(encodedFilters));
71 export const decodeFiltersFromSearch = (search: string) => {
72 const params = new URLSearchParams(search);
73 return decodeFilters(params.get('filters'));
76 export const encodeFilters = (filters: ItemFilters): string => encodeUtf8Base64(JSON.stringify(filters));
78 export const getPassWebUrl = (apiUrl: string, subPath: string = '') => {
79 const appUrl = getAppUrlFromApiUrl(apiUrl, APPS.PROTONPASS);
80 return appUrl.toString() + subPath;
83 export const getRouteError = (search: string) => new URLSearchParams(search).get('error');
85 export const getBootRedirectPath = (bootLocation: Location) => {
86 const searchParams = new URLSearchParams(bootLocation.search);
88 const redirectPath = (() => {
89 if (searchParams.get('filters') !== null) {
90 return bootLocation.pathname;
93 const [, shareId, itemId] = bootLocation.pathname.match('share/([^/]+)(/item/([^/]+))?') || [];
94 if (shareId || itemId) {
95 const filters = partialMerge(getInitialFilters(), { selectedShareId: shareId });
96 searchParams.set('filters', encodeFilters(filters));
97 return `${bootLocation.pathname}?${searchParams.toString()}`;
100 return bootLocation.pathname;
103 return stripLocalBasenameFromPathname(redirectPath);
105 const extractPathWithoutFragment = (path: string): string => {
106 const indexOfHash = path.indexOf('#');
107 return indexOfHash === -1 ? path : path.substring(0, indexOfHash);
110 const pathMatchesRoute = (path: string, routeTemplate: string): boolean => {
111 const cleanedPath = extractPathWithoutFragment(path);
112 const templateParts = routeTemplate.split('/');
113 const pathParts = cleanedPath.split('/');
116 templateParts.length === pathParts.length &&
117 templateParts.every((part, i) => part.startsWith(':') || part === pathParts[i])
121 export const isUnauthorizedPath = ({ pathname }: Location): boolean =>
122 Object.values(UnauthorizedRoutes).some((route) => pathMatchesRoute(pathname, route));
124 /** In Electron, direct `location.href` mutations don't work due
125 * to custom URL schemes. We use IPC via the `ContextBridgeApi` to
126 * properly reload the page in desktop builds. For non-desktop,
127 * standard `window.location.href` assignment is used. */
128 export const reloadHref = (href: string) => {
129 if (DESKTOP_BUILD) void window?.ctxBridge?.navigate(href);
130 else window.location.href = href;