@nihinmedia/medgen-react-native 1.0.12

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,152 @@
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, onClose: onClose}) => {
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: () => {
128
+ setUrl(null);
129
+ setCapabilities(null);
130
+ started.current = false;
131
+ onClose?.();
132
+ }
133
+ }) : React.createElement(View, {
134
+ style: styles.loading
135
+ }, loading ? React.createElement(ActivityIndicator, {
136
+ size: "large"
137
+ }) : null));
138
+ };
139
+
140
+ const styles = StyleSheet.create({
141
+ container: {
142
+ flex: 1
143
+ },
144
+ hidden: {
145
+ display: "none"
146
+ },
147
+ loading: {
148
+ flex: 1,
149
+ alignItems: "center",
150
+ justifyContent: "center"
151
+ }
152
+ });
@@ -0,0 +1,171 @@
1
+ import { PermissionsAndroid, Platform, Share } from "react-native";
2
+
3
+ import { addPdfLinks } from "./pdfLinks";
4
+
5
+ let registry = {};
6
+
7
+ export const registerNativeModules = mods => {
8
+ registry = {
9
+ ...registry,
10
+ ...mods
11
+ };
12
+ };
13
+
14
+ const unwrap = mod => mod?.default ?? mod;
15
+
16
+ const fromRegistry = key => {
17
+ const mod = registry[key];
18
+ return mod ? unwrap(mod) : null;
19
+ };
20
+
21
+ const loadClipboard = () => fromRegistry("clipboard");
22
+
23
+ const loadShare = () => fromRegistry("share");
24
+
25
+ const loadHtmlToPdf = () => fromRegistry("htmlToPdf");
26
+
27
+ const loadBlobUtil = () => fromRegistry("blobUtil");
28
+
29
+ export const ensureStoragePermission = async () => {
30
+ if (Platform.OS !== "android") return true;
31
+ if (typeof Platform.Version === "number" && Platform.Version >= 29) return true;
32
+ const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
33
+ if (!permission) return false;
34
+ try {
35
+ const granted = await PermissionsAndroid.request(permission, {
36
+ title: "Save to your device",
37
+ message: "MedGen needs permission to save this document.",
38
+ buttonPositive: "Allow"
39
+ });
40
+ return granted === PermissionsAndroid.RESULTS.GRANTED;
41
+ } catch {
42
+ return false;
43
+ }
44
+ };
45
+
46
+ export const writeClipboard = async (text, html) => {
47
+ const Clipboard = loadClipboard();
48
+ if (!Clipboard) return false;
49
+ try {
50
+ if (html && typeof Clipboard.setStringHTML === "function") {
51
+ Clipboard.setStringHTML(html, text);
52
+ } else {
53
+ Clipboard.setString(text);
54
+ }
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ };
60
+
61
+ export const canShareText = () => true;
62
+
63
+ export const canCopy = () => loadClipboard() !== null;
64
+
65
+ export const canRenderPdf = () => loadHtmlToPdf() !== null;
66
+
67
+ export const canSaveToDownloads = () => Platform.OS === "android" && loadBlobUtil() !== null;
68
+
69
+ export const shareText = async text => {
70
+ try {
71
+ const result = await Share.share({
72
+ message: text
73
+ });
74
+ return result.action !== Share.dismissedAction;
75
+ } catch {
76
+ return false;
77
+ }
78
+ };
79
+
80
+ export const shareFile = async (uri, mimeType = "application/pdf", fileName) => {
81
+ const RNShare = loadShare();
82
+ if (!RNShare) return {
83
+ saved: false,
84
+ reason: "no_module"
85
+ };
86
+ try {
87
+ await RNShare.open({
88
+ url: uri.startsWith("file://") ? uri : `file://${uri}`,
89
+ type: mimeType,
90
+ filename: fileName,
91
+ failOnCancel: false
92
+ });
93
+ return {
94
+ saved: true,
95
+ uri: uri
96
+ };
97
+ } catch (e) {
98
+ const msg = e instanceof Error ? e.message : "";
99
+ return {
100
+ saved: false,
101
+ reason: /cancel/i.test(msg) ? "cancelled" : "failed"
102
+ };
103
+ }
104
+ };
105
+
106
+ export const renderPdf = async (html, fileName, links = []) => {
107
+ const HtmlToPdf = loadHtmlToPdf();
108
+ if (!HtmlToPdf) return null;
109
+ const withLinks = Platform.OS === "ios" && links.length > 0;
110
+ try {
111
+ const {filePath: filePath, base64: base64} = await HtmlToPdf.convert({
112
+ html: html,
113
+ fileName: fileName.replace(/\.pdf$/i, ""),
114
+ directory: "Documents",
115
+ base64: withLinks
116
+ });
117
+ if (filePath && withLinks && base64) await writeLinks(filePath, base64, links);
118
+ return filePath ?? null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ };
123
+
124
+ const writeLinks = async (filePath, base64, links) => {
125
+ const Blob = loadBlobUtil();
126
+ if (!Blob?.fs?.writeFile) return;
127
+ try {
128
+ const linked = await addPdfLinks(base64, links);
129
+ await Blob.fs.writeFile(filePath, linked, "base64");
130
+ } catch {}
131
+ };
132
+
133
+ export const saveToDownloads = async (sourcePath, fileName) => {
134
+ const Blob = loadBlobUtil();
135
+ if (Platform.OS !== "android") return {
136
+ saved: false,
137
+ reason: "no_module"
138
+ };
139
+ if (!Blob) return {
140
+ saved: false,
141
+ reason: "no_module"
142
+ };
143
+ if (!await ensureStoragePermission()) {
144
+ return {
145
+ saved: false,
146
+ reason: "permission_denied"
147
+ };
148
+ }
149
+ try {
150
+ const target = `${Blob.fs.dirs.DownloadDir}/${fileName}`;
151
+ await Blob.fs.cp(sourcePath.replace("file://", ""), target);
152
+ if (Platform.OS === "android" && Blob.android?.addCompleteDownload) {
153
+ Blob.android.addCompleteDownload({
154
+ title: fileName,
155
+ description: "MedGen document",
156
+ mime: "application/pdf",
157
+ path: target,
158
+ showNotification: true
159
+ });
160
+ }
161
+ return {
162
+ saved: true,
163
+ uri: target
164
+ };
165
+ } catch {
166
+ return {
167
+ saved: false,
168
+ reason: "failed"
169
+ };
170
+ }
171
+ };
@@ -0,0 +1,45 @@
1
+ import { PDFDocument, PDFString } from "pdf-lib";
2
+
3
+ const MAX_LINKS = 2e3;
4
+
5
+ const isFiniteNumber = n => typeof n === "number" && Number.isFinite(n);
6
+
7
+ export const sanitizeLinks = raw => {
8
+ if (!Array.isArray(raw)) return [];
9
+ return raw.slice(0, MAX_LINKS).filter(l => {
10
+ const link = l;
11
+ return !!link && Number.isInteger(link.page) && link.page >= 0 && [ link.x, link.y, link.width, link.height ].every(isFiniteNumber) && link.width > 0 && link.height > 0 && typeof link.url === "string" && /^https?:\/\/[^\s]+$/i.test(link.url);
12
+ });
13
+ };
14
+
15
+ const pdfLiteral = url => url.replace(/[^\x20-\x7E]/g, c => encodeURIComponent(c)).replace(/[\\()]/g, c => `\\${c}`);
16
+
17
+ export const addPdfLinks = async (base64, links) => {
18
+ const doc = await PDFDocument.load(base64);
19
+ const pages = doc.getPages();
20
+ for (const link of links) {
21
+ const page = pages[link.page];
22
+ if (!page) continue;
23
+ const {width: pw, height: ph} = page.getSize();
24
+ const left = Math.max(0, link.x);
25
+ const right = Math.min(pw, link.x + link.width);
26
+ const top = Math.min(ph, ph - link.y);
27
+ const bottom = Math.max(0, ph - link.y - link.height);
28
+ if (right <= left || top <= bottom) continue;
29
+ const annot = doc.context.obj({
30
+ Type: "Annot",
31
+ Subtype: "Link",
32
+ Rect: [ left, bottom, right, top ],
33
+ Border: [ 0, 0, 0 ],
34
+ A: {
35
+ Type: "Action",
36
+ S: "URI",
37
+ URI: PDFString.of(pdfLiteral(link.url))
38
+ }
39
+ });
40
+ page.node.addAnnot(doc.context.register(annot));
41
+ }
42
+ return doc.saveAsBase64({
43
+ useObjectStreams: false
44
+ });
45
+ };
@@ -0,0 +1,17 @@
1
+ export let MedGenEnvironment = function(MedGenEnvironment) {
2
+ MedGenEnvironment["Development"] = "development";
3
+ MedGenEnvironment["Staging"] = "staging";
4
+ MedGenEnvironment["Production"] = "production";
5
+ return MedGenEnvironment;
6
+ }({});
7
+
8
+ export class MedGenError extends Error {
9
+ constructor(code, message, retryable = false, supportReference) {
10
+ super(message);
11
+ this.name = "MedGenError";
12
+ this.code = code;
13
+ this.retryable = retryable;
14
+ this.supportReference = supportReference;
15
+ Object.setPrototypeOf(this, MedGenError.prototype);
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { MedGenCapabilities, MedGenNativeHandlers } from './types';
3
+
4
+ export declare const originOf: (u: string) => string;
5
+
6
+ export type NavigationDecision = 'load' | 'external' | 'block';
7
+ export declare const decideNavigation: (url: string, base: string, isTopFrame?: boolean) => NavigationDecision;
8
+
9
+ export declare const isSameOrigin: (target: string, base: string) => boolean;
10
+ interface Props {
11
+ url: string;
12
+ handlers: MedGenNativeHandlers;
13
+ capabilities: MedGenCapabilities;
14
+ onClose: () => void;
15
+ }
16
+
17
+ export declare const MedGenWebView: React.FC<Props>;
18
+ export {};
@@ -0,0 +1,12 @@
1
+ import { MedGenCapabilities, MedGenNativeHandlers } from './types';
2
+ type Reply = (json: string) => void;
3
+ export interface BridgeContext {
4
+ handlers: MedGenNativeHandlers;
5
+ capabilities: MedGenCapabilities;
6
+ onClose: () => void;
7
+
8
+ nonce: string;
9
+ }
10
+
11
+ export declare function handleBridgeMessage(raw: string, reply: Reply, ctx: BridgeContext): Promise<void>;
12
+ export {};
@@ -0,0 +1,30 @@
1
+ import { MedGenBranding, ExchangeResult, MedGenCapabilities, MedGenConfig, MedGenEventName } from './types';
2
+ export declare const SDK_VERSION = "1.0.12";
3
+
4
+ export declare function configure(cfg: MedGenConfig): void;
5
+
6
+ export declare function resetConfig(): void;
7
+
8
+ export declare function requireConfig(): MedGenConfig;
9
+ export declare const apiBase: () => string;
10
+ export declare const embedBase: () => string;
11
+
12
+ export declare function emit(name: MedGenEventName, detail?: Record<string, unknown>): void;
13
+ export interface Credentials {
14
+ access: string;
15
+ refresh: string;
16
+ }
17
+
18
+ export declare function storeCredentials(creds: Credentials): Promise<void>;
19
+
20
+ export declare function readCredentials(): Promise<Credentials | null>;
21
+
22
+ export declare function clearCredentials(): Promise<void>;
23
+
24
+ export declare function parseBranding(raw: unknown): MedGenBranding;
25
+
26
+ export declare function parseCapabilities(raw: unknown): MedGenCapabilities;
27
+
28
+ export declare function exchange(): Promise<ExchangeResult>;
29
+
30
+ export declare function buildEmbedUrl(handoffCode: string, entryQuestion?: string, capabilities?: MedGenCapabilities): string;
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { MedGenConfig, MedGenError, MedGenPresentOptions } from './types';
3
+ export * from './types';
4
+ export { SDK_VERSION } from './client';
5
+ export declare const MedGen: {
6
+
7
+ initialize(config: MedGenConfig): void;
8
+
9
+ present(options?: MedGenPresentOptions): Promise<void>;
10
+
11
+ signOut(): Promise<void>;
12
+ };
13
+
14
+ export declare const MedGenHost: React.FC;
15
+
16
+ export declare const MedGenSurface: React.FC<{
17
+
18
+ visible?: boolean;
19
+ options?: MedGenPresentOptions;
20
+ onError?: (e: MedGenError) => void;
21
+
22
+ onClose?: () => void;
23
+ }>;
@@ -0,0 +1,38 @@
1
+ import { type PdfLink } from './pdfLinks';
2
+
3
+ export interface OptionalNativeModules {
4
+
5
+ clipboard?: unknown;
6
+
7
+ share?: unknown;
8
+
9
+ htmlToPdf?: unknown;
10
+
11
+ blobUtil?: unknown;
12
+ }
13
+
14
+ export declare const registerNativeModules: (mods: OptionalNativeModules) => void;
15
+
16
+ export interface SaveResult {
17
+ saved: boolean;
18
+
19
+ uri?: string;
20
+ reason?: 'no_module' | 'permission_denied' | 'cancelled' | 'failed';
21
+ }
22
+
23
+ export declare const ensureStoragePermission: () => Promise<boolean>;
24
+
25
+ export declare const writeClipboard: (text: string, html?: string) => Promise<boolean>;
26
+
27
+ export declare const canShareText: () => boolean;
28
+ export declare const canCopy: () => boolean;
29
+ export declare const canRenderPdf: () => boolean;
30
+ export declare const canSaveToDownloads: () => boolean;
31
+
32
+ export declare const shareText: (text: string) => Promise<boolean>;
33
+
34
+ export declare const shareFile: (uri: string, mimeType?: string, fileName?: string) => Promise<SaveResult>;
35
+
36
+ export declare const renderPdf: (html: string, fileName: string, links?: PdfLink[]) => Promise<string | null>;
37
+
38
+ export declare const saveToDownloads: (sourcePath: string, fileName: string) => Promise<SaveResult>;
@@ -0,0 +1,14 @@
1
+ export interface PdfLink {
2
+
3
+ page: number;
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+
9
+ url: string;
10
+ }
11
+
12
+ export declare const sanitizeLinks: (raw: unknown) => PdfLink[];
13
+
14
+ export declare const addPdfLinks: (base64: string, links: PdfLink[]) => Promise<string>;