@dl2app/react-native 0.0.0-stage → 0.1.1

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,174 @@
1
+ "use strict";
2
+ /**
3
+ * Expo config plugin for @dl2app/react-native.
4
+ *
5
+ * It only edits the Expo config object (`ios.associatedDomains`, `android.intentFilters`,
6
+ * `scheme`); Expo's own prebuild steps turn those into entitlements and manifest entries.
7
+ * That keeps it a pure function: same input, same output, safe to run any number of times.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeOptions = normalizeOptions;
11
+ exports.mergeAssociatedDomains = mergeAssociatedDomains;
12
+ exports.mergeIntentFilters = mergeIntentFilters;
13
+ exports.mergeSchemes = mergeSchemes;
14
+ exports.applyDL2 = applyDL2;
15
+ const NAME = "@dl2app/react-native";
16
+ const ALLOWED = ["subdomain", "rootDomain", "domains", "iosDeveloperMode", "androidAutoVerify", "uriScheme"];
17
+ const HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
18
+ const LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
19
+ const SCHEME = /^[a-z][a-z0-9+.-]*$/;
20
+ function fail(message) {
21
+ throw new Error(`[${NAME}] ${message}`);
22
+ }
23
+ function checkHost(value, where) {
24
+ if (typeof value !== "string" || !value.trim())
25
+ fail(`${where} must be a host name such as "myapp.dl2.app".`);
26
+ const host = value.trim().toLowerCase();
27
+ if (/^[a-z][a-z0-9+.-]*:\/\//.test(host))
28
+ fail(`${where} is "${value}". Remove the "https://": use the host only, for example "myapp.dl2.app".`);
29
+ if (/[/?#]/.test(host))
30
+ fail(`${where} is "${value}". Remove the path: use the host only, for example "myapp.dl2.app".`);
31
+ if (/:\d+$/.test(host))
32
+ fail(`${where} is "${value}". Remove the port: app links only work on the default HTTPS port.`);
33
+ if (!HOST.test(host))
34
+ fail(`${where} is "${value}", which is not a valid host name. Example: "myapp.dl2.app".`);
35
+ return host;
36
+ }
37
+ function checkBool(value, name, fallback) {
38
+ if (value === undefined)
39
+ return fallback;
40
+ if (typeof value !== "boolean")
41
+ fail(`"${name}" must be true or false, got ${JSON.stringify(value)}.`);
42
+ return value;
43
+ }
44
+ /** Validates the plugin options and returns the full host list. Throws with a fix in the message. */
45
+ function normalizeOptions(raw) {
46
+ if (raw === undefined || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
47
+ fail(`Missing options. Use ["${NAME}", { "subdomain": "myapp" }] or ["${NAME}", { "domains": ["myapp.dl2.app", "myapp-alt.dl2.app"] }].`);
48
+ }
49
+ const o = raw;
50
+ for (const key of Object.keys(o)) {
51
+ if (ALLOWED.indexOf(key) < 0)
52
+ fail(`Unknown option "${key}". Allowed options: ${ALLOWED.join(", ")}.`);
53
+ }
54
+ const domains = [];
55
+ const add = (h) => {
56
+ if (domains.indexOf(h) < 0)
57
+ domains.push(h);
58
+ };
59
+ if (o.rootDomain !== undefined && o.subdomain === undefined)
60
+ fail(`"rootDomain" only makes sense together with "subdomain".`);
61
+ if (o.subdomain !== undefined) {
62
+ if (typeof o.subdomain !== "string" || !LABEL.test(o.subdomain.trim().toLowerCase())) {
63
+ fail(`"subdomain" is ${JSON.stringify(o.subdomain)}. Use only the first part of your link host: "myapp" for myapp.dl2.app.`);
64
+ }
65
+ const sub = o.subdomain.trim().toLowerCase();
66
+ if (/-alt$/.test(sub))
67
+ fail(`"subdomain" is "${sub}". Leave out "-alt": the alternate host is added for you.`);
68
+ const root = o.rootDomain === undefined ? "dl2.app" : checkHost(o.rootDomain, `"rootDomain"`);
69
+ add(`${sub}.${root}`);
70
+ add(`${sub}-alt.${root}`);
71
+ }
72
+ if (o.domains !== undefined) {
73
+ if (!Array.isArray(o.domains))
74
+ fail(`"domains" must be a list of host names, got ${JSON.stringify(o.domains)}.`);
75
+ o.domains.forEach((d, i) => add(checkHost(d, `"domains[${i}]"`)));
76
+ }
77
+ if (!domains.length)
78
+ fail(`No link hosts. Pass "subdomain" (your DL2 project subdomain) or a non-empty "domains" list.`);
79
+ let uriScheme = null;
80
+ if (o.uriScheme !== undefined) {
81
+ if (typeof o.uriScheme !== "string" || !o.uriScheme)
82
+ fail(`"uriScheme" must be text such as "myapp".`);
83
+ const s = o.uriScheme.replace(/:\/\/$/, "");
84
+ if (!SCHEME.test(s))
85
+ fail(`"uriScheme" is "${o.uriScheme}". Use lowercase letters, digits, "+", "-" or ".", starting with a letter, and no "://".`);
86
+ if (s === "http" || s === "https")
87
+ fail(`"uriScheme" cannot be "${s}". Use a scheme of your own such as "myapp".`);
88
+ uriScheme = s;
89
+ }
90
+ return {
91
+ domains,
92
+ iosDeveloperMode: checkBool(o.iosDeveloperMode, "iosDeveloperMode", false),
93
+ androidAutoVerify: checkBool(o.androidAutoVerify, "androidAutoVerify", true),
94
+ uriScheme,
95
+ };
96
+ }
97
+ /** Adds `applinks:` entries, keeping every entry the app already has exactly as it is. */
98
+ function mergeAssociatedDomains(existing, domains, developerMode) {
99
+ const out = Array.isArray(existing) ? existing.filter((e) => typeof e === "string") : [];
100
+ for (const d of domains) {
101
+ // A host the app already lists keeps its own mode: the option only applies to entries added here.
102
+ if (out.some((e) => e.split("?")[0].toLowerCase() === `applinks:${d}`))
103
+ continue;
104
+ out.push(`applinks:${d}${developerMode ? "?mode=developer" : ""}`);
105
+ }
106
+ return out;
107
+ }
108
+ function list(v) {
109
+ return v === undefined ? [] : Array.isArray(v) ? v : [v];
110
+ }
111
+ function isBrowsableView(f) {
112
+ return f.action === "VIEW" && list(f.category).indexOf("BROWSABLE") >= 0;
113
+ }
114
+ const RESTRICTIONS = ["port", "path", "pathPrefix", "pathPattern", "pathSuffix", "pathAdvancedPattern", "mimeType"];
115
+ /** Android applies every path, port and type in a filter to all of its hosts, so one of them narrows the whole filter. */
116
+ function isRestricted(f) {
117
+ return list(f.data).some((d) => RESTRICTIONS.some((k) => d[k] !== undefined && d[k] !== null && d[k] !== ""));
118
+ }
119
+ /** Adds one https intent filter for the hosts that are not covered yet, and one for the scheme. */
120
+ function mergeIntentFilters(existing, domains, autoVerify, uriScheme) {
121
+ const out = Array.isArray(existing) ? existing.slice() : [];
122
+ const covered = new Set();
123
+ const schemes = new Set();
124
+ for (const f of out) {
125
+ if (!f || !isBrowsableView(f))
126
+ continue;
127
+ // A filter limited to some paths does not open every link of the host.
128
+ const open = !isRestricted(f);
129
+ for (const d of list(f.data)) {
130
+ if (open && d.scheme === "https" && typeof d.host === "string" && (!autoVerify || f.autoVerify === true))
131
+ covered.add(d.host.toLowerCase());
132
+ if (d.scheme && !d.host)
133
+ schemes.add(d.scheme);
134
+ }
135
+ }
136
+ const missing = domains.filter((d) => !covered.has(d));
137
+ if (missing.length) {
138
+ const filter = {
139
+ action: "VIEW",
140
+ data: missing.map((host) => ({ scheme: "https", host })),
141
+ category: ["BROWSABLE", "DEFAULT"],
142
+ };
143
+ if (autoVerify)
144
+ filter.autoVerify = true;
145
+ out.push(filter);
146
+ }
147
+ if (uriScheme && !schemes.has(uriScheme)) {
148
+ out.push({ action: "VIEW", data: [{ scheme: uriScheme }], category: ["BROWSABLE", "DEFAULT"] });
149
+ }
150
+ return out;
151
+ }
152
+ function mergeSchemes(existing, uriScheme) {
153
+ const current = list(existing).filter((s) => typeof s === "string");
154
+ if (!uriScheme || current.indexOf(uriScheme) >= 0)
155
+ return existing;
156
+ return current.length ? current.concat(uriScheme) : uriScheme;
157
+ }
158
+ /** The whole plugin as a pure function. Does not modify its input. */
159
+ function applyDL2(config, rawOptions) {
160
+ const input = config;
161
+ const o = normalizeOptions(rawOptions);
162
+ const next = { ...input };
163
+ next.ios = { ...(input.ios || {}), associatedDomains: mergeAssociatedDomains(input.ios?.associatedDomains, o.domains, o.iosDeveloperMode) };
164
+ next.android = {
165
+ ...(input.android || {}),
166
+ intentFilters: mergeIntentFilters(input.android?.intentFilters, o.domains, o.androidAutoVerify, o.uriScheme),
167
+ };
168
+ const scheme = mergeSchemes(input.scheme, o.uriScheme);
169
+ if (scheme !== undefined)
170
+ next.scheme = scheme;
171
+ return next;
172
+ }
173
+ const withDL2 = (config, options) => applyDL2(config, options);
174
+ exports.default = withDL2;
@@ -0,0 +1,14 @@
1
+ import { type ReactElement } from "react";
2
+ export type DL2PasteButtonProps = {
3
+ /** Called with the pasted text after the user taps the button. */
4
+ onPaste: (text: string) => void;
5
+ /** Size the button here; the system draws its label and icon. */
6
+ style?: unknown;
7
+ };
8
+ /**
9
+ * The system paste button (UIPasteControl, iOS 16+). iOS hands over the clipboard only when the
10
+ * user taps it, and shows no "pasted from" banner. Renders nothing on Android and on older iOS.
11
+ *
12
+ * <DL2PasteButton style={{ width: 160, height: 44 }} onPaste={(text) => DL2.matchClipboard(text)} />
13
+ */
14
+ export declare function DL2PasteButton(props: DL2PasteButtonProps): ReactElement | null;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DL2PasteButton = DL2PasteButton;
4
+ const react_1 = require("react");
5
+ const react_native_1 = require("react-native");
6
+ const expo_modules_core_1 = require("expo-modules-core");
7
+ let NativeView;
8
+ function nativeView() {
9
+ if (NativeView === undefined) {
10
+ try {
11
+ NativeView = (0, expo_modules_core_1.requireNativeViewManager)("DL2");
12
+ }
13
+ catch {
14
+ NativeView = null;
15
+ }
16
+ }
17
+ return NativeView || null;
18
+ }
19
+ /**
20
+ * The system paste button (UIPasteControl, iOS 16+). iOS hands over the clipboard only when the
21
+ * user taps it, and shows no "pasted from" banner. Renders nothing on Android and on older iOS.
22
+ *
23
+ * <DL2PasteButton style={{ width: 160, height: 44 }} onPaste={(text) => DL2.matchClipboard(text)} />
24
+ */
25
+ function DL2PasteButton(props) {
26
+ if (react_native_1.Platform.OS !== "ios" || parseInt(String(react_native_1.Platform.Version), 10) < 16)
27
+ return null;
28
+ const View = nativeView();
29
+ if (!View)
30
+ return null;
31
+ return (0, react_1.createElement)(View, {
32
+ style: props.style || { width: 160, height: 44 },
33
+ onPaste: (e) => {
34
+ try {
35
+ const text = e && e.nativeEvent ? e.nativeEvent.text : undefined;
36
+ if (typeof text === "string" && text)
37
+ props.onPaste(text);
38
+ }
39
+ catch {
40
+ /* never let a handler error reach the native event dispatcher */
41
+ }
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,32 @@
1
+ import type { DL2Config, DL2CreateLinkInput, DL2DebugInfo, DL2Deps, DL2LinkEvent, DL2TrackOptions } from "./types";
2
+ export declare const SDK_VERSION = "0.1.1";
3
+ type Listener = (event: DL2LinkEvent) => void;
4
+ type Parsed = {
5
+ scheme: string;
6
+ host: string;
7
+ path: string;
8
+ params: Record<string, string>;
9
+ };
10
+ /** Small URL parser: React Native's built-in `URL` is incomplete on some versions. */
11
+ export declare function parseUrl(url: unknown): Parsed | null;
12
+ /** Finds a DL2 clipboard token (`dl2:{clickId}:{signature}`) in pasted text. */
13
+ export declare function extractClipboardToken(text: unknown): string | null;
14
+ export declare function createDL2(deps: DL2Deps): {
15
+ configure: (config: DL2Config) => void;
16
+ onLink: (listener: Listener) => () => void;
17
+ getInitialLink: () => Promise<DL2LinkEvent | null>;
18
+ resolve: (url: string) => Promise<DL2LinkEvent | null>;
19
+ track: (name: string, options?: DL2TrackOptions) => void;
20
+ /** Sends queued events now. Resolves when the attempt is over; never rejects. */
21
+ flush: () => Promise<void | undefined>;
22
+ createLink: (input: DL2CreateLinkInput) => Promise<{
23
+ url: string;
24
+ } | null>;
25
+ matchClipboard: (text: string) => Promise<DL2LinkEvent | null>;
26
+ reset: () => Promise<void>;
27
+ getInstallId: () => Promise<string | null>;
28
+ getDebugInfo: () => DL2DebugInfo;
29
+ version: string;
30
+ };
31
+ export type DL2Api = ReturnType<typeof createDL2>;
32
+ export {};