1import { type NavigationProp, type ParamListBase, useNavigation } from "@react-navigation/native";
2import { useCallback, useLayoutEffect, useState } from "react";
3import { Linking, Pressable, View } from "react-native";
4import { WebView } from "react-native-webview";
5
6import { AppActivityIndicator } from "../../../components/AppActivityIndicator";
7import { AppText as Text } from "../../../components/AppText";
8import { LoadingStrip } from "../../../components/LoadingStrip";
9import { SymbolView } from "../../../components/AppSymbol";
10import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url";
11
12export function SettingsLegalDocumentCloseHeaderButton() {
13 const navigation = useNavigation();
14
15 return (
16 <Pressable
17 accessibilityLabel="Close legal document"
18 accessibilityRole="button"
19 hitSlop={12}
20 onPress={() => navigation.goBack()}
21 className="p-2 active:opacity-60"
22 >
23 <SymbolView
24 name="xmark"
25 size={18}
26 tintColorClassName={"accent-icon"}
27 type="monochrome"
28 weight="semibold"
29 />
30 </Pressable>
31 );
32}
33
34export function SettingsLegalDocumentExternalHeaderButton({
35 externalUrl = LEGAL_URL,
36}: {
37 readonly externalUrl?: string;
38}) {
39 const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL;
40
41 return (
42 <Pressable
43 accessibilityLabel="Open legal documents in external browser"
44 accessibilityRole="button"
45 hitSlop={12}
46 onPress={() => void Linking.openURL(safeExternalUrl).catch(() => undefined)}
47 className="p-2 active:opacity-60"
48 >
49 <SymbolView
50 name="safari"
51 size={19}
52 tintColorClassName={"accent-icon"}
53 type="monochrome"
54 weight="regular"
55 />
56 </Pressable>
57 );
58}
59
60interface SettingsLegalDocumentRouteScreenProps {
61 readonly documentName: string;
62 readonly documentUrl: string;
63}
64
65export function SettingsLegalDocumentRouteScreen({
66 documentName,
67 documentUrl,
68}: SettingsLegalDocumentRouteScreenProps) {
69 const navigation = useNavigation<NavigationProp<ParamListBase>>();
70 const [reloadKey, setReloadKey] = useState(0);
71 const [loadProgress, setLoadProgress] = useState(0);
72 const [loadError, setLoadError] = useState<string | null>(null);
73 const [externalUrl, setExternalUrl] = useState(documentUrl);
74 const renderExternalHeaderButton = useCallback(
75 () => <SettingsLegalDocumentExternalHeaderButton externalUrl={externalUrl} />,
76 [externalUrl],
77 );
78
79 useLayoutEffect(() => {
80 navigation.setOptions({
81 headerRight: renderExternalHeaderButton,
82 });
83 }, [navigation, renderExternalHeaderButton]);
84
85 const openExternalUrl = useCallback((url: string) => {
86 void Linking.openURL(url).catch(() => undefined);
87 }, []);
88 if (loadError) {
89 return (
90 <View className="flex-1 items-center justify-center gap-4 bg-sheet px-8">
91 <SymbolView
92 name="exclamationmark.triangle"
93 size={32}
94 tintColorClassName={"accent-icon"}
95 type="monochrome"
96 weight="regular"
97 />
98 <View className="items-center gap-2">
99 <Text className="text-center font-t3-bold text-lg text-foreground">
100 Couldn&apos;t load the {documentName.toLowerCase()}
101 </Text>
102 <Text selectable className="text-center text-sm leading-normal text-foreground-muted">
103 {loadError}
104 </Text>
105 </View>
106 <View className="w-full max-w-[320px] gap-2">
107 <Pressable
108 accessibilityRole="button"
109 onPress={() => {
110 setLoadError(null);
111 setReloadKey((value) => value + 1);
112 }}
113 className="items-center rounded-xl bg-foreground px-4 py-3 active:opacity-80"
114 >
115 <Text className="font-t3-bold text-base text-sheet">Try Again</Text>
116 </Pressable>
117 <Pressable
118 accessibilityRole="link"
119 onPress={() => openExternalUrl(documentUrl)}
120 className="items-center rounded-xl px-4 py-3 active:bg-foreground/5"
121 >
122 <Text className="font-t3-medium text-base text-foreground-muted">Open in Browser</Text>
123 </Pressable>
124 </View>
125 </View>
126 );
127 }
128
129 return (
130 <View className="relative flex-1 bg-sheet">
131 {loadProgress > 0 && loadProgress < 1 ? <LoadingStrip progress={loadProgress} /> : null}
132 <WebView
133 key={reloadKey}
134 source={{ uri: documentUrl }}
135 originWhitelist={["http://*", "https://*", "mailto:*"]}
136 cacheEnabled={false}
137 cacheMode="LOAD_NO_CACHE"
138 allowsBackForwardNavigationGestures
139 domStorageEnabled={false}
140 incognito
141 javaScriptEnabled={false}
142 setSupportMultipleWindows={false}
143 sharedCookiesEnabled={false}
144 thirdPartyCookiesEnabled={false}
145 startInLoadingState
146 onShouldStartLoadWithRequest={(request) => {
147 if (isLegalDocumentUrl(request.url)) return true;
148
149 openExternalUrl(request.url);
150 return false;
151 }}
152 onLoadProgress={(event) => {
153 setLoadProgress(event.nativeEvent.progress);
154 }}
155 onLoadStart={() => {
156 setLoadProgress(0.05);
157 setLoadError(null);
158 }}
159 onLoadEnd={(event) => {
160 if (isLegalDocumentUrl(event.nativeEvent.url)) {
161 setExternalUrl(event.nativeEvent.url);
162 }
163 setLoadProgress(0);
164 }}
165 onError={(event) => {
166 setLoadProgress(0);
167 setLoadError(event.nativeEvent.description || "The page could not be loaded.");
168 }}
169 onHttpError={(event) => {
170 if (!isLegalDocumentUrl(event.nativeEvent.url)) return;
171 setLoadProgress(0);
172 setLoadError(`The server returned status ${event.nativeEvent.statusCode}.`);
173 }}
174 renderLoading={() => (
175 <View className="absolute inset-0 items-center justify-center bg-sheet">
176 <AppActivityIndicator />
177 </View>
178 )}
179 style={{ flex: 1, backgroundColor: "transparent" }}
180 />
181 </View>
182 );
183}