@ait-co/polyfill 0.1.2 → 0.1.4

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
@@ -69,6 +69,122 @@ async function loadTossSdk() {
69
69
  }
70
70
  }
71
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
+ /**
120
+ * Mutate methods on an existing object rather than replacing the object
121
+ * itself. This is the path we take for `navigator.geolocation`, `navigator.share`,
122
+ * and `navigator.vibrate` in Chromium, where the slot on `navigator` is a
123
+ * non-configurable own property that we cannot replace — but the methods
124
+ * themselves (or the methods on the referenced object) are still
125
+ * `configurable: true, writable: true`.
126
+ *
127
+ * Each replacement is installed via plain assignment. If any slot is not
128
+ * writable (e.g. frozen object), install is aborted and previously-applied
129
+ * replacements are rolled back, so the caller observes an atomic "all or
130
+ * nothing" failure as `null`. The caller is expected to degrade gracefully
131
+ * (e.g. log a one-time `console.warn`) when that happens.
132
+ */
133
+ function installObjectMethods(target, replacements) {
134
+ const methods = {};
135
+ const applied = [];
136
+ const bag = target;
137
+ for (const key of Object.keys(replacements)) {
138
+ const hadOwn = Object.hasOwn(target, key);
139
+ const original = bag[key];
140
+ try {
141
+ bag[key] = replacements[key];
142
+ } catch {
143
+ for (const applieKey of applied) {
144
+ const prev = methods[applieKey];
145
+ if (!prev) continue;
146
+ if (prev.hadOwn) bag[applieKey] = prev.original;
147
+ else delete bag[applieKey];
148
+ }
149
+ return null;
150
+ }
151
+ if (bag[key] !== replacements[key]) {
152
+ for (const applieKey of applied) {
153
+ const prev = methods[applieKey];
154
+ if (!prev) continue;
155
+ if (prev.hadOwn) bag[applieKey] = prev.original;
156
+ else delete bag[applieKey];
157
+ }
158
+ return null;
159
+ }
160
+ methods[key] = {
161
+ hadOwn,
162
+ original
163
+ };
164
+ applied.push(key);
165
+ }
166
+ return {
167
+ target,
168
+ methods
169
+ };
170
+ }
171
+ /**
172
+ * Reverse an `installObjectMethods` snapshot. Reassigns originals for slots
173
+ * that were own properties before install; deletes the override for slots
174
+ * that were inherited (so the prototype method surfaces again).
175
+ */
176
+ function restoreObjectMethods(snapshot) {
177
+ const bag = snapshot.target;
178
+ for (const key of Object.keys(snapshot.methods)) {
179
+ const entry = snapshot.methods[key];
180
+ if (!entry) continue;
181
+ try {
182
+ if (entry.hadOwn) bag[key] = entry.original;
183
+ else delete bag[key];
184
+ } catch {}
185
+ }
186
+ }
187
+ //#endregion
72
188
  //#region src/shims/clipboard.ts
73
189
  /**
74
190
  * `navigator.clipboard` shim.
@@ -81,7 +197,7 @@ async function loadTossSdk() {
81
197
  * surfaces unchanged — we don't paper over missing support.
82
198
  */
83
199
  const BACKUP_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.original");
84
- const HAD_KEY$1 = Symbol.for("@ait-co/polyfill/clipboard.hadOriginal");
200
+ const SNAPSHOT_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.snapshot");
85
201
  /**
86
202
  * Produces a Clipboard-compatible object whose `readText` / `writeText` methods
87
203
  * route to the SDK when in Toss, else fall through to the supplied `fallback`.
@@ -132,34 +248,24 @@ function installClipboardShim() {
132
248
  if (BACKUP_KEY$2 in host) return () => uninstallClipboardShim();
133
249
  const original = navigator.clipboard;
134
250
  host[BACKUP_KEY$2] = original;
135
- host[HAD_KEY$1] = "clipboard" in navigator;
136
- const shim = createClipboardShim(original);
137
- Object.defineProperty(navigator, "clipboard", {
138
- value: shim,
251
+ host[SNAPSHOT_KEY$2] = installNavigatorProperty("clipboard", {
252
+ value: createClipboardShim(original),
139
253
  configurable: true,
140
254
  writable: true
141
255
  });
142
256
  return uninstallClipboardShim;
143
257
  }
144
258
  /**
145
- * Remove the shim and restore the pre-install shape. Uses delete + conditional
146
- * redefine so a prototype-level `navigator.clipboard` (non-configurable in real
147
- * browsers) becomes visible again instead of being permanently shadowed.
259
+ * Remove the shim and restore the pre-install shape.
148
260
  */
