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 ImpactStyle string-literal type alias (not re-exported).

READS FROM

  • ./platformPlatform.isNative() — early-exit gate.
  • @capacitor/haptics (dynamic import) → Haptics, ImpactStyle, NotificationType.

PUSHES TO

  • Native OS haptic engine via Haptics.impact({ style }) and Haptics.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/haptics statically; 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’s ImpactStyle[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/haptics module 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.