1 import { LOCALSTORAGE_DRAWER_KEY } from '@proton/shared/lib/drawer/constants';
2 import { isURLProtonInternal } from '@proton/shared/lib/helpers/url';
4 import { getAppHref } from '../apps/helper';
5 import { getLocalIDFromPathname } from '../authentication/pathnameHelper';
6 import type { APP_NAMES } from '../constants';
7 import { APPS, APPS_CONFIGURATION } from '../constants';
8 import window from '../window';
9 import type { DRAWER_ACTION, DrawerApp } from './interfaces';
10 import { DRAWER_EVENTS, DRAWER_NATIVE_APPS } from './interfaces';
12 const { PROTONMAIL, PROTONCALENDAR, PROTONDRIVE } = APPS;
13 export const drawerAuthorizedApps = [
14 APPS_CONFIGURATION[PROTONMAIL].subdomain,
15 APPS_CONFIGURATION[PROTONCALENDAR].subdomain,
16 APPS_CONFIGURATION[PROTONDRIVE].subdomain,
19 export const authorizedApps: string[] = [APPS.PROTONMAIL, APPS.PROTONCALENDAR, APPS.PROTONDRIVE];
21 export const drawerNativeApps: DrawerApp[] = Object.values(DRAWER_NATIVE_APPS);
22 export const drawerIframeApps: DrawerApp[] = [APPS.PROTONCALENDAR];
24 export const getLocalStorageUserDrawerKey = (userID: string) => `${LOCALSTORAGE_DRAWER_KEY}-${userID}`;
26 export const getIsNativeDrawerApp = (app: string): app is DrawerApp => {
27 const tsDrawerNativeApps: string[] = [...drawerNativeApps];
29 return tsDrawerNativeApps.includes(app);
32 export const getIsIframedDrawerApp = (app: string): app is DrawerApp & APP_NAMES => {
33 const tsDrawerIframeApps: string[] = [...drawerIframeApps];
35 return tsDrawerIframeApps.includes(app);
38 export const getIsDrawerApp = (app: string): app is DrawerApp => {
39 return getIsNativeDrawerApp(app) || getIsIframedDrawerApp(app);
42 export const isAuthorizedDrawerUrl = (url: string, hostname: string) => {
44 const originURL = new URL(url);
46 // Get subdomain of the url => e.g. mail, calendar, drive
47 const appFromUrl = originURL.hostname.split('.')[0];
49 return isURLProtonInternal(url, hostname) && drawerAuthorizedApps.includes(appFromUrl);
51 // the URL constructor will throw if no URL can be built out of url
56 export const getIsAuthorizedApp = (appName: string): appName is APP_NAMES => {
57 return authorizedApps.includes(appName);
60 export const getIsDrawerPostMessage = (
62 hostname = window.location.hostname
63 ): event is MessageEvent<DRAWER_ACTION> => {
64 const origin = event.origin;
66 // sandboxed iframes might have a "null" origin instead of a valid one
67 // so we need to handle this case, otherwise we will get an error
68 if (!origin || origin === 'null') {
73 * The message is a "valid" side app message if
74 * - The message is coming from an authorized app
75 * - event.data is defined
76 * - event.data.type is part of the SIDE_APP_EVENT enum
79 isAuthorizedDrawerUrl(origin, hostname) && event.data && Object.values(DRAWER_EVENTS).includes(event.data.type)
83 export const postMessageFromIframe = (message: DRAWER_ACTION, parentApp: APP_NAMES, location = window.location) => {
84 if (!getIsAuthorizedApp(parentApp)) {
87 const parentUrl = getAppHref('/', parentApp, getLocalIDFromPathname(location.pathname), location);
89 window.parent?.postMessage(message, parentUrl);
92 export const postMessageToIframe = (message: DRAWER_ACTION, iframedApp: DrawerApp, location = window.location) => {
93 if (!getIsAuthorizedApp(iframedApp)) {
96 const iframe = document.querySelector('[id^=drawer-app-iframe]') as HTMLIFrameElement | null;
97 const targetOrigin = getAppHref('/', iframedApp, getLocalIDFromPathname(location.pathname), location);
99 iframe?.contentWindow?.postMessage(message, targetOrigin);
103 * Allow to add the parent app in a URL we will open in the Drawer
104 * From the child application, we need to know who is the parent. For that, we add it in the URL we want to open
105 * Depending on the case you might want to replace path or not
107 * - "replacePath === true": You want to replace the path by the app
108 * e.g. url = "https://calendar.proton.pink/u/0/event?Action=VIEW&EventID=eventID&RecurrenceID=1670835600" and currentApp = "proton-mail"
109 * ==> https://calendar.proton.pink/u/0/mail?Action=VIEW&EventID=eventID&RecurrenceID=1670835600
110 * "event" has been replaced by "mail"
111 * - "replacePath === false": You want to add your app to the path
112 * e.g. url = "https://calendar.proton.pink/u/0/something" and currentApp = "proton-mail"
113 * ==> "https://calendar.proton.pink/u/0/mail/something"
114 * "mail" has been added to the path
116 export const addParentAppToUrl = (url: string, currentApp: APP_NAMES, replacePath = true) => {
117 const targetUrl = new URL(url);
118 const splitPathname = targetUrl.pathname.split('/').filter((el) => el !== '');
120 const currentAppSubdomain = APPS_CONFIGURATION[currentApp].subdomain;
122 // splitPathname[0] & splitPathname[1] corresponds to the user local id /u/localID
123 // splitPathname[2] should be the parentApp name
124 // If we find parentApp, we don't need to add it to the pathname
125 if (splitPathname[2] !== currentAppSubdomain) {
126 // In some cases, we want to replace completely this param (e.g. Calendar "view" parameter needs to be mail instead of week)
128 splitPathname.splice(2, 1, currentAppSubdomain);
130 splitPathname.splice(2, 0, currentAppSubdomain);
133 targetUrl.pathname = splitPathname.join('/');
136 return targetUrl.href;
139 export const getDisplayContactsInDrawer = (app: APP_NAMES) => {
140 return app === APPS.PROTONMAIL || app === APPS.PROTONCALENDAR || app === APPS.PROTONDRIVE;
143 export const getDisplaySettingsInDrawer = (app: APP_NAMES) => {
144 return app === APPS.PROTONMAIL || app === APPS.PROTONCALENDAR || app === APPS.PROTONDRIVE;
147 export const getDisplaySecurityCenterInDrawer = (app: APP_NAMES) => {
148 return app === APPS.PROTONMAIL;
151 export const closeDrawerFromChildApp = (parentApp: APP_NAMES, currentApp: APP_NAMES, closeDefinitely?: boolean) => {
152 if (!getIsIframedDrawerApp(currentApp)) {
153 throw new Error('Cannot close non-iframed app');
156 postMessageFromIframe(
158 type: DRAWER_EVENTS.CLOSE,
159 payload: { app: currentApp, closeDefinitely },
165 export const isAppInView = (currentApp: DrawerApp, appInView?: DrawerApp) => {
166 return appInView ? appInView === currentApp : false;
169 export const getDisplayDrawerApp = (currentApp: APP_NAMES, toOpenApp: DrawerApp) => {
170 if (toOpenApp === APPS.PROTONCALENDAR) {
171 return currentApp === APPS.PROTONMAIL || currentApp === APPS.PROTONDRIVE;
172 } else if (toOpenApp === DRAWER_NATIVE_APPS.CONTACTS) {
173 return getDisplayContactsInDrawer(currentApp);
174 } else if (toOpenApp === DRAWER_NATIVE_APPS.QUICK_SETTINGS) {
175 return getDisplaySettingsInDrawer(currentApp);
176 } else if (toOpenApp === DRAWER_NATIVE_APPS.SECURITY_CENTER) {
177 return getDisplaySecurityCenterInDrawer(currentApp);
181 export const getDrawerAppFromURL = (url: string) => {
182 if (!isAuthorizedDrawerUrl(url, window.location.hostname)) {
186 const originURL = new URL(url);
188 // Get subdomain of the url => e.g. mail, calendar, drive
189 const subdomainFromUrl = originURL.hostname.split('.')[0];
191 const appName = Object.keys(APPS_CONFIGURATION).find((app) => {
192 return APPS_CONFIGURATION[app as APP_NAMES].subdomain === subdomainFromUrl;
195 return appName as DrawerApp;