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