index.ts raw

   1  import { requireNativeModule, type EventSubscription } from 'expo-modules-core';
   2  
   3  export type LaunchMode =
   4    | 'normal'
   5    | 'cct-normal'       // Custom-Tabs-shaped normal launch (toggle on home screen)
   6    | 'cct-private'      // Custom-Tabs-shaped private launch (CCT toggle + Private button)
   7                         //   Sends multiple hints: ENABLE_EPHEMERAL_BROWSING (Chrome 137+),
   8                         //   EXTRA_OPEN_NEW_INCOGNITO_TAB (Brave/Edge/Vivaldi).
   9                         //   Browser honors whichever it understands; Firefox ignores both.
  10    | 'firefox-normal'   // Firefox/IronFox — forced non-private via CCT shape (legacy, unused)
  11    | 'chromium-class'   // Chrome — IncognitoTabLauncher class with URL via intent.data
  12    | 'chromium-extra'   // Brave/Vivaldi/Edge — VIEW + EXTRA_OPEN_NEW_INCOGNITO_TAB cascade
  13    | 'firefox-private';
  14  
  15  type DefaultBrowserModuleType = {
  16    isDefaultBrowser(): boolean;
  17    getDefaultBrowserPackage(): string | null;
  18    getInstalledBrowsers(): string[];
  19    getBrowserIcon(pkg: string): string | null;
  20    getBrowserActivities(pkg: string): string[];
  21    getInitialSharedText(): string | null;
  22    getInitialViewUrl(): string | null;
  23    markViewUrlConsumed(): void;
  24    markSharedTextConsumed(): void;
  25    openInBrowser(
  26      url: string,
  27      pkg: string,
  28      mode: LaunchMode | null,
  29      extras: { key: string; type: 'bool' | 'string' | 'int'; value: string | boolean | number }[] | null,
  30    ): boolean;
  31    launchPackage(pkg: string): boolean;
  32    getBoolPref(key: string, defaultValue: boolean): boolean;
  33    setBoolPref(key: string, value: boolean): void;
  34    getStringPref(key: string, defaultValue: string): string;
  35    setStringPref(key: string, value: string): void;
  36    addListener(event: 'onSharedTextChanged', listener: (e: { text: string }) => void): EventSubscription;
  37    addListener(event: 'onIncomingUrl', listener: (e: { url: string }) => void): EventSubscription;
  38  };
  39  
  40  const DefaultBrowser = requireNativeModule<DefaultBrowserModuleType>('DefaultBrowser');
  41  
  42  export function isDefaultBrowser(): boolean {
  43    return DefaultBrowser.isDefaultBrowser();
  44  }
  45  
  46  export function getDefaultBrowserPackage(): string | null {
  47    return DefaultBrowser.getDefaultBrowserPackage();
  48  }
  49  
  50  export function getInstalledBrowsers(): string[] {
  51    return DefaultBrowser.getInstalledBrowsers();
  52  }
  53  
  54  export function getBrowserIcon(pkg: string): string | null {
  55    return DefaultBrowser.getBrowserIcon(pkg);
  56  }
  57  
  58  export function getBrowserActivities(pkg: string): string[] {
  59    return DefaultBrowser.getBrowserActivities(pkg);
  60  }
  61  
  62  export function getInitialSharedText(): string | null {
  63    return DefaultBrowser.getInitialSharedText();
  64  }
  65  
  66  /**
  67   * Symmetric to getInitialSharedText, but reads the URL from an
  68   * ACTION_VIEW intent (clicking an http/https link in another app).
  69   * Returns the URL once then self-clears, so polling on every foreground
  70   * transition is safe. Used as a safety net for the case where
  71   * expo-linking's onNewIntent listener drops the first delivery after
  72   * the activity is brought to front.
  73   */
  74  export function getInitialViewUrl(): string | null {
  75    return DefaultBrowser.getInitialViewUrl();
  76  }
  77  
  78  /**
  79   * Mark the native VIEW / SEND intent slots consumed without reading them.
  80   * Call right after handling a URL delivered by the onIncomingUrl /
  81   * onSharedTextChanged event, so the AppState-resume safety drain
  82   * (getInitial*) doesn't later re-serve the same already-handled intent
  83   * and overwrite the URL field with the stale link.
  84   */
  85  export function markViewUrlConsumed(): void {
  86    DefaultBrowser.markViewUrlConsumed();
  87  }
  88  
  89  export function markSharedTextConsumed(): void {
  90    DefaultBrowser.markSharedTextConsumed();
  91  }
  92  
  93  export function openInBrowser(
  94    url: string,
  95    pkg: string,
  96    mode: LaunchMode | null = 'normal',
  97    extras: { key: string; type: 'bool' | 'string' | 'int'; value: string | boolean | number }[] | null = null,
  98  ): boolean {
  99    return DefaultBrowser.openInBrowser(url, pkg, mode, extras);
 100  }
 101  
 102  /**
 103   * Launch a browser by its package's MAIN/LAUNCHER intent — no URL.
 104   * Used by the "Browser config" preset to open the browser to its home
 105   * screen so the user can paste a clipboarded internal-scheme URL
 106   * (chrome://flags, brave://flags, about:config) into the address bar.
 107   * Internal schemes can't reach the browser via VIEW intent, so this
 108   * + Clipboard is the only path that works.
 109   */
 110  export function launchPackage(pkg: string): boolean {
 111    return DefaultBrowser.launchPackage(pkg);
 112  }
 113  
 114  export function getBoolPref(key: string, defaultValue: boolean): boolean {
 115    return DefaultBrowser.getBoolPref(key, defaultValue);
 116  }
 117  
 118  export function setBoolPref(key: string, value: boolean): void {
 119    DefaultBrowser.setBoolPref(key, value);
 120  }
 121  
 122  export function getStringPref(key: string, defaultValue: string): string {
 123    return DefaultBrowser.getStringPref(key, defaultValue);
 124  }
 125  
 126  export function setStringPref(key: string, value: string): void {
 127    DefaultBrowser.setStringPref(key, value);
 128  }
 129  
 130  /**
 131   * Subscribe to fresh SEND intents while the app is already running.
 132   * Fires when the system delivers a new intent (typical share-sheet flow
 133   * into an already-resumed activity) so the JS layer doesn't have to wait
 134   * for an AppState transition to poll getInitialSharedText.
 135   */
 136  export function onSharedTextChanged(listener: (text: string) => void): EventSubscription {
 137    return DefaultBrowser.addListener('onSharedTextChanged', (e) => listener(e.text));
 138  }
 139  
 140  /**
 141   * Subscribe to fresh ACTION_VIEW intents while the app is already
 142   * running. Fires alongside expo-linking's 'url' event — both pipes
 143   * funnel into the same `consumeIncoming` dedup so duplicates are
 144   * harmless. Belt-and-suspenders for the case where expo-linking misses
 145   * the first delivery after a cold-to-warm transition.
 146   */
 147  export function onIncomingUrl(listener: (url: string) => void): EventSubscription {
 148    return DefaultBrowser.addListener('onIncomingUrl', (e) => listener(e.url));
 149  }
 150