1import * as Schema from "effect/Schema";
2import { Linking } from "react-native";
3
4const ExternalUrlTarget = Schema.Literals([
5 "file-preview",
6 "markdown-link",
7 "pull-request",
8 "provider-auth",
9 "html-render",
10 "mcp-app",
11 "secret-request",
12]);
13
14export type ExternalUrlTarget = typeof ExternalUrlTarget.Type;
15
16export class ExternalUrlOpenError extends Schema.TaggedError<ExternalUrlOpenError>()(
17 "ExternalUrlOpenError",
18 {
19 target: ExternalUrlTarget,
20 scheme: Schema.String,
21 host: Schema.optional(Schema.String),
22 cause: Schema.Defect(),
23 },
24) {
25 override get message(): string {
26 return `Failed to open ${this.target} URL with the ${this.scheme} scheme.`;
27 }
28}
29
30function externalUrlMetadata(url: string): { readonly scheme: string; readonly host?: string } {
31 try {
32 const parsed = new URL(url);
33 return {
34 scheme: parsed.protocol.replace(/:$/, "") || "unknown",
35 host: parsed.hostname || undefined,
36 };
37 } catch {
38 return {
39 scheme: /^([a-z][a-z\d+.-]*):/i.exec(url)?.[1]?.toLowerCase() ?? "unknown",
40 };
41 }
42}
43
44export async function tryOpenExternalUrl(url: string, target: ExternalUrlTarget): Promise<boolean> {
45 try {
46 await Linking.openURL(url);
47 return true;
48 } catch (cause) {
49 const error = new ExternalUrlOpenError({ target, ...externalUrlMetadata(url), cause });
50 console.error(error.message, {
51 _tag: error._tag,
52 target: error.target,
53 scheme: error.scheme,
54 host: error.host,
55 stack: error.stack,
56 });
57 return false;
58 }
59}