@ait-co/polyfill 0.1.1 → 0.1.3

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 CHANGED
@@ -2,14 +2,19 @@
2
2
  /**
3
3
  * Environment detection: are we running inside Apps in Toss, or a plain browser?
4
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.
5
+ * Strategy: call the SDK's `getAppsInTossGlobals()` a synchronous export
6
+ * that returns the runtime's Toss globals (deploymentId, brand name, …)
7
+ * inside the Apps in Toss runtime and throws (RN bridge unavailable)
8
+ * anywhere else. The SDK itself is an **optional** peer dependency; if its
9
+ * module can't be imported we are definitely not inside Toss.
9
10
  *
10
- * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK
11
- * function during detection (could prompt permission dialogs, fire analytics,
12
- * etc.).
11
+ * Just having the SDK module resolvable is not enough apps can bundle it
12
+ * and still run in a plain browser. We need the bridge probe to confirm.
13
+ *
14
+ * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but
15
+ * that's a constant read from the bridge — no permission dialogs, no
16
+ * analytics fire. In a plain browser the bridge lookup fails fast (sync
17
+ * throw, microsecond-scale), so the startup cost is negligible.
13
18
  */
14
19
  let cached;
15
20
  /**
@@ -39,7 +44,17 @@ async function isTossEnvironment() {
39
44
  if (force === "toss") return true;
40
45
  if (force === "browser") return false;
41
46
  if (cached !== void 0) return cached;
42
- cached = typeof (await loadTossSdk())?.getClipboardText === "function";
47
+ const mod = await loadTossSdk();
48
+ if (typeof mod?.getAppsInTossGlobals !== "function") {
49
+ cached = false;
50
+ return cached;
51
+ }
52
+ try {
53
+ const globals = mod.getAppsInTossGlobals();
54
+ cached = Boolean(globals) && typeof globals === "object";
55
+ } catch {
56
+ cached = false;
57
+ }
43
58
  return cached;
44
59
  }
45
60
  /**
@@ -54,6 +69,54 @@ async function loadTossSdk() {
54
69
  }
55
70
  }
56
71
  //#endregion
72
+ //#region src/shims/_install-helpers.ts
73
+ /**
74
+ * Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the
75
+ * browser refuses (property is non-configurable on the instance), install on
76
+ * `Navigator.prototype` instead.
77
+ *
78
+ * Returns a snapshot describing where the original value was, which
79
+ * `restoreNavigatorProperty` uses to undo the install.
80
+ */
81
+ function installNavigatorProperty(prop, descriptor) {
82
+ const nav = navigator;
83
+ const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);
84
+ const instanceHadOwn = instanceDesc !== void 0;
85
+ if (!instanceDesc || instanceDesc.configurable) try {
86
+ Object.defineProperty(nav, prop, descriptor);
87
+ return {
88
+ location: "instance",
89
+ originalDescriptor: instanceDesc,
90
+ instanceHadOwn
91
+ };
92
+ } catch {}
93
+ const proto = Object.getPrototypeOf(nav);
94
+ const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);
95
+ if (instanceHadOwn) try {
96
+ delete nav[prop];
97
+ } catch {}
98
+ Object.defineProperty(proto, prop, descriptor);
99
+ return {
100
+ location: "prototype",
101
+ originalDescriptor: protoDesc,
102
+ instanceHadOwn
103
+ };
104
+ }
105
+ /**
106
+ * Reverse the install recorded in `snapshot`. If the original descriptor was
107
+ * `undefined` (property didn't exist before), delete the property instead of
108
+ * re-defining it.
109
+ */
110
+ function restoreNavigatorProperty(prop, snapshot) {
111
+ const target = snapshot.location === "instance" ? navigator : Object.getPrototypeOf(navigator);
112
+ if (snapshot.originalDescriptor) try {
113
+ Object.defineProperty(target, prop, snapshot.originalDescriptor);
114
+ } catch {}
115
+ else try {
116
+ delete target[prop];
117
+ } catch {}
118
+ }
119
+ //#endregion
57
120
  //#region src/shims/clipboard.ts
58
121
  /**
59
122
  * `navigator.clipboard` shim.
@@ -66,7 +129,7 @@ async function loadTossSdk() {
66
129
  * surfaces unchanged — we don't paper over missing support.
67
130
  */
68
131
  const BACKUP_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.original");
69
- const HAD_KEY$1 = Symbol.for("@ait-co/polyfill/clipboard.hadOriginal");
132
+ const SNAPSHOT_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.snapshot");
70
133
  /**
71
134
  * Produces a Clipboard-compatible object whose `readText` / `writeText` methods
72
135
  * route to the SDK when in Toss, else fall through to the supplied `fallback`.
@@ -117,34 +180,24 @@ function installClipboardShim() {
117
180
  if (BACKUP_KEY$2 in host) return () => uninstallClipboardShim();
118
181
  const original = navigator.clipboard;
119
182
  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,
183
+ host[SNAPSHOT_KEY$2] = installNavigatorProperty("clipboard", {
184
+ value: createClipboardShim(original),
124
185
  configurable: true,
125
186
  writable: true
126
187
  });
127
188
  return uninstallClipboardShim;
128
189
  }
129
190
  /**
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.
191
+ * Remove the shim and restore the pre-install shape.
133
192
  */
