@ait-co/polyfill 0.1.5 → 0.1.6

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.
Files changed (47) hide show
  1. package/README.md +53 -3
  2. package/dist/auto.cjs +919 -0
  3. package/dist/auto.cjs.map +1 -0
  4. package/dist/auto.d.cts +1 -0
  5. package/dist/auto.js +18 -6
  6. package/dist/auto.js.map +1 -1
  7. package/dist/detect.cjs +84 -0
  8. package/dist/detect.cjs.map +1 -0
  9. package/dist/detect.d.cts +50 -0
  10. package/dist/detect.d.cts.map +1 -0
  11. package/dist/index.cjs +982 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +222 -0
  14. package/dist/index.d.cts.map +1 -0
  15. package/dist/index.d.ts +26 -27
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +68 -8
  18. package/dist/index.js.map +1 -1
  19. package/dist/shims/clipboard.cjs +192 -0
  20. package/dist/shims/clipboard.cjs.map +1 -0
  21. package/dist/shims/clipboard.d.cts +26 -0
  22. package/dist/shims/clipboard.d.cts.map +1 -0
  23. package/dist/shims/geolocation.cjs +356 -0
  24. package/dist/shims/geolocation.cjs.map +1 -0
  25. package/dist/shims/geolocation.d.cts +41 -0
  26. package/dist/shims/geolocation.d.cts.map +1 -0
  27. package/dist/shims/network.cjs +290 -0
  28. package/dist/shims/network.cjs.map +1 -0
  29. package/dist/shims/network.d.cts +56 -0
  30. package/dist/shims/network.d.cts.map +1 -0
  31. package/dist/shims/share.cjs +239 -0
  32. package/dist/shims/share.cjs.map +1 -0
  33. package/dist/shims/share.d.cts +26 -0
  34. package/dist/shims/share.d.cts.map +1 -0
  35. package/dist/shims/vibrate-semantic.d.ts +27 -0
  36. package/dist/shims/vibrate-semantic.d.ts.map +1 -0
  37. package/dist/shims/vibrate-semantic.js +150 -0
  38. package/dist/shims/vibrate-semantic.js.map +1 -0
  39. package/dist/shims/vibrate.cjs +235 -0
  40. package/dist/shims/vibrate.cjs.map +1 -0
  41. package/dist/shims/vibrate.d.cts +43 -0
  42. package/dist/shims/vibrate.d.cts.map +1 -0
  43. package/dist/shims/vibrate.d.ts +16 -5
  44. package/dist/shims/vibrate.d.ts.map +1 -1
  45. package/dist/shims/vibrate.js +19 -7
  46. package/dist/shims/vibrate.js.map +1 -1
  47. package/package.json +73 -19
