@ait-co/polyfill 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.
package/dist/index.js ADDED
@@ -0,0 +1,754 @@
1
+ //#region src/detect.ts
2
+ /**
3
+ * Environment detection: are we running inside Apps in Toss, or a plain browser?
4
+ *
5
+ * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as
6
+ * an **optional** peer dependency. If it resolves and exposes a known export,
7
+ * we assume we can route calls through it; otherwise we fall back to the
8
+ * browser's native implementation in each shim.
9
+ *
10
+ * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK
11
+ * function during detection (could prompt permission dialogs, fire analytics,
12
+ * etc.).
13
+ */
14
+ let cached;
15
+ /**
16
+ * Synchronous read of the cached detection result. Returns:
17
+ * - `true` / `false` if an override is active or the async detection has
18
+ * already resolved
19
+ * - `undefined` if detection hasn't run yet
20
+ *
21
+ * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`
22
+ * detection.
23
+ */
24
+ function isTossEnvironmentCached() {
25
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
26
+ if (force === "toss") return true;
27
+ if (force === "browser") return false;
28
+ return cached;
29
+ }
30
+ /**
31
+ * Returns `true` iff we detect we are running in an environment where the
32
+ * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
33
+ *
34
+ * Async because we use dynamic `import()` to probe the optional peer dep
35
+ * without forcing it into the consumer's bundle.
36
+ */
37
+ async function isTossEnvironment() {
38
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
39
+ if (force === "toss") return true;
40
+ if (force === "browser") return false;
41
+ if (cached !== void 0) return cached;
42
+ cached = typeof (await loadTossSdk())?.getClipboardText === "function";
43
+ return cached;
44
+ }
45
+ /**
46
+ * Lazy SDK accessor — returns the module if available, else `null`. Callers
47
+ * are expected to `await` and null-check. Never throws.
48
+ */
49
+ async function loadTossSdk() {
50
+ try {
51
+ return await import("@apps-in-toss/web-framework");
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ //#endregion
57
+ //#region src/shims/clipboard.ts
58
+ /**
59
+ * `navigator.clipboard` shim.
60
+ *
61
+ * Inside Apps in Toss → routes `readText` / `writeText` through the SDK
62
+ * (`getClipboardText` / `setClipboardText`).
63
+ *
64
+ * Outside Apps in Toss → defers to the browser's native `navigator.clipboard`.
65
+ * If the browser doesn't implement it, the standard `TypeError` / `DOMException`
66
+ * surfaces unchanged — we don't paper over missing support.
67
+ */
68
+ const BACKUP_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.original");
69
+ const HAD_KEY$1 = Symbol.for("@ait-co/polyfill/clipboard.hadOriginal");
70
+ /**
71
+ * Produces a Clipboard-compatible object whose `readText` / `writeText` methods
72
+ * route to the SDK when in Toss, else fall through to the supplied `fallback`.
73
+ */
74
+ function createClipboardShim(fallback) {
75
+ return {
76
+ async readText() {
77
+ if (await isTossEnvironment()) {
78
+ const sdk = await loadTossSdk();
79
+ if (sdk?.getClipboardText) return sdk.getClipboardText();
80
+ }
81
+ if (!fallback) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.readText is not available in this environment.", "NotSupportedError");
82
+ return fallback.readText();
83
+ },
84
+ async writeText(text) {
85
+ if (await isTossEnvironment()) {
86
+ const sdk = await loadTossSdk();
87
+ if (sdk?.setClipboardText) return sdk.setClipboardText(text);
88
+ }
89
+ if (!fallback) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.writeText is not available in this environment.", "NotSupportedError");
90
+ return fallback.writeText(text);
91
+ },
92
+ async read() {
93
+ if (await isTossEnvironment()) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.read (rich content) is not supported in the Apps in Toss environment. Use readText instead.", "NotSupportedError");
94
+ if (!fallback?.read) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.read is not available.", "NotSupportedError");
95
+ return fallback.read();
96
+ },
97
+ async write(items) {
98
+ if (await isTossEnvironment()) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.write (rich content) is not supported in the Apps in Toss environment. Use writeText instead.", "NotSupportedError");
99
+ if (!fallback?.write) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.write is not available.", "NotSupportedError");
100
+ return fallback.write(items);
101
+ },
102
+ addEventListener: (...args) => fallback?.addEventListener(...args),
103
+ removeEventListener: (...args) => fallback?.removeEventListener(...args),
104
+ dispatchEvent: (event) => fallback?.dispatchEvent(event) ?? false
105
+ };
106
+ }
107
+ /**
108
+ * Install the `navigator.clipboard` shim.
109
+ *
110
+ * @returns an uninstall function that restores the original `navigator.clipboard`.
111
+ * Calling install twice without uninstalling is a no-op on the second call
112
+ * and returns the same uninstall function.
113
+ */
114
+ function installClipboardShim() {
115
+ if (typeof navigator === "undefined") return () => {};
116
+ const host = navigator;
117
+ if (BACKUP_KEY$2 in host) return () => uninstallClipboardShim();
118
+ const original = navigator.clipboard;
119
+ host[BACKUP_KEY$2] = original;
120
+ host[HAD_KEY$1] = "clipboard" in navigator;
121
+ const shim = createClipboardShim(original);
122
+ Object.defineProperty(navigator, "clipboard", {
123
+ value: shim,
124
+ configurable: true,
125
+ writable: true
126
+ });
127
+ return uninstallClipboardShim;
128
+ }
129
+ /**
130
+ * Remove the shim and restore the pre-install shape. Uses delete + conditional
131
+ * redefine so a prototype-level `navigator.clipboard` (non-configurable in real
132
+ * browsers) becomes visible again instead of being permanently shadowed.
133
+ */
134
+ function uninstallClipboardShim() {
135
+ if (typeof navigator === "undefined") return;
136
+ const host = navigator;
137
+ if (!(BACKUP_KEY$2 in host)) return;
138
+ const original = host[BACKUP_KEY$2];
139
+ const had = host[HAD_KEY$1];
140
+ delete navigator.clipboard;
141
+ if (had && navigator.clipboard !== original) Object.defineProperty(navigator, "clipboard", {
142
+ value: original,
143
+ configurable: true,
144
+ writable: true
145
+ });
146
+ delete host[BACKUP_KEY$2];
147
+ delete host[HAD_KEY$1];
148
+ }
149
+ //#endregion
150
+ //#region src/shims/geolocation.ts
151
+ /**
152
+ * `navigator.geolocation` shim.
153
+ *
154
+ * Inside Apps in Toss → routes through the SDK:
155
+ * - `getCurrentPosition` → `getCurrentLocation({ accuracy })`
156
+ * - `watchPosition` / `clearWatch` → `startUpdateLocation({ onEvent, onError, options })`
157
+ *
158
+ * Outside Apps in Toss → defers to the browser's native `navigator.geolocation`.
159
+ * If neither is available, the error callback receives a `GeolocationPositionError`.
160
+ *
161
+ * SDK/Web shape mismatch handled here:
162
+ * - SDK `Accuracy` is a numeric enum (1 = Lowest … 6 = BestForNavigation); the
163
+ * standard `PositionOptions.enableHighAccuracy` is a boolean. We map
164
+ * `true → Accuracy.High (4, "~10m")` and `false → Accuracy.Balanced (3)`.
165
+ * `Highest (5)` / `BestForNavigation (6)` are available but carry a battery
166
+ * cost that's rarely what mini-apps want; consumers who need them should
167
+ * call the SDK directly.
168
+ * - SDK coords lack `speed`; we surface `null` (per the W3C spec when unknown).
169
+ * - SDK `startUpdateLocation` returns an `unsubscribe` fn; we wrap it behind
170
+ * a numeric watch id so `clearWatch(id)` behaves like the standard.
171
+ *
172
+ * Caveat: watch ids reset whenever the shim is uninstalled and reinstalled;
173
+ * they are not stable across such cycles. Ids obtained before uninstall
174
+ * cannot be cleared after uninstall — `clearWatch(id)` on the restored native
175
+ * `navigator.geolocation` uses a different id space, so the SDK subscription
176
+ * leaks. Consumers should `clearWatch` all outstanding ids before calling
177
+ * `uninstall()`.
178
+ */
179
+ const BACKUP_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.original");
180
+ const ACCURACY_BALANCED = 3;
181
+ const ACCURACY_HIGH = 4;
182
+ function toStandardPosition(sdk) {
183
+ const coordsData = {
184
+ latitude: sdk.coords.latitude,
185
+ longitude: sdk.coords.longitude,
186
+ altitude: sdk.coords.altitude,
187
+ accuracy: sdk.coords.accuracy,
188
+ altitudeAccuracy: sdk.coords.altitudeAccuracy,
189
+ heading: sdk.coords.heading,
190
+ speed: null
191
+ };
192
+ return {
193
+ coords: {
194
+ ...coordsData,
195
+ toJSON() {
196
+ return { ...coordsData };
197
+ }
198
+ },
199
+ timestamp: sdk.timestamp,
200
+ toJSON() {
201
+ return {
202
+ coords: { ...coordsData },
203
+ timestamp: sdk.timestamp
204
+ };
205
+ }
206
+ };
207
+ }
208
+ function toPositionError(code, message) {
209
+ const Ctor = globalThis.GeolocationPositionError;
210
+ if (typeof Ctor === "function") {
211
+ const proto = Ctor.prototype;
212
+ if (proto) {
213
+ const shape = {
214
+ code,
215
+ message
216
+ };
217
+ Object.setPrototypeOf(shape, proto);
218
+ return shape;
219
+ }
220
+ }
221
+ return {
222
+ code,
223
+ message,
224
+ PERMISSION_DENIED: 1,
225
+ POSITION_UNAVAILABLE: 2,
226
+ TIMEOUT: 3
227
+ };
228
+ }
229
+ function accuracyFromOptions(options) {
230
+ return options?.enableHighAccuracy ? ACCURACY_HIGH : ACCURACY_BALANCED;
231
+ }
232
+ function createGeolocationShim(fallback) {
233
+ let nextWatchId = 1;
234
+ const sdkWatches = /* @__PURE__ */ new Map();
235
+ const nativeWatches = /* @__PURE__ */ new Map();
236
+ const pendingWatches = /* @__PURE__ */ new Map();
237
+ return {
238
+ getCurrentPosition(success, error, options) {
239
+ (async () => {
240
+ if (await isTossEnvironment()) {
241
+ const fn = (await loadTossSdk())?.getCurrentLocation;
242
+ if (typeof fn === "function") {
243
+ try {
244
+ success(toStandardPosition(await fn({ accuracy: accuracyFromOptions(options) })));
245
+ } catch (e) {
246
+ error?.(toPositionError(2, e instanceof Error ? e.message : "[@ait-co/polyfill] getCurrentLocation failed."));
247
+ }
248
+ return;
249
+ }
250
+ }
251
+ if (!fallback) {
252
+ error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
253
+ return;
254
+ }
255
+ fallback.getCurrentPosition(success, error, options);
256
+ })();
257
+ },
258
+ watchPosition(success, error, options) {
259
+ const id = nextWatchId++;
260
+ const pending = { cancelled: false };
261
+ pendingWatches.set(id, pending);
262
+ (async () => {
263
+ if (await isTossEnvironment()) {
264
+ const fn = (await loadTossSdk())?.startUpdateLocation;
265
+ if (typeof fn === "function") {
266
+ if (pending.cancelled) {
267
+ pendingWatches.delete(id);
268
+ return;
269
+ }
270
+ const unsubscribe = fn({
271
+ onEvent: (loc) => success(toStandardPosition(loc)),
272
+ onError: (err) => error?.(toPositionError(2, err instanceof Error ? err.message : "[@ait-co/polyfill] startUpdateLocation failed.")),
273
+ options: {
274
+ accuracy: accuracyFromOptions(options),
275
+ timeInterval: 1e3,
276
+ distanceInterval: 0
277
+ }
278
+ });
279
+ if (pending.cancelled) {
280
+ unsubscribe();
281
+ pendingWatches.delete(id);
282
+ return;
283
+ }
284
+ sdkWatches.set(id, unsubscribe);
285
+ pendingWatches.delete(id);
286
+ return;
287
+ }
288
+ }
289
+ if (!fallback) {
290
+ pendingWatches.delete(id);
291
+ error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
292
+ return;
293
+ }
294
+ if (pending.cancelled) {
295
+ pendingWatches.delete(id);
296
+ return;
297
+ }
298
+ const nativeId = fallback.watchPosition(success, error, options);
299
+ if (pending.cancelled) {
300
+ fallback.clearWatch(nativeId);
301
+ pendingWatches.delete(id);
302
+ return;
303
+ }
304
+ nativeWatches.set(id, nativeId);
305
+ pendingWatches.delete(id);
306
+ })();
307
+ return id;
308
+ },
309
+ clearWatch(id) {
310
+ const pending = pendingWatches.get(id);
311
+ if (pending) {
312
+ pending.cancelled = true;
313
+ pendingWatches.delete(id);
314
+ return;
315
+ }
316
+ const unsubscribe = sdkWatches.get(id);
317
+ if (unsubscribe) {
318
+ unsubscribe();
319
+ sdkWatches.delete(id);
320
+ return;
321
+ }
322
+ const nativeId = nativeWatches.get(id);
323
+ if (nativeId !== void 0 && fallback) {
324
+ fallback.clearWatch(nativeId);
325
+ nativeWatches.delete(id);
326
+ }
327
+ }
328
+ };
329
+ }
330
+ function installGeolocationShim() {
331
+ if (typeof navigator === "undefined") return () => {};
332
+ const host = navigator;
333
+ if (BACKUP_KEY$1 in host) return () => uninstallGeolocationShim();
334
+ const original = navigator.geolocation;
335
+ host[BACKUP_KEY$1] = original;
336
+ const shim = createGeolocationShim(original);
337
+ Object.defineProperty(navigator, "geolocation", {
338
+ value: shim,
339
+ configurable: true,
340
+ writable: true
341
+ });
342
+ return uninstallGeolocationShim;
343
+ }
344
+ function uninstallGeolocationShim() {
345
+ if (typeof navigator === "undefined") return;
346
+ const host = navigator;
347
+ if (!(BACKUP_KEY$1 in host)) return;
348
+ const original = host[BACKUP_KEY$1];
349
+ delete navigator.geolocation;
350
+ if (original !== void 0 && navigator.geolocation !== original) Object.defineProperty(navigator, "geolocation", {
351
+ value: original,
352
+ configurable: true,
353
+ writable: true
354
+ });
355
+ delete host[BACKUP_KEY$1];
356
+ }
357
+ //#endregion
358
+ //#region src/shims/network.ts
359
+ /**
360
+ * `navigator.onLine` + `navigator.connection` shim.
361
+ *
362
+ * Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and
363
+ * refreshed on read (throttled):
364
+ * - `'OFFLINE'` → `onLine = false`
365
+ * - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)
366
+ * - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`
367
+ * - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)
368
+ *
369
+ * Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`
370
+ * read through to the native value. Install installs own-instance getters
371
+ * that consult the Toss-seeded cache first; when the cache is empty (which
372
+ * it always is in browser mode), the getter temporarily removes its own
373
+ * shadow, reads the prototype value, and reinstates the shadow.
374
+ *
375
+ * Uninstall `delete`s the instance-level override so the prototype descriptor
376
+ * (where `onLine` and `connection` actually live in real browsers) becomes
377
+ * visible again. We never mutate the prototype — doing so would throw in
378
+ * browsers where the descriptor is non-configurable.
379
+ *
380
+ * Caveat: the Web NetworkInformation API is evented (`change` fires on
381
+ * transitions). The SDK exposes only a one-shot query, so listeners attached
382
+ * to `navigator.connection` are accepted but never fire from a `change` event
383
+ * unless the shim observes a real status transition. Synthesising richer
384
+ * events via polling is tracked in TODO.md.
385
+ *
386
+ * Lifecycle: `navigator.connection` is a ShimConnection instance that lives in
387
+ * the install closure. On uninstall the instance-level override is removed,
388
+ * but listeners the consumer attached to the old instance stay bound to that
389
+ * (now-orphan) object and will not see events from a subsequent install.
390
+ * Consumers should re-attach listeners after each install.
391
+ *
392
+ * Seed-boundary race: in Toss mode, reads before the install-time SDK seed
393
+ * completes fall through to the native `navigator.connection`. After the seed
394
+ * lands, subsequent reads return the shim's ShimConnection. Consumers that
395
+ * specifically need the ShimConnection instance (e.g., to attach `change`
396
+ * listeners that fire on Toss network transitions) should wait a microtask
397
+ * after `install()` before attaching listeners, or accept that pre-seed
398
+ * reads may return the native object.
399
+ */
400
+ const INSTALLED_KEY = Symbol.for("@ait-co/polyfill/network.installed");
401
+ const REFRESH_THROTTLE_MS = 500;
402
+ function statusToOnline(status) {
403
+ return status !== "OFFLINE";
404
+ }
405
+ function statusToEffectiveType(status) {
406
+ switch (status) {
407
+ case "2G": return "2g";
408
+ case "3G": return "3g";
409
+ default: return "4g";
410
+ }
411
+ }
412
+ function statusToConnectionType(status) {
413
+ switch (status) {
414
+ case "WIFI": return "wifi";
415
+ case "2G":
416
+ case "3G":
417
+ case "4G":
418
+ case "5G":
419
+ case "WWAN": return "cellular";
420
+ case "OFFLINE": return "none";
421
+ default: return "unknown";
422
+ }
423
+ }
424
+ const SET_STATUS = Symbol("@ait-co/polyfill/network.setStatus");
425
+ var ShimConnection = class extends EventTarget {
426
+ #status = null;
427
+ onchange = null;
428
+ constructor() {
429
+ super();
430
+ this.addEventListener("change", (ev) => this.onchange?.call(this, ev));
431
+ }
432
+ [SET_STATUS](next) {
433
+ this.#status = next;
434
+ }
435
+ get effectiveType() {
436
+ return statusToEffectiveType(this.#status ?? "UNKNOWN");
437
+ }
438
+ get downlink() {
439
+ return 0;
440
+ }
441
+ get rtt() {
442
+ return 0;
443
+ }
444
+ get saveData() {
445
+ return false;
446
+ }
447
+ get type() {
448
+ return statusToConnectionType(this.#status ?? "UNKNOWN");
449
+ }
450
+ };
451
+ function installNetworkShim() {
452
+ if (typeof navigator === "undefined") return () => {};
453
+ const host = navigator;
454
+ if (host[INSTALLED_KEY]) return () => uninstallNetworkShim();
455
+ host[INSTALLED_KEY] = true;
456
+ let cachedStatus = null;
457
+ let lastRefresh = 0;
458
+ let inflight = null;
459
+ const connection = new ShimConnection();
460
+ async function refresh() {
461
+ if (inflight) return inflight;
462
+ if (Date.now() - lastRefresh < REFRESH_THROTTLE_MS) return;
463
+ inflight = (async () => {
464
+ try {
465
+ if (!await isTossEnvironment()) return;
466
+ const fn = (await loadTossSdk())?.getNetworkStatus;
467
+ if (typeof fn !== "function") return;
468
+ const next = await fn();
469
+ const prev = cachedStatus;
470
+ cachedStatus = next;
471
+ connection[SET_STATUS](next);
472
+ if (prev !== null && prev !== next) connection.dispatchEvent(new Event("change"));
473
+ } catch {} finally {
474
+ lastRefresh = Date.now();
475
+ inflight = null;
476
+ }
477
+ })();
478
+ return inflight;
479
+ }
480
+ refresh();
481
+ Object.defineProperty(navigator, "onLine", {
482
+ configurable: true,
483
+ get() {
484
+ refresh();
485
+ if (cachedStatus !== null) return statusToOnline(cachedStatus);
486
+ const desc = Object.getOwnPropertyDescriptor(navigator, "onLine");
487
+ delete navigator.onLine;
488
+ try {
489
+ return navigator.onLine;
490
+ } finally {
491
+ if (desc) Object.defineProperty(navigator, "onLine", desc);
492
+ }
493
+ }
494
+ });
495
+ Object.defineProperty(navigator, "connection", {
496
+ configurable: true,
497
+ get() {
498
+ refresh();
499
+ if (cachedStatus === null) {
500
+ const desc = Object.getOwnPropertyDescriptor(navigator, "connection");
501
+ delete navigator.connection;
502
+ try {
503
+ const native = navigator.connection;
504
+ if (native !== void 0) return native;
505
+ } finally {
506
+ if (desc) Object.defineProperty(navigator, "connection", desc);
507
+ }
508
+ }
509
+ return connection;
510
+ }
511
+ });
512
+ return uninstallNetworkShim;
513
+ }
514
+ function uninstallNetworkShim() {
515
+ if (typeof navigator === "undefined") return;
516
+ const host = navigator;
517
+ if (!host[INSTALLED_KEY]) return;
518
+ delete navigator.onLine;
519
+ delete navigator.connection;
520
+ delete host[INSTALLED_KEY];
521
+ }
522
+ //#endregion
523
+ //#region src/shims/share.ts
524
+ /**
525
+ * `navigator.share` shim.
526
+ *
527
+ * Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only
528
+ * accepts a single `message` string, so we concatenate `title`, `text`, and
529
+ * `url` with newline separators (skipping missing/empty values).
530
+ *
531
+ * Outside Apps in Toss → defers to the browser's native `navigator.share`, or
532
+ * throws `NotSupportedError` if unavailable.
533
+ *
534
+ * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).
535
+ * `canShare({ files })` returns `false` whenever the sync-accessible detection
536
+ * says Toss is active (or is being forced via the test override).
537
+ */
538
+ const SHARE_BACKUP_KEY = Symbol.for("@ait-co/polyfill/share.original");
539
+ function buildSdkMessage(data) {
540
+ const parts = [];
541
+ if (data?.title != null && data.title !== "") parts.push(data.title);
542
+ if (data?.text != null && data.text !== "") parts.push(data.text);
543
+ if (data?.url != null && data.url !== "") parts.push(data.url);
544
+ return parts.join("\n");
545
+ }
546
+ async function shareShim(data) {
547
+ if (await isTossEnvironment()) {
548
+ const fn = (await loadTossSdk())?.share;
549
+ if (typeof fn === "function") {
550
+ const message = buildSdkMessage(data);
551
+ if (!message) throw new TypeError("[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.");
552
+ try {
553
+ await fn({ message });
554
+ } catch (e) {
555
+ const message_ = e instanceof Error ? e.message : String(e);
556
+ const wrapped = new DOMException(message_, "AbortError");
557
+ if (e instanceof Error) wrapped.cause = e;
558
+ throw wrapped;
559
+ }
560
+ return;
561
+ }
562
+ }
563
+ const original = navigator[SHARE_BACKUP_KEY]?.share;
564
+ if (!original) throw new DOMException("[@ait-co/polyfill] navigator.share is not available in this environment.", "NotSupportedError");
565
+ return original.call(navigator, data);
566
+ }
567
+ function canShareShim(data) {
568
+ const hasFiles = Boolean(data?.files && data.files.length > 0);
569
+ const toss = isTossEnvironmentCached();
570
+ if (hasFiles) {
571
+ if (toss === true) return false;
572
+ if (toss === void 0) return false;
573
+ }
574
+ if (toss === true) return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
575
+ const originalCanShare = navigator[SHARE_BACKUP_KEY]?.canShare;
576
+ if (originalCanShare) return originalCanShare.call(navigator, data);
577
+ return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
578
+ }
579
+ function installShareShim() {
580
+ if (typeof navigator === "undefined") return () => {};
581
+ const host = navigator;
582
+ if (SHARE_BACKUP_KEY in host) return () => uninstallShareShim();
583
+ const nav = navigator;
584
+ host[SHARE_BACKUP_KEY] = {
585
+ share: nav.share,
586
+ canShare: nav.canShare,
587
+ hadShare: "share" in nav,
588
+ hadCanShare: "canShare" in nav
589
+ };
590
+ Object.defineProperty(navigator, "share", {
591
+ value: shareShim,
592
+ configurable: true,
593
+ writable: true
594
+ });
595
+ Object.defineProperty(navigator, "canShare", {
596
+ value: canShareShim,
597
+ configurable: true,
598
+ writable: true
599
+ });
600
+ return uninstallShareShim;
601
+ }
602
+ function uninstallShareShim() {
603
+ if (typeof navigator === "undefined") return;
604
+ const host = navigator;
605
+ if (!(SHARE_BACKUP_KEY in host)) return;
606
+ const backup = host[SHARE_BACKUP_KEY];
607
+ delete navigator.share;
608
+ if (backup?.hadShare && navigator.share !== backup.share) Object.defineProperty(navigator, "share", {
609
+ value: backup.share,
610
+ configurable: true,
611
+ writable: true
612
+ });
613
+ delete navigator.canShare;
614
+ if (backup?.hadCanShare && navigator.canShare !== backup.canShare) Object.defineProperty(navigator, "canShare", {
615
+ value: backup.canShare,
616
+ configurable: true,
617
+ writable: true
618
+ });
619
+ delete host[SHARE_BACKUP_KEY];
620
+ }
621
+ //#endregion
622
+ //#region src/shims/vibrate.ts
623
+ /**
624
+ * `navigator.vibrate` shim.
625
+ *
626
+ * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`:
627
+ * - `vibrate(0)` → no-op (web standard: cancels pending vibration)
628
+ * - `vibrate(number)`: short (< 40ms) → `tickWeak`, long (≥ 40ms) → `basicMedium`
629
+ * - `vibrate(number[])`: iterate "on" segments (even indices) as `tap` pulses
630
+ *
631
+ * Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
632
+ * or returns `false` when unavailable (matches the spec — browsers that don't
633
+ * support vibration simply return `false`).
634
+ *
635
+ * Caveats (documented in CLAUDE.md as the known lossy trade-off):
636
+ * - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
637
+ * durations. The shim approximates intensity from duration but cannot
638
+ * reproduce exact patterns.
639
+ * - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
640
+ * honoured only as "time until the next tap", not as silent-vs-vibrating.
641
+ * - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
642
+ * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
643
+ */
644
+ const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
645
+ const HAD_KEY = Symbol.for("@ait-co/polyfill/vibrate.hadOriginal");
646
+ const SHORT_VIBRATION_MS = 40;
647
+ async function haptic(type) {
648
+ const fn = (await loadTossSdk())?.generateHapticFeedback;
649
+ if (typeof fn === "function") try {
650
+ await fn({ type });
651
+ } catch {}
652
+ }
653
+ function durationToHaptic(duration) {
654
+ return duration < SHORT_VIBRATION_MS ? "tickWeak" : "basicMedium";
655
+ }
656
+ function vibrateShim(pattern) {
657
+ const arr = Array.isArray(pattern) ? pattern : [pattern];
658
+ if (arr.length === 0 || arr.every((n) => n === 0)) {
659
+ (async () => {
660
+ if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.call(navigator, pattern);
661
+ })();
662
+ return true;
663
+ }
664
+ (async () => {
665
+ if (await isTossEnvironment()) {
666
+ if (!Array.isArray(pattern)) {
667
+ await haptic(durationToHaptic(pattern));
668
+ return;
669
+ }
670
+ for (let i = 0; i < pattern.length; i += 2) {
671
+ const on = pattern[i];
672
+ if (on === void 0) break;
673
+ if (on > 0) await haptic("tap");
674
+ const pause = pattern[i + 1];
675
+ if (typeof pause === "number" && pause > 0) await new Promise((r) => setTimeout(r, pause));
676
+ }
677
+ return;
678
+ }
679
+ navigator[BACKUP_KEY]?.call(navigator, pattern);
680
+ })();
681
+ return true;
682
+ }
683
+ function installVibrateShim() {
684
+ if (typeof navigator === "undefined") return () => {};
685
+ const host = navigator;
686
+ if (BACKUP_KEY in host) return () => uninstallVibrateShim();
687
+ const nav = navigator;
688
+ host[BACKUP_KEY] = nav.vibrate;
689
+ host[HAD_KEY] = "vibrate" in nav;
690
+ Object.defineProperty(navigator, "vibrate", {
691
+ value: vibrateShim,
692
+ configurable: true,
693
+ writable: true
694
+ });
695
+ return uninstallVibrateShim;
696
+ }
697
+ function uninstallVibrateShim() {
698
+ if (typeof navigator === "undefined") return;
699
+ const host = navigator;
700
+ if (!(BACKUP_KEY in host)) return;
701
+ const original = host[BACKUP_KEY];
702
+ const had = host[HAD_KEY];
703
+ delete navigator.vibrate;
704
+ if (had && navigator.vibrate !== original) Object.defineProperty(navigator, "vibrate", {
705
+ value: original,
706
+ configurable: true,
707
+ writable: true
708
+ });
709
+ delete host[BACKUP_KEY];
710
+ delete host[HAD_KEY];
711
+ }
712
+ //#endregion
713
+ //#region src/index.ts
714
+ const VERSION = "0.1.1";
715
+ /**
716
+ * Install every shim this library ships. Idempotent — safe to call more than
717
+ * once. Returns an uninstall function that restores every original API.
718
+ *
719
+ * Install order: clipboard → geolocation → share → vibrate → network.
720
+ * `uninstall()` tears them down in the same order (each per-shim uninstall is
721
+ * independent, so order doesn't affect correctness; documented for clarity).
722
+ *
723
+ * Not atomic on failure: if a later per-shim install throws (e.g., a consumer
724
+ * has pinned one of the target navigator properties as non-configurable),
725
+ * earlier shims are already installed. Callers should catch and invoke
726
+ * `uninstall()` to roll back.
727
+ */
728
+ function install() {
729
+ const uninstalls = [
730
+ installClipboardShim(),
731
+ installGeolocationShim(),
732
+ installShareShim(),
733
+ installVibrateShim(),
734
+ installNetworkShim()
735
+ ];
736
+ return () => {
737
+ for (const fn of uninstalls) fn();
738
+ };
739
+ }
740
+ /**
741
+ * Uninstall every shim installed by `install()`. Safe to call when no shim is
742
+ * installed — each installer's uninstall is a no-op in that case.
743
+ */
744
+ function uninstall() {
745
+ uninstallClipboardShim();
746
+ uninstallGeolocationShim();
747
+ uninstallShareShim();
748
+ uninstallVibrateShim();
749
+ uninstallNetworkShim();
750
+ }
751
+ //#endregion
752
+ export { VERSION, install, installClipboardShim, installGeolocationShim, installNetworkShim, installShareShim, installVibrateShim, isTossEnvironment, isTossEnvironmentCached, loadTossSdk, uninstall, uninstallClipboardShim, uninstallGeolocationShim, uninstallNetworkShim, uninstallShareShim, uninstallVibrateShim };
753
+
754
+ //# sourceMappingURL=index.js.map