Skip to content

Commit

Permalink
Improve product could not be added bad request error handling.
Browse files Browse the repository at this point in the history
Signed-off-by: Dmytro Turskyi <[email protected]>
  • Loading branch information
Turskyi committed Feb 13, 2024
1 parent a7eac69 commit c56b16a
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 57 deletions.
1 change: 1 addition & 0 deletions components/core/entities/lib/entities.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export 'src/enums/language.dart';
export 'src/enums/product_info_type.dart';
export 'src/enums/vegan.dart';
export 'src/enums/vegetarian.dart';
export 'src/exception/bad_request_error.dart';
export 'src/exception/internal_server_error.dart';
export 'src/exception/not_found_exception.dart';
export 'src/logging_interceptor/logging_interceptor.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class BadRequestError implements Exception {
const BadRequestError(this.message);

final String message;

@override
String toString() => message;
}
35 changes: 30 additions & 5 deletions components/interface_adapters/lib/src/error_message_extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,41 @@ import 'package:html/parser.dart';
String extractErrorMessage(String exceptionSource) {
final Document document = parse(exceptionSource);

// Extract the text content within the <div class="main-content"> element
final Element? mainContentElement = document.querySelector(
'div.main-content',
);
// Prioritize the main content element if it exists.
final Element? mainContentElement =
document.querySelector('div.main-content');
if (mainContentElement != null) {
final String message = mainContentElement.text.trim();
return message;
}

// If the main content element is not found, extract from other potential
// elements.
final String message = _extractMessageFromAlternativeElements(document);
return message.isNotEmpty ? message : _defaultErrorMessage();
}

String _extractMessageFromAlternativeElements(Document document) {
final List<Element?> potentialElements = <Element?>[
document.querySelector('h1'),
document.querySelector('pre'),
document.querySelector('p'),
];

final StringBuffer messageBuffer = StringBuffer();
for (Element? element in potentialElements) {
if (element != null) {
messageBuffer.write(element.text.trim());
// Add a newline for better readability.
messageBuffer.write('\n');
}
}

return messageBuffer.toString().trim();
}