@@ -0,0 +1,290 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/detect.ts
3
+ /**
4
+ * Environment detection: are we running inside Apps in Toss, or a plain browser?
5
+ *
6
+ * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export
7
+ * that returns the runtime's Toss globals (deploymentId, brand name, …)
8
+ * inside the Apps in Toss runtime and throws (RN bridge unavailable)
9
+ * anywhere else. The SDK itself is an **optional** peer dependency; if its
10
+ * module can't be imported we are definitely not inside Toss.
11
+ *
12
+ * Just having the SDK module resolvable is not enough — apps can bundle it
13
+ * and still run in a plain browser. We need the bridge probe to confirm.
14
+ *
15
+ * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but
16
+ * that's a constant read from the bridge — no permission dialogs, no
17
+ * analytics fire. In a plain browser the bridge lookup fails fast (sync
18
+ * throw, microsecond-scale), so the startup cost is negligible.
19
+ */
20
+ let cached;
21
+ /**
22
+ * Returns `true` iff we detect we are running in an environment where the
23
+ * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
24
+ *
25
+ * Async because we use dynamic `import()` to probe the optional peer dep
26
+ * without forcing it into the consumer's bundle.
27
+ */
28
+ async function isTossEnvironment() {
29
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
30
+ if (force === "toss") return true;
31
+ if (force === "browser") return false;
32
+ if (cached !== void 0) return cached;
33
+ const mod = await loadTossSdk();
34
+ if (typeof mod?.getAppsInTossGlobals !== "function") {
35
+ cached = false;
36
+ return cached;
37
+ }
38
+ try {
39
+ const globals = mod.getAppsInTossGlobals();
40
+ cached = Boolean(globals) && typeof globals === "object";
41
+ } catch {
42
+ cached = false;
43
+ }
44
+ return cached;
45
+ }
46
+ /**
47
+ * Lazy SDK accessor — returns the module if available, else `null`. Callers
48
+ * are expected to `await` and null-check. Never throws.
49
+ */
50
+ async function loadTossSdk() {
51
+ try {
52
+ return await import("@apps-in-toss/web-framework");
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ //#endregion
58
+ //#region src/shims/_install-helpers.ts
59
+ /**
60
+ * Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the
61
+ * browser refuses (property is non-configurable on the instance), install on
62
+ * `Navigator.prototype` instead.
63
+ *
64
+ * Returns a snapshot describing where the original value was, which
65
+ * `restoreNavigatorProperty` uses to undo the install.
66
+ */
67
+ function installNavigatorProperty(prop, descriptor) {
68
+ const nav = navigator;
69
+ const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);
70
+ const instanceHadOwn = instanceDesc !== void 0;
71
+ if (!instanceDesc || instanceDesc.configurable) try {
72
+ Object.defineProperty(nav, prop, descriptor);
73
+ return {
74
+ location: "instance",
75
+ originalDescriptor: instanceDesc,
76
+ instanceHadOwn
77
+ };
78
+ } catch {}
79
+ const proto = Object.getPrototypeOf(nav);
80
+ const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);
81
+ if (instanceHadOwn) try {
82
+ delete nav[prop];
83
+ } catch {}
84
+ Object.defineProperty(proto, prop, descriptor);
85
+ return {
86
+ location: "prototype",
87
+ originalDescriptor: protoDesc,
88
+ instanceHadOwn
89
+ };
90
+ }
91
+ /**
92
+ * Reverse the install recorded in `snapshot`. If the original descriptor was
93
+ * `undefined` (property didn't exist before), delete the property instead of
94
+ * re-defining it.
95
+ */
96
+ function restoreNavigatorProperty(prop, snapshot) {
97
+ const target = snapshot.location === "instance" ? navigator : Object.getPrototypeOf(navigator);
98
+ if (snapshot.originalDescriptor) try {
99
+ Object.defineProperty(target, prop, snapshot.originalDescriptor);
100
+ } catch {}
101
+ else try {
102
+ delete target[prop];
103
+ } catch {}
104
+ }
105
+ //#endregion
106
+ //#region src/shims/network.ts
107
+ /**
108
+ * `navigator.onLine` + `navigator.connection` shim.
109
+ *
110
+ * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and
111
+ * refreshed on read (throttled):
112
+ * - `'OFFLINE'` → `onLine = false`
113
+ * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)
114
+ * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`
115
+ * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)
116
+ *
117
+ * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`
118
+ * read through to the native value. Install installs own-instance getters
119
+ * that consult the Toss-seeded cache first; when the cache is empty (which
120
+ * it always is in browser mode), the getter temporarily removes its own
121
+ * shadow, reads the prototype value, and reinstates the shadow.
122
+ *
123
+ * Uninstall `delete`s the instance-level override so the prototype descriptor
124
+ * (where `onLine` and `connection` actually live in real browsers) becomes
125
+ * visible again. We never mutate the prototype — doing so would throw in
126
+ * browsers where the descriptor is non-configurable.
127
+ *
128
+ * Browser-compat caveat (Chromium): `navigator.onLine` and `navigator.connection`
129
+ * are value slots, not methods, so the method-level install trick we use for
130
+ * `geolocation`/`share`/`vibrate` does not apply here. When Chromium marks the
131
+ * instance descriptor as non-configurable AND the prototype descriptor is also
132
+ * non-configurable, we cannot install. In that case the shim logs a one-time
133
+ * `console.warn` and leaves the native values in place — consumers keep the
134
+ * browser's own `onLine`/`connection` values; the SDK-synced state is simply
135
+ * disabled for that session.
136
+ *
137
+ * Caveat: the Web NetworkInformation API is evented (`change` fires on
138
+ * transitions). The SDK exposes only a one-shot query, so listeners attached
139
+ * to `navigator.connection` are accepted but never fire from a `change` event
140
+ * unless the shim observes a real status transition. Synthesising richer
141
+ * events via polling is tracked in TODO.md.
142
+ *
143
+ * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in
144
+ * the install closure. On uninstall the instance-level override is removed,
145
+ * but listeners the consumer attached to the old instance stay bound to that
146
+ * (now-orphan) object and will not see events from a subsequent install.
147
+ * Consumers should re-attach listeners after each install.
148
+ *
149
+ * Seed-boundary race: in Toss mode, reads before the install-time SDK seed
150
+ * completes fall through to the native `navigator.connection`. After the seed
151
+ * lands, subsequent reads return the shim's ShimConnection. Consumers that
152
+ * specifically need the ShimConnection instance (e.g., to attach `change`
153
+ * listeners that fire on Toss network transitions) should wait a microtask
154
+ * after `install()` before attaching listeners, or accept that pre-seed
155
+ * reads may return the native object.
156
+ */
157
+ const INSTALLED_KEY = Symbol.for("@ait-co/polyfill/network.installed");
158
+ const ON_LINE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.onLine.snapshot");
159
+ const CONNECTION_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.connection.snapshot");
160
+ const REFRESH_THROTTLE_MS = 500;
161
+ function statusToOnline(status) {
162
+ return status !== "OFFLINE";
163
+ }
164
+ function statusToEffectiveType(status) {
165
+ switch (status) {
166
+ case "2G": return "2g";
167
+ case "3G": return "3g";
168
+ default: return "4g";
169
+ }
170
+ }
171
+ function statusToConnectionType(status) {
172
+ switch (status) {
173
+ case "WIFI": return "wifi";
174
+ case "2G":
175
+ case "3G":
176
+ case "4G":
177
+ case "5G":
178
+ case "WWAN": return "cellular";
179
+ case "OFFLINE": return "none";
180
+ default: return "unknown";
181
+ }
182
+ }
183
+ const SET_STATUS = Symbol("@ait-co/polyfill/network.setStatus");
184
+ var ShimConnection = class extends EventTarget {
185
+ #status = null;
186
+ onchange = null;
187
+ constructor() {
188
+ super();
189
+ this.addEventListener("change", (ev) => this.onchange?.call(this, ev));
190
+ }
191
+ [SET_STATUS](next) {
192
+ this.#status = next;
193
+ }
194
+ get effectiveType() {
195
+ return statusToEffectiveType(this.#status ?? "UNKNOWN");
196
+ }
197
+ get downlink() {
198
+ return 0;
199
+ }
200
+ get rtt() {
201
+ return 0;
202
+ }
203
+ get saveData() {
204
+ return false;
205
+ }
206
+ get type() {
207
+ return statusToConnectionType(this.#status ?? "UNKNOWN");
208
+ }
209
+ };
210
+ function installNetworkShim() {
211
+ if (typeof navigator === "undefined") return () => {};
212
+ const host = navigator;
213
+ if (host[INSTALLED_KEY]) return () => uninstallNetworkShim();
214
+ host[INSTALLED_KEY] = true;
215
+ let cachedStatus = null;
216
+ let lastRefresh = 0;
217
+ let inflight = null;
218
+ const connection = new ShimConnection();
219
+ async function refresh() {
220
+ if (inflight) return inflight;
221
+ if (Date.now() - lastRefresh < REFRESH_THROTTLE_MS) return;
222
+ inflight = (async () => {
223
+ try {
224
+ if (!await isTossEnvironment()) return;
225
+ const fn = (await loadTossSdk())?.getNetworkStatus;
226
+ if (typeof fn !== "function") return;
227
+ const next = await fn();
228
+ const prev = cachedStatus;
229
+ cachedStatus = next;
230
+ connection[SET_STATUS](next);
231
+ if (prev !== null && prev !== next) connection.dispatchEvent(new Event("change"));
232
+ } catch {} finally {
233
+ lastRefresh = Date.now();
234
+ inflight = null;
235
+ }
236
+ })();
237
+ return inflight;
238
+ }
239
+ const nativeOnLine = navigator.onLine;
240
+ const nativeConnection = navigator.connection;
241
+ refresh();
242
+ let installWarned = false;
243
+ const warnNonConfigurable = (e) => {
244
+ if (installWarned) return;
245
+ installWarned = true;
246
+ console.warn("[@ait-co/polyfill] navigator.onLine/connection is non-configurable in this browser; Toss network status sync disabled.", e);
247
+ };
248
+ try {
249
+ host[ON_LINE_SNAPSHOT_KEY] = installNavigatorProperty("onLine", {
250
+ configurable: true,
251
+ get() {
252
+ refresh();
253
+ if (cachedStatus !== null) return statusToOnline(cachedStatus);
254
+ return nativeOnLine ?? true;
255
+ }
256
+ });
257
+ } catch (e) {
258
+ warnNonConfigurable(e);
259
+ }
260
+ try {
261
+ host[CONNECTION_SNAPSHOT_KEY] = installNavigatorProperty("connection", {
262
+ configurable: true,
263
+ get() {
264
+ refresh();
265
+ if (cachedStatus === null && nativeConnection !== void 0) return nativeConnection;
266
+ return connection;
267
+ }
268
+ });
269
+ } catch (e) {
270
+ warnNonConfigurable(e);
271
+ }
272
+ return uninstallNetworkShim;
273
+ }
274
+ function uninstallNetworkShim() {
275
+ if (typeof navigator === "undefined") return;
276
+ const host = navigator;
277
+ if (!host[INSTALLED_KEY]) return;
278
+ const onLineSnap = host[ON_LINE_SNAPSHOT_KEY];
279
+ if (onLineSnap) restoreNavigatorProperty("onLine", onLineSnap);
280
+ const connSnap = host[CONNECTION_SNAPSHOT_KEY];
281
+ if (connSnap) restoreNavigatorProperty("connection", connSnap);
282
+ delete host[INSTALLED_KEY];
283
+ delete host[ON_LINE_SNAPSHOT_KEY];
284
+ delete host[CONNECTION_SNAPSHOT_KEY];
285
+ }
286
+ //#endregion
287
+ exports.installNetworkShim = installNetworkShim;
288
+ exports.uninstallNetworkShim = uninstallNetworkShim;
289
+
290
+ //# sourceMappingURL=network.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.cjs","names":["#status"],"sources":["../../src/detect.ts","../../src/shims/_install-helpers.ts","../../src/shims/network.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * Shared helpers for installing shims on `navigator`.\n *\n * Chromium now marks a handful of `navigator` properties (e.g. `geolocation`,\n * `clipboard`) as non-configurable **own** properties on the instance. That\n * means a plain `Object.defineProperty(navigator, 'x', …)` throws\n * `TypeError: Cannot redefine property`.\n *\n * The workaround is to shim at the prototype level — `Navigator.prototype`\n * keeps these as configurable accessors, so we can swap them there and every\n * instance that falls through to the prototype (including `window.navigator`)\n * sees the shim. We only reach for the prototype when the instance-level\n * assignment refuses.\n *\n * For restoration we remember the descriptor chain (instance + prototype) so\n * `uninstall()` puts the browser back in its original state.\n */\n\ntype PropertyLocation = 'instance' | 'prototype';\n\nexport interface InstallSnapshot {\n /** Where we ended up writing the shim. */\n location: PropertyLocation;\n /** Original descriptor at that location (may be undefined if nothing was there). */\n originalDescriptor: PropertyDescriptor | undefined;\n /** `true` iff the original property lived on the instance before we touched it. */\n instanceHadOwn: boolean;\n}\n\n/**\n * Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the\n * browser refuses (property is non-configurable on the instance), install on\n * `Navigator.prototype` instead.\n *\n * Returns a snapshot describing where the original value was, which\n * `restoreNavigatorProperty` uses to undo the install.\n */\nexport function installNavigatorProperty(\n prop: string,\n descriptor: PropertyDescriptor,\n): InstallSnapshot {\n const nav = navigator as unknown as Record<PropertyKey, unknown>;\n const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);\n const instanceHadOwn = instanceDesc !== undefined;\n\n // Fast path: instance-level property is missing or configurable.\n if (!instanceDesc || instanceDesc.configurable) {\n try {\n Object.defineProperty(nav, prop, descriptor);\n return { location: 'instance', originalDescriptor: instanceDesc, instanceHadOwn };\n } catch {\n // Fall through to prototype-level install.\n }\n }\n\n // Prototype-level install. Drop the instance-level shadow so the prototype\n // accessor is visible to readers on `navigator`.\n const proto = Object.getPrototypeOf(nav) as object;\n const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);\n\n if (instanceHadOwn) {\n // Try to remove the instance-level shadow. On non-configurable it throws —\n // we deliberately ignore that; prototype-level install still wins because\n // the prototype accessor shows through when we read via `navigator[prop]`.\n try {\n delete nav[prop];\n } catch {\n /* non-configurable own — leave it; prototype install still useful */\n }\n }\n\n Object.defineProperty(proto, prop, descriptor);\n return { location: 'prototype', originalDescriptor: protoDesc, instanceHadOwn };\n}\n\n/**\n * Reverse the install recorded in `snapshot`. If the original descriptor was\n * `undefined` (property didn't exist before), delete the property instead of\n * re-defining it.\n */\nexport function restoreNavigatorProperty(prop: string, snapshot: InstallSnapshot): void {\n const target =\n snapshot.location === 'instance'\n ? (navigator as unknown as Record<PropertyKey, unknown>)\n : (Object.getPrototypeOf(navigator) as object);\n\n if (snapshot.originalDescriptor) {\n try {\n Object.defineProperty(target, prop, snapshot.originalDescriptor);\n } catch {\n /* descriptor was non-configurable upstream; we can't undo — rare. */\n }\n } else {\n try {\n delete (target as Record<PropertyKey, unknown>)[prop];\n } catch {\n /* non-configurable — rare. */\n }\n }\n\n // If our install pushed past an instance shadow, we leave the instance alone\n // — the descriptor we captured for `instanceHadOwn: true` lives on the\n // instance and was not modified at install time.\n}\n\n/**\n * Method-level install snapshot. Captured per-key so `restoreObjectMethods`\n * can distinguish \"was an own property, reassign it\" from \"was inherited,\n * delete the override so the prototype method surfaces again\".\n */\nexport interface MethodInstallSnapshot {\n target: object;\n methods: Record<string, { hadOwn: boolean; original: unknown }>;\n}\n\n/**\n * Mutate methods on an existing object rather than replacing the object\n * itself. This is the path we take for `navigator.geolocation`, `navigator.share`,\n * and `navigator.vibrate` in Chromium, where the slot on `navigator` is a\n * non-configurable own property that we cannot replace — but the methods\n * themselves (or the methods on the referenced object) are still\n * `configurable: true, writable: true`.\n *\n * Each replacement is installed via plain assignment. If any slot is not\n * writable (e.g. frozen object), install is aborted and previously-applied\n * replacements are rolled back, so the caller observes an atomic \"all or\n * nothing\" failure as `null`. The caller is expected to degrade gracefully\n * (e.g. log a one-time `console.warn`) when that happens.\n */\nexport function installObjectMethods(\n target: object,\n replacements: Record<string, (...args: never[]) => unknown>,\n): MethodInstallSnapshot | null {\n const methods: Record<string, { hadOwn: boolean; original: unknown }> = {};\n const applied: string[] = [];\n const bag = target as Record<string, unknown>;\n\n for (const key of Object.keys(replacements)) {\n const hadOwn = Object.hasOwn(target, key);\n const original = bag[key];\n try {\n bag[key] = replacements[key] as unknown;\n } catch {\n // Non-writable / frozen. Roll back and return null.\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n // Verify the assignment actually stuck — silent-failure descriptors (e.g.\n // `writable: false` without strict mode) can skip the throw and leave the\n // original value in place. Treat that the same as a throw.\n if (bag[key] !== (replacements[key] as unknown)) {\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n methods[key] = { hadOwn, original };\n applied.push(key);\n }\n\n return { target, methods };\n}\n\n/**\n * Reverse an `installObjectMethods` snapshot. Reassigns originals for slots\n * that were own properties before install; deletes the override for slots\n * that were inherited (so the prototype method surfaces again).\n */\nexport function restoreObjectMethods(snapshot: MethodInstallSnapshot): void {\n const bag = snapshot.target as Record<string, unknown>;\n for (const key of Object.keys(snapshot.methods)) {\n const entry = snapshot.methods[key];\n if (!entry) continue;\n try {\n if (entry.hadOwn) {\n bag[key] = entry.original;\n } else {\n delete bag[key];\n }\n } catch {\n /* frozen between install and restore — rare. */\n }\n }\n}\n","/**\n * `navigator.onLine` + `navigator.connection` shim.\n *\n * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and\n * refreshed on read (throttled):\n * - `'OFFLINE'` → `onLine = false`\n * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)\n * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`\n * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)\n *\n * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`\n * read through to the native value. Install installs own-instance getters\n * that consult the Toss-seeded cache first; when the cache is empty (which\n * it always is in browser mode), the getter temporarily removes its own\n * shadow, reads the prototype value, and reinstates the shadow.\n *\n * Uninstall `delete`s the instance-level override so the prototype descriptor\n * (where `onLine` and `connection` actually live in real browsers) becomes\n * visible again. We never mutate the prototype — doing so would throw in\n * browsers where the descriptor is non-configurable.\n *\n * Browser-compat caveat (Chromium): `navigator.onLine` and `navigator.connection`\n * are value slots, not methods, so the method-level install trick we use for\n * `geolocation`/`share`/`vibrate` does not apply here. When Chromium marks the\n * instance descriptor as non-configurable AND the prototype descriptor is also\n * non-configurable, we cannot install. In that case the shim logs a one-time\n * `console.warn` and leaves the native values in place — consumers keep the\n * browser's own `onLine`/`connection` values; the SDK-synced state is simply\n * disabled for that session.\n *\n * Caveat: the Web NetworkInformation API is evented (`change` fires on\n * transitions). The SDK exposes only a one-shot query, so listeners attached\n * to `navigator.connection` are accepted but never fire from a `change` event\n * unless the shim observes a real status transition. Synthesising richer\n * events via polling is tracked in TODO.md.\n *\n * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in\n * the install closure. On uninstall the instance-level override is removed,\n * but listeners the consumer attached to the old instance stay bound to that\n * (now-orphan) object and will not see events from a subsequent install.\n * Consumers should re-attach listeners after each install.\n *\n * Seed-boundary race: in Toss mode, reads before the install-time SDK seed\n * completes fall through to the native `navigator.connection`. After the seed\n * lands, subsequent reads return the shim's ShimConnection. Consumers that\n * specifically need the ShimConnection instance (e.g., to attach `change`\n * listeners that fire on Toss network transitions) should wait a microtask\n * after `install()` before attaching listeners, or accept that pre-seed\n * reads may return the native object.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\nimport {\n type InstallSnapshot,\n installNavigatorProperty,\n restoreNavigatorProperty,\n} from './_install-helpers.js';\n\nconst INSTALLED_KEY = Symbol.for('@ait-co/polyfill/network.installed');\nconst ON_LINE_SNAPSHOT_KEY = Symbol.for('@ait-co/polyfill/network.onLine.snapshot');\nconst CONNECTION_SNAPSHOT_KEY = Symbol.for('@ait-co/polyfill/network.connection.snapshot');\n\ninterface BackupHost {\n [INSTALLED_KEY]?: boolean;\n [ON_LINE_SNAPSHOT_KEY]?: InstallSnapshot | undefined;\n [CONNECTION_SNAPSHOT_KEY]?: InstallSnapshot | undefined;\n}\n\ntype SdkNetworkStatus = 'OFFLINE' | 'WIFI' | '2G' | '3G' | '4G' | '5G' | 'WWAN' | 'UNKNOWN';\ntype EffectiveType = 'slow-2g' | '2g' | '3g' | '4g';\n\nconst REFRESH_THROTTLE_MS = 500;\n\nfunction statusToOnline(status: SdkNetworkStatus): boolean {\n return status !== 'OFFLINE';\n}\n\nfunction statusToEffectiveType(status: SdkNetworkStatus): EffectiveType {\n switch (status) {\n case '2G':\n return '2g';\n case '3G':\n return '3g';\n default:\n return '4g';\n }\n}\n\nfunction statusToConnectionType(status: SdkNetworkStatus): string {\n switch (status) {\n case 'WIFI':\n return 'wifi';\n case '2G':\n case '3G':\n case '4G':\n case '5G':\n case 'WWAN':\n return 'cellular';\n case 'OFFLINE':\n return 'none';\n default:\n return 'unknown';\n }\n}\n\n// Symbol-keyed setter: the install closure can mutate status without exposing\n// a `setStatus` name on `navigator.connection` (real NetworkInformation has\n// no mutator). `Object.getOwnPropertySymbols(navigator.connection)` returns\n// nothing, so casual enumeration can't find it. A determined caller walking\n// the prototype chain (`Object.getOwnPropertySymbols(Object.getPrototypeOf(...))`)\n// can still surface the symbol — there is no trust boundary between polyfill\n// and consumer code in the same realm, so this is a discouragement, not a\n// security control.\nconst SET_STATUS = Symbol('@ait-co/polyfill/network.setStatus');\n\nclass ShimConnection extends EventTarget {\n #status: SdkNetworkStatus | null = null;\n onchange: ((this: ShimConnection, ev: Event) => unknown) | null = null;\n\n constructor() {\n super();\n // Forward `change` events to the legacy `onchange` handler for parity with\n // the NetworkInformation API.\n this.addEventListener('change', (ev) => this.onchange?.call(this, ev));\n }\n\n [SET_STATUS](next: SdkNetworkStatus | null): void {\n this.#status = next;\n }\n\n get effectiveType(): EffectiveType {\n return statusToEffectiveType(this.#status ?? 'UNKNOWN');\n }\n // `downlink` / `rtt` / `saveData` are placeholders — the SDK does not expose\n // these. We return 0/false rather than fabricate plausible numbers. Noted\n // in CLAUDE.md.\n get downlink(): number {\n return 0;\n }\n get rtt(): number {\n return 0;\n }\n get saveData(): boolean {\n return false;\n }\n get type(): string {\n return statusToConnectionType(this.#status ?? 'UNKNOWN');\n }\n}\n\nexport function installNetworkShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (host[INSTALLED_KEY]) {\n return () => uninstallNetworkShim();\n }\n host[INSTALLED_KEY] = true;\n\n // Per-install state. Kept in closure so uninstall/reinstall cycles don't\n // leak state between instances (module-scope would leak across tests).\n let cachedStatus: SdkNetworkStatus | null = null;\n let lastRefresh = 0;\n let inflight: Promise<void> | null = null;\n const connection = new ShimConnection();\n\n async function refresh(): Promise<void> {\n // Coalesce concurrent refreshes — without this, rapid reads during an\n // in-flight SDK call each set `lastRefresh` and return early, without\n // anyone actually fetching fresh data.\n if (inflight) return inflight;\n const now = Date.now();\n if (now - lastRefresh < REFRESH_THROTTLE_MS) return;\n inflight = (async () => {\n try {\n if (!(await isTossEnvironment())) return;\n const sdk = await loadTossSdk();\n const fn = (sdk as { getNetworkStatus?: unknown } | null)?.getNetworkStatus;\n if (typeof fn !== 'function') return;\n const next = (await (fn as () => Promise<SdkNetworkStatus>)()) as SdkNetworkStatus;\n const prev = cachedStatus;\n cachedStatus = next;\n connection[SET_STATUS](next);\n // Only dispatch `change` on real transitions — the null → X seed on\n // first install is learning, not a transition, and would otherwise\n // mis-trigger consumer handlers.\n if (prev !== null && prev !== next) {\n connection.dispatchEvent(new Event('change'));\n }\n } catch {\n // Advisory — refresh failures keep the prior cache. `void refresh()`\n // callers would otherwise surface unhandled rejections if\n // isTossEnvironment / loadTossSdk / getNetworkStatus ever throw.\n } finally {\n lastRefresh = Date.now();\n inflight = null;\n }\n })();\n return inflight;\n }\n\n // Capture the native values **before** we install so the getters can fall\n // through without needing to temporarily remove their own shadow (which is\n // incompatible with prototype-level installs — Chromium keeps\n // `navigator.onLine` / `connection` non-configurable on the instance, so we\n // may end up installing on Navigator.prototype instead).\n const nativeOnLine = (navigator as Navigator & { onLine?: boolean }).onLine;\n const nativeConnection = (navigator as Navigator & { connection?: unknown }).connection;\n\n // Seed the cache on install so the first sync read is meaningful.\n void refresh();\n\n // Guard both descriptor installs with try/catch. These properties are value\n // slots that the plan calls out as not having a method-level equivalent;\n // when Chromium makes them non-configurable at both the instance and the\n // prototype, `installNavigatorProperty` may throw. In that case we warn once\n // and proceed — consumers keep the browser's native values.\n let installWarned = false;\n const warnNonConfigurable = (e: unknown): void => {\n if (installWarned) return;\n installWarned = true;\n console.warn(\n '[@ait-co/polyfill] navigator.onLine/connection is non-configurable in this browser; Toss network status sync disabled.',\n e,\n );\n };\n\n try {\n host[ON_LINE_SNAPSHOT_KEY] = installNavigatorProperty('onLine', {\n configurable: true,\n get() {\n void refresh();\n if (cachedStatus !== null) return statusToOnline(cachedStatus);\n return nativeOnLine ?? true;\n },\n });\n } catch (e) {\n warnNonConfigurable(e);\n }\n\n try {\n host[CONNECTION_SNAPSHOT_KEY] = installNavigatorProperty('connection', {\n configurable: true,\n get() {\n void refresh();\n if (cachedStatus === null && nativeConnection !== undefined) return nativeConnection;\n return connection;\n },\n });\n } catch (e) {\n warnNonConfigurable(e);\n }\n\n return uninstallNetworkShim;\n}\n\nexport function uninstallNetworkShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!host[INSTALLED_KEY]) return;\n\n const onLineSnap = host[ON_LINE_SNAPSHOT_KEY];\n if (onLineSnap) restoreNavigatorProperty('onLine', onLineSnap);\n const connSnap = host[CONNECTION_SNAPSHOT_KEY];\n if (connSnap) restoreNavigatorProperty('connection', connSnap);\n\n delete host[INSTALLED_KEY];\n delete host[ON_LINE_SNAPSHOT_KEY];\n delete host[CONNECTION_SNAPSHOT_KEY];\n}\n"],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ //#region src/shims/network.d.ts
2
+ /**
3
+ * `navigator.onLine` + `navigator.connection` shim.
4
+ *
5
+ * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and
6
+ * refreshed on read (throttled):
7
+ * - `'OFFLINE'` → `onLine = false`
8
+ * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)
9
+ * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`
10
+ * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)
11
+ *
12
+ * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`
13
+ * read through to the native value. Install installs own-instance getters
14
+ * that consult the Toss-seeded cache first; when the cache is empty (which
15
+ * it always is in browser mode), the getter temporarily removes its own
16
+ * shadow, reads the prototype value, and reinstates the shadow.
17
+ *
18
+ * Uninstall `delete`s the instance-level override so the prototype descriptor
19
+ * (where `onLine` and `connection` actually live in real browsers) becomes
20
+ * visible again. We never mutate the prototype — doing so would throw in
21
+ * browsers where the descriptor is non-configurable.
22
+ *
23
+ * Browser-compat caveat (Chromium): `navigator.onLine` and `navigator.connection`
24
+ * are value slots, not methods, so the method-level install trick we use for
25
+ * `geolocation`/`share`/`vibrate` does not apply here. When Chromium marks the
26
+ * instance descriptor as non-configurable AND the prototype descriptor is also
27
+ * non-configurable, we cannot install. In that case the shim logs a one-time
28
+ * `console.warn` and leaves the native values in place — consumers keep the
29
+ * browser's own `onLine`/`connection` values; the SDK-synced state is simply
30
+ * disabled for that session.
31
+ *
32
+ * Caveat: the Web NetworkInformation API is evented (`change` fires on
33
+ * transitions). The SDK exposes only a one-shot query, so listeners attached
34
+ * to `navigator.connection` are accepted but never fire from a `change` event
35
+ * unless the shim observes a real status transition. Synthesising richer
36
+ * events via polling is tracked in TODO.md.
37
+ *
38
+ * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in
39
+ * the install closure. On uninstall the instance-level override is removed,
40
+ * but listeners the consumer attached to the old instance stay bound to that
41
+ * (now-orphan) object and will not see events from a subsequent install.
42
+ * Consumers should re-attach listeners after each install.
43
+ *
44
+ * Seed-boundary race: in Toss mode, reads before the install-time SDK seed
45
+ * completes fall through to the native `navigator.connection`. After the seed
46
+ * lands, subsequent reads return the shim's ShimConnection. Consumers that
47
+ * specifically need the ShimConnection instance (e.g., to attach `change`
48
+ * listeners that fire on Toss network transitions) should wait a microtask
49
+ * after `install()` before attaching listeners, or accept that pre-seed
50
+ * reads may return the native object.
51
+ */
52
+ declare function installNetworkShim(): () => void;
53
+ declare function uninstallNetworkShim(): void;
54
+ //#endregion
55
+ export { installNetworkShim, uninstallNetworkShim };
56
+ //# sourceMappingURL=network.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.d.cts","names":[],"sources":["../../src/shims/network.ts"],"mappings":";;AAsJA;;;;;AA4GA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA5GgB,kBAAA,CAAA;AAAA,iBA4GA,oBAAA,CAAA"}
@@ -0,0 +1,239 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/detect.ts
3
+ /**
4
+ * Environment detection: are we running inside Apps in Toss, or a plain browser?
5
+ *
6
+ * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export
7
+ * that returns the runtime's Toss globals (deploymentId, brand name, …)
8
+ * inside the Apps in Toss runtime and throws (RN bridge unavailable)
9
+ * anywhere else. The SDK itself is an **optional** peer dependency; if its
10
+ * module can't be imported we are definitely not inside Toss.
11
+ *
12
+ * Just having the SDK module resolvable is not enough — apps can bundle it
13
+ * and still run in a plain browser. We need the bridge probe to confirm.
14
+ *
15
+ * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but
16
+ * that's a constant read from the bridge — no permission dialogs, no
17
+ * analytics fire. In a plain browser the bridge lookup fails fast (sync
18
+ * throw, microsecond-scale), so the startup cost is negligible.
19
+ */
20
+ let cached;
21
+ /**
22
+ * Synchronous read of the cached detection result. Returns:
23
+ * - `true` / `false` if an override is active or the async detection has
24
+ * already resolved
25
+ * - `undefined` if detection hasn't run yet
26
+ *
27
+ * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`
28
+ * detection.
29
+ */
30
+ function isTossEnvironmentCached() {
31
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
32
+ if (force === "toss") return true;
33
+ if (force === "browser") return false;
34
+ return cached;
35
+ }
36
+ /**
37
+ * Returns `true` iff we detect we are running in an environment where the
38
+ * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
39
+ *
40
+ * Async because we use dynamic `import()` to probe the optional peer dep
41
+ * without forcing it into the consumer's bundle.
42
+ */
43
+ async function isTossEnvironment() {
44
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
45
+ if (force === "toss") return true;
46
+ if (force === "browser") return false;
47
+ if (cached !== void 0) return cached;
48
+ const mod = await loadTossSdk();
49
+ if (typeof mod?.getAppsInTossGlobals !== "function") {
50
+ cached = false;
51
+ return cached;
52
+ }
53
+ try {
54
+ const globals = mod.getAppsInTossGlobals();
55
+ cached = Boolean(globals) && typeof globals === "object";
56
+ } catch {
57
+ cached = false;
58
+ }
59
+ return cached;
60
+ }
61
+ /**
62
+ * Lazy SDK accessor — returns the module if available, else `null`. Callers
63
+ * are expected to `await` and null-check. Never throws.
64
+ */
65
+ async function loadTossSdk() {
66
+ try {
67
+ return await import("@apps-in-toss/web-framework");
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ //#endregion
73
+ //#region src/shims/_install-helpers.ts
74
+ /**
75
+ * Mutate methods on an existing object rather than replacing the object
76
+ * itself. This is the path we take for `navigator.geolocation`, `navigator.share`,
77
+ * and `navigator.vibrate` in Chromium, where the slot on `navigator` is a
78
+ * non-configurable own property that we cannot replace — but the methods
79
+ * themselves (or the methods on the referenced object) are still
80
+ * `configurable: true, writable: true`.
81
+ *
82
+ * Each replacement is installed via plain assignment. If any slot is not
83
+ * writable (e.g. frozen object), install is aborted and previously-applied
84
+ * replacements are rolled back, so the caller observes an atomic "all or
85
+ * nothing" failure as `null`. The caller is expected to degrade gracefully
86
+ * (e.g. log a one-time `console.warn`) when that happens.
87
+ */
88
+ function installObjectMethods(target, replacements) {
89
+ const methods = {};
90
+ const applied = [];
91
+ const bag = target;
92
+ for (const key of Object.keys(replacements)) {
93
+ const hadOwn = Object.hasOwn(target, key);
94
+ const original = bag[key];
95
+ try {
96
+ bag[key] = replacements[key];
97
+ } catch {
98
+ for (const applieKey of applied) {
99
+ const prev = methods[applieKey];
100
+ if (!prev) continue;
101
+ if (prev.hadOwn) bag[applieKey] = prev.original;
102
+ else delete bag[applieKey];
103
+ }
104
+ return null;
105
+ }
106
+ if (bag[key] !== replacements[key]) {
107
+ for (const applieKey of applied) {
108
+ const prev = methods[applieKey];
109
+ if (!prev) continue;
110
+ if (prev.hadOwn) bag[applieKey] = prev.original;
111
+ else delete bag[applieKey];
112
+ }
113
+ return null;
114
+ }
115
+ methods[key] = {
116
+ hadOwn,
117
+ original
118
+ };
119
+ applied.push(key);
120
+ }
121
+ return {
122
+ target,
123
+ methods
124
+ };
125
+ }
126
+ /**
127
+ * Reverse an `installObjectMethods` snapshot. Reassigns originals for slots
128
+ * that were own properties before install; deletes the override for slots
129
+ * that were inherited (so the prototype method surfaces again).
130
+ */
131
+ function restoreObjectMethods(snapshot) {
132
+ const bag = snapshot.target;
133
+ for (const key of Object.keys(snapshot.methods)) {
134
+ const entry = snapshot.methods[key];
135
+ if (!entry) continue;
136
+ try {
137
+ if (entry.hadOwn) bag[key] = entry.original;
138
+ else delete bag[key];
139
+ } catch {}
140
+ }
141
+ }
142
+ //#endregion
143
+ //#region src/shims/share.ts
144
+ /**
145
+ * `navigator.share` shim.
146
+ *
147
+ * Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only
148
+ * accepts a single `message` string, so we concatenate `title`, `text`, and
149
+ * `url` with newline separators (skipping missing/empty values).
150
+ *
151
+ * Outside Apps in Toss → defers to the browser's native `navigator.share`, or
152
+ * throws `NotSupportedError` if unavailable.
153
+ *
154
+ * Install strategy: **method-level** on `navigator`. Assigning
155
+ * `navigator.share = fn` creates an own property that shadows the prototype
156
+ * method. Uninstall deletes the own shadow so the prototype method surfaces
157
+ * again. We do not mutate `Navigator.prototype` — in real browsers its
158
+ * descriptor may be non-configurable, which would throw on reassignment.
159
+ *
160
+ * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).
161
+ * `canShare({ files })` returns `false` whenever the sync-accessible detection
162
+ * says Toss is active (or is being forced via the test override).
163
+ */
164
+ const SHARE_BACKUP_KEY = Symbol.for("@ait-co/polyfill/share.original");
165
+ const SHARE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/share.snapshot");
166
+ function buildSdkMessage(data) {
167
+ const parts = [];
168
+ if (data?.title != null && data.title !== "") parts.push(data.title);
169
+ if (data?.text != null && data.text !== "") parts.push(data.text);
170
+ if (data?.url != null && data.url !== "") parts.push(data.url);
171
+ return parts.join("\n");
172
+ }
173
+ async function shareShim(data) {
174
+ if (await isTossEnvironment()) {
175
+ const fn = (await loadTossSdk())?.share;
176
+ if (typeof fn === "function") {
177
+ const message = buildSdkMessage(data);
178
+ if (!message) throw new TypeError("[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.");
179
+ try {
180
+ await fn({ message });
181
+ } catch (e) {
182
+ const message_ = e instanceof Error ? e.message : String(e);
183
+ const wrapped = new DOMException(message_, "AbortError");
184
+ if (e instanceof Error) wrapped.cause = e;
185
+ throw wrapped;
186
+ }
187
+ return;
188
+ }
189
+ }
190
+ const original = navigator[SHARE_BACKUP_KEY]?.share;
191
+ if (!original) throw new DOMException("[@ait-co/polyfill] navigator.share is not available in this environment.", "NotSupportedError");
192
+ return original(data);
193
+ }
194
+ function canShareShim(data) {
195
+ const hasFiles = Boolean(data?.files && data.files.length > 0);
196
+ const toss = isTossEnvironmentCached();
197
+ if (hasFiles) {
198
+ if (toss === true) return false;
199
+ if (toss === void 0) return false;
200
+ }
201
+ if (toss === true) return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
202
+ const originalCanShare = navigator[SHARE_BACKUP_KEY]?.canShare;
203
+ if (originalCanShare) return originalCanShare(data);
204
+ return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
205
+ }
206
+ function installShareShim() {
207
+ if (typeof navigator === "undefined") return () => {};
208
+ const host = navigator;
209
+ if (SHARE_BACKUP_KEY in host) return () => uninstallShareShim();
210
+ const nav = navigator;
211
+ host[SHARE_BACKUP_KEY] = {
212
+ share: nav.share ? nav.share.bind(navigator) : void 0,
213
+ canShare: nav.canShare ? nav.canShare.bind(navigator) : void 0
214
+ };
215
+ const snapshot = installObjectMethods(navigator, {
216
+ share: shareShim,
217
+ canShare: canShareShim
218
+ });
219
+ if (!snapshot) {
220
+ delete host[SHARE_BACKUP_KEY];
221
+ return () => uninstallShareShim();
222
+ }
223
+ host[SHARE_SNAPSHOT_KEY] = snapshot;
224
+ return uninstallShareShim;
225
+ }
226
+ function uninstallShareShim() {
227
+ if (typeof navigator === "undefined") return;
228
+ const host = navigator;
229
+ if (!(SHARE_BACKUP_KEY in host)) return;
230
+ const snapshot = host[SHARE_SNAPSHOT_KEY];
231
+ if (snapshot) restoreObjectMethods(snapshot);
232
+ delete host[SHARE_BACKUP_KEY];
233
+ delete host[SHARE_SNAPSHOT_KEY];
234
+ }
235
+ //#endregion
236
+ exports.installShareShim = installShareShim;
237
+ exports.uninstallShareShim = uninstallShareShim;
238
+
239
+ //# sourceMappingURL=share.cjs.map