134
193
  function uninstallClipboardShim() {
135
194
  if (typeof navigator === "undefined") return;
136
195
  const host = navigator;
137
196
  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
- });
197
+ const snapshot = host[SNAPSHOT_KEY$2];
198
+ if (snapshot) restoreNavigatorProperty("clipboard", snapshot);
146
199
  delete host[BACKUP_KEY$2];
147
- delete host[HAD_KEY$1];
200
+ delete host[SNAPSHOT_KEY$2];
148
201
  }
149
202
  //#endregion
150
203
  //#region src/shims/geolocation.ts
@@ -177,6 +230,7 @@ function uninstallClipboardShim() {
177
230
  * `uninstall()`.
178
231
  */
179
232
  const BACKUP_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.original");
233
+ const SNAPSHOT_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.snapshot");
180
234
  const ACCURACY_BALANCED = 3;
181
235
  const ACCURACY_HIGH = 4;
182
236
  function toStandardPosition(sdk) {
@@ -333,9 +387,8 @@ function installGeolocationShim() {
333
387
  if (BACKUP_KEY$1 in host) return () => uninstallGeolocationShim();
334
388
  const original = navigator.geolocation;
335
389
  host[BACKUP_KEY$1] = original;
336
- const shim = createGeolocationShim(original);
337
- Object.defineProperty(navigator, "geolocation", {
338
- value: shim,
390
+ host[SNAPSHOT_KEY$1] = installNavigatorProperty("geolocation", {
391
+ value: createGeolocationShim(original),
339
392
  configurable: true,
340
393
  writable: true
341
394
  });
@@ -345,14 +398,10 @@ function uninstallGeolocationShim() {
345
398
  if (typeof navigator === "undefined") return;
346
399
  const host = navigator;
347
400
  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
- });
401
+ const snapshot = host[SNAPSHOT_KEY$1];
402
+ if (snapshot) restoreNavigatorProperty("geolocation", snapshot);
355
403
  delete host[BACKUP_KEY$1];
404
+ delete host[SNAPSHOT_KEY$1];
356
405
  }
357
406
  //#endregion
358
407
  //#region src/shims/network.ts
@@ -398,6 +447,8 @@ function uninstallGeolocationShim() {
398
447
  * reads may return the native object.
399
448
  */
400
449
  const INSTALLED_KEY = Symbol.for("@ait-co/polyfill/network.installed");
450
+ const ON_LINE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.onLine.snapshot");
451
+ const CONNECTION_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.connection.snapshot");
401
452
  const REFRESH_THROTTLE_MS = 500;
402
453
  function statusToOnline(status) {
403
454
  return status !== "OFFLINE";
@@ -477,35 +528,22 @@ function installNetworkShim() {
477
528
  })();
478
529
  return inflight;
479
530
  }
531
+ const nativeOnLine = navigator.onLine;
532
+ const nativeConnection = navigator.connection;
480
533
  refresh();
481
- Object.defineProperty(navigator, "onLine", {
534
+ host[ON_LINE_SNAPSHOT_KEY] = installNavigatorProperty("onLine", {
482
535
  configurable: true,
483
536
  get() {
484
537
  refresh();
485
538
  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
- }
539
+ return nativeOnLine ?? true;
493
540
  }
494
541
  });
495
- Object.defineProperty(navigator, "connection", {
542
+ host[CONNECTION_SNAPSHOT_KEY] = installNavigatorProperty("connection", {
496
543
  configurable: true,
497
544
  get() {
498
545
  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
- }
546
+ if (cachedStatus === null && nativeConnection !== void 0) return nativeConnection;
509
547
  return connection;
510
548
  }
511
549
  });
@@ -515,9 +553,13 @@ function uninstallNetworkShim() {
515
553
  if (typeof navigator === "undefined") return;
516
554
  const host = navigator;
517
555
  if (!host[INSTALLED_KEY]) return;
518
- delete navigator.onLine;
519
- delete navigator.connection;
556
+ const onLineSnap = host[ON_LINE_SNAPSHOT_KEY];
557
+ if (onLineSnap) restoreNavigatorProperty("onLine", onLineSnap);
558
+ const connSnap = host[CONNECTION_SNAPSHOT_KEY];
559
+ if (connSnap) restoreNavigatorProperty("connection", connSnap);
520
560
  delete host[INSTALLED_KEY];
561
+ delete host[ON_LINE_SNAPSHOT_KEY];
562
+ delete host[CONNECTION_SNAPSHOT_KEY];
521
563
  }
522
564
  //#endregion
523
565
  //#region src/shims/share.ts
@@ -536,6 +578,8 @@ function uninstallNetworkShim() {
536
578
  * says Toss is active (or is being forced via the test override).
537
579
  */
538
580
  const SHARE_BACKUP_KEY = Symbol.for("@ait-co/polyfill/share.original");
581
+ const SHARE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/share.snapshot");
582
+ const CAN_SHARE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/canShare.snapshot");
539
583
  function buildSdkMessage(data) {
540
584
  const parts = [];
541
585
  if (data?.title != null && data.title !== "") parts.push(data.title);
@@ -587,12 +631,12 @@ function installShareShim() {
587
631
  hadShare: "share" in nav,
588
632
  hadCanShare: "canShare" in nav
589
633
  };
590
- Object.defineProperty(navigator, "share", {
634
+ host[SHARE_SNAPSHOT_KEY] = installNavigatorProperty("share", {
591
635
  value: shareShim,
592
636
  configurable: true,
593
637
  writable: true
594
638
  });
595
- Object.defineProperty(navigator, "canShare", {
639
+ host[CAN_SHARE_SNAPSHOT_KEY] = installNavigatorProperty("canShare", {
596
640
  value: canShareShim,
597
641
  configurable: true,
598
642
  writable: true
@@ -603,20 +647,13 @@ function uninstallShareShim() {
603
647
  if (typeof navigator === "undefined") return;
604
648
  const host = navigator;
605
649
  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
- });
650
+ const shareSnap = host[SHARE_SNAPSHOT_KEY];
651
+ if (shareSnap) restoreNavigatorProperty("share", shareSnap);
652
+ const canShareSnap = host[CAN_SHARE_SNAPSHOT_KEY];
653
+ if (canShareSnap) restoreNavigatorProperty("canShare", canShareSnap);
619
654
  delete host[SHARE_BACKUP_KEY];
655
+ delete host[SHARE_SNAPSHOT_KEY];
656
+ delete host[CAN_SHARE_SNAPSHOT_KEY];
620
657
  }
621
658
  //#endregion
622
659
  //#region src/shims/vibrate.ts
@@ -642,7 +679,7 @@ function uninstallShareShim() {
642
679
  * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
643
680
  */
644
681
  const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
645
- const HAD_KEY = Symbol.for("@ait-co/polyfill/vibrate.hadOriginal");
682
+ const SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/vibrate.snapshot");
646
683
  const SHORT_VIBRATION_MS = 40;
647
684
  async function haptic(type) {
648
685
  const fn = (await loadTossSdk())?.generateHapticFeedback;
@@ -684,10 +721,8 @@ function installVibrateShim() {
684
721
  if (typeof navigator === "undefined") return () => {};
685
722
  const host = navigator;
686
723
  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", {
724
+ host[BACKUP_KEY] = navigator.vibrate?.bind(navigator);
725
+ host[SNAPSHOT_KEY] = installNavigatorProperty("vibrate", {
691
726
  value: vibrateShim,
692
727
  configurable: true,
693
728
  writable: true
@@ -698,34 +733,31 @@ function uninstallVibrateShim() {
698
733
  if (typeof navigator === "undefined") return;
699
734
  const host = navigator;
700
735
  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
- });
736
+ const snapshot = host[SNAPSHOT_KEY];
737
+ if (snapshot) restoreNavigatorProperty("vibrate", snapshot);
709
738
  delete host[BACKUP_KEY];
710
- delete host[HAD_KEY];
739
+ delete host[SNAPSHOT_KEY];
711
740
  }
712
741
  //#endregion
713
742
  //#region src/index.ts
714
- const VERSION = "0.1.1";
743
+ const VERSION = "0.1.3";
744
+ const NOOP = () => {};
715
745
  /**
716
- * Install every shim this library ships. Idempotent safe to call more than
717
- * once. Returns an uninstall function that restores every original API.
746
+ * Install every shim this library ships, but only if we detect an Apps in
747
+ * Toss runtime. In a plain browser `install()` is a no-op — the browser's
748
+ * native APIs stay untouched.
718
749
  *
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).
750
+ * Returns a promise that resolves with an uninstall function. If the
751
+ * environment turns out not to be Toss, the uninstall function is a no-op.
722
752
  *
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.
753
+ * Install order (when active): clipboard geolocation share vibrate
754
+ * network. Not atomic on failure if a per-shim install throws (e.g., a
755
+ * consumer pinned a target navigator property as non-configurable), earlier
756
+ * shims are already in place. Callers should catch and invoke the returned
757
+ * uninstall to roll back.
727
758
  */
728
- function install() {
759
+ async function install() {
760
+ if (!await isTossEnvironment()) return NOOP;
729
761
  const uninstalls = [
730
762
  installClipboardShim(),
731
763
  installGeolocationShim(),