From ff71a5022973199ce018b8988a7d084c52dbcb90 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 6 May 2026 14:35:35 +0530 Subject: [PATCH] fix: ignore raw Dio blobs in API responses and replace email change dialog with a scrollable bottom sheet. --- lib/core/network/api_client.dart | 37 ++- .../widgets/profile_settings_tab.dart | 284 +++++++++++++----- 2 files changed, 239 insertions(+), 82 deletions(-) diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index e9a3a52..146fc8c 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -255,20 +255,31 @@ class ApiClient { : rawMessage?.toString(); if (message != null && message.isNotEmpty) { - final errors = (dataMap['errors'] as List?) - ?.map((e) => FieldError.fromJson(e as Map)) - .toList() ?? - []; + // Defensive: if the backend echoed back the raw Dio exception + // toString (e.g. "ApiException: This exception was thrown..."), + // ignore it and fall through to the friendly status-code message. + final looksLikeRawDioBlob = + message.startsWith('ApiException:') || + message.contains('RequestOptions.validateStatus') || + message.contains('This exception was thrown because'); - return DioException( - requestOptions: error.requestOptions, - response: error.response, - error: ApiException( - message: message, - statusCode: response.statusCode, - fieldErrors: errors, - ), - ); + if (!looksLikeRawDioBlob) { + final errors = (dataMap['errors'] as List?) + ?.map( + (e) => FieldError.fromJson(e as Map)) + .toList() ?? + []; + + return DioException( + requestOptions: error.requestOptions, + response: error.response, + error: ApiException( + message: message, + statusCode: response.statusCode, + fieldErrors: errors, + ), + ); + } } } } diff --git a/lib/features/profile/presentation/widgets/profile_settings_tab.dart b/lib/features/profile/presentation/widgets/profile_settings_tab.dart index ba70109..75d8b1c 100644 --- a/lib/features/profile/presentation/widgets/profile_settings_tab.dart +++ b/lib/features/profile/presentation/widgets/profile_settings_tab.dart @@ -435,103 +435,206 @@ class _ProfileSettingsTabState extends ConsumerState { final passwordCtl = TextEditingController(); String? localError; bool submitting = false; + bool obscurePw = true; - await showDialog( + await showModalBottomSheet( context: context, - builder: (dialogCtx) { + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (sheetCtx) { return StatefulBuilder( - builder: (dialogCtx, setDialogState) { + builder: (sheetCtx, setSheetState) { Future submit() async { final email = newEmailCtl.text.trim(); final pw = passwordCtl.text; if (email.isEmpty || pw.isEmpty) { - setDialogState(() => localError = + setSheetState(() => localError = 'Please enter a new email and your current password'); return; } - setDialogState(() { + setSheetState(() { submitting = true; localError = null; }); try { final repo = ref.read(profileRepositoryProvider); final message = await repo.requestEmailChange(email, pw); - if (dialogCtx.mounted) Navigator.of(dialogCtx).pop(); + if (sheetCtx.mounted) Navigator.of(sheetCtx).pop(); if (mounted) _showSnackBar(message); } catch (e) { - setDialogState(() { + setSheetState(() { submitting = false; localError = e.toString().replaceFirst('Exception: ', ''); }); } } - return AlertDialog( - scrollable: true, - title: const Text('Change Email Address'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'A verification link will be sent to your new email. Your email changes only after you click the link.', - style: TextStyle(fontSize: 13), + // Add bottom inset so the sheet rises above the keyboard. + final bottomInset = MediaQuery.of(sheetCtx).viewInsets.bottom; + + return Padding( + padding: EdgeInsets.only(bottom: bottomInset), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + top: Radius.circular(24), ), - const SizedBox(height: 16), - TextField( - controller: newEmailCtl, - keyboardType: TextInputType.emailAddress, - autocorrect: false, - enableSuggestions: false, - textCapitalization: TextCapitalization.none, - autofillHints: const [AutofillHints.email], - decoration: const InputDecoration( - labelText: 'New Email', - hintText: 'new@example.com', + ), + padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + child: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Drag handle + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: const Color(0xFFE5E7EB), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const Text( + 'Change Email Address', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.primaryDark, + ), + ), + const SizedBox(height: 8), + const Text( + 'A verification link will be sent to your new email. Your email changes only after you click the link.', + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 13, + color: Color(0xFF6B7280), + height: 1.4, + ), + ), + const SizedBox(height: 20), + _buildSheetLabel('New Email'), + const SizedBox(height: 6), + TextField( + controller: newEmailCtl, + keyboardType: TextInputType.emailAddress, + autocorrect: false, + enableSuggestions: false, + textCapitalization: TextCapitalization.none, + autofillHints: const [AutofillHints.email], + decoration: _sheetInputDecoration( + 'new@example.com', + ), + ), + const SizedBox(height: 14), + _buildSheetLabel('Current Password'), + const SizedBox(height: 6), + TextField( + controller: passwordCtl, + obscureText: obscurePw, + autocorrect: false, + enableSuggestions: false, + // Empty autofillHints prevents iOS from showing the + // "Strong Password" suggestion bar, which was adding + // extra bottom inset. + autofillHints: const [], + decoration: _sheetInputDecoration( + 'Enter your current password', + suffixIcon: IconButton( + icon: Icon( + obscurePw + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: const Color(0xFF6B7280), + size: 20, + ), + onPressed: () => setSheetState( + () => obscurePw = !obscurePw, + ), + ), + ), + ), + if (localError != null) ...[ + const SizedBox(height: 12), + Text( + localError!, + style: const TextStyle( + fontFamily: 'SourceSerif4', + color: Colors.red, + fontSize: 12, + ), + ), + ], + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: submitting + ? null + : () => Navigator.of(sheetCtx).pop(), + style: OutlinedButton.styleFrom( + side: const BorderSide( + color: Color(0xFFE5E7EB), + ), + padding: const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryDark, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: submitting ? null : submit, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.accentOrange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 14, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: Text( + submitting ? 'Sending...' : 'Send Link', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ], ), ), - const SizedBox(height: 12), - TextField( - controller: passwordCtl, - obscureText: true, - autocorrect: false, - enableSuggestions: false, - // Empty autofillHints prevents iOS from showing the - // "Strong Password" suggestion bar, which was adding - // extra bottom inset and shifting the dialog upward. - autofillHints: const [], - decoration: const InputDecoration( - labelText: 'Current Password', - ), - ), - if (localError != null) ...[ - const SizedBox(height: 12), - Text( - localError!, - style: const TextStyle( - color: Colors.red, - fontSize: 12, - ), - ), - ], - ], + ), ), - actions: [ - TextButton( - onPressed: submitting - ? null - : () => Navigator.of(dialogCtx).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: submitting ? null : submit, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.accentOrange, - foregroundColor: Colors.white, - ), - child: Text(submitting ? 'Sending...' : 'Send Link'), - ), - ], ); }, ); @@ -541,4 +644,47 @@ class _ProfileSettingsTabState extends ConsumerState { newEmailCtl.dispose(); passwordCtl.dispose(); } + + Widget _buildSheetLabel(String text) { + return Text( + text, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.primaryDark, + ), + ); + } + + InputDecoration _sheetInputDecoration(String hint, {Widget? suffixIcon}) { + return InputDecoration( + hintText: hint, + hintStyle: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 14, + color: Color(0xFF9CA3AF), + ), + filled: true, + fillColor: const Color(0xFFF9FAFB), + contentPadding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE5E7EB)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE5E7EB)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: AppColors.accentOrange, + width: 1.5, + ), + ), + suffixIcon: suffixIcon, + ); + } }