@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.
@@ -0,0 +1,135 @@
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
+ * Returns `true` iff we detect we are running in an environment where the
17
+ * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
18
+ *
19
+ * Async because we use dynamic `import()` to probe the optional peer dep
20
+ * without forcing it into the consumer's bundle.
21
+ */
22
+ async function isTossEnvironment() {
23
+ const force = globalThis.__AIT_POLYFILL_FORCE__;
24
+ if (force === "toss") return true;
25
+ if (force === "browser") return false;
26
+ if (cached !== void 0) return cached;
27
+ cached = typeof (await loadTossSdk())?.getClipboardText === "function";
28
+ return cached;
29
+ }
30
+ /**
31
+ * Lazy SDK accessor — returns the module if available, else `null`. Callers
32
+ * are expected to `await` and null-check. Never throws.
33
+ */
34
+ async function loadTossSdk() {
35
+ try {
36
+ return await import("@apps-in-toss/web-framework");
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ //#endregion
42
+ //#region src/shims/vibrate.ts
43
+ /**
44
+ * `navigator.vibrate` shim.
45
+ *
46
+ * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`:
47
+ * - `vibrate(0)` → no-op (web standard: cancels pending vibration)
48
+ * - `vibrate(number)`: short (< 40ms) → `tickWeak`, long (≥ 40ms) → `basicMedium`
49
+ * - `vibrate(number[])`: iterate "on" segments (even indices) as `tap` pulses
50
+ *
51
+ * Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
52
+ * or returns `false` when unavailable (matches the spec — browsers that don't
53
+ * support vibration simply return `false`).
54
+ *
55
+ * Caveats (documented in CLAUDE.md as the known lossy trade-off):
56
+ * - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
57
+ * durations. The shim approximates intensity from duration but cannot
58
+ * reproduce exact patterns.
59
+ * - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
60
+ * honoured only as "time until the next tap", not as silent-vs-vibrating.
61
+ * - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
62
+ * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
63
+ */
64
+ const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
65
+ const HAD_KEY = Symbol.for("@ait-co/polyfill/vibrate.hadOriginal");
66
+ const SHORT_VIBRATION_MS = 40;
67
+ async function haptic(type) {
68
+ const fn = (await loadTossSdk())?.generateHapticFeedback;
69
+ if (typeof fn === "function") try {
70
+ await fn({ type });
71
+ } catch {}
72
+ }
73
+ function durationToHaptic(duration) {
74
+ return duration < SHORT_VIBRATION_MS ? "tickWeak" : "basicMedium";
75
+ }
76
+ function vibrateShim(pattern) {
77
+ const arr = Array.isArray(pattern) ? pattern : [pattern];
78
+ if (arr.length === 0 || arr.every((n) => n === 0)) {
79
+ (async () => {
80
+ if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.call(navigator, pattern);
81
+ })();
82
+ return true;
83
+ }
84
+ (async () => {
85
+ if (await isTossEnvironment()) {
86
+ if (!Array.isArray(pattern)) {
87
+ await haptic(durationToHaptic(pattern));
88
+ return;
89
+ }
90
+ for (let i = 0; i < pattern.length; i += 2) {
91
+ const on = pattern[i];
92
+ if (on === void 0) break;
93
+ if (on > 0) await haptic("tap");
94
+ const pause = pattern[i + 1];
95
+ if (typeof pause === "number" && pause > 0) await new Promise((r) => setTimeout(r, pause));
96
+ }
97
+ return;
98
+ }
99
+ navigator[BACKUP_KEY]?.call(navigator, pattern);
100
+ })();
101
+ return true;
102
+ }
103
+ function installVibrateShim() {
104
+ if (typeof navigator === "undefined") return () => {};
105
+ const host = navigator;
106
+ if (BACKUP_KEY in host) return () => uninstallVibrateShim();
107
+ const nav = navigator;
108
+ host[BACKUP_KEY] = nav.vibrate;
109
+ host[HAD_KEY] = "vibrate" in nav;
110
+ Object.defineProperty(navigator, "vibrate", {
111
+ value: vibrateShim,
112
+ configurable: true,
113
+ writable: true
114
+ });
115
+ return uninstallVibrateShim;
116
+ }
117
+ function uninstallVibrateShim() {
118
+ if (typeof navigator === "undefined") return;
119
+ const host = navigator;
120
+ if (!(BACKUP_KEY in host)) return;
121
+ const original = host[BACKUP_KEY];
122
+ const had = host[HAD_KEY];
123
+ delete navigator.vibrate;
124
+ if (had && navigator.vibrate !== original) Object.defineProperty(navigator, "vibrate", {
125
+ value: original,
126
+ configurable: true,
127
+ writable: true
128
+ });
129
+ delete host[BACKUP_KEY];
130
+ delete host[HAD_KEY];
131
+ }
132
+ //#endregion
133
+ export { installVibrateShim, uninstallVibrateShim };
134
+
135
+ //# sourceMappingURL=vibrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vibrate.js","names":[],"sources":["../../src/detect.ts","../../src/shims/vibrate.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: feature-sniff `@apps-in-toss/web-framework`. The SDK is declared as\n * an **optional** peer dependency. If it resolves and exposes a known export,\n * we assume we can route calls through it; otherwise we fall back to the\n * browser's native implementation in each shim.\n *\n * We deliberately avoid UA sniffing (spoofable) and avoid calling any SDK\n * function during detection (could prompt permission dialogs, fire analytics,\n * etc.).\n */\n\nlet cached: boolean | undefined;\n\n/**\n * Reset the cached detection result. Primarily for tests.\n */\nexport function resetDetection(): void {\n cached = undefined;\n}\n\n/**\n * Synchronous read of the cached detection result. Returns:\n * - `true` / `false` if an override is active or the async detection has\n * already resolved\n * - `undefined` if detection hasn't run yet\n *\n * Used by spec-sync APIs (e.g. `navigator.canShare`) that can't `await`\n * detection.\n */\nexport function isTossEnvironmentCached(): boolean | undefined {\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n return cached;\n}\n\n/**\n * Returns `true` iff we detect we are running in an environment where the\n * Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.\n *\n * Async because we use dynamic `import()` to probe the optional peer dep\n * without forcing it into the consumer's bundle.\n */\nexport async function isTossEnvironment(): Promise<boolean> {\n // Override check precedes cache so `devtools` / tests can flip the result\n // mid-session without a `resetDetection()` call.\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n if (force === 'toss') return true;\n if (force === 'browser') return false;\n\n if (cached !== undefined) return cached;\n\n const mod = await loadTossSdk();\n // Presence of a well-known export is our smoke test.\n cached = typeof mod?.getClipboardText === 'function';\n return cached;\n}\n\n/**\n * Lazy SDK accessor — returns the module if available, else `null`. Callers\n * are expected to `await` and null-check. Never throws.\n */\nexport async function loadTossSdk(): Promise<typeof import('@apps-in-toss/web-framework') | null> {\n try {\n return await import('@apps-in-toss/web-framework');\n } catch {\n return null;\n }\n}\n","/**\n * `navigator.vibrate` shim.\n *\n * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`:\n * - `vibrate(0)` → no-op (web standard: cancels pending vibration)\n * - `vibrate(number)`: short (< 40ms) → `tickWeak`, long (≥ 40ms) → `basicMedium`\n * - `vibrate(number[])`: iterate \"on\" segments (even indices) as `tap` pulses\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,\n * or returns `false` when unavailable (matches the spec — browsers that don't\n * support vibration simply return `false`).\n *\n * Caveats (documented in CLAUDE.md as the known lossy trade-off):\n * - SDK haptics are qualitative (\"tickWeak\", \"basicMedium\"), not millisecond\n * durations. The shim approximates intensity from duration but cannot\n * reproduce exact patterns.\n * - Arrays are fired sequentially via `setTimeout`; gaps between pulses are\n * honoured only as \"time until the next tap\", not as silent-vs-vibrating.\n * - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return\n * `true` immediately (fire-and-forget). Errors from the SDK are swallowed.\n */\n\nimport { isTossEnvironment, loadTossSdk } from '../detect.js';\n\nconst BACKUP_KEY = Symbol.for('@ait-co/polyfill/vibrate.original');\nconst HAD_KEY = Symbol.for('@ait-co/polyfill/vibrate.hadOriginal');\n\ninterface BackupHost {\n [BACKUP_KEY]?: ((pattern: VibratePattern) => boolean) | undefined;\n [HAD_KEY]?: boolean;\n}\n\nconst SHORT_VIBRATION_MS = 40;\n\ntype HapticType =\n | 'tickWeak'\n | 'tap'\n | 'tickMedium'\n | 'softMedium'\n | 'basicWeak'\n | 'basicMedium'\n | 'success'\n | 'error'\n | 'wiggle'\n | 'confetti';\n\nasync function haptic(type: HapticType): Promise<void> {\n const sdk = await loadTossSdk();\n const fn = (sdk as { generateHapticFeedback?: unknown } | null)?.generateHapticFeedback;\n if (typeof fn === 'function') {\n try {\n await (fn as (o: { type: HapticType }) => Promise<void>)({ type });\n } catch {\n // Best-effort; spec-level `vibrate` cannot surface errors.\n }\n }\n}\n\nfunction durationToHaptic(duration: number): HapticType {\n return duration < SHORT_VIBRATION_MS ? 'tickWeak' : 'basicMedium';\n}\n\nfunction vibrateShim(pattern: VibratePattern): boolean {\n // Matches the spec: `vibrate(0)` or `vibrate([])` cancels pending vibration.\n // We can't cancel an in-flight SDK haptic (no cancel API), but we still\n // forward the cancel to the browser fallback so native vibration stops.\n const arr = Array.isArray(pattern) ? pattern : [pattern];\n if (arr.length === 0 || arr.every((n) => n === 0)) {\n void (async () => {\n if (!(await isTossEnvironment())) {\n const host = navigator as unknown as BackupHost;\n host[BACKUP_KEY]?.call(navigator, pattern);\n }\n })();\n return true;\n }\n\n void (async () => {\n if (await isTossEnvironment()) {\n if (!Array.isArray(pattern)) {\n await haptic(durationToHaptic(pattern));\n return;\n }\n // Even indices = \"on\" durations, odd indices = pauses. `pattern[i]` is\n // `number | undefined` under `noUncheckedIndexedAccess`; the `undefined`\n // case only arises on out-of-bounds, which our length bound prevents.\n for (let i = 0; i < pattern.length; i += 2) {\n const on = pattern[i];\n if (on === undefined) break;\n if (on > 0) {\n await haptic('tap');\n }\n const pause = pattern[i + 1];\n if (typeof pause === 'number' && pause > 0) {\n await new Promise<void>((r) => setTimeout(r, pause));\n }\n }\n return;\n }\n const host = navigator as unknown as BackupHost;\n const original = host[BACKUP_KEY];\n original?.call(navigator, pattern);\n })();\n\n return true;\n}\n\nexport function installVibrateShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (BACKUP_KEY in host) {\n return () => uninstallVibrateShim();\n }\n\n const nav = navigator as Navigator & { vibrate?: (p: VibratePattern) => boolean };\n host[BACKUP_KEY] = nav.vibrate;\n host[HAD_KEY] = 'vibrate' in nav;\n\n Object.defineProperty(navigator, 'vibrate', {\n value: vibrateShim,\n configurable: true,\n writable: true,\n });\n\n return uninstallVibrateShim;\n}\n\nexport function uninstallVibrateShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(BACKUP_KEY in host)) return;\n\n const original = host[BACKUP_KEY];\n const had = host[HAD_KEY];\n // Prototype-safe restore: delete the instance override first, then only\n // redefine on the instance if the original was an own property the\n // prototype doesn't provide — prevents permanent shadowing of a prototype\n // `vibrate` getter on real browsers.\n delete (navigator as unknown as { vibrate?: (p: VibratePattern) => boolean }).vibrate;\n if (had && navigator.vibrate !== original) {\n Object.defineProperty(navigator, 'vibrate', {\n value: original,\n configurable: true,\n writable: true,\n });\n }\n delete host[BACKUP_KEY];\n delete host[HAD_KEY];\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAI;;;;;;;;AAgCJ,eAAsB,oBAAsC;CAG1D,MAAM,QAAQ,WAAW;AACzB,KAAI,UAAU,OAAQ,QAAO;AAC7B,KAAI,UAAU,UAAW,QAAO;AAEhC,KAAI,WAAW,KAAA,EAAW,QAAO;AAIjC,UAAS,QAFG,MAAM,aAAa,GAEV,qBAAqB;AAC1C,QAAO;;;;;;AAOT,eAAsB,cAA4E;AAChG,KAAI;AACF,SAAO,MAAM,OAAO;SACd;AACN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AC5CX,MAAM,aAAa,OAAO,IAAI,oCAAoC;AAClE,MAAM,UAAU,OAAO,IAAI,uCAAuC;AAOlE,MAAM,qBAAqB;AAc3B,eAAe,OAAO,MAAiC;CAErD,MAAM,MADM,MAAM,aAAa,GACkC;AACjE,KAAI,OAAO,OAAO,WAChB,KAAI;AACF,QAAO,GAAkD,EAAE,MAAM,CAAC;SAC5D;;AAMZ,SAAS,iBAAiB,UAA8B;AACtD,QAAO,WAAW,qBAAqB,aAAa;;AAGtD,SAAS,YAAY,SAAkC;CAIrD,MAAM,MAAM,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ;AACxD,KAAI,IAAI,WAAW,KAAK,IAAI,OAAO,MAAM,MAAM,EAAE,EAAE;AACjD,GAAM,YAAY;AAChB,OAAI,CAAE,MAAM,mBAAmB,CAChB,WACR,aAAa,KAAK,WAAW,QAAQ;MAE1C;AACJ,SAAO;;AAGT,EAAM,YAAY;AAChB,MAAI,MAAM,mBAAmB,EAAE;AAC7B,OAAI,CAAC,MAAM,QAAQ,QAAQ,EAAE;AAC3B,UAAM,OAAO,iBAAiB,QAAQ,CAAC;AACvC;;AAKF,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,KAAK,QAAQ;AACnB,QAAI,OAAO,KAAA,EAAW;AACtB,QAAI,KAAK,EACP,OAAM,OAAO,MAAM;IAErB,MAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,OAAO,UAAU,YAAY,QAAQ,EACvC,OAAM,IAAI,SAAe,MAAM,WAAW,GAAG,MAAM,CAAC;;AAGxD;;AAEW,YACS,aACZ,KAAK,WAAW,QAAQ;KAChC;AAEJ,QAAO;;AAGT,SAAgB,qBAAiC;AAC/C,KAAI,OAAO,cAAc,YACvB,cAAa;CAGf,MAAM,OAAO;AACb,KAAI,cAAc,KAChB,cAAa,sBAAsB;CAGrC,MAAM,MAAM;AACZ,MAAK,cAAc,IAAI;AACvB,MAAK,WAAW,aAAa;AAE7B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEF,QAAO;;AAGT,SAAgB,uBAA6B;AAC3C,KAAI,OAAO,cAAc,YAAa;CACtC,MAAM,OAAO;AACb,KAAI,EAAE,cAAc,MAAO;CAE3B,MAAM,WAAW,KAAK;CACtB,MAAM,MAAM,KAAK;AAKjB,QAAQ,UAAsE;AAC9E,KAAI,OAAO,UAAU,YAAY,SAC/B,QAAO,eAAe,WAAW,WAAW;EAC1C,OAAO;EACP,cAAc;EACd,UAAU;EACX,CAAC;AAEJ,QAAO,KAAK;AACZ,QAAO,KAAK"}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@ait-co/polyfill",
3
+ "version": "0.1.1",
4
+ "description": "Polyfill so you can build Apps in Toss mini-apps with standard Web APIs (navigator.clipboard, navigator.geolocation, ...) instead of the proprietary SDK",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./clipboard": {
17
+ "types": "./dist/shims/clipboard.d.ts",
18
+ "import": "./dist/shims/clipboard.js"
19
+ },
20
+ "./geolocation": {
21
+ "types": "./dist/shims/geolocation.d.ts",
22
+ "import": "./dist/shims/geolocation.js"
23
+ },
24
+ "./share": {
25
+ "types": "./dist/shims/share.d.ts",
26
+ "import": "./dist/shims/share.js"
27
+ },
28
+ "./vibrate": {
29
+ "types": "./dist/shims/vibrate.d.ts",
30
+ "import": "./dist/shims/vibrate.js"
31
+ },
32
+ "./network": {
33
+ "types": "./dist/shims/network.d.ts",
34
+ "import": "./dist/shims/network.js"
35
+ },
36
+ "./detect": {
37
+ "types": "./dist/detect.d.ts",
38
+ "import": "./dist/detect.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "sideEffects": false,
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "dev": "tsdown --watch",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest run",
50
+ "lint": "biome check .",
51
+ "lint:fix": "biome check --write .",
52
+ "format": "biome format --write ."
53
+ },
54
+ "peerDependencies": {
55
+ "@apps-in-toss/web-framework": ">=2.4.0 <3.0.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@apps-in-toss/web-framework": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@apps-in-toss/web-framework": "2.4.7",
64
+ "@biomejs/biome": "2.4.12",
65
+ "@changesets/cli": "^2.31.0",
66
+ "@types/node": "^24.0.0",
67
+ "jsdom": "^29.0.2",
68
+ "tsdown": "^0.21.7",
69
+ "typescript": "^6.0.2",
70
+ "vitest": "^4.1.4"
71
+ },
72
+ "keywords": [
73
+ "apps-in-toss",
74
+ "toss",
75
+ "mini-app",
76
+ "polyfill",
77
+ "web-api"
78
+ ],
79
+ "packageManager": "pnpm@10.33.0",
80
+ "license": "BSD-3-Clause",
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "https://github.com/apps-in-toss-community/polyfill"
87
+ },
88
+ "homepage": "https://www.npmjs.com/package/@ait-co/polyfill",
89
+ "bugs": {
90
+ "url": "https://github.com/apps-in-toss-community/polyfill/issues"
91
+ }
92
+ }