@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.
- package/README.md +53 -3
- package/dist/auto.cjs +919 -0
- package/dist/auto.cjs.map +1 -0
- package/dist/auto.d.cts +1 -0
- package/dist/auto.js +18 -6
- package/dist/auto.js.map +1 -1
- package/dist/detect.cjs +84 -0
- package/dist/detect.cjs.map +1 -0
- package/dist/detect.d.cts +50 -0
- package/dist/detect.d.cts.map +1 -0
- package/dist/index.cjs +982 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +222 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +26 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -8
- package/dist/index.js.map +1 -1
- package/dist/shims/clipboard.cjs +192 -0
- package/dist/shims/clipboard.cjs.map +1 -0
- package/dist/shims/clipboard.d.cts +26 -0
- package/dist/shims/clipboard.d.cts.map +1 -0
- package/dist/shims/geolocation.cjs +356 -0
- package/dist/shims/geolocation.cjs.map +1 -0
- package/dist/shims/geolocation.d.cts +41 -0
- package/dist/shims/geolocation.d.cts.map +1 -0
- package/dist/shims/network.cjs +290 -0
- package/dist/shims/network.cjs.map +1 -0
- package/dist/shims/network.d.cts +56 -0
- package/dist/shims/network.d.cts.map +1 -0
- package/dist/shims/share.cjs +239 -0
- package/dist/shims/share.cjs.map +1 -0
- package/dist/shims/share.d.cts +26 -0
- package/dist/shims/share.d.cts.map +1 -0
- package/dist/shims/vibrate-semantic.d.ts +27 -0
- package/dist/shims/vibrate-semantic.d.ts.map +1 -0
- package/dist/shims/vibrate-semantic.js +150 -0
- package/dist/shims/vibrate-semantic.js.map +1 -0
- package/dist/shims/vibrate.cjs +235 -0
- package/dist/shims/vibrate.cjs.map +1 -0
- package/dist/shims/vibrate.d.cts +43 -0
- package/dist/shims/vibrate.d.cts.map +1 -0
- package/dist/shims/vibrate.d.ts +16 -5
- package/dist/shims/vibrate.d.ts.map +1 -1
- package/dist/shims/vibrate.js +19 -7
- package/dist/shims/vibrate.js.map +1 -1
- package/package.json +73 -19
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
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
|
+
* Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the
|
|
76
|
+
* browser refuses (property is non-configurable on the instance), install on
|
|
77
|
+
* `Navigator.prototype` instead.
|
|
78
|
+
*
|
|
79
|
+
* Returns a snapshot describing where the original value was, which
|
|
80
|
+
* `restoreNavigatorProperty` uses to undo the install.
|
|
81
|
+
*/
|
|
82
|
+
function installNavigatorProperty(prop, descriptor) {
|
|
83
|
+
const nav = navigator;
|
|
84
|
+
const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);
|
|
85
|
+
const instanceHadOwn = instanceDesc !== void 0;
|
|
86
|
+
if (!instanceDesc || instanceDesc.configurable) try {
|
|
87
|
+
Object.defineProperty(nav, prop, descriptor);
|
|
88
|
+
return {
|
|
89
|
+
location: "instance",
|
|
90
|
+
originalDescriptor: instanceDesc,
|
|
91
|
+
instanceHadOwn
|
|
92
|
+
};
|
|
93
|
+
} catch {}
|
|
94
|
+
const proto = Object.getPrototypeOf(nav);
|
|
95
|
+
const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
96
|
+
if (instanceHadOwn) try {
|
|
97
|
+
delete nav[prop];
|
|
98
|
+
} catch {}
|
|
99
|
+
Object.defineProperty(proto, prop, descriptor);
|
|
100
|
+
return {
|
|
101
|
+
location: "prototype",
|
|
102
|
+
originalDescriptor: protoDesc,
|
|
103
|
+
instanceHadOwn
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reverse the install recorded in `snapshot`. If the original descriptor was
|
|
108
|
+
* `undefined` (property didn't exist before), delete the property instead of
|
|
109
|
+
* re-defining it.
|
|
110
|
+
*/
|
|
111
|
+
function restoreNavigatorProperty(prop, snapshot) {
|
|
112
|
+
const target = snapshot.location === "instance" ? navigator : Object.getPrototypeOf(navigator);
|
|
113
|
+
if (snapshot.originalDescriptor) try {
|
|
114
|
+
Object.defineProperty(target, prop, snapshot.originalDescriptor);
|
|
115
|
+
} catch {}
|
|
116
|
+
else try {
|
|
117
|
+
delete target[prop];
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Mutate methods on an existing object rather than replacing the object
|
|
122
|
+
* itself. This is the path we take for `navigator.geolocation`, `navigator.share`,
|
|
123
|
+
* and `navigator.vibrate` in Chromium, where the slot on `navigator` is a
|
|
124
|
+
* non-configurable own property that we cannot replace — but the methods
|
|
125
|
+
* themselves (or the methods on the referenced object) are still
|
|
126
|
+
* `configurable: true, writable: true`.
|
|
127
|
+
*
|
|
128
|
+
* Each replacement is installed via plain assignment. If any slot is not
|
|
129
|
+
* writable (e.g. frozen object), install is aborted and previously-applied
|
|
130
|
+
* replacements are rolled back, so the caller observes an atomic "all or
|
|
131
|
+
* nothing" failure as `null`. The caller is expected to degrade gracefully
|
|
132
|
+
* (e.g. log a one-time `console.warn`) when that happens.
|
|
133
|
+
*/
|
|
134
|
+
function installObjectMethods(target, replacements) {
|
|
135
|
+
const methods = {};
|
|
136
|
+
const applied = [];
|
|
137
|
+
const bag = target;
|
|
138
|
+
for (const key of Object.keys(replacements)) {
|
|
139
|
+
const hadOwn = Object.hasOwn(target, key);
|
|
140
|
+
const original = bag[key];
|
|
141
|
+
try {
|
|
142
|
+
bag[key] = replacements[key];
|
|
143
|
+
} catch {
|
|
144
|
+
for (const applieKey of applied) {
|
|
145
|
+
const prev = methods[applieKey];
|
|
146
|
+
if (!prev) continue;
|
|
147
|
+
if (prev.hadOwn) bag[applieKey] = prev.original;
|
|
148
|
+
else delete bag[applieKey];
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (bag[key] !== replacements[key]) {
|
|
153
|
+
for (const applieKey of applied) {
|
|
154
|
+
const prev = methods[applieKey];
|
|
155
|
+
if (!prev) continue;
|
|
156
|
+
if (prev.hadOwn) bag[applieKey] = prev.original;
|
|
157
|
+
else delete bag[applieKey];
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
methods[key] = {
|
|
162
|
+
hadOwn,
|
|
163
|
+
original
|
|
164
|
+
};
|
|
165
|
+
applied.push(key);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
target,
|
|
169
|
+
methods
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reverse an `installObjectMethods` snapshot. Reassigns originals for slots
|
|
174
|
+
* that were own properties before install; deletes the override for slots
|
|
175
|
+
* that were inherited (so the prototype method surfaces again).
|
|
176
|
+
*/
|
|
177
|
+
function restoreObjectMethods(snapshot) {
|
|
178
|
+
const bag = snapshot.target;
|
|
179
|
+
for (const key of Object.keys(snapshot.methods)) {
|
|
180
|
+
const entry = snapshot.methods[key];
|
|
181
|
+
if (!entry) continue;
|
|
182
|
+
try {
|
|
183
|
+
if (entry.hadOwn) bag[key] = entry.original;
|
|
184
|
+
else delete bag[key];
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/shims/clipboard.ts
|
|
190
|
+
/**
|
|
191
|
+
* `navigator.clipboard` shim.
|
|
192
|
+
*
|
|
193
|
+
* Inside Apps in Toss → routes `readText` / `writeText` through the SDK
|
|
194
|
+
* (`getClipboardText` / `setClipboardText`).
|
|
195
|
+
*
|
|
196
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.clipboard`.
|
|
197
|
+
* If the browser doesn't implement it, the standard `TypeError` / `DOMException`
|
|
198
|
+
* surfaces unchanged — we don't paper over missing support.
|
|
199
|
+
*/
|
|
200
|
+
const BACKUP_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.original");
|
|
201
|
+
const SNAPSHOT_KEY$2 = Symbol.for("@ait-co/polyfill/clipboard.snapshot");
|
|
202
|
+
/**
|
|
203
|
+
* Produces a Clipboard-compatible object whose `readText` / `writeText` methods
|
|
204
|
+
* route to the SDK when in Toss, else fall through to the supplied `fallback`.
|
|
205
|
+
*/
|
|
206
|
+
function createClipboardShim(fallback) {
|
|
207
|
+
return {
|
|
208
|
+
async readText() {
|
|
209
|
+
if (await isTossEnvironment()) {
|
|
210
|
+
const sdk = await loadTossSdk();
|
|
211
|
+
if (sdk?.getClipboardText) return sdk.getClipboardText();
|
|
212
|
+
}
|
|
213
|
+
if (!fallback) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.readText is not available in this environment.", "NotSupportedError");
|
|
214
|
+
return fallback.readText();
|
|
215
|
+
},
|
|
216
|
+
async writeText(text) {
|
|
217
|
+
if (await isTossEnvironment()) {
|
|
218
|
+
const sdk = await loadTossSdk();
|
|
219
|
+
if (sdk?.setClipboardText) return sdk.setClipboardText(text);
|
|
220
|
+
}
|
|
221
|
+
if (!fallback) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.writeText is not available in this environment.", "NotSupportedError");
|
|
222
|
+
return fallback.writeText(text);
|
|
223
|
+
},
|
|
224
|
+
async read() {
|
|
225
|
+
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");
|
|
226
|
+
if (!fallback?.read) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.read is not available.", "NotSupportedError");
|
|
227
|
+
return fallback.read();
|
|
228
|
+
},
|
|
229
|
+
async write(items) {
|
|
230
|
+
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");
|
|
231
|
+
if (!fallback?.write) throw new DOMException("[@ait-co/polyfill] navigator.clipboard.write is not available.", "NotSupportedError");
|
|
232
|
+
return fallback.write(items);
|
|
233
|
+
},
|
|
234
|
+
addEventListener: (...args) => fallback?.addEventListener(...args),
|
|
235
|
+
removeEventListener: (...args) => fallback?.removeEventListener(...args),
|
|
236
|
+
dispatchEvent: (event) => fallback?.dispatchEvent(event) ?? false
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Install the `navigator.clipboard` shim.
|
|
241
|
+
*
|
|
242
|
+
* @returns an uninstall function that restores the original `navigator.clipboard`.
|
|
243
|
+
* Calling install twice without uninstalling is a no-op on the second call
|
|
244
|
+
* and returns the same uninstall function.
|
|
245
|
+
*/
|
|
246
|
+
function installClipboardShim() {
|
|
247
|
+
if (typeof navigator === "undefined") return () => {};
|
|
248
|
+
const host = navigator;
|
|
249
|
+
if (BACKUP_KEY$2 in host) return () => uninstallClipboardShim();
|
|
250
|
+
const original = navigator.clipboard;
|
|
251
|
+
host[BACKUP_KEY$2] = original;
|
|
252
|
+
host[SNAPSHOT_KEY$2] = installNavigatorProperty("clipboard", {
|
|
253
|
+
value: createClipboardShim(original),
|
|
254
|
+
configurable: true,
|
|
255
|
+
writable: true
|
|
256
|
+
});
|
|
257
|
+
return uninstallClipboardShim;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Remove the shim and restore the pre-install shape.
|
|
261
|
+
*/
|
|
262
|
+
function uninstallClipboardShim() {
|
|
263
|
+
if (typeof navigator === "undefined") return;
|
|
264
|
+
const host = navigator;
|
|
265
|
+
if (!(BACKUP_KEY$2 in host)) return;
|
|
266
|
+
const snapshot = host[SNAPSHOT_KEY$2];
|
|
267
|
+
if (snapshot) restoreNavigatorProperty("clipboard", snapshot);
|
|
268
|
+
delete host[BACKUP_KEY$2];
|
|
269
|
+
delete host[SNAPSHOT_KEY$2];
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/shims/geolocation.ts
|
|
273
|
+
/**
|
|
274
|
+
* `navigator.geolocation` shim.
|
|
275
|
+
*
|
|
276
|
+
* Inside Apps in Toss → routes through the SDK:
|
|
277
|
+
* - `getCurrentPosition` → `getCurrentLocation({ accuracy })`
|
|
278
|
+
* - `watchPosition` / `clearWatch` → `startUpdateLocation({ onEvent, onError, options })`
|
|
279
|
+
*
|
|
280
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.geolocation`.
|
|
281
|
+
* If neither is available, the error callback receives a `GeolocationPositionError`.
|
|
282
|
+
*
|
|
283
|
+
* Install strategy: **method-level**. We do **not** replace `navigator.geolocation`
|
|
284
|
+
* itself — Chromium marks that slot as a non-configurable own property, which
|
|
285
|
+
* both `defineProperty(navigator, 'geolocation', …)` and the prototype-level
|
|
286
|
+
* fallback cannot override (the instance shadow blocks prototype reads). We
|
|
287
|
+
* instead mutate the methods on the existing `Geolocation` object, whose own
|
|
288
|
+
* method descriptors are configurable+writable in every browser we've seen.
|
|
289
|
+
*
|
|
290
|
+
* SDK/Web shape mismatch handled here:
|
|
291
|
+
* - SDK `Accuracy` is a numeric enum (1 = Lowest … 6 = BestForNavigation); the
|
|
292
|
+
* standard `PositionOptions.enableHighAccuracy` is a boolean. We map
|
|
293
|
+
* `true → Accuracy.High (4, "~10m")` and `false → Accuracy.Balanced (3)`.
|
|
294
|
+
* `Highest (5)` / `BestForNavigation (6)` are available but carry a battery
|
|
295
|
+
* cost that's rarely what mini-apps want; consumers who need them should
|
|
296
|
+
* call the SDK directly.
|
|
297
|
+
* - SDK coords lack `speed`; we surface `null` (per the W3C spec when unknown).
|
|
298
|
+
* - SDK `startUpdateLocation` returns an `unsubscribe` fn; we wrap it behind
|
|
299
|
+
* a numeric watch id so `clearWatch(id)` behaves like the standard.
|
|
300
|
+
*
|
|
301
|
+
* Caveat: watch ids reset whenever the shim is uninstalled and reinstalled;
|
|
302
|
+
* they are not stable across such cycles. Ids obtained before uninstall
|
|
303
|
+
* cannot be cleared after uninstall — `clearWatch(id)` on the restored native
|
|
304
|
+
* `navigator.geolocation` uses a different id space, so the SDK subscription
|
|
305
|
+
* leaks. Consumers should `clearWatch` all outstanding ids before calling
|
|
306
|
+
* `uninstall()`.
|
|
307
|
+
*/
|
|
308
|
+
const BACKUP_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.original");
|
|
309
|
+
const SNAPSHOT_KEY$1 = Symbol.for("@ait-co/polyfill/geolocation.snapshot");
|
|
310
|
+
const ACCURACY_BALANCED = 3;
|
|
311
|
+
const ACCURACY_HIGH = 4;
|
|
312
|
+
function toStandardPosition(sdk) {
|
|
313
|
+
const coordsData = {
|
|
314
|
+
latitude: sdk.coords.latitude,
|
|
315
|
+
longitude: sdk.coords.longitude,
|
|
316
|
+
altitude: sdk.coords.altitude,
|
|
317
|
+
accuracy: sdk.coords.accuracy,
|
|
318
|
+
altitudeAccuracy: sdk.coords.altitudeAccuracy,
|
|
319
|
+
heading: sdk.coords.heading,
|
|
320
|
+
speed: null
|
|
321
|
+
};
|
|
322
|
+
return {
|
|
323
|
+
coords: {
|
|
324
|
+
...coordsData,
|
|
325
|
+
toJSON() {
|
|
326
|
+
return { ...coordsData };
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
timestamp: sdk.timestamp,
|
|
330
|
+
toJSON() {
|
|
331
|
+
return {
|
|
332
|
+
coords: { ...coordsData },
|
|
333
|
+
timestamp: sdk.timestamp
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function toPositionError(code, message) {
|
|
339
|
+
const Ctor = globalThis.GeolocationPositionError;
|
|
340
|
+
if (typeof Ctor === "function") {
|
|
341
|
+
const proto = Ctor.prototype;
|
|
342
|
+
if (proto) {
|
|
343
|
+
const shape = {
|
|
344
|
+
code,
|
|
345
|
+
message
|
|
346
|
+
};
|
|
347
|
+
Object.setPrototypeOf(shape, proto);
|
|
348
|
+
return shape;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
code,
|
|
353
|
+
message,
|
|
354
|
+
PERMISSION_DENIED: 1,
|
|
355
|
+
POSITION_UNAVAILABLE: 2,
|
|
356
|
+
TIMEOUT: 3
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function accuracyFromOptions(options) {
|
|
360
|
+
return options?.enableHighAccuracy ? ACCURACY_HIGH : ACCURACY_BALANCED;
|
|
361
|
+
}
|
|
362
|
+
function createGeolocationShim(fallback) {
|
|
363
|
+
let nextWatchId = 1;
|
|
364
|
+
const sdkWatches = /* @__PURE__ */ new Map();
|
|
365
|
+
const nativeWatches = /* @__PURE__ */ new Map();
|
|
366
|
+
const pendingWatches = /* @__PURE__ */ new Map();
|
|
367
|
+
return {
|
|
368
|
+
getCurrentPosition(success, error, options) {
|
|
369
|
+
(async () => {
|
|
370
|
+
if (await isTossEnvironment()) {
|
|
371
|
+
const fn = (await loadTossSdk())?.getCurrentLocation;
|
|
372
|
+
if (typeof fn === "function") {
|
|
373
|
+
try {
|
|
374
|
+
success(toStandardPosition(await fn({ accuracy: accuracyFromOptions(options) })));
|
|
375
|
+
} catch (e) {
|
|
376
|
+
error?.(toPositionError(2, e instanceof Error ? e.message : "[@ait-co/polyfill] getCurrentLocation failed."));
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!fallback.getCurrentPosition) {
|
|
382
|
+
error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
fallback.getCurrentPosition(success, error, options);
|
|
386
|
+
})();
|
|
387
|
+
},
|
|
388
|
+
watchPosition(success, error, options) {
|
|
389
|
+
const id = nextWatchId++;
|
|
390
|
+
const pending = { cancelled: false };
|
|
391
|
+
pendingWatches.set(id, pending);
|
|
392
|
+
(async () => {
|
|
393
|
+
if (await isTossEnvironment()) {
|
|
394
|
+
const fn = (await loadTossSdk())?.startUpdateLocation;
|
|
395
|
+
if (typeof fn === "function") {
|
|
396
|
+
if (pending.cancelled) {
|
|
397
|
+
pendingWatches.delete(id);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const unsubscribe = fn({
|
|
401
|
+
onEvent: (loc) => success(toStandardPosition(loc)),
|
|
402
|
+
onError: (err) => error?.(toPositionError(2, err instanceof Error ? err.message : "[@ait-co/polyfill] startUpdateLocation failed.")),
|
|
403
|
+
options: {
|
|
404
|
+
accuracy: accuracyFromOptions(options),
|
|
405
|
+
timeInterval: 1e3,
|
|
406
|
+
distanceInterval: 0
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
if (pending.cancelled) {
|
|
410
|
+
unsubscribe();
|
|
411
|
+
pendingWatches.delete(id);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
sdkWatches.set(id, unsubscribe);
|
|
415
|
+
pendingWatches.delete(id);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!fallback.watchPosition) {
|
|
420
|
+
pendingWatches.delete(id);
|
|
421
|
+
error?.(toPositionError(2, "[@ait-co/polyfill] navigator.geolocation is not available in this environment."));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (pending.cancelled) {
|
|
425
|
+
pendingWatches.delete(id);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const nativeId = fallback.watchPosition(success, error, options);
|
|
429
|
+
if (pending.cancelled) {
|
|
430
|
+
fallback.clearWatch?.(nativeId);
|
|
431
|
+
pendingWatches.delete(id);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
nativeWatches.set(id, nativeId);
|
|
435
|
+
pendingWatches.delete(id);
|
|
436
|
+
})();
|
|
437
|
+
return id;
|
|
438
|
+
},
|
|
439
|
+
clearWatch(id) {
|
|
440
|
+
const pending = pendingWatches.get(id);
|
|
441
|
+
if (pending) {
|
|
442
|
+
pending.cancelled = true;
|
|
443
|
+
pendingWatches.delete(id);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const unsubscribe = sdkWatches.get(id);
|
|
447
|
+
if (unsubscribe) {
|
|
448
|
+
unsubscribe();
|
|
449
|
+
sdkWatches.delete(id);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const nativeId = nativeWatches.get(id);
|
|
453
|
+
if (nativeId !== void 0 && fallback.clearWatch) {
|
|
454
|
+
fallback.clearWatch(nativeId);
|
|
455
|
+
nativeWatches.delete(id);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function installGeolocationShim() {
|
|
461
|
+
if (typeof navigator === "undefined") return () => {};
|
|
462
|
+
const host = navigator;
|
|
463
|
+
if (BACKUP_KEY$1 in host) return () => uninstallGeolocationShim();
|
|
464
|
+
const target = navigator.geolocation;
|
|
465
|
+
if (!target) {
|
|
466
|
+
host[BACKUP_KEY$1] = void 0;
|
|
467
|
+
return () => uninstallGeolocationShim();
|
|
468
|
+
}
|
|
469
|
+
const shim = createGeolocationShim({
|
|
470
|
+
getCurrentPosition: target.getCurrentPosition?.bind(target),
|
|
471
|
+
watchPosition: target.watchPosition?.bind(target),
|
|
472
|
+
clearWatch: target.clearWatch?.bind(target)
|
|
473
|
+
});
|
|
474
|
+
const snapshot = installObjectMethods(target, {
|
|
475
|
+
getCurrentPosition: shim.getCurrentPosition,
|
|
476
|
+
watchPosition: shim.watchPosition,
|
|
477
|
+
clearWatch: shim.clearWatch
|
|
478
|
+
});
|
|
479
|
+
if (!snapshot) {
|
|
480
|
+
host[BACKUP_KEY$1] = void 0;
|
|
481
|
+
return () => uninstallGeolocationShim();
|
|
482
|
+
}
|
|
483
|
+
host[BACKUP_KEY$1] = { target };
|
|
484
|
+
host[SNAPSHOT_KEY$1] = snapshot;
|
|
485
|
+
return uninstallGeolocationShim;
|
|
486
|
+
}
|
|
487
|
+
function uninstallGeolocationShim() {
|
|
488
|
+
if (typeof navigator === "undefined") return;
|
|
489
|
+
const host = navigator;
|
|
490
|
+
if (!(BACKUP_KEY$1 in host)) return;
|
|
491
|
+
const snapshot = host[SNAPSHOT_KEY$1];
|
|
492
|
+
if (snapshot) restoreObjectMethods(snapshot);
|
|
493
|
+
delete host[BACKUP_KEY$1];
|
|
494
|
+
delete host[SNAPSHOT_KEY$1];
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/shims/network.ts
|
|
498
|
+
/**
|
|
499
|
+
* `navigator.onLine` + `navigator.connection` shim.
|
|
500
|
+
*
|
|
501
|
+
* Inside Apps in Toss → seeded from SDK `getNetworkStatus()` on install and
|
|
502
|
+
* refreshed on read (throttled):
|
|
503
|
+
* - `'OFFLINE'` → `onLine = false`
|
|
504
|
+
* - `'WIFI'` → `onLine = true`, `effectiveType = '4g'` (no web wifi value)
|
|
505
|
+
* - `'2G'/'3G'/'4G'/'5G'` → `onLine = true`, `effectiveType = <lowercased>`
|
|
506
|
+
* - `'WWAN'/'UNKNOWN'` → `onLine = true`, `effectiveType = '4g'` (best guess)
|
|
507
|
+
*
|
|
508
|
+
* Outside Apps in Toss → both `navigator.onLine` and `navigator.connection`
|
|
509
|
+
* read through to the native value. Install installs own-instance getters
|
|
510
|
+
* that consult the Toss-seeded cache first; when the cache is empty (which
|
|
511
|
+
* it always is in browser mode), the getter temporarily removes its own
|
|
512
|
+
* shadow, reads the prototype value, and reinstates the shadow.
|
|
513
|
+
*
|
|
514
|
+
* Uninstall `delete`s the instance-level override so the prototype descriptor
|
|
515
|
+
* (where `onLine` and `connection` actually live in real browsers) becomes
|
|
516
|
+
* visible again. We never mutate the prototype — doing so would throw in
|
|
517
|
+
* browsers where the descriptor is non-configurable.
|
|
518
|
+
*
|
|
519
|
+
* Browser-compat caveat (Chromium): `navigator.onLine` and `navigator.connection`
|
|
520
|
+
* are value slots, not methods, so the method-level install trick we use for
|
|
521
|
+
* `geolocation`/`share`/`vibrate` does not apply here. When Chromium marks the
|
|
522
|
+
* instance descriptor as non-configurable AND the prototype descriptor is also
|
|
523
|
+
* non-configurable, we cannot install. In that case the shim logs a one-time
|
|
524
|
+
* `console.warn` and leaves the native values in place — consumers keep the
|
|
525
|
+
* browser's own `onLine`/`connection` values; the SDK-synced state is simply
|
|
526
|
+
* disabled for that session.
|
|
527
|
+
*
|
|
528
|
+
* Caveat: the Web NetworkInformation API is evented (`change` fires on
|
|
529
|
+
* transitions). The SDK exposes only a one-shot query, so listeners attached
|
|
530
|
+
* to `navigator.connection` are accepted but never fire from a `change` event
|
|
531
|
+
* unless the shim observes a real status transition. Synthesising richer
|
|
532
|
+
* events via polling is tracked in TODO.md.
|
|
533
|
+
*
|
|
534
|
+
* Lifecycle: `navigator.connection` is a ShimConnection instance that lives in
|
|
535
|
+
* the install closure. On uninstall the instance-level override is removed,
|
|
536
|
+
* but listeners the consumer attached to the old instance stay bound to that
|
|
537
|
+
* (now-orphan) object and will not see events from a subsequent install.
|
|
538
|
+
* Consumers should re-attach listeners after each install.
|
|
539
|
+
*
|
|
540
|
+
* Seed-boundary race: in Toss mode, reads before the install-time SDK seed
|
|
541
|
+
* completes fall through to the native `navigator.connection`. After the seed
|
|
542
|
+
* lands, subsequent reads return the shim's ShimConnection. Consumers that
|
|
543
|
+
* specifically need the ShimConnection instance (e.g., to attach `change`
|
|
544
|
+
* listeners that fire on Toss network transitions) should wait a microtask
|
|
545
|
+
* after `install()` before attaching listeners, or accept that pre-seed
|
|
546
|
+
* reads may return the native object.
|
|
547
|
+
*/
|
|
548
|
+
const INSTALLED_KEY = Symbol.for("@ait-co/polyfill/network.installed");
|
|
549
|
+
const ON_LINE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.onLine.snapshot");
|
|
550
|
+
const CONNECTION_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/network.connection.snapshot");
|
|
551
|
+
const REFRESH_THROTTLE_MS = 500;
|
|
552
|
+
function statusToOnline(status) {
|
|
553
|
+
return status !== "OFFLINE";
|
|
554
|
+
}
|
|
555
|
+
function statusToEffectiveType(status) {
|
|
556
|
+
switch (status) {
|
|
557
|
+
case "2G": return "2g";
|
|
558
|
+
case "3G": return "3g";
|
|
559
|
+
default: return "4g";
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function statusToConnectionType(status) {
|
|
563
|
+
switch (status) {
|
|
564
|
+
case "WIFI": return "wifi";
|
|
565
|
+
case "2G":
|
|
566
|
+
case "3G":
|
|
567
|
+
case "4G":
|
|
568
|
+
case "5G":
|
|
569
|
+
case "WWAN": return "cellular";
|
|
570
|
+
case "OFFLINE": return "none";
|
|
571
|
+
default: return "unknown";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const SET_STATUS = Symbol("@ait-co/polyfill/network.setStatus");
|
|
575
|
+
var ShimConnection = class extends EventTarget {
|
|
576
|
+
#status = null;
|
|
577
|
+
onchange = null;
|
|
578
|
+
constructor() {
|
|
579
|
+
super();
|
|
580
|
+
this.addEventListener("change", (ev) => this.onchange?.call(this, ev));
|
|
581
|
+
}
|
|
582
|
+
[SET_STATUS](next) {
|
|
583
|
+
this.#status = next;
|
|
584
|
+
}
|
|
585
|
+
get effectiveType() {
|
|
586
|
+
return statusToEffectiveType(this.#status ?? "UNKNOWN");
|
|
587
|
+
}
|
|
588
|
+
get downlink() {
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
get rtt() {
|
|
592
|
+
return 0;
|
|
593
|
+
}
|
|
594
|
+
get saveData() {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
get type() {
|
|
598
|
+
return statusToConnectionType(this.#status ?? "UNKNOWN");
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
function installNetworkShim() {
|
|
602
|
+
if (typeof navigator === "undefined") return () => {};
|
|
603
|
+
const host = navigator;
|
|
604
|
+
if (host[INSTALLED_KEY]) return () => uninstallNetworkShim();
|
|
605
|
+
host[INSTALLED_KEY] = true;
|
|
606
|
+
let cachedStatus = null;
|
|
607
|
+
let lastRefresh = 0;
|
|
608
|
+
let inflight = null;
|
|
609
|
+
const connection = new ShimConnection();
|
|
610
|
+
async function refresh() {
|
|
611
|
+
if (inflight) return inflight;
|
|
612
|
+
if (Date.now() - lastRefresh < REFRESH_THROTTLE_MS) return;
|
|
613
|
+
inflight = (async () => {
|
|
614
|
+
try {
|
|
615
|
+
if (!await isTossEnvironment()) return;
|
|
616
|
+
const fn = (await loadTossSdk())?.getNetworkStatus;
|
|
617
|
+
if (typeof fn !== "function") return;
|
|
618
|
+
const next = await fn();
|
|
619
|
+
const prev = cachedStatus;
|
|
620
|
+
cachedStatus = next;
|
|
621
|
+
connection[SET_STATUS](next);
|
|
622
|
+
if (prev !== null && prev !== next) connection.dispatchEvent(new Event("change"));
|
|
623
|
+
} catch {} finally {
|
|
624
|
+
lastRefresh = Date.now();
|
|
625
|
+
inflight = null;
|
|
626
|
+
}
|
|
627
|
+
})();
|
|
628
|
+
return inflight;
|
|
629
|
+
}
|
|
630
|
+
const nativeOnLine = navigator.onLine;
|
|
631
|
+
const nativeConnection = navigator.connection;
|
|
632
|
+
refresh();
|
|
633
|
+
let installWarned = false;
|
|
634
|
+
const warnNonConfigurable = (e) => {
|
|
635
|
+
if (installWarned) return;
|
|
636
|
+
installWarned = true;
|
|
637
|
+
console.warn("[@ait-co/polyfill] navigator.onLine/connection is non-configurable in this browser; Toss network status sync disabled.", e);
|
|
638
|
+
};
|
|
639
|
+
try {
|
|
640
|
+
host[ON_LINE_SNAPSHOT_KEY] = installNavigatorProperty("onLine", {
|
|
641
|
+
configurable: true,
|
|
642
|
+
get() {
|
|
643
|
+
refresh();
|
|
644
|
+
if (cachedStatus !== null) return statusToOnline(cachedStatus);
|
|
645
|
+
return nativeOnLine ?? true;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
} catch (e) {
|
|
649
|
+
warnNonConfigurable(e);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
host[CONNECTION_SNAPSHOT_KEY] = installNavigatorProperty("connection", {
|
|
653
|
+
configurable: true,
|
|
654
|
+
get() {
|
|
655
|
+
refresh();
|
|
656
|
+
if (cachedStatus === null && nativeConnection !== void 0) return nativeConnection;
|
|
657
|
+
return connection;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
} catch (e) {
|
|
661
|
+
warnNonConfigurable(e);
|
|
662
|
+
}
|
|
663
|
+
return uninstallNetworkShim;
|
|
664
|
+
}
|
|
665
|
+
function uninstallNetworkShim() {
|
|
666
|
+
if (typeof navigator === "undefined") return;
|
|
667
|
+
const host = navigator;
|
|
668
|
+
if (!host[INSTALLED_KEY]) return;
|
|
669
|
+
const onLineSnap = host[ON_LINE_SNAPSHOT_KEY];
|
|
670
|
+
if (onLineSnap) restoreNavigatorProperty("onLine", onLineSnap);
|
|
671
|
+
const connSnap = host[CONNECTION_SNAPSHOT_KEY];
|
|
672
|
+
if (connSnap) restoreNavigatorProperty("connection", connSnap);
|
|
673
|
+
delete host[INSTALLED_KEY];
|
|
674
|
+
delete host[ON_LINE_SNAPSHOT_KEY];
|
|
675
|
+
delete host[CONNECTION_SNAPSHOT_KEY];
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/shims/share.ts
|
|
679
|
+
/**
|
|
680
|
+
* `navigator.share` shim.
|
|
681
|
+
*
|
|
682
|
+
* Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only
|
|
683
|
+
* accepts a single `message` string, so we concatenate `title`, `text`, and
|
|
684
|
+
* `url` with newline separators (skipping missing/empty values).
|
|
685
|
+
*
|
|
686
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.share`, or
|
|
687
|
+
* throws `NotSupportedError` if unavailable.
|
|
688
|
+
*
|
|
689
|
+
* Install strategy: **method-level** on `navigator`. Assigning
|
|
690
|
+
* `navigator.share = fn` creates an own property that shadows the prototype
|
|
691
|
+
* method. Uninstall deletes the own shadow so the prototype method surfaces
|
|
692
|
+
* again. We do not mutate `Navigator.prototype` — in real browsers its
|
|
693
|
+
* descriptor may be non-configurable, which would throw on reassignment.
|
|
694
|
+
*
|
|
695
|
+
* Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).
|
|
696
|
+
* `canShare({ files })` returns `false` whenever the sync-accessible detection
|
|
697
|
+
* says Toss is active (or is being forced via the test override).
|
|
698
|
+
*/
|
|
699
|
+
const SHARE_BACKUP_KEY = Symbol.for("@ait-co/polyfill/share.original");
|
|
700
|
+
const SHARE_SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/share.snapshot");
|
|
701
|
+
function buildSdkMessage(data) {
|
|
702
|
+
const parts = [];
|
|
703
|
+
if (data?.title != null && data.title !== "") parts.push(data.title);
|
|
704
|
+
if (data?.text != null && data.text !== "") parts.push(data.text);
|
|
705
|
+
if (data?.url != null && data.url !== "") parts.push(data.url);
|
|
706
|
+
return parts.join("\n");
|
|
707
|
+
}
|
|
708
|
+
async function shareShim(data) {
|
|
709
|
+
if (await isTossEnvironment()) {
|
|
710
|
+
const fn = (await loadTossSdk())?.share;
|
|
711
|
+
if (typeof fn === "function") {
|
|
712
|
+
const message = buildSdkMessage(data);
|
|
713
|
+
if (!message) throw new TypeError("[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.");
|
|
714
|
+
try {
|
|
715
|
+
await fn({ message });
|
|
716
|
+
} catch (e) {
|
|
717
|
+
const message_ = e instanceof Error ? e.message : String(e);
|
|
718
|
+
const wrapped = new DOMException(message_, "AbortError");
|
|
719
|
+
if (e instanceof Error) wrapped.cause = e;
|
|
720
|
+
throw wrapped;
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const original = navigator[SHARE_BACKUP_KEY]?.share;
|
|
726
|
+
if (!original) throw new DOMException("[@ait-co/polyfill] navigator.share is not available in this environment.", "NotSupportedError");
|
|
727
|
+
return original(data);
|
|
728
|
+
}
|
|
729
|
+
function canShareShim(data) {
|
|
730
|
+
const hasFiles = Boolean(data?.files && data.files.length > 0);
|
|
731
|
+
const toss = isTossEnvironmentCached();
|
|
732
|
+
if (hasFiles) {
|
|
733
|
+
if (toss === true) return false;
|
|
734
|
+
if (toss === void 0) return false;
|
|
735
|
+
}
|
|
736
|
+
if (toss === true) return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
|
|
737
|
+
const originalCanShare = navigator[SHARE_BACKUP_KEY]?.canShare;
|
|
738
|
+
if (originalCanShare) return originalCanShare(data);
|
|
739
|
+
return Boolean(data?.title != null && data.title !== "" || data?.text != null && data.text !== "" || data?.url != null && data.url !== "");
|
|
740
|
+
}
|
|
741
|
+
function installShareShim() {
|
|
742
|
+
if (typeof navigator === "undefined") return () => {};
|
|
743
|
+
const host = navigator;
|
|
744
|
+
if (SHARE_BACKUP_KEY in host) return () => uninstallShareShim();
|
|
745
|
+
const nav = navigator;
|
|
746
|
+
host[SHARE_BACKUP_KEY] = {
|
|
747
|
+
share: nav.share ? nav.share.bind(navigator) : void 0,
|
|
748
|
+
canShare: nav.canShare ? nav.canShare.bind(navigator) : void 0
|
|
749
|
+
};
|
|
750
|
+
const snapshot = installObjectMethods(navigator, {
|
|
751
|
+
share: shareShim,
|
|
752
|
+
canShare: canShareShim
|
|
753
|
+
});
|
|
754
|
+
if (!snapshot) {
|
|
755
|
+
delete host[SHARE_BACKUP_KEY];
|
|
756
|
+
return () => uninstallShareShim();
|
|
757
|
+
}
|
|
758
|
+
host[SHARE_SNAPSHOT_KEY] = snapshot;
|
|
759
|
+
return uninstallShareShim;
|
|
760
|
+
}
|
|
761
|
+
function uninstallShareShim() {
|
|
762
|
+
if (typeof navigator === "undefined") return;
|
|
763
|
+
const host = navigator;
|
|
764
|
+
if (!(SHARE_BACKUP_KEY in host)) return;
|
|
765
|
+
const snapshot = host[SHARE_SNAPSHOT_KEY];
|
|
766
|
+
if (snapshot) restoreObjectMethods(snapshot);
|
|
767
|
+
delete host[SHARE_BACKUP_KEY];
|
|
768
|
+
delete host[SHARE_SNAPSHOT_KEY];
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/shims/vibrate.ts
|
|
772
|
+
/**
|
|
773
|
+
* `navigator.vibrate` shim.
|
|
774
|
+
*
|
|
775
|
+
* Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`.
|
|
776
|
+
* Single-duration calls land in three buckets so the qualitative SDK haptic
|
|
777
|
+
* tracks intensity a little more closely than the original two-bucket split:
|
|
778
|
+
* - `vibrate(0)` → no-op (web standard: cancels pending vibration)
|
|
779
|
+
* - `vibrate(1..20ms)` → `tickWeak`
|
|
780
|
+
* - `vibrate(21..45ms)` → `tickMedium`
|
|
781
|
+
* - `vibrate(>=46ms)` → `basicMedium`
|
|
782
|
+
* - `vibrate(number[])` → iterates "on" segments (even indices) as `tap`
|
|
783
|
+
* pulses with `setTimeout` gaps. Per-element ms mapping is intentionally
|
|
784
|
+
* skipped: arrays in the wild are mostly rhythmic patterns, and the SDK
|
|
785
|
+
* has no "stronger heavy" variant to reach for, so per-pulse precision
|
|
786
|
+
* buys little. Callers needing intent should use `vibrateSemantic`.
|
|
787
|
+
*
|
|
788
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
|
|
789
|
+
* or returns `false` when unavailable (matches the spec — browsers that don't
|
|
790
|
+
* support vibration simply return `false`).
|
|
791
|
+
*
|
|
792
|
+
* Install strategy: **method-level** on `navigator`. We assign our wrapper to
|
|
793
|
+
* `navigator.vibrate` (creating an own shadow over the prototype method) and
|
|
794
|
+
* delete it on uninstall so the prototype re-surfaces. We do not mutate
|
|
795
|
+
* `Navigator.prototype` itself — browsers may mark it non-configurable.
|
|
796
|
+
*
|
|
797
|
+
* Caveats (documented in CLAUDE.md as the known lossy trade-off):
|
|
798
|
+
* - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
|
|
799
|
+
* durations. The shim approximates intensity from duration but cannot
|
|
800
|
+
* reproduce exact patterns. Length-only mapping cannot recover semantic
|
|
801
|
+
* intent (success vs. error vs. warning); use `vibrateSemantic` for that.
|
|
802
|
+
* - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
|
|
803
|
+
* honoured only as "time until the next tap", not as silent-vs-vibrating.
|
|
804
|
+
* - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
|
|
805
|
+
* `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
|
|
806
|
+
*/
|
|
807
|
+
const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
|
|
808
|
+
const SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/vibrate.snapshot");
|
|
809
|
+
const TICK_WEAK_MAX_MS = 20;
|
|
810
|
+
const TICK_MEDIUM_MAX_MS = 45;
|
|
811
|
+
async function haptic(type) {
|
|
812
|
+
const fn = (await loadTossSdk())?.generateHapticFeedback;
|
|
813
|
+
if (typeof fn === "function") try {
|
|
814
|
+
await fn({ type });
|
|
815
|
+
} catch {}
|
|
816
|
+
}
|
|
817
|
+
function durationToHaptic(duration) {
|
|
818
|
+
if (duration <= TICK_WEAK_MAX_MS) return "tickWeak";
|
|
819
|
+
if (duration <= TICK_MEDIUM_MAX_MS) return "tickMedium";
|
|
820
|
+
return "basicMedium";
|
|
821
|
+
}
|
|
822
|
+
function vibrateShim(pattern) {
|
|
823
|
+
const arr = Array.isArray(pattern) ? pattern : [pattern];
|
|
824
|
+
if (arr.length === 0 || arr.every((n) => n === 0)) {
|
|
825
|
+
(async () => {
|
|
826
|
+
if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.(pattern);
|
|
827
|
+
})();
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
(async () => {
|
|
831
|
+
if (await isTossEnvironment()) {
|
|
832
|
+
if (!Array.isArray(pattern)) {
|
|
833
|
+
await haptic(durationToHaptic(pattern));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
for (let i = 0; i < pattern.length; i += 2) {
|
|
837
|
+
const on = pattern[i];
|
|
838
|
+
if (on === void 0) break;
|
|
839
|
+
if (on > 0) await haptic("tap");
|
|
840
|
+
const pause = pattern[i + 1];
|
|
841
|
+
if (typeof pause === "number" && pause > 0) await new Promise((r) => setTimeout(r, pause));
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const original = navigator[BACKUP_KEY];
|
|
846
|
+
original?.(pattern);
|
|
847
|
+
})();
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
function installVibrateShim() {
|
|
851
|
+
if (typeof navigator === "undefined") return () => {};
|
|
852
|
+
const host = navigator;
|
|
853
|
+
if (BACKUP_KEY in host) return () => uninstallVibrateShim();
|
|
854
|
+
const nav = navigator;
|
|
855
|
+
host[BACKUP_KEY] = nav.vibrate ? nav.vibrate.bind(navigator) : void 0;
|
|
856
|
+
const snapshot = installObjectMethods(navigator, { vibrate: vibrateShim });
|
|
857
|
+
if (!snapshot) {
|
|
858
|
+
delete host[BACKUP_KEY];
|
|
859
|
+
return () => uninstallVibrateShim();
|
|
860
|
+
}
|
|
861
|
+
host[SNAPSHOT_KEY] = snapshot;
|
|
862
|
+
return uninstallVibrateShim;
|
|
863
|
+
}
|
|
864
|
+
function uninstallVibrateShim() {
|
|
865
|
+
if (typeof navigator === "undefined") return;
|
|
866
|
+
const host = navigator;
|
|
867
|
+
if (!(BACKUP_KEY in host)) return;
|
|
868
|
+
const snapshot = host[SNAPSHOT_KEY];
|
|
869
|
+
if (snapshot) restoreObjectMethods(snapshot);
|
|
870
|
+
delete host[BACKUP_KEY];
|
|
871
|
+
delete host[SNAPSHOT_KEY];
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/shims/vibrate-semantic.ts
|
|
875
|
+
/**
|
|
876
|
+
* Semantic haptic helper — escape hatch for callers who know their intent
|
|
877
|
+
* (`success`, `error`, `warning`, `selection`) and don't want to encode it as
|
|
878
|
+
* a millisecond pattern.
|
|
879
|
+
*
|
|
880
|
+
* `navigator.vibrate(pattern)` is duration-only by spec, so the polyfill's
|
|
881
|
+
* length-based mapping cannot recover semantic intent. This helper sits next
|
|
882
|
+
* to it: importable from `@ait-co/polyfill/vibrate-semantic`, no install, no
|
|
883
|
+
* `navigator` mutation. Inside Apps in Toss it routes to the SDK's haptic
|
|
884
|
+
* variants; outside Toss it falls back to a short `navigator.vibrate(...)`
|
|
885
|
+
* call so the browser at least produces *some* feedback.
|
|
886
|
+
*
|
|
887
|
+
* Why a sub-path and not an extension to `navigator.vibrate`:
|
|
888
|
+
* - The standard signature stays untouched (no smuggling of non-standard
|
|
889
|
+
* argument shapes through `navigator.vibrate`).
|
|
890
|
+
* - `sideEffects: ["./dist/auto.js"]` keeps this drop-if-unused for bundlers.
|
|
891
|
+
*
|
|
892
|
+
* Returns `true` when the request was dispatched (Toss SDK call queued, or
|
|
893
|
+
* native vibrate accepted the fallback) and `false` when no haptic surface is
|
|
894
|
+
* available — mirroring `navigator.vibrate`'s "supported/triggered" boolean.
|
|
895
|
+
*/
|
|
896
|
+
const INTENT_TO_HAPTIC = {
|
|
897
|
+
success: "success",
|
|
898
|
+
error: "error",
|
|
899
|
+
warning: "tickMedium",
|
|
900
|
+
selection: "tickWeak"
|
|
901
|
+
};
|
|
902
|
+
const INTENT_TO_FALLBACK_MS = {
|
|
903
|
+
success: 30,
|
|
904
|
+
error: 60,
|
|
905
|
+
warning: 25,
|
|
906
|
+
selection: 10
|
|
907
|
+
};
|
|
908
|
+
function vibrateSemantic(intent) {
|
|
909
|
+
const sdkType = INTENT_TO_HAPTIC[intent];
|
|
910
|
+
const hasNativeVibrate = typeof navigator !== "undefined" && typeof navigator.vibrate === "function";
|
|
911
|
+
(async () => {
|
|
912
|
+
if (await isTossEnvironment()) {
|
|
913
|
+
await haptic(sdkType);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (hasNativeVibrate) navigator.vibrate(INTENT_TO_FALLBACK_MS[intent]);
|
|
917
|
+
})();
|
|
918
|
+
if (hasNativeVibrate) return true;
|
|
919
|
+
return globalThis.__AIT_POLYFILL_FORCE__ === "toss";
|
|
920
|
+
}
|
|
921
|
+
//#endregion
|
|
922
|
+
//#region src/index.ts
|
|
923
|
+
const VERSION = "0.1.6";
|
|
924
|
+
const NOOP = () => {};
|
|
925
|
+
/**
|
|
926
|
+
* Install every shim this library ships, but only if we detect an Apps in
|
|
927
|
+
* Toss runtime. In a plain browser `install()` is a no-op — the browser's
|
|
928
|
+
* native APIs stay untouched.
|
|
929
|
+
*
|
|
930
|
+
* Returns a promise that resolves with an uninstall function. If the
|
|
931
|
+
* environment turns out not to be Toss, the uninstall function is a no-op.
|
|
932
|
+
*
|
|
933
|
+
* Install order (when active): clipboard → geolocation → share → vibrate →
|
|
934
|
+
* network. Not atomic on failure — if a per-shim install throws (e.g., a
|
|
935
|
+
* consumer pinned a target navigator property as non-configurable), earlier
|
|
936
|
+
* shims are already in place. Callers should catch and invoke the returned
|
|
937
|
+
* uninstall to roll back.
|
|
938
|
+
*/
|
|
939
|
+
async function install() {
|
|
940
|
+
if (!await isTossEnvironment()) return NOOP;
|
|
941
|
+
const uninstalls = [
|
|
942
|
+
installClipboardShim(),
|
|
943
|
+
installGeolocationShim(),
|
|
944
|
+
installShareShim(),
|
|
945
|
+
installVibrateShim(),
|
|
946
|
+
installNetworkShim()
|
|
947
|
+
];
|
|
948
|
+
return () => {
|
|
949
|
+
for (const fn of uninstalls) fn();
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Uninstall every shim installed by `install()`. Safe to call when no shim is
|
|
954
|
+
* installed — each installer's uninstall is a no-op in that case.
|
|
955
|
+
*/
|
|
956
|
+
function uninstall() {
|
|
957
|
+
uninstallClipboardShim();
|
|
958
|
+
uninstallGeolocationShim();
|
|
959
|
+
uninstallShareShim();
|
|
960
|
+
uninstallVibrateShim();
|
|
961
|
+
uninstallNetworkShim();
|
|
962
|
+
}
|
|
963
|
+
//#endregion
|
|
964
|
+
exports.VERSION = VERSION;
|
|
965
|
+
exports.install = install;
|
|
966
|
+
exports.installClipboardShim = installClipboardShim;
|
|
967
|
+
exports.installGeolocationShim = installGeolocationShim;
|
|
968
|
+
exports.installNetworkShim = installNetworkShim;
|
|
969
|
+
exports.installShareShim = installShareShim;
|
|
970
|
+
exports.installVibrateShim = installVibrateShim;
|
|
971
|
+
exports.isTossEnvironment = isTossEnvironment;
|
|
972
|
+
exports.isTossEnvironmentCached = isTossEnvironmentCached;
|
|
973
|
+
exports.loadTossSdk = loadTossSdk;
|
|
974
|
+
exports.uninstall = uninstall;
|
|
975
|
+
exports.uninstallClipboardShim = uninstallClipboardShim;
|
|
976
|
+
exports.uninstallGeolocationShim = uninstallGeolocationShim;
|
|
977
|
+
exports.uninstallNetworkShim = uninstallNetworkShim;
|
|
978
|
+
exports.uninstallShareShim = uninstallShareShim;
|
|
979
|
+
exports.uninstallVibrateShim = uninstallVibrateShim;
|
|
980
|
+
exports.vibrateSemantic = vibrateSemantic;
|
|
981
|
+
|
|
982
|
+
//# sourceMappingURL=index.cjs.map
|