Compare commits

...

2 Commits

Author SHA1 Message Date
NaiJi ✨ 3a5cb0f81b feat: Implement critical error handling page
Now when we can't handle an exception, application redirects user to a new ErrorPage where they can copy stacktrace, logs and share to our support chat
2022-11-08 01:22:11 +04:00
NaiJi ✨ 0df5940c68 refactor: Move external app url launching to its own function
Encapsulating logic and error handling into UiHelpers.launchExternalApplicationURL
2022-11-07 22:57:46 +04:00
7 changed files with 107 additions and 21 deletions

View File

@ -1,6 +1,11 @@
{ {
"test": "en-test", "test": "en-test",
"locale": "en", "locale": "en",
"error": {
"page_title": "Oops, something happened...",
"page_description": "We couldn't handle an exception, but it is important for us to know what just happened. Please share this log to our support!",
"page_share": "Share:"
},
"basis": { "basis": {
"providers": "Providers", "providers": "Providers",
"providers_title": "Your Data Center", "providers_title": "Your Data Center",

View File

@ -1,6 +1,11 @@
{ {
"test": "ru-test", "test": "ru-test",
"locale": "ru", "locale": "ru",
"error": {
"page_title": "Упс, что-то случилось...",
"page_description": "Нам не удалось обработать исключение, но нам важно знать, что только что произошло. Пожалуйста, отправьте нашей поддержке данный лог!",
"page_share": "Поделиться:"
},
"basis": { "basis": {
"providers": "Провайдеры", "providers": "Провайдеры",
"providers_title": "Ваш Дата Центр", "providers_title": "Ваш Дата Центр",

View File

@ -1,10 +1,12 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; import 'package:selfprivacy/theming/factory/app_theme_factory.dart';
import 'package:selfprivacy/ui/pages/error_page.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/root_route.dart';
@ -93,13 +95,15 @@ class MyApp extends StatelessWidget {
? const OnboardingPage(nextPage: InitializingPage()) ? const OnboardingPage(nextPage: InitializingPage())
: const RootPage(), : const RootPage(),
builder: (final BuildContext context, final Widget? widget) { builder: (final BuildContext context, final Widget? widget) {
Widget error = const Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder = ErrorWidget.builder =
(final FlutterErrorDetails errorDetails) => error; (final FlutterErrorDetails errorDetails) => ErrorPage(
return widget!; log:
'${errorDetails.stack?.toString() ?? ''}\n\n${errorDetails.exception}',
);
if (widget != null) {
return widget;
}
throw 'widget is null';
}, },
), ),
), ),

View File

@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:flutter/services.dart';
import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart';
import 'package:selfprivacy/utils/ui_helpers.dart';
class ErrorPage extends StatelessWidget {
const ErrorPage({required this.log, super.key});
final String log;
final String telegramUrl = 'https://t.me/selfprivacy';
@override
Widget build(final BuildContext context) => BrandHeroScreen(
heroTitle: 'error.page_title'.tr(),
heroSubtitle: 'error.page_description'.tr(),
hasBackButton: false,
hasFlashButton: false,
children: [
SizedBox(
width: 320,
child: Row(
children: [
IconButton(
icon: const Icon(Icons.content_copy_outlined),
onPressed: () async => Clipboard.setData(
ClipboardData(text: log),
),
),
const SizedBox(width: 48),
Text('error.page_share'.tr()),
const SizedBox(width: 16),
InkWell(
onTap: () async => UiHelpers.launchExternalApplicationURL(
telegramUrl,
),
child: const Icon(Icons.telegram_outlined),
),
],
),
),
const Divider(),
const SizedBox(height: 16),
Text(
log,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontFamily: 'RobotoMono',
),
),
],
);
}

View File

@ -33,6 +33,18 @@ class MorePage extends StatelessWidget {
final bool? usesBinds = final bool? usesBinds =
context.watch<ApiServerVolumeCubit>().state.usesBinds; context.watch<ApiServerVolumeCubit>().state.usesBinds;
return ListView(
children: [
ListView(
children: [
Row(
children: [Text("jfkhskdjfhdkjs")],
)
],
)
],
);
/*
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(52), preferredSize: const Size.fromHeight(52),
@ -132,7 +144,7 @@ class MorePage extends StatelessWidget {
) )
], ],
), ),
); ); */
} }
} }

View File

@ -19,7 +19,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/ui/pages/services/service_page.dart'; import 'package:selfprivacy/ui/pages/services/service_page.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';
import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:selfprivacy/utils/ui_helpers.dart';
import 'package:url_launcher/url_launcher.dart';
const switchableServices = [ const switchableServices = [
'bitwarden', 'bitwarden',
@ -36,18 +35,6 @@ class ServicesPage extends StatefulWidget {
State<ServicesPage> createState() => _ServicesPageState(); State<ServicesPage> createState() => _ServicesPageState();
} }
void _launchURL(final url) async {
try {
final Uri uri = Uri.parse(url);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} catch (e) {
print(e);
}
}
class _ServicesPageState extends State<ServicesPage> { class _ServicesPageState extends State<ServicesPage> {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
@ -181,7 +168,8 @@ class _Card extends StatelessWidget {
Column( Column(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () => _launchURL( onTap: () async =>
UiHelpers.launchExternalApplicationURL(
'https://${service.url}', 'https://${service.url}',
), ),
child: Text( child: Text(

View File

@ -1,8 +1,27 @@
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:url_launcher/url_launcher.dart';
/// it's ui helpers use only for ui components, don't use for logic components. /// it's ui helpers use only for ui components, don't use for logic components.
class UiHelpers { class UiHelpers {
static String getDomainName(final ServerInstallationState config) => static String getDomainName(final ServerInstallationState config) =>
config.isDomainSelected ? config.serverDomain!.domainName : 'example.com'; config.isDomainSelected ? config.serverDomain!.domainName : 'example.com';
static Future<bool> launchExternalApplicationURL(final url) async {
bool launched = false;
try {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
launched = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
}
} catch (e) {
print(e);
}
return launched;
}
} }