Files
eranos/src/lib/haptics.ts
T
Alex Gleason 1a106545f7 Fix haptics: call isNativePlatform() at invocation time, log errors
The platform check was cached as a module-level constant, which could
evaluate before the Capacitor bridge was ready. Moved to per-call checks
matching the pattern used everywhere else in the codebase. Also replaced
silent .catch(() => {}) with console.warn so failures are visible in
Safari Web Inspector / Xcode console.
2026-04-14 14:08:32 -05:00

118 lines
3.6 KiB
TypeScript

import { Capacitor } from '@capacitor/core';
/**
* Centralized haptic feedback utility.
*
* On native (iOS/Android) it uses @capacitor/haptics for true taptic engine
* feedback. On web it falls back to navigator.vibrate() which works on
* Android browsers but is a silent no-op elsewhere.
*/
type ImpactStyle = 'Heavy' | 'Medium' | 'Light';
type NotificationType = 'Success' | 'Warning' | 'Error';
// Lazy-loaded Haptics plugin — only imported on native to avoid bundling
// the plugin in web builds where it isn't useful.
let hapticsPromise: Promise<typeof import('@capacitor/haptics')> | null = null;
function getHaptics() {
if (!hapticsPromise) {
hapticsPromise = import('@capacitor/haptics');
}
return hapticsPromise;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
async function nativeImpact(style: ImpactStyle) {
const { Haptics, ImpactStyle } = await getHaptics();
await Haptics.impact({ style: ImpactStyle[style] });
}
async function nativeNotification(type: NotificationType) {
const { Haptics, NotificationType } = await getHaptics();
await Haptics.notification({ type: NotificationType[type] });
}
async function nativeSelectionChanged() {
const { Haptics } = await getHaptics();
await Haptics.selectionChanged();
}
function vibrate(ms: number) {
try {
navigator.vibrate?.(ms);
} catch {
/* Vibration API not available */
}
}
// ── Public API ───────────────────────────────────────────────────────────────
function warnHapticError(label: string, err: unknown) {
console.warn(`[haptics] ${label} failed:`, err);
}
/** Light tap — reactions, reposts, bookmarks, share. */
export function impactLight(): void {
if (Capacitor.isNativePlatform()) {
nativeImpact('Light').catch((e) => warnHapticError('impactLight', e));
} else {
vibrate(10);
}
}
/** Medium tap — zap button press, pull-to-refresh threshold, follow. */
export function impactMedium(): void {
if (Capacitor.isNativePlatform()) {
nativeImpact('Medium').catch((e) => warnHapticError('impactMedium', e));
} else {
vibrate(20);
}
}
/** Heavy tap — game button press, letter seal. */
export function impactHeavy(): void {
if (Capacitor.isNativePlatform()) {
nativeImpact('Heavy').catch((e) => warnHapticError('impactHeavy', e));
} else {
vibrate(30);
}
}
/** Success notification — zap payment success, post published. */
export function notificationSuccess(): void {
if (Capacitor.isNativePlatform()) {
nativeNotification('Success').catch((e) => warnHapticError('notificationSuccess', e));
} else {
vibrate(15);
}
}
/** Warning notification. */
export function notificationWarning(): void {
if (Capacitor.isNativePlatform()) {
nativeNotification('Warning').catch((e) => warnHapticError('notificationWarning', e));
} else {
vibrate(20);
}
}
/** Error notification. */
export function notificationError(): void {
if (Capacitor.isNativePlatform()) {
nativeNotification('Error').catch((e) => warnHapticError('notificationError', e));
} else {
vibrate(30);
}
}
/** Selection changed — toggle switches, tab taps, picker changes. */
export function selectionChanged(): void {
if (Capacitor.isNativePlatform()) {
nativeSelectionChanged().catch((e) => warnHapticError('selectionChanged', e));
} else {
vibrate(5);
}
}