src/lib/haptics.ts
PURPOSE
Thin async wrapper around @capacitor/haptics that gates all calls behind a native-platform check, so callers can fire haptic feedback without branching on platform or importing Capacitor directly.
OWNS
- Native-only guard for haptic calls (web returns immediately, no-op).
- Dynamic
import('@capacitor/haptics')so the Capacitor module never enters the web bundle’s eager graph. - Two public functions:
hapticImpact(style?: 'Heavy' | 'Medium' | 'Light')— default'Medium'.hapticNotification(type: 'Success' | 'Warning' | 'Error').
- Local
ImpactStylestring-literal type alias (not re-exported).
READS FROM
./platform→Platform.isNative()— early-exit gate.@capacitor/haptics(dynamic import) →Haptics,ImpactStyle,NotificationType.
PUSHES TO
- Native OS haptic engine via
Haptics.impact({ style })andHaptics.notification({ type }). - Nothing else — no store writes, no events, no logs.
DOES NOT
- Does not throw. Both functions wrap the dynamic import + call in
try { ... } catch { /* silent */ }. - Does not log on failure (intentional — haptics are non-essential UX).
- Does not import
@capacitor/hapticsstatically; the dynamic import keeps it tree-shakable on web. - Does not expose Capacitor types or enums to callers — only string-literal unions.
- Does not throttle, queue, or debounce. Each call hits the OS independently.
- Does not check user/settings opt-in for haptics — callers must gate that themselves if needed.
Signals
None emitted. Pure side-effect functions that return Promise<void>.
Entry points
hapticImpact(style?: 'Heavy' | 'Medium' | 'Light'): Promise<void>
hapticNotification(type: 'Success' | 'Warning' | 'Error'): Promise<void>Both async. Awaiting is optional — the promise resolves once the OS call returns (or immediately on web).
Pattern notes
- Single chokepoint for
@capacitor/haptics. Module header comment: “Never import Capacitor directly elsewhere.” Any new haptic call site goes through this file. - String enum on the public surface, Capacitor enum internally. Callers pass
'Heavy'/'Success'; the wrapper indexes into Capacitor’sImpactStyle[style]/NotificationType[type]to translate. Keeps the rest of the codebase free of Capacitor type imports. - Silent failure is intentional. Per CLAUDE.md “crash on bad data, no silent fallbacks” — haptics are an OS-boundary integration, which is the documented exception zone. A missing/broken
@capacitor/hapticsmodule on a native build degrades to no-haptic, not a crash. - Default impact style is
'Medium'. Callers that want subtler feedback must pass'Light'explicitly.
EXTRACT-CANDIDATE
The “native-only guard + dynamic-import + silent try/catch” pattern is shared with any other Capacitor wrapper (e.g. status bar, screen orientation, keyboard, splash screen). If a second Capacitor wrapper module appears, factor the guard into a withNativeCapacitor<T>(loader, call) helper in src/lib/platform.ts or a new src/lib/capacitor.ts. Until then, three lines duplicated is fine.