/ shared / utils / src / launch / launch-client.ts
launch-client.ts
  1  import { createClientLink } from './scheme';
  2  import type { Platform } from '../platform';
  3  
  4  /**
  5   * Navigator for older Microsoft (MS) browsers like Internet Explorer.
  6   */
  7  type MSNavigator = Navigator & {
  8      msLaunchUri: (
  9          href: string | URL,
 10          successCallback: () => void,
 11          failureCallback: () => void,
 12      ) => void;
 13  };
 14  
 15  /**
 16   * Check if the given value is an MSNavigator.
 17   */
 18  function isMSNavigator(value: Partial<MSNavigator>): value is MSNavigator {
 19      return typeof value?.msLaunchUri === 'function';
 20  }
 21  
 22  /**
 23   * Callback for client launches.
 24   */
 25  export type LaunchCallback = (result: {
 26      link: URL;
 27      success: boolean;
 28  }) => void | Promise<void>;
 29  
 30  /**
 31   * Attempt to launch the native client for the given Web URL.
 32   */
 33  export function launchClient(
 34      url: string | URL,
 35      platform: Platform,
 36      callback: LaunchCallback = () => {},
 37  ): void {
 38      const { window, browser, os } = platform;
 39  
 40      /** URL for opening the native application */
 41      const link = createClientLink(url, { platform });
 42  
 43      // macOS Safari
 44      if (os.isMacOS && browser.isSafari) {
 45          launchOnMacOS(link, platform, callback);
 46      }
 47      // Proprietary msLaunchUri method (IE 10+ on Windows 8+)
 48      else if (isMSNavigator(platform.navigator)) {
 49          platform.navigator.msLaunchUri(
 50              String(link),
 51              () => callback({ link, success: true }),
 52              () => callback({ link, success: false }),
 53          );
 54      }
 55      // Other platforms
 56      else {
 57          try {
 58              // on iOS, Windows and Android simply opening the href works
 59              window!.top!.window.location.href = String(link);
 60              callback({ link, success: true });
 61          } catch (e) {
 62              // we know this is NOT installed
 63              callback({ link, success: false });
 64          }
 65      }
 66  }
 67  
 68  function launchOnMacOS(
 69      link: URL,
 70      platform: Platform,
 71      callback: LaunchCallback,
 72  ): void {
 73      const { window } = platform;
 74  
 75      if (typeof window === 'undefined') {
 76          callback({ link, success: false });
 77          return;
 78      }
 79  
 80      /** Timer for blur fallback */
 81      let timer: number;
 82  
 83      /** IFrame reference for opening the client link */
 84      let iframe: HTMLIFrameElement | undefined;
 85  
 86      /** Cleanup function run after the client launch has been initiated */
 87      function finalize() {
 88          clearTimeout(timer);
 89          window!.removeEventListener('blur', finalize);
 90          if (iframe !== undefined) {
 91              window!.document.body.removeChild(iframe);
 92          }
 93  
 94          callback({ link, success: true });
 95      }
 96  
 97      // Add an iFrame window to the current document to open the URL
 98      iframe = window.document.createElement('iframe');
 99      iframe.id = 'launch-client-opener';
100      iframe.style.display = 'none';
101      window.document.body.appendChild(iframe);
102  
103      // Redirect the iFrame to the client link to trigger it to open
104      iframe.contentWindow!.location.href = String(link);
105  
106      // Wait a tiny amount of time for the client launch to appear
107      window.addEventListener('blur', finalize);
108      timer = setTimeout(finalize, 50) as unknown as number;
109  }