String _defaultErrorMessage() {
return 'We are currently undergoing a major migration, and our services, '
'including the Web platform, mobile app, API, and Producer Platform, '
'are temporarily unavailable.';
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:entities/entities.dart';
import 'package:flutter/material.dart';
import 'package:interface_adapters/src/error_message_extractor.dart';
import 'package:interface_adapters/src/ui/modules/photo/photo_event.dart';
import 'package:use_cases/use_cases.dart';

Expand Down Expand Up @@ -37,7 +38,9 @@ class PhotoPresenter extends Bloc<PhotoEvent, PhotoViewModel> {
emit(
AddIngredientsErrorState(
barcode: event.productPhoto.info.barcode,
errorMessage: error.toString(),
errorMessage: error is BadRequestError
? extractErrorMessage(error.message)
: error.toString(),
),
);
debugPrint('Stacktrace: $stacktrace');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import 'dart:io';

import 'package:camera/camera.dart';
import 'package:entities/entities.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:interface_adapters/src/ui/modules/photo/photo_event.dart';
import 'package:interface_adapters/src/ui/modules/photo/photo_presenter.dart';
import 'package:interface_adapters/src/ui/res/resources.dart';
import 'package:interface_adapters/src/ui/res/values/dimens.dart';
import 'package:url_launcher/url_launcher.dart';

class PhotoView extends StatefulWidget {
const PhotoView({
Expand All @@ -27,6 +30,7 @@ class PhotoView extends StatefulWidget {
class _CameraScreenState extends State<PhotoView> {
late CameraController _controller;
late Future<void> _initializeControllerFuture;
final RegExp _emailExp = RegExp(r'\b[\w.-]+@[\w.-]+\.\w{2,}\b');

@override
void initState() {
Expand Down Expand Up @@ -183,14 +187,33 @@ class _CameraScreenState extends State<PhotoView> {
if (viewModel is AddIngredientsErrorState)
Container(
padding: const EdgeInsets.all(20),
color: Colors.black.withOpacity(0.7),
alignment: Alignment.center,
child: Text(
viewModel.errorMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: Theme.of(context).textTheme.bodyLarge?.fontSize,
child: GestureDetector(
onLongPress: () {
Clipboard.setData(
ClipboardData(text: viewModel.errorMessage),
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Copied to clipboard',
textAlign: TextAlign.right,
),
),
);
},
child: RichText(
textAlign: TextAlign.center,
strutStyle: StrutStyle(
fontWeight: FontWeight.bold,
fontSize:
Theme.of(context).textTheme.bodyLarge?.fontSize,
),
text: _getTextSpan(
viewModel.errorMessage,
Theme.of(context).textTheme.bodyLarge?.fontSize,
),
),
),
),
Expand All @@ -200,48 +223,50 @@ class _CameraScreenState extends State<PhotoView> {
),
floatingActionButton: BlocBuilder<PhotoPresenter, PhotoViewModel>(
builder: (BuildContext context, PhotoViewModel viewModel) {
return FloatingActionButton(
onPressed: viewModel is LoadingState
? null
: viewModel is TakenPhotoState
? () {
context.read<PhotoPresenter>().add(
AddIngredientsPhotoEvent(
ProductPhoto(
path: viewModel.photoPath,
info: widget.productInfo,
),
),
);
}
: () async {
context
.read<PhotoPresenter>()
.add(const TakePhotoEvent());
try {
await _initializeControllerFuture;

// Take a picture and get the file path
XFile picture = await _controller.takePicture();

if (mounted) {
context
.read<PhotoPresenter>()
.add(TakenPhotoEvent(picture.path));
}
} catch (e) {
debugPrint('Error taking picture: $e');
}
},
child: viewModel is TakenPhotoState
? const Icon(Icons.send)
: viewModel is PhotoMakerReadyState ||
viewModel is AddIngredientsErrorState
? const Icon(Icons.camera)
: viewModel is LoadingState
? const Icon(Icons.stop)
: const SizedBox(),
);
return viewModel is AddIngredientsErrorState
? const SizedBox()
: FloatingActionButton(
onPressed: viewModel is LoadingState
? null
: viewModel is TakenPhotoState
? () {
context.read<PhotoPresenter>().add(
AddIngredientsPhotoEvent(
ProductPhoto(
path: viewModel.photoPath,
info: widget.productInfo,
),
),
);
}
: () async {
context
.read<PhotoPresenter>()
.add(const TakePhotoEvent());
try {
await _initializeControllerFuture;

// Take a picture and get the file path
XFile picture = await _controller.takePicture();

if (mounted) {
context
.read<PhotoPresenter>()
.add(TakenPhotoEvent(picture.path));
}
} catch (e) {
debugPrint('Error taking picture: $e');
}
},
child: viewModel is TakenPhotoState
? const Icon(Icons.send)
: viewModel is PhotoMakerReadyState ||
viewModel is AddIngredientsErrorState
? const Icon(Icons.camera)
: viewModel is LoadingState
? const Icon(Icons.stop)
: const SizedBox(),
);
},
),
);
Expand All @@ -252,4 +277,82 @@ class _CameraScreenState extends State<PhotoView> {
_controller.dispose();
super.dispose();
}

TextSpan _getTextSpan(String text, double? fontSize) {
final Iterable<Match> matches = _emailExp.allMatches(text);
if (matches.isEmpty) {
return TextSpan(
text: text,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
);
}

final List<TextSpan> spans = <TextSpan>[];
int start = 0;

for (final Match match in matches) {
if (match.start != start) {
spans.add(
TextSpan(
text: text.substring(start, match.start),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
),
);
}

final String email = match.group(0)!;
spans.add(
TextSpan(
text: email,
style: TextStyle(
color: Colors.lightBlueAccent,
fontWeight: FontWeight.bold,
fontSize: fontSize,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final Uri emailLaunchUri = Uri(
scheme: 'mailto',
path: email,
);

if (await canLaunchUrl(emailLaunchUri)) {
await launchUrl(emailLaunchUri);
} else {
throw PlatformException(
code: 'UNABLE_TO_LAUNCH_URL',
message: 'Could not launch $emailLaunchUri',
);
}
},
),
);

start = match.end;
}

if (start != text.length) {
spans.add(
TextSpan(
text: text.substring(start),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: fontSize,
),
),
);
}

return TextSpan(children: spans);
}
}
12 changes: 10 additions & 2 deletions lib/data/data_sources/remote/remote_data_source_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,16 @@ class RemoteDataSourceImpl implements RemoteDataSource {
newProduct,
);

if (result.status != 1) {
throw Exception('product could not be added: ${result.error}');
if (result.status == HttpStatus.badRequest) {
throw BadRequestError(
result.body != null
? '${result.body}'
: 'Product could not be added.\n${result.error ?? ''}',
);
} else if (result.status != 1) {
throw Exception(
'product could not be added ${result.error ?? ''}',
);
}
}

Expand Down

0 comments on commit c56b16a

Please sign in to comment.