diff --git a/mobile/lib/components/bottom_bar.dart b/mobile/lib/components/bottom_bar.dart index b443a99..32f17ce 100644 --- a/mobile/lib/components/bottom_bar.dart +++ b/mobile/lib/components/bottom_bar.dart @@ -6,6 +6,7 @@ import 'package:cancerlinc/pages/checklist_page.dart'; import 'package:cancerlinc/pages/home_page.dart'; import 'package:cancerlinc/pages/login_page.dart'; import 'package:cancerlinc/pages/referrals_page.dart'; +import 'package:cancerlinc/utils/haptics.dart'; class BottomBar extends StatefulWidget { const BottomBar({super.key}); @@ -48,6 +49,7 @@ class BottomBarState extends State { ), TextButton( onPressed: () { + AppHaptics.warning(); Navigator.pop(context); setState(() { _checklistHasUnsavedChanges = false; @@ -59,6 +61,7 @@ class BottomBarState extends State { ), TextButton( onPressed: () async { + AppHaptics.confirm(); Navigator.pop(context); await _checklistKey.currentState?.save(); setState(() { @@ -74,6 +77,7 @@ class BottomBarState extends State { } Future _logout() async { + AppHaptics.warning(); await FirebaseAuth.instance.signOut(); if (!mounted) return; Navigator.of(context).pushAndRemoveUntil( diff --git a/mobile/lib/pages/calendar_page.dart b/mobile/lib/pages/calendar_page.dart index fda20c7..b89d410 100644 --- a/mobile/lib/pages/calendar_page.dart +++ b/mobile/lib/pages/calendar_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:cancerlinc/models/event.dart'; import 'package:cancerlinc/services/event_service.dart'; +import 'package:cancerlinc/utils/haptics.dart'; // ─── Design tokens (match checklist page) ────────────────────────────────────── const _dark = Color(0xFF3F454F); @@ -227,13 +228,19 @@ class _CalendarHeader extends StatelessWidget { ? 'Filter' : 'Filter (${selectedTags.length})', icon: Icons.filter_list, - onPressed: () => _showFilterSheet(context), + onPressed: () { + AppHaptics.tap(); + _showFilterSheet(context); + }, ), const SizedBox(width: 8), _GreenButton( label: 'Pick Date', icon: Icons.calendar_today_outlined, - onPressed: () => _showCalendarPicker(context), + onPressed: () { + AppHaptics.tap(); + _showCalendarPicker(context); + }, ), ], ), @@ -313,6 +320,7 @@ class _CalendarHeader extends StatelessWidget { backgroundColor: _green, ), onPressed: () { + AppHaptics.confirm(); onFilterChanged(tempSelected); Navigator.pop(context); }, @@ -545,7 +553,10 @@ class _EventCard extends StatelessWidget { backgroundColor: _green, padding: const EdgeInsets.symmetric(vertical: 12), ), - onPressed: () => Navigator.pop(context), + onPressed: () { + AppHaptics.tap(); + Navigator.pop(context); + }, child: Text('Close', style: _bold(15)), ), ), diff --git a/mobile/lib/pages/change_password.dart b/mobile/lib/pages/change_password.dart index 7af42cf..c7a7d57 100644 --- a/mobile/lib/pages/change_password.dart +++ b/mobile/lib/pages/change_password.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:cancerlinc/utils/haptics.dart'; class ChangePasswordPage extends StatefulWidget { final String code; // verification code passed from forgot_password.dart @@ -39,6 +40,7 @@ class _ChangePasswordPageState extends State { void _onSubmit() { // require that the incoming verification code is not empty if (widget.code.isEmpty) { + AppHaptics.warning(); // shouldn't happen if caller passed a code, but guard anyway showDialog( context: context, @@ -57,6 +59,7 @@ class _ChangePasswordPageState extends State { } if (_formKey.currentState?.validate() ?? false) { + AppHaptics.confirm(); // at this point we'd call the backend to change the password using // the verification code (widget.code) and the new password. // for this model, just show a confirmation and pop to login diff --git a/mobile/lib/pages/chat_page.dart b/mobile/lib/pages/chat_page.dart index 8f5866e..515e535 100644 --- a/mobile/lib/pages/chat_page.dart +++ b/mobile/lib/pages/chat_page.dart @@ -10,6 +10,7 @@ import 'package:cancerlinc/services/chat_service.dart'; import 'package:cancerlinc/services/notification_service.dart'; import 'package:cancerlinc/components/call_number.dart'; import 'package:cancerlinc/components/search_panel.dart'; +import 'package:cancerlinc/utils/haptics.dart'; /// FEATURE FLAG (Ticket M4) — show a non-blocking "pending verification" /// notice to socially-unverified (non-banned) patients. They can STILL chat; @@ -959,7 +960,12 @@ class _InputButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: onTap == null + ? null + : () { + AppHaptics.tap(); + onTap!(); + }, child: Container( width: 48, height: 48, diff --git a/mobile/lib/pages/checklist_page.dart b/mobile/lib/pages/checklist_page.dart index e356eb7..0f4a0ed 100644 --- a/mobile/lib/pages/checklist_page.dart +++ b/mobile/lib/pages/checklist_page.dart @@ -2,6 +2,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:cancerlinc/models/checklist.dart'; import 'package:cancerlinc/services/checklist_service.dart'; +import 'package:cancerlinc/utils/haptics.dart'; // ─── Design tokens ───────────────────────────────────────────────────────────── const _dark = Color(0xFF3F454F); @@ -168,6 +169,7 @@ class ChecklistPageState extends State { _DarkButton( label: 'Add', onPressed: () { + AppHaptics.confirm(); final tempId = 'local_${DateTime.now().millisecondsSinceEpoch}'; setState(() { _localChecklists.add({ @@ -206,6 +208,7 @@ class ChecklistPageState extends State { _DarkButton( label: 'Delete', onPressed: () { + AppHaptics.warning(); final i = _localIndexByDocId(docId); if (i != -1) { setState(() => _localChecklists[i]['archived'] = true); @@ -238,6 +241,7 @@ class ChecklistPageState extends State { _DarkButton( label: 'Add', onPressed: () { + AppHaptics.tap(); final i = _localIndexByDocId(docId); if (i != -1) { setState(() { @@ -361,7 +365,12 @@ class ChecklistPageState extends State { ), const SizedBox(width: 8), GestureDetector( - onTap: canSave ? save : null, + onTap: canSave + ? () { + AppHaptics.confirm(); + save(); + } + : null, child: Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12), @@ -668,7 +677,10 @@ class ArchivePage extends StatelessWidget { style: _regular(14, color: _placeholder)), const SizedBox(height: 14), GestureDetector( - onTap: () => checklistService.duplicateChecklist(checklist), + onTap: () { + AppHaptics.tap(); + checklistService.duplicateChecklist(checklist); + }, child: Container( height: 40, decoration: BoxDecoration( diff --git a/mobile/lib/pages/create_account.dart b/mobile/lib/pages/create_account.dart index 4c52d2e..01dd15a 100644 --- a/mobile/lib/pages/create_account.dart +++ b/mobile/lib/pages/create_account.dart @@ -3,6 +3,7 @@ import 'package:cancerlinc/services/auth.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:cancerlinc/utils/haptics.dart'; class CreateAccountPage extends StatefulWidget { const CreateAccountPage({super.key}); @@ -111,6 +112,7 @@ class _CreateAccountPageState extends State { ); } } on FirebaseAuthException catch (e) { + AppHaptics.warning(); setState(() { _errorMessage = _getFirebaseError(e); }); @@ -357,7 +359,10 @@ class _CreateAccountPageState extends State { width: double.infinity, height: 48, child: ElevatedButton( - onPressed: _onSignUp, + onPressed: () { + AppHaptics.confirm(); + _onSignUp(); + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF43474F), shape: RoundedRectangleBorder( diff --git a/mobile/lib/pages/login_page.dart b/mobile/lib/pages/login_page.dart index 67c8c70..04f15c2 100644 --- a/mobile/lib/pages/login_page.dart +++ b/mobile/lib/pages/login_page.dart @@ -5,6 +5,7 @@ import 'package:cancerlinc/pages/forgot_password.dart'; import 'package:cancerlinc/pages/create_account.dart'; import 'package:cancerlinc/components/bottom_bar.dart'; import 'package:cancerlinc/services/auth.dart'; +import 'package:cancerlinc/utils/haptics.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -185,6 +186,7 @@ class LoginPage extends StatefulWidget { height: 46, child: ElevatedButton( onPressed: () async { + AppHaptics.confirm(); try { final userCredential = await _authService.signIn( _emailController.text.trim(), @@ -202,6 +204,7 @@ class LoginPage extends StatefulWidget { ); } } on FirebaseAuthException { + AppHaptics.warning(); setState(() { _errorMessage = 'Incorrect Email or Password Entered'; }); @@ -230,6 +233,7 @@ class LoginPage extends StatefulWidget { height: 46, child: ElevatedButton( onPressed: () { + AppHaptics.tap(); Navigator.push( context, MaterialPageRoute(builder: (context) => CreateAccountPage()), diff --git a/mobile/lib/pages/verify_email.dart b/mobile/lib/pages/verify_email.dart index 410791c..c7a2a80 100644 --- a/mobile/lib/pages/verify_email.dart +++ b/mobile/lib/pages/verify_email.dart @@ -3,6 +3,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:cancerlinc/components/bottom_bar.dart'; +import 'package:cancerlinc/utils/haptics.dart'; class VerifyEmail extends StatefulWidget { final String email; @@ -131,8 +132,11 @@ class _VerifyEmailState extends State { width: double.infinity, height: 46, child: OutlinedButton( - onPressed: () => - FirebaseAuth.instance.currentUser?.sendEmailVerification(), + onPressed: () { + AppHaptics.tap(); + FirebaseAuth.instance.currentUser + ?.sendEmailVerification(); + }, style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), overlayColor: Colors.black, @@ -147,7 +151,10 @@ class _VerifyEmailState extends State { width: double.infinity, height: 46, child: ElevatedButton( - onPressed: () => Navigator.popUntil(context, (route) => route.isFirst), + onPressed: () { + AppHaptics.tap(); + Navigator.popUntil(context, (route) => route.isFirst); + }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF43474F), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), diff --git a/mobile/lib/utils/haptics.dart b/mobile/lib/utils/haptics.dart new file mode 100644 index 0000000..4d6a5af --- /dev/null +++ b/mobile/lib/utils/haptics.dart @@ -0,0 +1,10 @@ +import 'package:flutter/services.dart'; + +// handles haptic feedback consistently throughout app +class AppHaptics { + static void tap() => HapticFeedback.lightImpact(); // routine buttons + static void confirm() => HapticFeedback.mediumImpact(); // submit/save actions + static void warning() => + HapticFeedback.heavyImpact(); // destructive/critical actions + static void select() => HapticFeedback.selectionClick(); // nav/tab switches +}