From c46eb3a35df95f23c4268eba73b2f6b3c21e107b Mon Sep 17 00:00:00 2001 From: edwh Date: Sun, 14 Jun 2026 20:49:53 +0100 Subject: [PATCH] feat(app): haptic feedback (@capacitor/haptics) + useHaptics composable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds @capacitor/haptics and a useHaptics() composable — a no-op on web, lazily importing the native plugin in-app, exposing light/medium/heavy impacts and success/warning/error notification feedback (all best-effort). First wiring: a success haptic when a chat reply is sent from a push notification (mobile store handleReplyAction). Reusable for posting, sending, swipe actions etc. Co-Authored-By: Claude Opus 4.8 --- iznik-nuxt3/composables/useHaptics.js | 59 +++++++++++++++++++++++++++ iznik-nuxt3/package-lock.json | 18 ++++++-- iznik-nuxt3/package.json | 3 +- iznik-nuxt3/stores/mobile.js | 9 ++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 iznik-nuxt3/composables/useHaptics.js diff --git a/iznik-nuxt3/composables/useHaptics.js b/iznik-nuxt3/composables/useHaptics.js new file mode 100644 index 0000000000..8a728c16a3 --- /dev/null +++ b/iznik-nuxt3/composables/useHaptics.js @@ -0,0 +1,59 @@ +// Lightweight haptic-feedback wrapper for Vue components. +// +// No-ops on the web (and silently when the native engine is unavailable) and +// lazily imports @capacitor/haptics only inside the app, so the web bundle +// never pulls in the native plugin. Every method is fire-and-forget — haptics +// are a nicety and must never break or block a user flow. +// +// Usage: +// const haptics = useHaptics() +// haptics.success() // e.g. after a post/message succeeds +// haptics.light() // e.g. on a small confirm / selection +import { useMobileStore } from '~/stores/mobile' + +let _mod = null +async function load() { + if (_mod) return _mod + _mod = await import('@capacitor/haptics') + return _mod +} + +export function useHaptics() { + const mobileStore = useMobileStore() + + const fire = async (fn) => { + if (!mobileStore.isApp) return + try { + await fn(await load()) + } catch { + // best-effort: a missing/unsupported haptic engine must not surface. + } + } + + return { + light: () => + fire(({ Haptics, ImpactStyle }) => + Haptics.impact({ style: ImpactStyle.Light }) + ), + medium: () => + fire(({ Haptics, ImpactStyle }) => + Haptics.impact({ style: ImpactStyle.Medium }) + ), + heavy: () => + fire(({ Haptics, ImpactStyle }) => + Haptics.impact({ style: ImpactStyle.Heavy }) + ), + success: () => + fire(({ Haptics, NotificationType }) => + Haptics.notification({ type: NotificationType.Success }) + ), + warning: () => + fire(({ Haptics, NotificationType }) => + Haptics.notification({ type: NotificationType.Warning }) + ), + error: () => + fire(({ Haptics, NotificationType }) => + Haptics.notification({ type: NotificationType.Error }) + ), + } +} diff --git a/iznik-nuxt3/package-lock.json b/iznik-nuxt3/package-lock.json index ebebc09caa..6b23e8e097 100644 --- a/iznik-nuxt3/package-lock.json +++ b/iznik-nuxt3/package-lock.json @@ -15,6 +15,7 @@ "@capacitor/camera": "^7.0.0", "@capacitor/core": "^7.0.0", "@capacitor/device": "^7.0.0", + "@capacitor/haptics": "^7.0.5", "@capacitor/ios": "^7.0.0", "@capacitor/network": "^7.0.0", "@capacitor/screen-orientation": "^7.0.2", @@ -30,7 +31,7 @@ "@fortawesome/free-brands-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/vue-fontawesome": "^3.0.0-5", - "@freegle/capacitor-push-notifications-cap7": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/1846573c11648763ddfc893deaec5b35b3196275.tar.gz", + "@freegle/capacitor-push-notifications-cap7": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/29ec671e99882e155df289ebad8e81c80d4fd893.tar.gz", "@google-pay/button-element": "^3.1.0", "@nuxtjs/sentry": "^5.1.7", "@pinia/nuxt": "0.9.0", @@ -2069,6 +2070,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/haptics": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.5.tgz", + "integrity": "sha512-ujXuUkL8UG1YLhuJo/q+jiHr0K5JmclGZHmV1RttmiUBRG1XpC4Fc4gXUJyEX+6IoyfwWhgtPvJFRUB31HYnRQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.4.4.tgz", @@ -3114,9 +3124,9 @@ } }, "node_modules/@freegle/capacitor-push-notifications-cap7": { - "version": "7.0.4", - "resolved": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/1846573c11648763ddfc893deaec5b35b3196275.tar.gz", - "integrity": "sha512-TbNyOHSX7Urlga9WgSJ8PDqdmts5CJX/5ULiBNOR15JBZHVSfMQ0H7+g/3/c0gpKaM03oINvaDLgydZh4aB9iA==", + "version": "7.0.3", + "resolved": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/29ec671e99882e155df289ebad8e81c80d4fd893.tar.gz", + "integrity": "sha512-3+ZIdeRRFFUWrqb/hr4hB335w/lPKDIxS24pcR4yQ8WUuchmSOi1cwFsTpNNRb/o2Wxve4rYu6emZ6iLPy5Nag==", "license": "MIT", "peerDependencies": { "@capacitor/core": ">=7.0.0" diff --git a/iznik-nuxt3/package.json b/iznik-nuxt3/package.json index 00d75d2fe4..0876b46456 100644 --- a/iznik-nuxt3/package.json +++ b/iznik-nuxt3/package.json @@ -102,6 +102,7 @@ "@capacitor/camera": "^7.0.0", "@capacitor/core": "^7.0.0", "@capacitor/device": "^7.0.0", + "@capacitor/haptics": "^7.0.5", "@capacitor/ios": "^7.0.0", "@capacitor/network": "^7.0.0", "@capacitor/screen-orientation": "^7.0.2", @@ -117,7 +118,7 @@ "@fortawesome/free-brands-svg-icons": "^6.2.0", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/vue-fontawesome": "^3.0.0-5", - "@freegle/capacitor-push-notifications-cap7": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/1846573c11648763ddfc893deaec5b35b3196275.tar.gz", + "@freegle/capacitor-push-notifications-cap7": "https://github.com/Freegle/capacitor-push-notifications-cap7/archive/29ec671e99882e155df289ebad8e81c80d4fd893.tar.gz", "@google-pay/button-element": "^3.1.0", "@nuxtjs/sentry": "^5.1.7", "@pinia/nuxt": "0.9.0", diff --git a/iznik-nuxt3/stores/mobile.js b/iznik-nuxt3/stores/mobile.js index 0e7a81ba09..a64241d5f9 100644 --- a/iznik-nuxt3/stores/mobile.js +++ b/iznik-nuxt3/stores/mobile.js @@ -671,6 +671,15 @@ export const useMobileStore = defineStore({ message: replyText, }) console.log('handleReplyAction: message sent successfully') + // Confirm the reply with a success haptic (best-effort; in-app only). + try { + const { Haptics, NotificationType } = await import( + '@capacitor/haptics' + ) + await Haptics.notification({ type: NotificationType.Success }) + } catch (he) { + dbg()?.debug('haptic not available', he?.message) + } } catch (e) { console.error('handleReplyAction error:', e.message) }