149
261
  function uninstallClipboardShim() {
150
262
  if (typeof navigator === "undefined") return;
151
263
  const host = navigator;
152
264
  if (!(BACKUP_KEY$2 in host)) return;
153
- const original = host[BACKUP_KEY$2];
154
- const had = host[HAD_KEY$1];
155
- delete navigator.clipboard;
156
- if (had && navigator.clipboard !== original) Object.defineProperty(navigator, "clipboard", {
157
- value: original,
158
- configurable: true,
159
- writable: true
160
- });
265
+ const snapshot = host[SNAPSHOT_KEY$2];
266
+ if (snapshot) restoreNavigatorProperty("clipboard", snapshot);
161
267
  delete host[BACKUP_KEY$2];
162
- delete host[HAD_KEY$1];
268
+ delete host[SNAPSHOT_KEY$2];
163
269
  }
164
270
  //#endregion
165
271
  //#region src/shims/geolocation.ts
@@ -173,6 +279,13 @@ function uninstallClipboardShim() {
173
279
  * Outside Apps in Toss → defers to the browser's native `navigator.geolocation`.
174
280
  * If neither is available, the error callback receives a `GeolocationPositionError`.
175
281
  *
282
+ * Install strategy: **method-level**. We do **not** replace `navigator.geolocation`
283
+ * itself — Chromium marks that slot as a non-configurable own property, which
284
+ * both `defineProperty(navigator, 'geolocation', …)` and the prototype-level
285
+ * fallback cannot override (the instance shadow blocks prototype reads). We
286
+ * instead mutate the methods on the existing `Geolocation` object, whose own
287
+ * method descriptors are configurable+writable in every browser we've seen.
288
+ *
176
289
  * SDK/Web shape mismatch handled here:
177
290
  * - SDK `Accuracy` is a numeric enum (1 = Lowest … 6 = BestForNavigation); the
178
291
  * standard `PositionOptions.enableHighAccuracy` is a boolean. We map
@@ -192,6 +305,7 @@ function uninstallClipboardShim() {
192
305
  * `uninstall()`.
193
306
  */
194
307
  const BACKUP_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.original");
308
+ const SNAPSHOT_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.snapshot");
195
309
  const ACCURACY_BALANCED = 3;
196
310
  const ACCURACY_HIGH = 4;
197
311
  function toStandardPosition(sdk) {
@@ -263,7 +377,7 @@ function createGeolocationShim(fallback) {
263
377
  return;
264
378
  }
265
379
  }
266
- if (!fallback) {
380
+ if (!fallback.getCurrentPosition) {
267
381
  error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
268
382
  return;
269
383
  }
@@ -301,7 +415,7 @@ function createGeolocationShim(fallback) {
301
415
  return;
302
416
  }
303
417
  }
304
- if (!fallback) {
418
+ if (!fallback.watchPosition) {
305
419
  pendingWatches.delete(id);
306
420
  error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
307
421
  return;
@@ -312,7 +426,7 @@ function createGeolocationShim(fallback) {
312
426
  }
313
427
  const nativeId = fallback.watchPosition(success, error, options);
314
428
  if (pending.cancelled) {
315
- fallback.clearWatch(nativeId);
429
+ fallback.clearWatch?.(nativeId);
316
430
  pendingWatches.delete(id);
317
431
  return;
318
432
  }
@@ -335,7 +449,7 @@ function createGeolocationShim(fallback) {
335
449
  return;
336
450
  }
337
451
  const nativeId = nativeWatches.get(id);
338
- if (nativeId !== void 0 && fallback) {
452
+ if (nativeId !== void 0 && fallback.clearWatch) {
339
453
  fallback.clearWatch(nativeId);
340
454
  nativeWatches.delete(id);
341
455
  }
@@ -346,28 +460,37 @@ function installGeolocationShim() {
346
460
  if (typeof navigator === "undefined") return () => {};
347
461
  const host = navigator;
348
462
  if (BACKUP_KEY$1 in host) return () => uninstallGeolocationShim();
349
- const original = navigator.geolocation;
350
- host[BACKUP_KEY$1] = original;
351
- const shim = createGeolocationShim(original);
352
- Object.defineProperty(navigator, "geolocation", {
353
- value: shim,
354
- configurable: true,
355
- writable: true
463
+ const target = navigator.geolocation;
464
+ if (!target) {
465
+ host[BACKUP_KEY$1] = void 0;
466
+ return () => uninstallGeolocationShim();
467
+ }
468
+ const shim = createGeolocationShim({
469
+ getCurrentPosition: target.getCurrentPosition?.bind(target),
470
+ watchPosition: target.watchPosition?.bind(target),
471
+ clearWatch: target.clearWatch?.bind(target)
356
472
  });
473
+ const snapshot = installObjectMethods(target, {
474
+ getCurrentPosition: shim.getCurrentPosition,
475
+ watchPosition: shim.watchPosition,
476
+ clearWatch: shim.clearWatch
477
+ });
478
+ if (!snapshot) {
479
+ host[BACKUP_KEY$1] = void 0;
480
+ return () => uninstallGeolocationShim();
481
+ }
482
+ host[BACKUP_KEY$1] = { target };
483
+ host[SNAPSHOT_KEY$1] = snapshot;
357
484
  return uninstallGeolocationShim;
358
485
  }
359
486
  function uninstallGeolocationShim() {
360
487
  if (typeof navigator === "undefined") return;
361
488
  const host = navigator;
362
489
  if (!(BACKUP_KEY$1 in host)) return;
363
- const original = host[BACKUP_KEY$1];
364
- delete navigator.geolocation;
365
- if (original !== void 0 && navigator.geolocation !== original) Object.defineProperty(navigator, "geolocation", {
366
- value: original,
367
- configurable: true,
368
- writable: true
369
- });
490
+ const snapshot = host[SNAPSHOT_KEY$1];
491
+ if (snapshot) restoreObjectMethods(snapshot);
370
492
  delete host[BACKUP_KEY$1];
493
+ delete host[SNAPSHOT_KEY$1];
371
494
  }
372
495
  //#endregion
373
496
  //#region src/shims/network.ts
@@ -392,6 +515,15 @@ function uninstallGeolocationShim() {
392
515
  * visible again. We never mutate the prototype — doing so would throw in
393
516
  * browsers where the descriptor is non-configurable.
394
517
  *
518
+ * Browser-compat caveat (Chromium): `navigator.onLine` and `navigator.connection`
519
+ * are value slots, not methods, so the method-level install trick we use for
520
+ * `geolocation`/`share`/`vibrate` does not apply here. When Chromium marks the
521
+ * instance descriptor as non-configurable AND the prototype descriptor is also
522
+ * non-configurable, we cannot install. In that case the shim logs a one-time
523
+ * `console.warn` and leaves the native values in place — consumers keep the
524
+ * browser's own `onLine`/`connection` values; the SDK-synced state is simply
525
+ * disabled for that session.
526
+ *
395
527
  * Caveat: the Web NetworkInformation API is evented (`change` fires on
396
528
  * transitions). The SDK exposes only a one-shot query, so listeners attached
397
529
  * to `navigator.connection` are accepted but never fire from a `change` event
@@ -413,6 +545,8 @@ function uninstallGeolocationShim() {
413
545
  * reads may return the native object.
414
546
  */
415
547
  const INSTALLED_KEY = Symbol.for("@ait-co/polyfill/network.installed");
548
+ const ON_LINE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.onLine.snapshot");
549
+ const CONNECTION_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.connection.snapshot");
416
550
  const REFRESH_THROTTLE_MS = 500;
417
551
  function statusToOnline(status) {
418
552
  return status !== "OFFLINE";
@@ -492,47 +626,52 @@ function installNetworkShim() {
492
626
  })();
493
627
  return inflight;
494
628
  }
629
+ const nativeOnLine = navigator.onLine;
630
+ const nativeConnection = navigator.connection;
495
631
  refresh();
496
- Object.defineProperty(navigator, "onLine", {
497
- configurable: true,
498
- get() {
499
- refresh();
500
- if (cachedStatus !== null) return statusToOnline(cachedStatus);
501
- const desc = Object.getOwnPropertyDescriptor(navigator, "onLine");
502
- delete navigator.onLine;
503
- try {
504
- return navigator.onLine;
505
- } finally {
506
- if (desc) Object.defineProperty(navigator, "onLine", desc);
632
+ let installWarned = false;
633
+ const warnNonConfigurable = (e) => {
634
+ if (installWarned) return;
635
+ installWarned = true;
636
+ console.warn("[@ait-co/polyfill] navigator.onLine/connection is non-configurable in this browser; Toss network status sync disabled.", e);
637
+ };
638
+ try {
639
+ host[ON_LINE_SNAPSHOT_KEY] = installNavigatorProperty("onLine", {
640
+ configurable: true,
641
+ get() {
642
+ refresh();
643
+ if (cachedStatus !== null) return statusToOnline(cachedStatus);
644
+ return nativeOnLine ?? true;
507
645
  }
508
- }
509
- });
510
- Object.defineProperty(navigator, "connection", {
511
- configurable: true,
512
- get() {
513
- refresh();
514
- if (cachedStatus === null) {
515
- const desc = Object.getOwnPropertyDescriptor(navigator, "connection");
516
- delete navigator.connection;
517
- try {
518
- const native = navigator.connection;
519
- if (native !== void 0) return native;
520
- } finally {
521
- if (desc) Object.defineProperty(navigator, "connection", desc);
522
- }
646
+ });
647
+ } catch (e) {
648
+ warnNonConfigurable(e);
649
+ }
650
+ try {
651
+ host[CONNECTION_SNAPSHOT_KEY] = installNavigatorProperty("connection", {
652
+ configurable: true,
653
+ get() {
654
+ refresh();
655
+ if (cachedStatus === null && nativeConnection !== void 0) return nativeConnection;
656
+ return connection;
523
657
  }
524
- return connection;
525
- }
526
- });
658
+ });
659
+ } catch (e) {
660
+ warnNonConfigurable(e);
661
+ }
527
662
  return uninstallNetworkShim;
528
663
  }
529
664
  function uninstallNetworkShim() {
530
665
  if (typeof navigator === "undefined") return;
531
666
  const host = navigator;
532
667
  if (!host[INSTALLED_KEY]) return;
533
- delete navigator.onLine;
534
- delete navigator.connection;
668
+ const onLineSnap = host[ON_LINE_SNAPSHOT_KEY];
669
+ if (onLineSnap) restoreNavigatorProperty("onLine", onLineSnap);
670
+ const connSnap = host[CONNECTION_SNAPSHOT_KEY];
671
+ if (connSnap) restoreNavigatorProperty("connection", connSnap);
535
672
  delete host[INSTALLED_KEY];
673
+ delete host[ON_LINE_SNAPSHOT_KEY];
674
+ delete host[CONNECTION_SNAPSHOT_KEY];
536
675
  }
537
676
  //#endregion
538
677
  //#region src/shims/share.ts
@@ -546,11 +685,18 @@ function uninstallNetworkShim() {
546
685
  * Outside Apps in Toss → defers to the browser's native `navigator.share`, or
547
686
  * throws `NotSupportedError` if unavailable.
548
687
  *
688
+ * Install strategy: **method-level** on `navigator`. Assigning
689
+ * `navigator.share = fn` creates an own property that shadows the prototype
690
+ * method. Uninstall deletes the own shadow so the prototype method surfaces
691
+ * again. We do not mutate `Navigator.prototype` — in real browsers its
692
+ * descriptor may be non-configurable, which would throw on reassignment.
693
+ *
549
694
  * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).
550
695
  * `canShare({ files })` returns `false` whenever the sync-accessible detection
551
696
  * says Toss is active (or is being forced via the test override).
552
697
  */
553
698
  const SHARE_BACKUP_KEY = Symbol.for("@ait-co/polyfill/share.original");
699
+ const SHARE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/share.snapshot");
554
700
  function buildSdkMessage(data) {
555
701
  const parts = [];
556
702
  if (data?.title != null && data.title !== "") parts.push(data.title);
@@ -577,7 +723,7 @@ async function shareShim(data) {
577
723
  }
578
724
  const original = navigator[SHARE_BACKUP_KEY]?.share;
579
725
  if (!original) throw new DOMException("[@ait-co/polyfill] navigator.share is not available in this environment.", "NotSupportedError");
580
- return original.call(navigator, data);
726
+ return original(data);
581
727
  }
582
728
  function canShareShim(data) {
583
729
  const hasFiles = Boolean(data?.files && data.files.length > 0);
@@ -588,7 +734,7 @@ function canShareShim(data) {
588
734
  }
589
735
  if (toss === true) return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
590
736
  const originalCanShare = navigator[SHARE_BACKUP_KEY]?.canShare;
591
- if (originalCanShare) return originalCanShare.call(navigator, data);
737
+ if (originalCanShare) return originalCanShare(data);
592
738
  return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
593
739
  }
594
740
  function installShareShim() {
@@ -597,41 +743,28 @@ function installShareShim() {
597
743
  if (SHARE_BACKUP_KEY in host) return () => uninstallShareShim();
598
744
  const nav = navigator;
599
745
  host[SHARE_BACKUP_KEY] = {
600
- share: nav.share,
601
- canShare: nav.canShare,
602
- hadShare: "share" in nav,
603
- hadCanShare: "canShare" in nav
746
+ share: nav.share ? nav.share.bind(navigator) : void 0,
747
+ canShare: nav.canShare ? nav.canShare.bind(navigator) : void 0
604
748
  };
605
- Object.defineProperty(navigator, "share", {
606
- value: shareShim,
607
- configurable: true,
608
- writable: true
609
- });
610
- Object.defineProperty(navigator, "canShare", {
611
- value: canShareShim,
612
- configurable: true,
613
- writable: true
749
+ const snapshot = installObjectMethods(navigator, {
750
+ share: shareShim,
751
+ canShare: canShareShim
614
752
  });
753
+ if (!snapshot) {
754
+ delete host[SHARE_BACKUP_KEY];
755
+ return () => uninstallShareShim();
756
+ }
757
+ host[SHARE_SNAPSHOT_KEY] = snapshot;
615
758
  return uninstallShareShim;
616
759
  }
617
760
  function uninstallShareShim() {
618
761
  if (typeof navigator === "undefined") return;
619
762
  const host = navigator;
620
763
  if (!(SHARE_BACKUP_KEY in host)) return;
621
- const backup = host[SHARE_BACKUP_KEY];
622
- delete navigator.share;
623
- if (backup?.hadShare && navigator.share !== backup.share) Object.defineProperty(navigator, "share", {
624
- value: backup.share,
625
- configurable: true,
626
- writable: true
627
- });
628
- delete navigator.canShare;
629
- if (backup?.hadCanShare && navigator.canShare !== backup.canShare) Object.defineProperty(navigator, "canShare", {
630
- value: backup.canShare,
631
- configurable: true,
632
- writable: true
633
- });
764
+ const snapshot = host[SHARE_SNAPSHOT_KEY];
765
+ if (snapshot) restoreObjectMethods(snapshot);
634
766
  delete host[SHARE_BACKUP_KEY];
767
+ delete host[SHARE_SNAPSHOT_KEY];
635
768
  }
636
769
  //#endregion
637
770
  //#region src/shims/vibrate.ts
@@ -647,6 +780,11 @@ function uninstallShareShim() {
647
780
  * or returns `false` when unavailable (matches the spec — browsers that don't
648
781
  * support vibration simply return `false`).
649
782
  *
783
+ * Install strategy: **method-level** on `navigator`. We assign our wrapper to
784
+ * `navigator.vibrate` (creating an own shadow over the prototype method) and
785
+ * delete it on uninstall so the prototype re-surfaces. We do not mutate
786
+ * `Navigator.prototype` itself — browsers may mark it non-configurable.
787
+ *
650
788
  * Caveats (documented in CLAUDE.md as the known lossy trade-off):
651
789
  * - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
652
790
  * durations. The shim approximates intensity from duration but cannot
@@ -657,7 +795,7 @@ function uninstallShareShim() {
657
795
  * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
658
796
  */
659
797
  const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
660
- const HAD_KEY = Symbol.for("@ait-co/polyfill/vibrate.hadOriginal");
798
+ const SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/vibrate.snapshot");
661
799
  const SHORT_VIBRATION_MS = 40;
662
800
  async function haptic(type) {
663
801
  const fn = (await loadTossSdk())?.generateHapticFeedback;
@@ -672,7 +810,7 @@ function vibrateShim(pattern) {
672
810
  const arr = Array.isArray(pattern) ? pattern : [pattern];
673
811
  if (arr.length === 0 || arr.every((n) => n === 0)) {
674
812
  (async () => {
675
- if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.call(navigator, pattern);
813
+ if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.(pattern);
676
814
  })();
677
815
  return true;
678
816
  }
@@ -691,7 +829,8 @@ function vibrateShim(pattern) {
691
829
  }
692
830
  return;
693
831
  }
694
- navigator[BACKUP_KEY]?.call(navigator, pattern);
832
+ const original = navigator[BACKUP_KEY];
833
+ original?.(pattern);
695
834
  })();
696
835
  return true;
697
836
  }
@@ -700,33 +839,27 @@ function installVibrateShim() {
700
839
  const host = navigator;
701
840
  if (BACKUP_KEY in host) return () => uninstallVibrateShim();
702
841
  const nav = navigator;
703
- host[BACKUP_KEY] = nav.vibrate;
704
- host[HAD_KEY] = "vibrate" in nav;
705
- Object.defineProperty(navigator, "vibrate", {
706
- value: vibrateShim,
707
- configurable: true,
708
- writable: true
709
- });
842
+ host[BACKUP_KEY] = nav.vibrate ? nav.vibrate.bind(navigator) : void 0;
843
+ const snapshot = installObjectMethods(navigator, { vibrate: vibrateShim });
844
+ if (!snapshot) {
845
+ delete host[BACKUP_KEY];
846
+ return () => uninstallVibrateShim();
847
+ }
848
+ host[SNAPSHOT_KEY] = snapshot;
710
849
  return uninstallVibrateShim;
711
850
  }
712
851
  function uninstallVibrateShim() {
713
852
  if (typeof navigator === "undefined") return;
714
853
  const host = navigator;
715
854
  if (!(BACKUP_KEY in host)) return;
716
- const original = host[BACKUP_KEY];
717
- const had = host[HAD_KEY];
718
- delete navigator.vibrate;
719
- if (had && navigator.vibrate !== original) Object.defineProperty(navigator, "vibrate", {
720
- value: original,
721
- configurable: true,
722
- writable: true
723
- });
855
+ const snapshot = host[SNAPSHOT_KEY];
856
+ if (snapshot) restoreObjectMethods(snapshot);
724
857
  delete host[BACKUP_KEY];
725
- delete host[HAD_KEY];
858
+ delete host[SNAPSHOT_KEY];
726
859
  }
727
860
  //#endregion
728
861
  //#region src/index.ts
729
- const VERSION = "0.1.2";
862
+ const VERSION = "0.1.4";
730
863
  const NOOP = () => {};
731
864
  /**
732
865
  * Install every shim this library ships, but only if we detect an Apps in