@nihinmedia/medgen-react-native 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,137 @@
1
+ import React, { useCallback, useMemo, useRef } from "react";
2
+
3
+ import { ActivityIndicator, Linking, StyleSheet, View } from "react-native";
4
+
5
+ import { WebView as RNWebView } from "react-native-webview";
6
+
7
+ const WebView = RNWebView;
8
+
9
+ import { handleBridgeMessage } from "./bridge";
10
+
11
+ import { embedBase, emit } from "./client";
12
+
13
+ export const originOf = u => {
14
+ const m = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]+/i.exec(u);
15
+ return m ? m[0].toLowerCase() : "";
16
+ };
17
+
18
+ export const decideNavigation = (url, base, isTopFrame) => {
19
+ if (isSameOrigin(url, base)) return "load";
20
+ if (isTopFrame === false) return "block";
21
+ if (!/^https?:\/\//i.test(url)) return "block";
22
+ return "external";
23
+ };
24
+
25
+ const openExternally = async url => {
26
+ try {
27
+ if (!await Linking.canOpenURL(url)) {
28
+ emit("surface.externalLinkFailed", {
29
+ url: url,
30
+ reason: "unsupported"
31
+ });
32
+ return false;
33
+ }
34
+ await Linking.openURL(url);
35
+ emit("surface.externalLinkOpened", {
36
+ url: url
37
+ });
38
+ return true;
39
+ } catch (error) {
40
+ emit("surface.externalLinkFailed", {
41
+ url: url,
42
+ reason: error instanceof Error ? error.message : "unknown"
43
+ });
44
+ return false;
45
+ }
46
+ };
47
+
48
+ const makeNonce = () => `${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
49
+
50
+ export const isSameOrigin = (target, base) => {
51
+ if (target === "about:blank") return true;
52
+ const b = originOf(base);
53
+ return b !== "" && originOf(target) === b;
54
+ };
55
+
56
+ export const MedGenWebView = ({url: url, handlers: handlers, capabilities: capabilities, onClose: onClose}) => {
57
+ const ref = useRef(null);
58
+ const nonce = useMemo(() => makeNonce(), [ url ]);
59
+ const isMedGenOrigin = useCallback(target => isSameOrigin(target, embedBase()), []);
60
+ const onShouldStartLoadWithRequest = useCallback(req => {
61
+ const decision = decideNavigation(req.url, embedBase(), req.isTopFrame);
62
+ if (decision === "load") return true;
63
+ if (decision === "external") void openExternally(req.url);
64
+ return false;
65
+ }, [ isMedGenOrigin ]);
66
+ const onMessage = useCallback(event => {
67
+ if (!isMedGenOrigin(event.nativeEvent.url)) return;
68
+ void handleBridgeMessage(event.nativeEvent.data, replyJson => {
69
+ const safe = JSON.stringify(replyJson).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
70
+ ref.current?.injectJavaScript(`window.__medgen && window.__medgen.receive(${safe}); true;`);
71
+ }, {
72
+ handlers: handlers,
73
+ capabilities: capabilities,
74
+ onClose: onClose,
75
+ nonce: nonce
76
+ });
77
+ }, [ isMedGenOrigin, handlers, capabilities, onClose, nonce ]);
78
+ return React.createElement(WebView, {
79
+ ref: ref,
80
+ source: {
81
+ uri: url
82
+ },
83
+ style: styles.web,
84
+ injectedJavaScriptBeforeContentLoaded: `window.__medgenNonce=${JSON.stringify(nonce)};true;`,
85
+ injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true,
86
+ originWhitelist: [ originOf(embedBase()) ],
87
+ onShouldStartLoadWithRequest: onShouldStartLoadWithRequest,
88
+ onMessage: onMessage,
89
+ allowFileAccess: false,
90
+ allowUniversalAccessFromFileURLs: false,
91
+ mixedContentMode: "never",
92
+ javaScriptCanOpenWindowsAutomatically: false,
93
+ setSupportMultipleWindows: false,
94
+ onOpenWindow: event => {
95
+ const url = event?.nativeEvent?.targetUrl;
96
+ if (!url) return;
97
+ const decision = decideNavigation(url, embedBase(), true);
98
+ if (decision === "load") {
99
+ ref.current?.injectJavaScript(`window.location.assign(${JSON.stringify(url)});true;`);
100
+ } else if (decision === "external") {
101
+ void openExternally(url);
102
+ }
103
+ },
104
+ onError: event => {
105
+ emit("surface.loadFailed", {
106
+ description: event?.nativeEvent?.description,
107
+ code: event?.nativeEvent?.code
108
+ });
109
+ },
110
+ onHttpError: event => {
111
+ emit("surface.loadFailed", {
112
+ statusCode: event?.nativeEvent?.statusCode
113
+ });
114
+ },
115
+ sharedCookiesEnabled: true,
116
+ thirdPartyCookiesEnabled: false,
117
+ startInLoadingState: true,
118
+ renderLoading: () => React.createElement(View, {
119
+ style: styles.loading
120
+ }, React.createElement(ActivityIndicator, {
121
+ size: "large"
122
+ })),
123
+ bounces: false,
124
+ overScrollMode: "never"
125
+ });
126
+ };
127
+
128
+ const styles = StyleSheet.create({
129
+ web: {
130
+ flex: 1
131
+ },
132
+ loading: {
133
+ ...StyleSheet.absoluteFillObject,
134
+ alignItems: "center",
135
+ justifyContent: "center"
136
+ }
137
+ });
@@ -0,0 +1,127 @@
1
+ import { SDK_VERSION } from "./client";
2
+
3
+ const V = 1;
4
+
5
+ const ok = (id, payload) => JSON.stringify({
6
+ v: V,
7
+ id: id,
8
+ ok: true,
9
+ payload: payload
10
+ });
11
+
12
+ const fail = (id, code, message) => JSON.stringify({
13
+ v: V,
14
+ id: id,
15
+ ok: false,
16
+ error: {
17
+ code: code,
18
+ message: message
19
+ }
20
+ });
21
+
22
+ export async function handleBridgeMessage(raw, reply, ctx) {
23
+ let msg;
24
+ try {
25
+ msg = JSON.parse(raw);
26
+ } catch {
27
+ return;
28
+ }
29
+ if (msg.v !== V || !msg.id || !msg.method) return;
30
+ const {handlers: handlers, capabilities: capabilities, onClose: onClose, nonce: nonce} = ctx;
31
+ if (typeof msg.nonce !== "string" || msg.nonce.length !== nonce.length || msg.nonce !== nonce) {
32
+ return;
33
+ }
34
+ const {id: id, method: method} = msg;
35
+ const payload = msg.payload ?? {};
36
+ try {
37
+ switch (method) {
38
+ case "system.ready":
39
+ reply(ok(id, {
40
+ platform: "react-native",
41
+ version: SDK_VERSION,
42
+ features: {
43
+ share: Boolean(handlers.onShare) && capabilities.share,
44
+ clipboard: Boolean(handlers.onCopy) && capabilities.clipboard,
45
+ speech: Boolean(handlers.onSpeechStart) && capabilities.dictation,
46
+ fileUpload: Boolean(handlers.onFilePick) && capabilities.fileUpload
47
+ }
48
+ }));
49
+ return;
50
+
51
+ case "system.dismiss":
52
+ onClose();
53
+ reply(ok(id));
54
+ return;
55
+
56
+ case "share.content":
57
+ {
58
+ if (!capabilities.share) return reply(fail(id, "FORBIDDEN", "share not granted"));
59
+ if (!handlers.onShare) return reply(fail(id, "UNAVAILABLE", "No share handler"));
60
+ const completed = await handlers.onShare(String(payload.text ?? ""));
61
+ reply(ok(id, {
62
+ completed: completed
63
+ }));
64
+ return;
65
+ }
66
+
67
+ case "clipboard.write":
68
+ {
69
+ if (!capabilities.clipboard) return reply(fail(id, "FORBIDDEN", "clipboard not granted"));
70
+ if (!handlers.onCopy) return reply(fail(id, "UNAVAILABLE", "No clipboard handler"));
71
+ const html = capabilities.richClipboard ? payload.html : undefined;
72
+ const written = await handlers.onCopy({
73
+ text: String(payload.text ?? ""),
74
+ html: html
75
+ });
76
+ reply(ok(id, {
77
+ written: written
78
+ }));
79
+ return;
80
+ }
81
+
82
+ case "speech.start":
83
+ {
84
+ if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
85
+ if (!handlers.onSpeechStart) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
86
+ await handlers.onSpeechStart();
87
+ reply(ok(id, {
88
+ listening: true
89
+ }));
90
+ return;
91
+ }
92
+
93
+ case "speech.stop":
94
+ {
95
+ if (!capabilities.dictation) return reply(fail(id, "FORBIDDEN", "dictation not granted"));
96
+ if (!handlers.onSpeechStop) return reply(fail(id, "UNAVAILABLE", "No speech handler"));
97
+ const transcript = await handlers.onSpeechStop();
98
+ reply(ok(id, {
99
+ transcript: transcript
100
+ }));
101
+ return;
102
+ }
103
+
104
+ case "file.pick":
105
+ case "file.capture":
106
+ {
107
+ if (!capabilities.fileUpload) return reply(fail(id, "FORBIDDEN", "file upload not granted"));
108
+ if (!handlers.onFilePick) return reply(ok(id, {
109
+ files: []
110
+ }));
111
+ const files = await handlers.onFilePick();
112
+ reply(ok(id, {
113
+ files: files
114
+ }));
115
+ return;
116
+ }
117
+
118
+ default:
119
+ return;
120
+ }
121
+ } catch (e) {
122
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
123
+ console.warn("[MedGen] native handler failed:", e);
124
+ }
125
+ reply(fail(id, "HANDLER_ERROR", "Handler failed"));
126
+ }
127
+ }
@@ -0,0 +1,298 @@
1
+ import { Platform } from "react-native";
2
+
3
+ import * as Keychain from "react-native-keychain";
4
+
5
+ import { MedGenError } from "./types";
6
+
7
+ const KEYCHAIN_SERVICE = "jp.nihinmedia.medgen";
8
+
9
+ const ASSERTION_TIMEOUT_MS = 1e4;
10
+
11
+ const ATTESTATION_TIMEOUT_MS = 15e3;
12
+
13
+ export const SDK_VERSION = "1.0.9";
14
+
15
+ const DEFAULT_API = {
16
+ development: "https://dev.sugoimed.com",
17
+ staging: "https://staging.sugoimed.com",
18
+ production: "https://medgen.nihinmedia.jp"
19
+ };
20
+
21
+ const DEFAULT_EMBED = {
22
+ development: "https://dev.sugoimed.com",
23
+ staging: "https://staging.sugoimed.com",
24
+ production: "https://medgen.nihinmedia.jp"
25
+ };
26
+
27
+ let config = null;
28
+
29
+ export function configure(cfg) {
30
+ config = cfg;
31
+ }
32
+
33
+ export function resetConfig() {
34
+ config = null;
35
+ }
36
+
37
+ export function requireConfig() {
38
+ if (!config) {
39
+ throw new MedGenError("NOT_INITIALIZED", "Call MedGen.initialize() before present()");
40
+ }
41
+ return config;
42
+ }
43
+
44
+ function requireHttps(url, field) {
45
+ const lower = url.toLowerCase();
46
+ const isLocal = lower.startsWith("http://localhost") || lower.startsWith("http://127.0.0.1");
47
+ if (!lower.startsWith("https://") && !isLocal) {
48
+ throw new MedGenError("NOT_INITIALIZED", `${field} must use https:// — refusing to send an assertion over plaintext`);
49
+ }
50
+ return url.replace(/\/+$/, "");
51
+ }
52
+
53
+ export const apiBase = () => {
54
+ const cfg = requireConfig();
55
+ const base = cfg.apiBaseUrl ?? DEFAULT_API[cfg.environment] ?? DEFAULT_API.staging;
56
+ return requireHttps(base, "apiBaseUrl");
57
+ };
58
+
59
+ export const embedBase = () => {
60
+ const cfg = requireConfig();
61
+ const base = cfg.embedBaseUrl ?? DEFAULT_EMBED[cfg.environment] ?? DEFAULT_EMBED.staging;
62
+ return requireHttps(base, "embedBaseUrl");
63
+ };
64
+
65
+ export function emit(name, detail) {
66
+ const handler = config?.onEvent;
67
+ if (!handler) return;
68
+ const event = {
69
+ name: name,
70
+ at: Date.now(),
71
+ detail: detail
72
+ };
73
+ try {
74
+ handler(event);
75
+ } catch {}
76
+ }
77
+
78
+ export async function storeCredentials(creds) {
79
+ await Keychain.setGenericPassword("medgen", JSON.stringify(creds), {
80
+ service: KEYCHAIN_SERVICE,
81
+ accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY
82
+ });
83
+ }
84
+
85
+ export async function readCredentials() {
86
+ try {
87
+ const res = await Keychain.getGenericPassword({
88
+ service: KEYCHAIN_SERVICE
89
+ });
90
+ if (!res) return null;
91
+ return JSON.parse(res.password);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ export async function clearCredentials() {
98
+ await Keychain.resetGenericPassword({
99
+ service: KEYCHAIN_SERVICE
100
+ });
101
+ }
102
+
103
+ function withTimeout(p, ms, code, what) {
104
+ let timer;
105
+ const timeout = new Promise((_, reject) => {
106
+ timer = setTimeout(() => reject(new MedGenError(code, `${what} exceeded ${ms}ms`, true)), ms);
107
+ });
108
+ return Promise.race([ p, timeout ]).finally(() => clearTimeout(timer));
109
+ }
110
+
111
+ async function fetchAssertion() {
112
+ const {assertionProvider: assertionProvider, assertion: assertion} = requireConfig();
113
+ if (!assertionProvider) {
114
+ if (assertion) return assertion;
115
+ throw new MedGenError("NOT_INITIALIZED", "Set assertionProvider (or, to try the SDK once, assertion) in MedGen.initialize()");
116
+ }
117
+ try {
118
+ return await withTimeout(assertionProvider(), ASSERTION_TIMEOUT_MS, "ASSERTION_PROVIDER_TIMEOUT", "Assertion provider");
119
+ } catch (e) {
120
+ if (e instanceof MedGenError) throw e;
121
+ throw new MedGenError("ASSERTION_PROVIDER_FAILED", e instanceof Error ? e.message : "Assertion provider failed", true);
122
+ }
123
+ }
124
+
125
+ async function fetchAttestation(assertion) {
126
+ const {attestationProvider: attestationProvider} = requireConfig();
127
+ if (!attestationProvider) return "";
128
+ try {
129
+ return await withTimeout(attestationProvider(assertion), ATTESTATION_TIMEOUT_MS, "ATTESTATION_FAILED", "Attestation provider");
130
+ } catch (e) {
131
+ if (e instanceof MedGenError) throw e;
132
+ throw new MedGenError("ATTESTATION_FAILED", e instanceof Error ? e.message : "Attestation provider failed", true);
133
+ }
134
+ }
135
+
136
+ const CAPABILITY_DEFAULTS = {
137
+ theming: false,
138
+ customLogo: false,
139
+ hideAttribution: false,
140
+ quickAnswer: true,
141
+ detailedAnswer: true,
142
+ japaneseAnswer: true,
143
+ dictation: false,
144
+ fileUpload: false,
145
+ clipboard: true,
146
+ richClipboard: true,
147
+ share: true,
148
+ chatHistory: true,
149
+ bookmarks: false,
150
+ citationsPanel: true,
151
+ directLink: true
152
+ };
153
+
154
+ export function parseCapabilities(raw) {
155
+ const src = raw ?? {};
156
+ const pick = (key, fallback) => typeof src[key] === "boolean" ? src[key] : fallback;
157
+ return {
158
+ theming: pick("theming", CAPABILITY_DEFAULTS.theming),
159
+ customLogo: pick("custom_logo", CAPABILITY_DEFAULTS.customLogo),
160
+ hideAttribution: pick("hide_attribution", CAPABILITY_DEFAULTS.hideAttribution),
161
+ quickAnswer: pick("quick_answer", CAPABILITY_DEFAULTS.quickAnswer),
162
+ detailedAnswer: pick("detailed_answer", CAPABILITY_DEFAULTS.detailedAnswer),
163
+ japaneseAnswer: pick("japanese_answer", CAPABILITY_DEFAULTS.japaneseAnswer),
164
+ dictation: pick("dictation", CAPABILITY_DEFAULTS.dictation),
165
+ fileUpload: pick("file_upload", CAPABILITY_DEFAULTS.fileUpload),
166
+ clipboard: pick("clipboard", CAPABILITY_DEFAULTS.clipboard),
167
+ richClipboard: pick("rich_clipboard", CAPABILITY_DEFAULTS.richClipboard),
168
+ share: pick("share", CAPABILITY_DEFAULTS.share),
169
+ chatHistory: pick("chat_history", CAPABILITY_DEFAULTS.chatHistory),
170
+ bookmarks: pick("bookmarks", CAPABILITY_DEFAULTS.bookmarks),
171
+ citationsPanel: pick("citations_panel", CAPABILITY_DEFAULTS.citationsPanel),
172
+ directLink: pick("direct_link", CAPABILITY_DEFAULTS.directLink)
173
+ };
174
+ }
175
+
176
+ const CLIENT_BY_PLATFORM = {
177
+ ios: "sdk-ios",
178
+ android: "sdk-android"
179
+ };
180
+
181
+ export async function exchange() {
182
+ emit("exchange.started");
183
+ const assertion = await fetchAssertion();
184
+ const attestation = await fetchAttestation(assertion);
185
+ const client = CLIENT_BY_PLATFORM[Platform.OS] ?? "sdk-ios";
186
+ const headers = {
187
+ "Content-Type": "application/json",
188
+ "X-MedGen-Client": `rn/${SDK_VERSION}`
189
+ };
190
+ if (attestation) headers["X-MedGen-Attestation"] = attestation;
191
+ let res;
192
+ try {
193
+ res = await fetch(`${apiBase()}/api/v1/partner/session/exchange`, {
194
+ method: "POST",
195
+ headers: headers,
196
+ body: JSON.stringify({
197
+ assertion: assertion,
198
+ client: client
199
+ })
200
+ });
201
+ } catch (e) {
202
+ const err = new MedGenError("NETWORK_UNAVAILABLE", e instanceof Error ? e.message : "Network unavailable", true);
203
+ emit("exchange.failed", {
204
+ code: err.code
205
+ });
206
+ throw err;
207
+ }
208
+ const body = await res.json().catch(() => ({}));
209
+ if (!res.ok) {
210
+ const err = new MedGenError(body.error ?? "UNKNOWN", body.message ?? "Exchange failed", Boolean(body.retryable), body.support_reference);
211
+ emit("exchange.failed", {
212
+ code: err.code,
213
+ status: res.status
214
+ });
215
+ throw err;
216
+ }
217
+ const access = body.access;
218
+ const refresh = body.refresh;
219
+ if (typeof access !== "string" || !access || typeof refresh !== "string" || !refresh) {
220
+ const err = new MedGenError("UNKNOWN", "Exchange response is missing session tokens", true);
221
+ emit("exchange.failed", {
222
+ code: err.code
223
+ });
224
+ throw err;
225
+ }
226
+ if (typeof body.handoff_code !== "string" || !body.handoff_code) {
227
+ const err = new MedGenError("UNKNOWN", "Exchange response is missing a handoff code", true);
228
+ emit("exchange.failed", {
229
+ code: err.code
230
+ });
231
+ throw err;
232
+ }
233
+ await storeCredentials({
234
+ access: access,
235
+ refresh: refresh
236
+ });
237
+ const user = body.user ?? {};
238
+ const partner = body.partner ?? {};
239
+ const result = {
240
+ handoffCode: body.handoff_code,
241
+ isNewUser: Boolean(user.is_new),
242
+ emailCollisionPending: body.error === "EMAIL_COLLISION_PENDING",
243
+ brandingVersion: String(partner.branding_version ?? "1"),
244
+ capabilities: parseCapabilities(partner.capabilities ?? body.capabilities)
245
+ };
246
+ emit("exchange.succeeded", {
247
+ isNewUser: result.isNewUser,
248
+ emailCollisionPending: result.emailCollisionPending
249
+ });
250
+ return result;
251
+ }
252
+
253
+ const B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
254
+
255
+ function toBase64(input) {
256
+ const bytes = [];
257
+ const utf8 = encodeURIComponent(input);
258
+ for (let i = 0; i < utf8.length; i += 1) {
259
+ if (utf8[i] === "%") {
260
+ bytes.push(parseInt(utf8.substr(i + 1, 2), 16));
261
+ i += 2;
262
+ } else {
263
+ bytes.push(utf8.charCodeAt(i));
264
+ }
265
+ }
266
+ let out = "";
267
+ for (let i = 0; i < bytes.length; i += 3) {
268
+ const b0 = bytes[i];
269
+ const b1 = bytes[i + 1];
270
+ const b2 = bytes[i + 2];
271
+ out += B64_ALPHABET[b0 >> 2];
272
+ out += B64_ALPHABET[(b0 & 3) << 4 | (b1 ?? 0) >> 4];
273
+ out += b1 === undefined ? "=" : B64_ALPHABET[(b1 & 15) << 2 | (b2 ?? 0) >> 6];
274
+ out += b2 === undefined ? "=" : B64_ALPHABET[b2 & 63];
275
+ }
276
+ return out;
277
+ }
278
+
279
+ export function buildEmbedUrl(handoffCode, entryQuestion, capabilities) {
280
+ const cfg = requireConfig();
281
+ const params = [ [ "hc", handoffCode ], [ "locale", cfg.locale ?? "ja" ] ];
282
+ if (cfg.theme && capabilities?.theming !== false) {
283
+ const branding = {
284
+ colors: {
285
+ primary: cfg.theme.primary,
286
+ secondary: cfg.theme.secondary,
287
+ accent: cfg.theme.accent,
288
+ surface: cfg.theme.surface,
289
+ onSurface: cfg.theme.onSurface
290
+ },
291
+ colorsDark: cfg.theme.darkVariant
292
+ };
293
+ params.push([ "theme", toBase64(JSON.stringify(branding)) ]);
294
+ }
295
+ if (entryQuestion) params.push([ "q", entryQuestion ]);
296
+ const query = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
297
+ return `${embedBase()}/embed?${query}`;
298
+ }
@@ -0,0 +1,147 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ import { ActivityIndicator, Modal, SafeAreaView, StyleSheet, View } from "react-native";
4
+
5
+ import { buildEmbedUrl, clearCredentials, configure, emit, exchange, requireConfig } from "./client";
6
+
7
+ import { MedGenWebView } from "./MedGenWebView";
8
+
9
+ import { MedGenError } from "./types";
10
+
11
+ export * from "./types";
12
+
13
+ export { SDK_VERSION } from "./client";
14
+
15
+ const hostRef = {
16
+ current: null
17
+ };
18
+
19
+ const teardowns = new Set;
20
+
21
+ export const MedGen = {
22
+ initialize(config) {
23
+ if (!config.assertionProvider && !config.assertion) {
24
+ throw new MedGenError("NOT_INITIALIZED", "MedGen.initialize() needs assertionProvider (or, to try the SDK once, assertion)");
25
+ }
26
+ if (!config.assertionProvider && config.assertion) {
27
+ if (typeof __DEV__ !== "undefined" && !__DEV__) {
28
+ throw new MedGenError("NOT_INITIALIZED", "A static `assertion` cannot be used in a release build — it is " + "single-use and cannot be refreshed. Set `assertionProvider`.");
29
+ }
30
+ console.warn("[MedGen] Using a static `assertion`. It is single-use and expires " + "after 120s, so only the first present() will succeed. Set " + "`assertionProvider` before shipping.");
31
+ }
32
+ configure(config);
33
+ },
34
+ async present(options) {
35
+ if (!hostRef.current) {
36
+ throw new MedGenError("NOT_INITIALIZED", "Mount <MedGenHost /> near the root of your app");
37
+ }
38
+ await hostRef.current(options);
39
+ },
40
+ async signOut() {
41
+ teardowns.forEach(fn => {
42
+ try {
43
+ fn();
44
+ } catch {}
45
+ });
46
+ await clearCredentials();
47
+ }
48
+ };
49
+
50
+ export const MedGenHost = () => {
51
+ const [url, setUrl] = useState(null);
52
+ const [capabilities, setCapabilities] = useState(null);
53
+ const present = useCallback(async options => {
54
+ const result = await exchange();
55
+ setCapabilities(result.capabilities);
56
+ setUrl(buildEmbedUrl(result.handoffCode, options?.entryQuestion, result.capabilities));
57
+ emit("surface.opened");
58
+ }, []);
59
+ useEffect(() => {
60
+ hostRef.current = present;
61
+ return () => {
62
+ if (hostRef.current === present) hostRef.current = null;
63
+ };
64
+ }, [ present ]);
65
+ const close = useCallback(() => {
66
+ setUrl(null);
67
+ setCapabilities(null);
68
+ emit("surface.closed");
69
+ }, []);
70
+ useEffect(() => {
71
+ teardowns.add(close);
72
+ return () => {
73
+ teardowns.delete(close);
74
+ };
75
+ }, [ close ]);
76
+ return React.createElement(Modal, {
77
+ visible: url !== null,
78
+ animationType: "slide",
79
+ presentationStyle: "fullScreen",
80
+ onRequestClose: close
81
+ }, React.createElement(SafeAreaView, {
82
+ style: styles.container
83
+ }, url && capabilities ? React.createElement(MedGenWebView, {
84
+ url: url,
85
+ handlers: requireConfig().nativeHandlers ?? {},
86
+ capabilities: capabilities,
87
+ onClose: close
88
+ }) : null));
89
+ };
90
+
91
+ export const MedGenSurface = ({visible: visible = true, options: options, onError: onError}) => {
92
+ const [url, setUrl] = useState(null);
93
+ const [capabilities, setCapabilities] = useState(null);
94
+ const [loading, setLoading] = useState(false);
95
+ const started = useRef(false);
96
+ useEffect(() => {
97
+ if (!visible || started.current) return;
98
+ started.current = true;
99
+ setLoading(true);
100
+ exchange().then(result => {
101
+ setCapabilities(result.capabilities);
102
+ setUrl(buildEmbedUrl(result.handoffCode, options?.entryQuestion, result.capabilities));
103
+ emit("surface.opened");
104
+ }).catch(e => {
105
+ started.current = false;
106
+ onError?.(e);
107
+ }).finally(() => setLoading(false));
108
+ }, [ visible, options, onError ]);
109
+ useEffect(() => {
110
+ const reset = () => {
111
+ setUrl(null);
112
+ setCapabilities(null);
113
+ started.current = false;
114
+ };
115
+ teardowns.add(reset);
116
+ return () => {
117
+ teardowns.delete(reset);
118
+ };
119
+ }, []);
120
+ return React.createElement(View, {
121
+ style: [ styles.container, !visible && styles.hidden ],
122
+ pointerEvents: visible ? "auto" : "none"
123
+ }, url && capabilities ? React.createElement(MedGenWebView, {
124
+ url: url,
125
+ handlers: requireConfig().nativeHandlers ?? {},
126
+ capabilities: capabilities,
127
+ onClose: () => undefined
128
+ }) : React.createElement(View, {
129
+ style: styles.loading
130
+ }, loading ? React.createElement(ActivityIndicator, {
131
+ size: "large"
132
+ }) : null));
133
+ };
134
+
135
+ const styles = StyleSheet.create({
136
+ container: {
137
+ flex: 1
138
+ },
139
+ hidden: {
140
+ display: "none"
141
+ },
142
+ loading: {
143
+ flex: 1,
144
+ alignItems: "center",
145
+ justifyContent: "center"
146
+ }
147
+ });