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