@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.cjs","names":[],"sources":["../../src/detect.ts","../../src/shims/_install-helpers.ts","../../src/shims/share.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\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 if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\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 * Shared helpers for installing shims on `navigator`.\n *\n * Chromium now marks a handful of `navigator` properties (e.g. `geolocation`,\n * `clipboard`) as non-configurable **own** properties on the instance. That\n * means a plain `Object.defineProperty(navigator, 'x', …)` throws\n * `TypeError: Cannot redefine property`.\n *\n * The workaround is to shim at the prototype level — `Navigator.prototype`\n * keeps these as configurable accessors, so we can swap them there and every\n * instance that falls through to the prototype (including `window.navigator`)\n * sees the shim. We only reach for the prototype when the instance-level\n * assignment refuses.\n *\n * For restoration we remember the descriptor chain (instance + prototype) so\n * `uninstall()` puts the browser back in its original state.\n */\n\ntype PropertyLocation = 'instance' | 'prototype';\n\nexport interface InstallSnapshot {\n /** Where we ended up writing the shim. */\n location: PropertyLocation;\n /** Original descriptor at that location (may be undefined if nothing was there). */\n originalDescriptor: PropertyDescriptor | undefined;\n /** `true` iff the original property lived on the instance before we touched it. */\n instanceHadOwn: boolean;\n}\n\n/**\n * Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the\n * browser refuses (property is non-configurable on the instance), install on\n * `Navigator.prototype` instead.\n *\n * Returns a snapshot describing where the original value was, which\n * `restoreNavigatorProperty` uses to undo the install.\n */\nexport function installNavigatorProperty(\n prop: string,\n descriptor: PropertyDescriptor,\n): InstallSnapshot {\n const nav = navigator as unknown as Record<PropertyKey, unknown>;\n const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);\n const instanceHadOwn = instanceDesc !== undefined;\n\n // Fast path: instance-level property is missing or configurable.\n if (!instanceDesc || instanceDesc.configurable) {\n try {\n Object.defineProperty(nav, prop, descriptor);\n return { location: 'instance', originalDescriptor: instanceDesc, instanceHadOwn };\n } catch {\n // Fall through to prototype-level install.\n }\n }\n\n // Prototype-level install. Drop the instance-level shadow so the prototype\n // accessor is visible to readers on `navigator`.\n const proto = Object.getPrototypeOf(nav) as object;\n const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);\n\n if (instanceHadOwn) {\n // Try to remove the instance-level shadow. On non-configurable it throws —\n // we deliberately ignore that; prototype-level install still wins because\n // the prototype accessor shows through when we read via `navigator[prop]`.\n try {\n delete nav[prop];\n } catch {\n /* non-configurable own — leave it; prototype install still useful */\n }\n }\n\n Object.defineProperty(proto, prop, descriptor);\n return { location: 'prototype', originalDescriptor: protoDesc, instanceHadOwn };\n}\n\n/**\n * Reverse the install recorded in `snapshot`. If the original descriptor was\n * `undefined` (property didn't exist before), delete the property instead of\n * re-defining it.\n */\nexport function restoreNavigatorProperty(prop: string, snapshot: InstallSnapshot): void {\n const target =\n snapshot.location === 'instance'\n ? (navigator as unknown as Record<PropertyKey, unknown>)\n : (Object.getPrototypeOf(navigator) as object);\n\n if (snapshot.originalDescriptor) {\n try {\n Object.defineProperty(target, prop, snapshot.originalDescriptor);\n } catch {\n /* descriptor was non-configurable upstream; we can't undo — rare. */\n }\n } else {\n try {\n delete (target as Record<PropertyKey, unknown>)[prop];\n } catch {\n /* non-configurable — rare. */\n }\n }\n\n // If our install pushed past an instance shadow, we leave the instance alone\n // — the descriptor we captured for `instanceHadOwn: true` lives on the\n // instance and was not modified at install time.\n}\n\n/**\n * Method-level install snapshot. Captured per-key so `restoreObjectMethods`\n * can distinguish \"was an own property, reassign it\" from \"was inherited,\n * delete the override so the prototype method surfaces again\".\n */\nexport interface MethodInstallSnapshot {\n target: object;\n methods: Record<string, { hadOwn: boolean; original: unknown }>;\n}\n\n/**\n * Mutate methods on an existing object rather than replacing the object\n * itself. This is the path we take for `navigator.geolocation`, `navigator.share`,\n * and `navigator.vibrate` in Chromium, where the slot on `navigator` is a\n * non-configurable own property that we cannot replace — but the methods\n * themselves (or the methods on the referenced object) are still\n * `configurable: true, writable: true`.\n *\n * Each replacement is installed via plain assignment. If any slot is not\n * writable (e.g. frozen object), install is aborted and previously-applied\n * replacements are rolled back, so the caller observes an atomic \"all or\n * nothing\" failure as `null`. The caller is expected to degrade gracefully\n * (e.g. log a one-time `console.warn`) when that happens.\n */\nexport function installObjectMethods(\n target: object,\n replacements: Record<string, (...args: never[]) => unknown>,\n): MethodInstallSnapshot | null {\n const methods: Record<string, { hadOwn: boolean; original: unknown }> = {};\n const applied: string[] = [];\n const bag = target as Record<string, unknown>;\n\n for (const key of Object.keys(replacements)) {\n const hadOwn = Object.hasOwn(target, key);\n const original = bag[key];\n try {\n bag[key] = replacements[key] as unknown;\n } catch {\n // Non-writable / frozen. Roll back and return null.\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n // Verify the assignment actually stuck — silent-failure descriptors (e.g.\n // `writable: false` without strict mode) can skip the throw and leave the\n // original value in place. Treat that the same as a throw.\n if (bag[key] !== (replacements[key] as unknown)) {\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n methods[key] = { hadOwn, original };\n applied.push(key);\n }\n\n return { target, methods };\n}\n\n/**\n * Reverse an `installObjectMethods` snapshot. Reassigns originals for slots\n * that were own properties before install; deletes the override for slots\n * that were inherited (so the prototype method surfaces again).\n */\nexport function restoreObjectMethods(snapshot: MethodInstallSnapshot): void {\n const bag = snapshot.target as Record<string, unknown>;\n for (const key of Object.keys(snapshot.methods)) {\n const entry = snapshot.methods[key];\n if (!entry) continue;\n try {\n if (entry.hadOwn) {\n bag[key] = entry.original;\n } else {\n delete bag[key];\n }\n } catch {\n /* frozen between install and restore — rare. */\n }\n }\n}\n","/**\n * `navigator.share` shim.\n *\n * Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only\n * accepts a single `message` string, so we concatenate `title`, `text`, and\n * `url` with newline separators (skipping missing/empty values).\n *\n * Outside Apps in Toss → defers to the browser's native `navigator.share`, or\n * throws `NotSupportedError` if unavailable.\n *\n * Install strategy: **method-level** on `navigator`. Assigning\n * `navigator.share = fn` creates an own property that shadows the prototype\n * method. Uninstall deletes the own shadow so the prototype method surfaces\n * again. We do not mutate `Navigator.prototype` — in real browsers its\n * descriptor may be non-configurable, which would throw on reassignment.\n *\n * Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).\n * `canShare({ files })` returns `false` whenever the sync-accessible detection\n * says Toss is active (or is being forced via the test override).\n */\n\nimport { isTossEnvironment, isTossEnvironmentCached, loadTossSdk } from '../detect.js';\nimport {\n installObjectMethods,\n type MethodInstallSnapshot,\n restoreObjectMethods,\n} from './_install-helpers.js';\n\nconst SHARE_BACKUP_KEY = Symbol.for('@ait-co/polyfill/share.original');\nconst SHARE_SNAPSHOT_KEY = Symbol.for('@ait-co/polyfill/share.snapshot');\n\ntype ShareFn = (data?: ShareData) => Promise<void>;\ntype CanShareFn = (data?: ShareData) => boolean;\n\ninterface Backup {\n share: ShareFn | undefined;\n canShare: CanShareFn | undefined;\n}\n\ninterface BackupHost {\n [SHARE_BACKUP_KEY]?: Backup | undefined;\n [SHARE_SNAPSHOT_KEY]?: MethodInstallSnapshot | undefined;\n}\n\nfunction buildSdkMessage(data: ShareData | undefined): string {\n // Use presence checks rather than truthiness so an intentionally empty\n // string in one field is handled correctly alongside a non-empty sibling.\n const parts: string[] = [];\n if (data?.title != null && data.title !== '') parts.push(data.title);\n if (data?.text != null && data.text !== '') parts.push(data.text);\n if (data?.url != null && data.url !== '') parts.push(data.url);\n return parts.join('\\n');\n}\n\nasync function shareShim(data?: ShareData): Promise<void> {\n if (await isTossEnvironment()) {\n const sdk = await loadTossSdk();\n const fn = (sdk as { share?: unknown } | null)?.share;\n if (typeof fn === 'function') {\n const message = buildSdkMessage(data);\n if (!message) {\n throw new TypeError(\n '[@ait-co/polyfill] navigator.share requires at least one of title, text, or url.',\n );\n }\n try {\n await (fn as (o: { message: string }) => Promise<void>)({ message });\n } catch (e) {\n // Spec says navigator.share rejects with a DOMException. Wrap SDK\n // errors as AbortError (the most common cause is user cancellation),\n // attaching the original as `.cause` for Sentry-style telemetry.\n const message_ = e instanceof Error ? e.message : String(e);\n const wrapped = new DOMException(message_, 'AbortError');\n if (e instanceof Error) {\n (wrapped as Error).cause = e;\n }\n throw wrapped;\n }\n return;\n }\n }\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const original = backup?.share;\n if (!original) {\n throw new DOMException(\n '[@ait-co/polyfill] navigator.share is not available in this environment.',\n 'NotSupportedError',\n );\n }\n return original(data);\n}\n\nfunction canShareShim(data?: ShareData): boolean {\n const hasFiles = Boolean(data?.files && data.files.length > 0);\n const toss = isTossEnvironmentCached();\n\n if (hasFiles) {\n // SDK does not share files. If we know we're in Toss (or it's being\n // forced), say so honestly. If detection hasn't resolved yet, be\n // pessimistic — a false negative is safer than promising a capability\n // we'll turn around and deny.\n if (toss === true) return false;\n if (toss === undefined) return false;\n }\n\n // Toss with non-file payloads: true iff there's at least one field.\n if (toss === true) {\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n }\n\n // `toss === undefined` (detection not resolved) with non-file payload falls\n // through to the browser-native answer. Rationale: `canShare` is rarely\n // load-bearing — consumers care about `share()` itself, which awaits the\n // async detection correctly. A false-negative here would needlessly hide a\n // Share button while detection settles.\n // Browser path: delegate to native when present.\n const host = navigator as unknown as BackupHost;\n const backup = host[SHARE_BACKUP_KEY];\n const originalCanShare = backup?.canShare;\n if (originalCanShare) {\n return originalCanShare(data);\n }\n return Boolean(\n (data?.title != null && data.title !== '') ||\n (data?.text != null && data.text !== '') ||\n (data?.url != null && data.url !== ''),\n );\n}\n\nexport function installShareShim(): () => void {\n if (typeof navigator === 'undefined') {\n return () => {};\n }\n\n const host = navigator as unknown as BackupHost;\n if (SHARE_BACKUP_KEY in host) {\n // Already installed. Use `in` so the absence of `share` / `canShare` on\n // the pre-install navigator (legitimately stored as `undefined`) doesn't\n // re-trigger install.\n return () => uninstallShareShim();\n }\n\n const nav = navigator as Navigator & {\n share?: ShareFn;\n canShare?: CanShareFn;\n };\n // Capture the native methods BEFORE patching, bound to `navigator` so that\n // fallback calls keep the correct `this` and never recurse through our shim.\n host[SHARE_BACKUP_KEY] = {\n share: nav.share ? nav.share.bind(navigator) : undefined,\n canShare: nav.canShare ? nav.canShare.bind(navigator) : undefined,\n };\n\n const snapshot = installObjectMethods(navigator, {\n share: shareShim as (...args: never[]) => unknown,\n canShare: canShareShim as (...args: never[]) => unknown,\n });\n\n if (!snapshot) {\n // Slots frozen. Back out the backup bookkeeping so a later install can retry.\n delete host[SHARE_BACKUP_KEY];\n return () => uninstallShareShim();\n }\n\n host[SHARE_SNAPSHOT_KEY] = snapshot;\n return uninstallShareShim;\n}\n\nexport function uninstallShareShim(): void {\n if (typeof navigator === 'undefined') return;\n const host = navigator as unknown as BackupHost;\n if (!(SHARE_BACKUP_KEY in host)) return;\n\n const snapshot = host[SHARE_SNAPSHOT_KEY];\n if (snapshot) restoreObjectMethods(snapshot);\n\n delete host[SHARE_BACKUP_KEY];\n delete host[SHARE_SNAPSHOT_KEY];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/shims/share.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `navigator.share` shim.
|
|
4
|
+
*
|
|
5
|
+
* Inside Apps in Toss → routes through SDK `share({ message })`. The SDK only
|
|
6
|
+
* accepts a single `message` string, so we concatenate `title`, `text`, and
|
|
7
|
+
* `url` with newline separators (skipping missing/empty values).
|
|
8
|
+
*
|
|
9
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.share`, or
|
|
10
|
+
* throws `NotSupportedError` if unavailable.
|
|
11
|
+
*
|
|
12
|
+
* Install strategy: **method-level** on `navigator`. Assigning
|
|
13
|
+
* `navigator.share = fn` creates an own property that shadows the prototype
|
|
14
|
+
* method. Uninstall deletes the own shadow so the prototype method surfaces
|
|
15
|
+
* again. We do not mutate `Navigator.prototype` — in real browsers its
|
|
16
|
+
* descriptor may be non-configurable, which would throw on reassignment.
|
|
17
|
+
*
|
|
18
|
+
* Caveat: the SDK's share has no counterpart for `files` (Web Share Level 2).
|
|
19
|
+
* `canShare({ files })` returns `false` whenever the sync-accessible detection
|
|
20
|
+
* says Toss is active (or is being forced via the test override).
|
|
21
|
+
*/
|
|
22
|
+
declare function installShareShim(): () => void;
|
|
23
|
+
declare function uninstallShareShim(): void;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { installShareShim, uninstallShareShim };
|
|
26
|
+
//# sourceMappingURL=share.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share.d.cts","names":[],"sources":["../../src/shims/share.ts"],"mappings":";;AAsIA;;;;;AAuCA;;;;;;;;;;;;;;iBAvCgB,gBAAA,CAAA;AAAA,iBAuCA,kBAAA,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/shims/vibrate-semantic.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Semantic haptic helper — escape hatch for callers who know their intent
|
|
4
|
+
* (`success`, `error`, `warning`, `selection`) and don't want to encode it as
|
|
5
|
+
* a millisecond pattern.
|
|
6
|
+
*
|
|
7
|
+
* `navigator.vibrate(pattern)` is duration-only by spec, so the polyfill's
|
|
8
|
+
* length-based mapping cannot recover semantic intent. This helper sits next
|
|
9
|
+
* to it: importable from `@ait-co/polyfill/vibrate-semantic`, no install, no
|
|
10
|
+
* `navigator` mutation. Inside Apps in Toss it routes to the SDK's haptic
|
|
11
|
+
* variants; outside Toss it falls back to a short `navigator.vibrate(...)`
|
|
12
|
+
* call so the browser at least produces *some* feedback.
|
|
13
|
+
*
|
|
14
|
+
* Why a sub-path and not an extension to `navigator.vibrate`:
|
|
15
|
+
* - The standard signature stays untouched (no smuggling of non-standard
|
|
16
|
+
* argument shapes through `navigator.vibrate`).
|
|
17
|
+
* - `sideEffects: ["./dist/auto.js"]` keeps this drop-if-unused for bundlers.
|
|
18
|
+
*
|
|
19
|
+
* Returns `true` when the request was dispatched (Toss SDK call queued, or
|
|
20
|
+
* native vibrate accepted the fallback) and `false` when no haptic surface is
|
|
21
|
+
* available — mirroring `navigator.vibrate`'s "supported/triggered" boolean.
|
|
22
|
+
*/
|
|
23
|
+
type VibrateIntent = 'success' | 'error' | 'warning' | 'selection';
|
|
24
|
+
declare function vibrateSemantic(intent: VibrateIntent): boolean;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { VibrateIntent, vibrateSemantic };
|
|
27
|
+
//# sourceMappingURL=vibrate-semantic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vibrate-semantic.d.ts","names":[],"sources":["../../src/shims/vibrate-semantic.ts"],"mappings":";;AAyBA;;;;;AAoBA;;;;;;;;;;;;;;;KApBY,aAAA;AAAA,iBAoBI,eAAA,CAAgB,MAAA,EAAQ,aAAA"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//#region src/detect.ts
|
|
2
|
+
/**
|
|
3
|
+
* Environment detection: are we running inside Apps in Toss, or a plain browser?
|
|
4
|
+
*
|
|
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.
|
|
10
|
+
*
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
let cached;
|
|
20
|
+
/**
|
|
21
|
+
* Returns `true` iff we detect we are running in an environment where the
|
|
22
|
+
* Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
|
|
23
|
+
*
|
|
24
|
+
* Async because we use dynamic `import()` to probe the optional peer dep
|
|
25
|
+
* without forcing it into the consumer's bundle.
|
|
26
|
+
*/
|
|
27
|
+
async function isTossEnvironment() {
|
|
28
|
+
const force = globalThis.__AIT_POLYFILL_FORCE__;
|
|
29
|
+
if (force === "toss") return true;
|
|
30
|
+
if (force === "browser") return false;
|
|
31
|
+
if (cached !== void 0) return cached;
|
|
32
|
+
const mod = await loadTossSdk();
|
|
33
|
+
if (typeof mod?.getAppsInTossGlobals !== "function") {
|
|
34
|
+
cached = false;
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const globals = mod.getAppsInTossGlobals();
|
|
39
|
+
cached = Boolean(globals) && typeof globals === "object";
|
|
40
|
+
} catch {
|
|
41
|
+
cached = false;
|
|
42
|
+
}
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Lazy SDK accessor — returns the module if available, else `null`. Callers
|
|
47
|
+
* are expected to `await` and null-check. Never throws.
|
|
48
|
+
*/
|
|
49
|
+
async function loadTossSdk() {
|
|
50
|
+
try {
|
|
51
|
+
return await import("@apps-in-toss/web-framework");
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/shims/vibrate.ts
|
|
58
|
+
/**
|
|
59
|
+
* `navigator.vibrate` shim.
|
|
60
|
+
*
|
|
61
|
+
* Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`.
|
|
62
|
+
* Single-duration calls land in three buckets so the qualitative SDK haptic
|
|
63
|
+
* tracks intensity a little more closely than the original two-bucket split:
|
|
64
|
+
* - `vibrate(0)` → no-op (web standard: cancels pending vibration)
|
|
65
|
+
* - `vibrate(1..20ms)` → `tickWeak`
|
|
66
|
+
* - `vibrate(21..45ms)` → `tickMedium`
|
|
67
|
+
* - `vibrate(>=46ms)` → `basicMedium`
|
|
68
|
+
* - `vibrate(number[])` → iterates "on" segments (even indices) as `tap`
|
|
69
|
+
* pulses with `setTimeout` gaps. Per-element ms mapping is intentionally
|
|
70
|
+
* skipped: arrays in the wild are mostly rhythmic patterns, and the SDK
|
|
71
|
+
* has no "stronger heavy" variant to reach for, so per-pulse precision
|
|
72
|
+
* buys little. Callers needing intent should use `vibrateSemantic`.
|
|
73
|
+
*
|
|
74
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
|
|
75
|
+
* or returns `false` when unavailable (matches the spec — browsers that don't
|
|
76
|
+
* support vibration simply return `false`).
|
|
77
|
+
*
|
|
78
|
+
* Install strategy: **method-level** on `navigator`. We assign our wrapper to
|
|
79
|
+
* `navigator.vibrate` (creating an own shadow over the prototype method) and
|
|
80
|
+
* delete it on uninstall so the prototype re-surfaces. We do not mutate
|
|
81
|
+
* `Navigator.prototype` itself — browsers may mark it non-configurable.
|
|
82
|
+
*
|
|
83
|
+
* Caveats (documented in CLAUDE.md as the known lossy trade-off):
|
|
84
|
+
* - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
|
|
85
|
+
* durations. The shim approximates intensity from duration but cannot
|
|
86
|
+
* reproduce exact patterns. Length-only mapping cannot recover semantic
|
|
87
|
+
* intent (success vs. error vs. warning); use `vibrateSemantic` for that.
|
|
88
|
+
* - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
|
|
89
|
+
* honoured only as "time until the next tap", not as silent-vs-vibrating.
|
|
90
|
+
* - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
|
|
91
|
+
* `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
|
|
92
|
+
*/
|
|
93
|
+
async function haptic(type) {
|
|
94
|
+
const fn = (await loadTossSdk())?.generateHapticFeedback;
|
|
95
|
+
if (typeof fn === "function") try {
|
|
96
|
+
await fn({ type });
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/shims/vibrate-semantic.ts
|
|
101
|
+
/**
|
|
102
|
+
* Semantic haptic helper — escape hatch for callers who know their intent
|
|
103
|
+
* (`success`, `error`, `warning`, `selection`) and don't want to encode it as
|
|
104
|
+
* a millisecond pattern.
|
|
105
|
+
*
|
|
106
|
+
* `navigator.vibrate(pattern)` is duration-only by spec, so the polyfill's
|
|
107
|
+
* length-based mapping cannot recover semantic intent. This helper sits next
|
|
108
|
+
* to it: importable from `@ait-co/polyfill/vibrate-semantic`, no install, no
|
|
109
|
+
* `navigator` mutation. Inside Apps in Toss it routes to the SDK's haptic
|
|
110
|
+
* variants; outside Toss it falls back to a short `navigator.vibrate(...)`
|
|
111
|
+
* call so the browser at least produces *some* feedback.
|
|
112
|
+
*
|
|
113
|
+
* Why a sub-path and not an extension to `navigator.vibrate`:
|
|
114
|
+
* - The standard signature stays untouched (no smuggling of non-standard
|
|
115
|
+
* argument shapes through `navigator.vibrate`).
|
|
116
|
+
* - `sideEffects: ["./dist/auto.js"]` keeps this drop-if-unused for bundlers.
|
|
117
|
+
*
|
|
118
|
+
* Returns `true` when the request was dispatched (Toss SDK call queued, or
|
|
119
|
+
* native vibrate accepted the fallback) and `false` when no haptic surface is
|
|
120
|
+
* available — mirroring `navigator.vibrate`'s "supported/triggered" boolean.
|
|
121
|
+
*/
|
|
122
|
+
const INTENT_TO_HAPTIC = {
|
|
123
|
+
success: "success",
|
|
124
|
+
error: "error",
|
|
125
|
+
warning: "tickMedium",
|
|
126
|
+
selection: "tickWeak"
|
|
127
|
+
};
|
|
128
|
+
const INTENT_TO_FALLBACK_MS = {
|
|
129
|
+
success: 30,
|
|
130
|
+
error: 60,
|
|
131
|
+
warning: 25,
|
|
132
|
+
selection: 10
|
|
133
|
+
};
|
|
134
|
+
function vibrateSemantic(intent) {
|
|
135
|
+
const sdkType = INTENT_TO_HAPTIC[intent];
|
|
136
|
+
const hasNativeVibrate = typeof navigator !== "undefined" && typeof navigator.vibrate === "function";
|
|
137
|
+
(async () => {
|
|
138
|
+
if (await isTossEnvironment()) {
|
|
139
|
+
await haptic(sdkType);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (hasNativeVibrate) navigator.vibrate(INTENT_TO_FALLBACK_MS[intent]);
|
|
143
|
+
})();
|
|
144
|
+
if (hasNativeVibrate) return true;
|
|
145
|
+
return globalThis.__AIT_POLYFILL_FORCE__ === "toss";
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { vibrateSemantic };
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=vibrate-semantic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vibrate-semantic.js","names":[],"sources":["../../src/detect.ts","../../src/shims/vibrate.ts","../../src/shims/vibrate-semantic.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\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 if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\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 * Single-duration calls land in three buckets so the qualitative SDK haptic\n * tracks intensity a little more closely than the original two-bucket split:\n * - `vibrate(0)` → no-op (web standard: cancels pending vibration)\n * - `vibrate(1..20ms)` → `tickWeak`\n * - `vibrate(21..45ms)` → `tickMedium`\n * - `vibrate(>=46ms)` → `basicMedium`\n * - `vibrate(number[])` → iterates \"on\" segments (even indices) as `tap`\n * pulses with `setTimeout` gaps. Per-element ms mapping is intentionally\n * skipped: arrays in the wild are mostly rhythmic patterns, and the SDK\n * has no \"stronger heavy\" variant to reach for, so per-pulse precision\n * buys little. Callers needing intent should use `vibrateSemantic`.\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 * Install strategy: **method-level** on `navigator`. We assign our wrapper to\n * `navigator.vibrate` (creating an own shadow over the prototype method) and\n * delete it on uninstall so the prototype re-surfaces. We do not mutate\n * `Navigator.prototype` itself — browsers may mark it non-configurable.\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. Length-only mapping cannot recover semantic\n * intent (success vs. error vs. warning); use `vibrateSemantic` for that.\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';\nimport {\n installObjectMethods,\n type MethodInstallSnapshot,\n restoreObjectMethods,\n} from './_install-helpers.js';\n\nconst BACKUP_KEY = Symbol.for('@ait-co/polyfill/vibrate.original');\nconst SNAPSHOT_KEY = Symbol.for('@ait-co/polyfill/vibrate.snapshot');\n\ntype VibrateFn = (pattern: VibratePattern) => boolean;\n\ninterface BackupHost {\n [BACKUP_KEY]?: VibrateFn | undefined;\n [SNAPSHOT_KEY]?: MethodInstallSnapshot | undefined;\n}\n\n// Mapping thresholds chosen so the existing dog-food cases keep their old\n// haptic (vibrate(20) → tickWeak, vibrate(50)+ → basicMedium) while a 21–45ms\n// nudge — too long for \"tick\", too short for \"basic\" — gets `tickMedium`.\nconst TICK_WEAK_MAX_MS = 20;\nconst TICK_MEDIUM_MAX_MS = 45;\n\ntype HapticType =\n | 'tickWeak'\n | 'tap'\n | 'tickMedium'\n | 'softMedium'\n | 'basicWeak'\n | 'basicMedium'\n | 'success'\n | 'error'\n | 'wiggle'\n | 'confetti';\n\nexport async 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 if (duration <= TICK_WEAK_MAX_MS) return 'tickWeak';\n if (duration <= TICK_MEDIUM_MAX_MS) return 'tickMedium';\n return '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]?.(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?.(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?: VibrateFn };\n // Capture the native method BEFORE we patch, bound to `navigator` so our\n // fallback call keeps the correct `this` and never recurses into our shim.\n host[BACKUP_KEY] = nav.vibrate ? nav.vibrate.bind(navigator) : undefined;\n\n const snapshot = installObjectMethods(navigator, {\n vibrate: vibrateShim as (...args: never[]) => unknown,\n });\n\n if (!snapshot) {\n delete host[BACKUP_KEY];\n return () => uninstallVibrateShim();\n }\n\n host[SNAPSHOT_KEY] = snapshot;\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 snapshot = host[SNAPSHOT_KEY];\n if (snapshot) restoreObjectMethods(snapshot);\n delete host[BACKUP_KEY];\n delete host[SNAPSHOT_KEY];\n}\n","/**\n * Semantic haptic helper — escape hatch for callers who know their intent\n * (`success`, `error`, `warning`, `selection`) and don't want to encode it as\n * a millisecond pattern.\n *\n * `navigator.vibrate(pattern)` is duration-only by spec, so the polyfill's\n * length-based mapping cannot recover semantic intent. This helper sits next\n * to it: importable from `@ait-co/polyfill/vibrate-semantic`, no install, no\n * `navigator` mutation. Inside Apps in Toss it routes to the SDK's haptic\n * variants; outside Toss it falls back to a short `navigator.vibrate(...)`\n * call so the browser at least produces *some* feedback.\n *\n * Why a sub-path and not an extension to `navigator.vibrate`:\n * - The standard signature stays untouched (no smuggling of non-standard\n * argument shapes through `navigator.vibrate`).\n * - `sideEffects: [\"./dist/auto.js\"]` keeps this drop-if-unused for bundlers.\n *\n * Returns `true` when the request was dispatched (Toss SDK call queued, or\n * native vibrate accepted the fallback) and `false` when no haptic surface is\n * available — mirroring `navigator.vibrate`'s \"supported/triggered\" boolean.\n */\n\nimport { isTossEnvironment } from '../detect.js';\nimport { haptic } from './vibrate.js';\n\nexport type VibrateIntent = 'success' | 'error' | 'warning' | 'selection';\n\n// SDK-supported variants only. `success` and `error` map directly; `warning`\n// and `selection` synthesize from the closest qualitative variant since the\n// SDK's haptic palette has no `warning` / `selection` of its own. Documented\n// in README so callers don't expect platform-perfect parity.\nconst INTENT_TO_HAPTIC = {\n success: 'success',\n error: 'error',\n warning: 'tickMedium',\n selection: 'tickWeak',\n} as const;\n\nconst INTENT_TO_FALLBACK_MS: Record<VibrateIntent, number> = {\n success: 30,\n error: 60,\n warning: 25,\n selection: 10,\n};\n\nexport function vibrateSemantic(intent: VibrateIntent): boolean {\n const sdkType = INTENT_TO_HAPTIC[intent];\n\n // Mirror `navigator.vibrate`'s \"supported/triggered\" boolean. Toss detection\n // is async, but if the SDK isn't even loadable AND the browser has no\n // `navigator.vibrate`, there's no surface to dispatch to — return `false`\n // synchronously like the spec'd `vibrate()` does for unsupported environments.\n const hasNativeVibrate =\n typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';\n\n void (async () => {\n if (await isTossEnvironment()) {\n await haptic(sdkType);\n return;\n }\n if (hasNativeVibrate) {\n navigator.vibrate(INTENT_TO_FALLBACK_MS[intent]);\n }\n })();\n\n // We can't synchronously know whether Toss detection will resolve to true,\n // so we optimistically return `true` whenever *some* haptic surface is\n // plausibly available: native vibrate exists, or a Toss override is forced.\n // Only when both are absent do we report unsupported.\n if (hasNativeVibrate) return true;\n const force = globalThis.__AIT_POLYFILL_FORCE__;\n return force === 'toss';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,IAAI;;;;;;;;AAgCJ,eAAsB,oBAAsC;CAG1D,MAAM,QAAQ,WAAW;AACzB,KAAI,UAAU,OAAQ,QAAO;AAC7B,KAAI,UAAU,UAAW,QAAO;AAEhC,KAAI,WAAW,KAAA,EAAW,QAAO;CAEjC,MAAM,MAAM,MAAM,aAAa;AAC/B,KAAI,OAAO,KAAK,yBAAyB,YAAY;AACnD,WAAS;AACT,SAAO;;AAKT,KAAI;EACF,MAAM,UAAU,IAAI,sBAAsB;AAC1C,WAAS,QAAQ,QAAQ,IAAI,OAAO,YAAY;SAC1C;AACN,WAAS;;AAEX,QAAO;;;;;;AAOT,eAAsB,cAA4E;AAChG,KAAI;AACF,SAAO,MAAM,OAAO;SACd;AACN,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACbX,eAAsB,OAAO,MAAiC;CAE5D,MAAM,MADM,MAAM,aAAa,GACkC;AACjE,KAAI,OAAO,OAAO,WAChB,KAAI;AACF,QAAO,GAAkD,EAAE,MAAM,CAAC;SAC5D;;;;;;;;;;;;;;;;;;;;;;;;;AC9CZ,MAAM,mBAAmB;CACvB,SAAS;CACT,OAAO;CACP,SAAS;CACT,WAAW;CACZ;AAED,MAAM,wBAAuD;CAC3D,SAAS;CACT,OAAO;CACP,SAAS;CACT,WAAW;CACZ;AAED,SAAgB,gBAAgB,QAAgC;CAC9D,MAAM,UAAU,iBAAiB;CAMjC,MAAM,mBACJ,OAAO,cAAc,eAAe,OAAO,UAAU,YAAY;AAEnE,EAAM,YAAY;AAChB,MAAI,MAAM,mBAAmB,EAAE;AAC7B,SAAM,OAAO,QAAQ;AACrB;;AAEF,MAAI,iBACF,WAAU,QAAQ,sBAAsB,QAAQ;KAEhD;AAMJ,KAAI,iBAAkB,QAAO;AAE7B,QADc,WAAW,2BACR"}
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
* Returns `true` iff we detect we are running in an environment where the
|
|
23
|
+
* Apps in Toss SDK (`@apps-in-toss/web-framework`) is present and usable.
|
|
24
|
+
*
|
|
25
|
+
* Async because we use dynamic `import()` to probe the optional peer dep
|
|
26
|
+
* without forcing it into the consumer's bundle.
|
|
27
|
+
*/
|
|
28
|
+
async function isTossEnvironment() {
|
|
29
|
+
const force = globalThis.__AIT_POLYFILL_FORCE__;
|
|
30
|
+
if (force === "toss") return true;
|
|
31
|
+
if (force === "browser") return false;
|
|
32
|
+
if (cached !== void 0) return cached;
|
|
33
|
+
const mod = await loadTossSdk();
|
|
34
|
+
if (typeof mod?.getAppsInTossGlobals !== "function") {
|
|
35
|
+
cached = false;
|
|
36
|
+
return cached;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const globals = mod.getAppsInTossGlobals();
|
|
40
|
+
cached = Boolean(globals) && typeof globals === "object";
|
|
41
|
+
} catch {
|
|
42
|
+
cached = false;
|
|
43
|
+
}
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lazy SDK accessor — returns the module if available, else `null`. Callers
|
|
48
|
+
* are expected to `await` and null-check. Never throws.
|
|
49
|
+
*/
|
|
50
|
+
async function loadTossSdk() {
|
|
51
|
+
try {
|
|
52
|
+
return await import("@apps-in-toss/web-framework");
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/shims/_install-helpers.ts
|
|
59
|
+
/**
|
|
60
|
+
* Mutate methods on an existing object rather than replacing the object
|
|
61
|
+
* itself. This is the path we take for `navigator.geolocation`, `navigator.share`,
|
|
62
|
+
* and `navigator.vibrate` in Chromium, where the slot on `navigator` is a
|
|
63
|
+
* non-configurable own property that we cannot replace — but the methods
|
|
64
|
+
* themselves (or the methods on the referenced object) are still
|
|
65
|
+
* `configurable: true, writable: true`.
|
|
66
|
+
*
|
|
67
|
+
* Each replacement is installed via plain assignment. If any slot is not
|
|
68
|
+
* writable (e.g. frozen object), install is aborted and previously-applied
|
|
69
|
+
* replacements are rolled back, so the caller observes an atomic "all or
|
|
70
|
+
* nothing" failure as `null`. The caller is expected to degrade gracefully
|
|
71
|
+
* (e.g. log a one-time `console.warn`) when that happens.
|
|
72
|
+
*/
|
|
73
|
+
function installObjectMethods(target, replacements) {
|
|
74
|
+
const methods = {};
|
|
75
|
+
const applied = [];
|
|
76
|
+
const bag = target;
|
|
77
|
+
for (const key of Object.keys(replacements)) {
|
|
78
|
+
const hadOwn = Object.hasOwn(target, key);
|
|
79
|
+
const original = bag[key];
|
|
80
|
+
try {
|
|
81
|
+
bag[key] = replacements[key];
|
|
82
|
+
} catch {
|
|
83
|
+
for (const applieKey of applied) {
|
|
84
|
+
const prev = methods[applieKey];
|
|
85
|
+
if (!prev) continue;
|
|
86
|
+
if (prev.hadOwn) bag[applieKey] = prev.original;
|
|
87
|
+
else delete bag[applieKey];
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (bag[key] !== replacements[key]) {
|
|
92
|
+
for (const applieKey of applied) {
|
|
93
|
+
const prev = methods[applieKey];
|
|
94
|
+
if (!prev) continue;
|
|
95
|
+
if (prev.hadOwn) bag[applieKey] = prev.original;
|
|
96
|
+
else delete bag[applieKey];
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
methods[key] = {
|
|
101
|
+
hadOwn,
|
|
102
|
+
original
|
|
103
|
+
};
|
|
104
|
+
applied.push(key);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
target,
|
|
108
|
+
methods
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Reverse an `installObjectMethods` snapshot. Reassigns originals for slots
|
|
113
|
+
* that were own properties before install; deletes the override for slots
|
|
114
|
+
* that were inherited (so the prototype method surfaces again).
|
|
115
|
+
*/
|
|
116
|
+
function restoreObjectMethods(snapshot) {
|
|
117
|
+
const bag = snapshot.target;
|
|
118
|
+
for (const key of Object.keys(snapshot.methods)) {
|
|
119
|
+
const entry = snapshot.methods[key];
|
|
120
|
+
if (!entry) continue;
|
|
121
|
+
try {
|
|
122
|
+
if (entry.hadOwn) bag[key] = entry.original;
|
|
123
|
+
else delete bag[key];
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/shims/vibrate.ts
|
|
129
|
+
/**
|
|
130
|
+
* `navigator.vibrate` shim.
|
|
131
|
+
*
|
|
132
|
+
* Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`.
|
|
133
|
+
* Single-duration calls land in three buckets so the qualitative SDK haptic
|
|
134
|
+
* tracks intensity a little more closely than the original two-bucket split:
|
|
135
|
+
* - `vibrate(0)` → no-op (web standard: cancels pending vibration)
|
|
136
|
+
* - `vibrate(1..20ms)` → `tickWeak`
|
|
137
|
+
* - `vibrate(21..45ms)` → `tickMedium`
|
|
138
|
+
* - `vibrate(>=46ms)` → `basicMedium`
|
|
139
|
+
* - `vibrate(number[])` → iterates "on" segments (even indices) as `tap`
|
|
140
|
+
* pulses with `setTimeout` gaps. Per-element ms mapping is intentionally
|
|
141
|
+
* skipped: arrays in the wild are mostly rhythmic patterns, and the SDK
|
|
142
|
+
* has no "stronger heavy" variant to reach for, so per-pulse precision
|
|
143
|
+
* buys little. Callers needing intent should use `vibrateSemantic`.
|
|
144
|
+
*
|
|
145
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
|
|
146
|
+
* or returns `false` when unavailable (matches the spec — browsers that don't
|
|
147
|
+
* support vibration simply return `false`).
|
|
148
|
+
*
|
|
149
|
+
* Install strategy: **method-level** on `navigator`. We assign our wrapper to
|
|
150
|
+
* `navigator.vibrate` (creating an own shadow over the prototype method) and
|
|
151
|
+
* delete it on uninstall so the prototype re-surfaces. We do not mutate
|
|
152
|
+
* `Navigator.prototype` itself — browsers may mark it non-configurable.
|
|
153
|
+
*
|
|
154
|
+
* Caveats (documented in CLAUDE.md as the known lossy trade-off):
|
|
155
|
+
* - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
|
|
156
|
+
* durations. The shim approximates intensity from duration but cannot
|
|
157
|
+
* reproduce exact patterns. Length-only mapping cannot recover semantic
|
|
158
|
+
* intent (success vs. error vs. warning); use `vibrateSemantic` for that.
|
|
159
|
+
* - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
|
|
160
|
+
* honoured only as "time until the next tap", not as silent-vs-vibrating.
|
|
161
|
+
* - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
|
|
162
|
+
* `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
|
|
163
|
+
*/
|
|
164
|
+
const BACKUP_KEY = Symbol.for("@ait-co/polyfill/vibrate.original");
|
|
165
|
+
const SNAPSHOT_KEY = Symbol.for("@ait-co/polyfill/vibrate.snapshot");
|
|
166
|
+
const TICK_WEAK_MAX_MS = 20;
|
|
167
|
+
const TICK_MEDIUM_MAX_MS = 45;
|
|
168
|
+
async function haptic(type) {
|
|
169
|
+
const fn = (await loadTossSdk())?.generateHapticFeedback;
|
|
170
|
+
if (typeof fn === "function") try {
|
|
171
|
+
await fn({ type });
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
function durationToHaptic(duration) {
|
|
175
|
+
if (duration <= TICK_WEAK_MAX_MS) return "tickWeak";
|
|
176
|
+
if (duration <= TICK_MEDIUM_MAX_MS) return "tickMedium";
|
|
177
|
+
return "basicMedium";
|
|
178
|
+
}
|
|
179
|
+
function vibrateShim(pattern) {
|
|
180
|
+
const arr = Array.isArray(pattern) ? pattern : [pattern];
|
|
181
|
+
if (arr.length === 0 || arr.every((n) => n === 0)) {
|
|
182
|
+
(async () => {
|
|
183
|
+
if (!await isTossEnvironment()) navigator[BACKUP_KEY]?.(pattern);
|
|
184
|
+
})();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
(async () => {
|
|
188
|
+
if (await isTossEnvironment()) {
|
|
189
|
+
if (!Array.isArray(pattern)) {
|
|
190
|
+
await haptic(durationToHaptic(pattern));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
for (let i = 0; i < pattern.length; i += 2) {
|
|
194
|
+
const on = pattern[i];
|
|
195
|
+
if (on === void 0) break;
|
|
196
|
+
if (on > 0) await haptic("tap");
|
|
197
|
+
const pause = pattern[i + 1];
|
|
198
|
+
if (typeof pause === "number" && pause > 0) await new Promise((r) => setTimeout(r, pause));
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const original = navigator[BACKUP_KEY];
|
|
203
|
+
original?.(pattern);
|
|
204
|
+
})();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
function installVibrateShim() {
|
|
208
|
+
if (typeof navigator === "undefined") return () => {};
|
|
209
|
+
const host = navigator;
|
|
210
|
+
if (BACKUP_KEY in host) return () => uninstallVibrateShim();
|
|
211
|
+
const nav = navigator;
|
|
212
|
+
host[BACKUP_KEY] = nav.vibrate ? nav.vibrate.bind(navigator) : void 0;
|
|
213
|
+
const snapshot = installObjectMethods(navigator, { vibrate: vibrateShim });
|
|
214
|
+
if (!snapshot) {
|
|
215
|
+
delete host[BACKUP_KEY];
|
|
216
|
+
return () => uninstallVibrateShim();
|
|
217
|
+
}
|
|
218
|
+
host[SNAPSHOT_KEY] = snapshot;
|
|
219
|
+
return uninstallVibrateShim;
|
|
220
|
+
}
|
|
221
|
+
function uninstallVibrateShim() {
|
|
222
|
+
if (typeof navigator === "undefined") return;
|
|
223
|
+
const host = navigator;
|
|
224
|
+
if (!(BACKUP_KEY in host)) return;
|
|
225
|
+
const snapshot = host[SNAPSHOT_KEY];
|
|
226
|
+
if (snapshot) restoreObjectMethods(snapshot);
|
|
227
|
+
delete host[BACKUP_KEY];
|
|
228
|
+
delete host[SNAPSHOT_KEY];
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
exports.haptic = haptic;
|
|
232
|
+
exports.installVibrateShim = installVibrateShim;
|
|
233
|
+
exports.uninstallVibrateShim = uninstallVibrateShim;
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=vibrate.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vibrate.cjs","names":[],"sources":["../../src/detect.ts","../../src/shims/_install-helpers.ts","../../src/shims/vibrate.ts"],"sourcesContent":["/**\n * Environment detection: are we running inside Apps in Toss, or a plain browser?\n *\n * Strategy: call the SDK's `getAppsInTossGlobals()` — a synchronous export\n * that returns the runtime's Toss globals (deploymentId, brand name, …)\n * inside the Apps in Toss runtime and throws (RN bridge unavailable)\n * anywhere else. The SDK itself is an **optional** peer dependency; if its\n * module can't be imported we are definitely not inside Toss.\n *\n * Just having the SDK module resolvable is not enough — apps can bundle it\n * and still run in a plain browser. We need the bridge probe to confirm.\n *\n * UA sniffing (spoofable) is avoided. We do call `getAppsInTossGlobals`, but\n * that's a constant read from the bridge — no permission dialogs, no\n * analytics fire. In a plain browser the bridge lookup fails fast (sync\n * throw, microsecond-scale), so the startup cost is negligible.\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 if (typeof mod?.getAppsInTossGlobals !== 'function') {\n cached = false;\n return cached;\n }\n // Inside Toss the bridge returns a populated globals object. In a plain\n // browser the RN bridge isn't attached and the call throws — that's our\n // signal. Any non-throwing call with an object return is treated as Toss.\n try {\n const globals = mod.getAppsInTossGlobals();\n cached = Boolean(globals) && typeof globals === 'object';\n } catch {\n cached = false;\n }\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 * Shared helpers for installing shims on `navigator`.\n *\n * Chromium now marks a handful of `navigator` properties (e.g. `geolocation`,\n * `clipboard`) as non-configurable **own** properties on the instance. That\n * means a plain `Object.defineProperty(navigator, 'x', …)` throws\n * `TypeError: Cannot redefine property`.\n *\n * The workaround is to shim at the prototype level — `Navigator.prototype`\n * keeps these as configurable accessors, so we can swap them there and every\n * instance that falls through to the prototype (including `window.navigator`)\n * sees the shim. We only reach for the prototype when the instance-level\n * assignment refuses.\n *\n * For restoration we remember the descriptor chain (instance + prototype) so\n * `uninstall()` puts the browser back in its original state.\n */\n\ntype PropertyLocation = 'instance' | 'prototype';\n\nexport interface InstallSnapshot {\n /** Where we ended up writing the shim. */\n location: PropertyLocation;\n /** Original descriptor at that location (may be undefined if nothing was there). */\n originalDescriptor: PropertyDescriptor | undefined;\n /** `true` iff the original property lived on the instance before we touched it. */\n instanceHadOwn: boolean;\n}\n\n/**\n * Install `descriptor` at `navigator[prop]`. Prefer instance-level; if the\n * browser refuses (property is non-configurable on the instance), install on\n * `Navigator.prototype` instead.\n *\n * Returns a snapshot describing where the original value was, which\n * `restoreNavigatorProperty` uses to undo the install.\n */\nexport function installNavigatorProperty(\n prop: string,\n descriptor: PropertyDescriptor,\n): InstallSnapshot {\n const nav = navigator as unknown as Record<PropertyKey, unknown>;\n const instanceDesc = Object.getOwnPropertyDescriptor(nav, prop);\n const instanceHadOwn = instanceDesc !== undefined;\n\n // Fast path: instance-level property is missing or configurable.\n if (!instanceDesc || instanceDesc.configurable) {\n try {\n Object.defineProperty(nav, prop, descriptor);\n return { location: 'instance', originalDescriptor: instanceDesc, instanceHadOwn };\n } catch {\n // Fall through to prototype-level install.\n }\n }\n\n // Prototype-level install. Drop the instance-level shadow so the prototype\n // accessor is visible to readers on `navigator`.\n const proto = Object.getPrototypeOf(nav) as object;\n const protoDesc = Object.getOwnPropertyDescriptor(proto, prop);\n\n if (instanceHadOwn) {\n // Try to remove the instance-level shadow. On non-configurable it throws —\n // we deliberately ignore that; prototype-level install still wins because\n // the prototype accessor shows through when we read via `navigator[prop]`.\n try {\n delete nav[prop];\n } catch {\n /* non-configurable own — leave it; prototype install still useful */\n }\n }\n\n Object.defineProperty(proto, prop, descriptor);\n return { location: 'prototype', originalDescriptor: protoDesc, instanceHadOwn };\n}\n\n/**\n * Reverse the install recorded in `snapshot`. If the original descriptor was\n * `undefined` (property didn't exist before), delete the property instead of\n * re-defining it.\n */\nexport function restoreNavigatorProperty(prop: string, snapshot: InstallSnapshot): void {\n const target =\n snapshot.location === 'instance'\n ? (navigator as unknown as Record<PropertyKey, unknown>)\n : (Object.getPrototypeOf(navigator) as object);\n\n if (snapshot.originalDescriptor) {\n try {\n Object.defineProperty(target, prop, snapshot.originalDescriptor);\n } catch {\n /* descriptor was non-configurable upstream; we can't undo — rare. */\n }\n } else {\n try {\n delete (target as Record<PropertyKey, unknown>)[prop];\n } catch {\n /* non-configurable — rare. */\n }\n }\n\n // If our install pushed past an instance shadow, we leave the instance alone\n // — the descriptor we captured for `instanceHadOwn: true` lives on the\n // instance and was not modified at install time.\n}\n\n/**\n * Method-level install snapshot. Captured per-key so `restoreObjectMethods`\n * can distinguish \"was an own property, reassign it\" from \"was inherited,\n * delete the override so the prototype method surfaces again\".\n */\nexport interface MethodInstallSnapshot {\n target: object;\n methods: Record<string, { hadOwn: boolean; original: unknown }>;\n}\n\n/**\n * Mutate methods on an existing object rather than replacing the object\n * itself. This is the path we take for `navigator.geolocation`, `navigator.share`,\n * and `navigator.vibrate` in Chromium, where the slot on `navigator` is a\n * non-configurable own property that we cannot replace — but the methods\n * themselves (or the methods on the referenced object) are still\n * `configurable: true, writable: true`.\n *\n * Each replacement is installed via plain assignment. If any slot is not\n * writable (e.g. frozen object), install is aborted and previously-applied\n * replacements are rolled back, so the caller observes an atomic \"all or\n * nothing\" failure as `null`. The caller is expected to degrade gracefully\n * (e.g. log a one-time `console.warn`) when that happens.\n */\nexport function installObjectMethods(\n target: object,\n replacements: Record<string, (...args: never[]) => unknown>,\n): MethodInstallSnapshot | null {\n const methods: Record<string, { hadOwn: boolean; original: unknown }> = {};\n const applied: string[] = [];\n const bag = target as Record<string, unknown>;\n\n for (const key of Object.keys(replacements)) {\n const hadOwn = Object.hasOwn(target, key);\n const original = bag[key];\n try {\n bag[key] = replacements[key] as unknown;\n } catch {\n // Non-writable / frozen. Roll back and return null.\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n // Verify the assignment actually stuck — silent-failure descriptors (e.g.\n // `writable: false` without strict mode) can skip the throw and leave the\n // original value in place. Treat that the same as a throw.\n if (bag[key] !== (replacements[key] as unknown)) {\n for (const applieKey of applied) {\n const prev = methods[applieKey];\n if (!prev) continue;\n if (prev.hadOwn) {\n bag[applieKey] = prev.original;\n } else {\n delete bag[applieKey];\n }\n }\n return null;\n }\n methods[key] = { hadOwn, original };\n applied.push(key);\n }\n\n return { target, methods };\n}\n\n/**\n * Reverse an `installObjectMethods` snapshot. Reassigns originals for slots\n * that were own properties before install; deletes the override for slots\n * that were inherited (so the prototype method surfaces again).\n */\nexport function restoreObjectMethods(snapshot: MethodInstallSnapshot): void {\n const bag = snapshot.target as Record<string, unknown>;\n for (const key of Object.keys(snapshot.methods)) {\n const entry = snapshot.methods[key];\n if (!entry) continue;\n try {\n if (entry.hadOwn) {\n bag[key] = entry.original;\n } else {\n delete bag[key];\n }\n } catch {\n /* frozen between install and restore — rare. */\n }\n }\n}\n","/**\n * `navigator.vibrate` shim.\n *\n * Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`.\n * Single-duration calls land in three buckets so the qualitative SDK haptic\n * tracks intensity a little more closely than the original two-bucket split:\n * - `vibrate(0)` → no-op (web standard: cancels pending vibration)\n * - `vibrate(1..20ms)` → `tickWeak`\n * - `vibrate(21..45ms)` → `tickMedium`\n * - `vibrate(>=46ms)` → `basicMedium`\n * - `vibrate(number[])` → iterates \"on\" segments (even indices) as `tap`\n * pulses with `setTimeout` gaps. Per-element ms mapping is intentionally\n * skipped: arrays in the wild are mostly rhythmic patterns, and the SDK\n * has no \"stronger heavy\" variant to reach for, so per-pulse precision\n * buys little. Callers needing intent should use `vibrateSemantic`.\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 * Install strategy: **method-level** on `navigator`. We assign our wrapper to\n * `navigator.vibrate` (creating an own shadow over the prototype method) and\n * delete it on uninstall so the prototype re-surfaces. We do not mutate\n * `Navigator.prototype` itself — browsers may mark it non-configurable.\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. Length-only mapping cannot recover semantic\n * intent (success vs. error vs. warning); use `vibrateSemantic` for that.\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';\nimport {\n installObjectMethods,\n type MethodInstallSnapshot,\n restoreObjectMethods,\n} from './_install-helpers.js';\n\nconst BACKUP_KEY = Symbol.for('@ait-co/polyfill/vibrate.original');\nconst SNAPSHOT_KEY = Symbol.for('@ait-co/polyfill/vibrate.snapshot');\n\ntype VibrateFn = (pattern: VibratePattern) => boolean;\n\ninterface BackupHost {\n [BACKUP_KEY]?: VibrateFn | undefined;\n [SNAPSHOT_KEY]?: MethodInstallSnapshot | undefined;\n}\n\n// Mapping thresholds chosen so the existing dog-food cases keep their old\n// haptic (vibrate(20) → tickWeak, vibrate(50)+ → basicMedium) while a 21–45ms\n// nudge — too long for \"tick\", too short for \"basic\" — gets `tickMedium`.\nconst TICK_WEAK_MAX_MS = 20;\nconst TICK_MEDIUM_MAX_MS = 45;\n\ntype HapticType =\n | 'tickWeak'\n | 'tap'\n | 'tickMedium'\n | 'softMedium'\n | 'basicWeak'\n | 'basicMedium'\n | 'success'\n | 'error'\n | 'wiggle'\n | 'confetti';\n\nexport async 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 if (duration <= TICK_WEAK_MAX_MS) return 'tickWeak';\n if (duration <= TICK_MEDIUM_MAX_MS) return 'tickMedium';\n return '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]?.(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?.(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?: VibrateFn };\n // Capture the native method BEFORE we patch, bound to `navigator` so our\n // fallback call keeps the correct `this` and never recurses into our shim.\n host[BACKUP_KEY] = nav.vibrate ? nav.vibrate.bind(navigator) : undefined;\n\n const snapshot = installObjectMethods(navigator, {\n vibrate: vibrateShim as (...args: never[]) => unknown,\n });\n\n if (!snapshot) {\n delete host[BACKUP_KEY];\n return () => uninstallVibrateShim();\n }\n\n host[SNAPSHOT_KEY] = snapshot;\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 snapshot = host[SNAPSHOT_KEY];\n if (snapshot) restoreObjectMethods(snapshot);\n delete host[BACKUP_KEY];\n delete host[SNAPSHOT_KEY];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/shims/vibrate.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `navigator.vibrate` shim.
|
|
4
|
+
*
|
|
5
|
+
* Inside Apps in Toss → best-effort mapping to SDK `generateHapticFeedback`.
|
|
6
|
+
* Single-duration calls land in three buckets so the qualitative SDK haptic
|
|
7
|
+
* tracks intensity a little more closely than the original two-bucket split:
|
|
8
|
+
* - `vibrate(0)` → no-op (web standard: cancels pending vibration)
|
|
9
|
+
* - `vibrate(1..20ms)` → `tickWeak`
|
|
10
|
+
* - `vibrate(21..45ms)` → `tickMedium`
|
|
11
|
+
* - `vibrate(>=46ms)` → `basicMedium`
|
|
12
|
+
* - `vibrate(number[])` → iterates "on" segments (even indices) as `tap`
|
|
13
|
+
* pulses with `setTimeout` gaps. Per-element ms mapping is intentionally
|
|
14
|
+
* skipped: arrays in the wild are mostly rhythmic patterns, and the SDK
|
|
15
|
+
* has no "stronger heavy" variant to reach for, so per-pulse precision
|
|
16
|
+
* buys little. Callers needing intent should use `vibrateSemantic`.
|
|
17
|
+
*
|
|
18
|
+
* Outside Apps in Toss → defers to the browser's native `navigator.vibrate`,
|
|
19
|
+
* or returns `false` when unavailable (matches the spec — browsers that don't
|
|
20
|
+
* support vibration simply return `false`).
|
|
21
|
+
*
|
|
22
|
+
* Install strategy: **method-level** on `navigator`. We assign our wrapper to
|
|
23
|
+
* `navigator.vibrate` (creating an own shadow over the prototype method) and
|
|
24
|
+
* delete it on uninstall so the prototype re-surfaces. We do not mutate
|
|
25
|
+
* `Navigator.prototype` itself — browsers may mark it non-configurable.
|
|
26
|
+
*
|
|
27
|
+
* Caveats (documented in CLAUDE.md as the known lossy trade-off):
|
|
28
|
+
* - SDK haptics are qualitative ("tickWeak", "basicMedium"), not millisecond
|
|
29
|
+
* durations. The shim approximates intensity from duration but cannot
|
|
30
|
+
* reproduce exact patterns. Length-only mapping cannot recover semantic
|
|
31
|
+
* intent (success vs. error vs. warning); use `vibrateSemantic` for that.
|
|
32
|
+
* - Arrays are fired sequentially via `setTimeout`; gaps between pulses are
|
|
33
|
+
* honoured only as "time until the next tap", not as silent-vs-vibrating.
|
|
34
|
+
* - `vibrate` is spec'd as **synchronous**; the SDK call is async. We return
|
|
35
|
+
* `true` immediately (fire-and-forget). Errors from the SDK are swallowed.
|
|
36
|
+
*/
|
|
37
|
+
type HapticType = 'tickWeak' | 'tap' | 'tickMedium' | 'softMedium' | 'basicWeak' | 'basicMedium' | 'success' | 'error' | 'wiggle' | 'confetti';
|
|
38
|
+
declare function haptic(type: HapticType): Promise<void>;
|
|
39
|
+
declare function installVibrateShim(): () => void;
|
|
40
|
+
declare function uninstallVibrateShim(): void;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { haptic, installVibrateShim, uninstallVibrateShim };
|
|
43
|
+
//# sourceMappingURL=vibrate.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vibrate.d.cts","names":[],"sources":["../../src/shims/vibrate.ts"],"mappings":";;;;;;;AAuEA;;;;;;;;;AA+DA;;;;;AA4BA;;;;;;;;;;;;;;;KAvGK,UAAA;AAAA,iBAYiB,MAAA,CAAO,IAAA,EAAM,UAAA,GAAa,OAAA;AAAA,iBA+DhC,kBAAA,CAAA;AAAA,iBA4BA,oBAAA,CAAA"}
|