mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42:13 +00:00
display critical error instead of removing app data
This commit is contained in:
parent
db9d9022fd
commit
583368505d
13 changed files with 221 additions and 112 deletions
55
lib/app.dart
55
lib/app.dart
|
|
@ -17,6 +17,7 @@ import 'package:twonly/src/utils/pow.dart';
|
|||
import 'package:twonly/src/visual/components/app_outdated.comp.dart';
|
||||
import 'package:twonly/src/visual/themes/dark.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/views/critical_error.view.dart';
|
||||
import 'package:twonly/src/visual/views/home.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
|
||||
|
|
@ -31,6 +32,7 @@ class App extends StatefulWidget {
|
|||
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
bool wasPaused = false;
|
||||
Object? _storageError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -42,12 +44,21 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
try {
|
||||
final user = await getUser();
|
||||
if (user != null && mounted) {
|
||||
context.read<PurchasesProvider>().updatePlan(
|
||||
planFromString(user.subscriptionPlan),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Storage error in App.initAsync: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_storageError = e;
|
||||
});
|
||||
}
|
||||
}
|
||||
await apiService.connect();
|
||||
await apiService.listenToNetworkChanges();
|
||||
}
|
||||
|
|
@ -78,20 +89,38 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
return ListenableBuilder(
|
||||
listenable: context.watch<SettingsChangeProvider>(),
|
||||
builder: (context, child) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: routerProvider,
|
||||
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
|
||||
localizationsDelegates: const [
|
||||
const localizationsDelegates = [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
debugShowCheckedModeBanner: false,
|
||||
supportedLocales: const [
|
||||
];
|
||||
|
||||
const supportedLocales = [
|
||||
Locale('en', ''),
|
||||
Locale('de', ''),
|
||||
],
|
||||
];
|
||||
|
||||
if (_storageError != null) {
|
||||
return MaterialApp(
|
||||
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
supportedLocales: supportedLocales,
|
||||
title: 'twonly',
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: context.watch<SettingsChangeProvider>().themeMode,
|
||||
home: const CriticalErrorView(),
|
||||
);
|
||||
}
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: routerProvider,
|
||||
scaffoldMessengerKey: AppGlobalKeys.scaffoldMessengerKey,
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
supportedLocales: supportedLocales,
|
||||
title: 'twonly',
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
|
|
@ -116,6 +145,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
bool _isUserCreated = false;
|
||||
bool _showOnboarding = true;
|
||||
bool _isLoaded = false;
|
||||
Object? _storageError;
|
||||
bool _skipBackup = kDebugMode;
|
||||
bool _isTwonlyLocked = true;
|
||||
|
||||
|
|
@ -128,6 +158,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
try {
|
||||
_isUserCreated = await isUserCreated();
|
||||
|
||||
if (_isUserCreated) {
|
||||
|
|
@ -149,6 +180,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
_proofOfWork = (null, disabled);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Storage error in AppMainWidget.initAsync: $e');
|
||||
_storageError = e;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoaded = true;
|
||||
|
|
@ -161,6 +196,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
|||
return Center(child: Container());
|
||||
}
|
||||
|
||||
if (_storageError != null) {
|
||||
return const CriticalErrorView();
|
||||
}
|
||||
|
||||
late Widget child;
|
||||
|
||||
if (_isUserCreated) {
|
||||
|
|
|
|||
|
|
@ -41,10 +41,14 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
|
||||
_planSub = apiService.onPlanUpdated.listen(updatePlan);
|
||||
_connSub = apiService.onConnectionStateUpdated.listen((_) async {
|
||||
try {
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
updatePlan(planFromString(user.subscriptionPlan));
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
loadPurchases();
|
||||
|
|
@ -90,16 +94,22 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
storeState = StoreState.available;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final user = await getUser();
|
||||
if (user != null && isPayingUser(planFromString(user.subscriptionPlan))) {
|
||||
Log.info('Started IPA timer for verification.');
|
||||
globalForceIpaCheck = Timer(const Duration(seconds: 5), () async {
|
||||
Log.info('Force Ipa check was not stopped. Requesting forced check...');
|
||||
Log.info(
|
||||
'Force Ipa check was not stopped. Requesting forced check...',
|
||||
);
|
||||
await apiService.forceIpaCheck();
|
||||
});
|
||||
}
|
||||
|
||||
await iapConnection.restorePurchases();
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> buy(PurchasableProduct product) async {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
||||
late ThemeMode _themeMode;
|
||||
|
|
@ -8,8 +9,13 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
|
|||
ThemeMode get themeMode => _themeMode;
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
try {
|
||||
_themeMode = (await getUser())?.themeMode ?? ThemeMode.system;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_themeMode = ThemeMode.system;
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateThemeMode(ThemeMode? newThemeMode) async {
|
||||
|
|
|
|||
|
|
@ -416,23 +416,22 @@ class ApiService {
|
|||
return res;
|
||||
}
|
||||
|
||||
Future<bool> tryAuthenticateWithToken(int userId) async {
|
||||
Future<bool> tryAuthenticateWithToken() async {
|
||||
final apiAuthToken = await SecureStorage.instance.read(
|
||||
key: SecureStorageKeys.apiAuthToken,
|
||||
);
|
||||
final user = await getUser();
|
||||
|
||||
if (apiAuthToken != null && user != null) {
|
||||
if (user.appVersion < 62) {
|
||||
if (apiAuthToken != null) {
|
||||
if (userService.currentUser.appVersion < 62) {
|
||||
Log.error(
|
||||
'DID NOT authenticate the user, as he still has the old version!',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final authenticate = Handshake_Authenticate()
|
||||
..userId = Int64(userId)
|
||||
..userId = Int64(userService.currentUser.userId)
|
||||
..appVersion = (await PackageInfo.fromPlatform()).version
|
||||
..deviceId = Int64(user.deviceId)
|
||||
..deviceId = Int64(userService.currentUser.deviceId)
|
||||
..inBackground = AppState.isInBackgroundTask
|
||||
..authToken = base64Decode(apiAuthToken);
|
||||
|
||||
|
|
@ -474,7 +473,7 @@ class ApiService {
|
|||
final userData = await getUser();
|
||||
if (userData == null) return;
|
||||
|
||||
if (await tryAuthenticateWithToken(userData.userId)) {
|
||||
if (await tryAuthenticateWithToken()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +521,7 @@ class ApiService {
|
|||
value: apiAuthTokenB64,
|
||||
);
|
||||
|
||||
await tryAuthenticateWithToken(userData.userId);
|
||||
await tryAuthenticateWithToken();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -797,11 +796,10 @@ class ApiService {
|
|||
});
|
||||
return ballance;
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && user.lastPlanBallance != null && useCache) {
|
||||
if (userService.currentUser.lastPlanBallance != null && useCache) {
|
||||
try {
|
||||
return Response_PlanBallance.fromJson(
|
||||
user.lastPlanBallance!,
|
||||
userService.currentUser.lastPlanBallance!,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('from json: $e');
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ Future<void> handleBackupData(
|
|||
final originalDatabase = File(
|
||||
join(AppEnvironment.supportDir, 'twonly.sqlite'),
|
||||
);
|
||||
|
||||
// in case there was only a secure storage error, do not replace the original database
|
||||
if (!originalDatabase.existsSync()) {
|
||||
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
|
||||
}
|
||||
|
||||
const storage = SecureStorage.instance;
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ Future<void> updateUser(
|
|||
void Function(UserData userData) updateUser,
|
||||
) async {
|
||||
await updateProtection.protect(() async {
|
||||
try {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.defaultShowTime == 999999) {
|
||||
|
|
@ -91,6 +92,9 @@ Future<void> updateUser(
|
|||
value: jsonEncode(user),
|
||||
);
|
||||
userService.currentUser = user;
|
||||
} catch (e) {
|
||||
Log.error('Could not update the user: $e');
|
||||
}
|
||||
});
|
||||
|
||||
userService.triggerUserUpdate();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import 'package:gal/gal.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
|
|
@ -19,7 +18,6 @@ import 'package:twonly/src/utils/misc.dart';
|
|||
|
||||
extension ShortCutsExtension on BuildContext {
|
||||
AppLocalizations get lang => AppLocalizations.of(this)!;
|
||||
TwonlyDB get db => Provider.of<TwonlyDB>(this);
|
||||
ColorScheme get color => Theme.of(this).colorScheme;
|
||||
Future<dynamic> navPush(Widget route) async {
|
||||
return Navigator.push(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class FeedbackIconButtonComp extends StatefulWidget {
|
||||
|
|
@ -24,10 +24,9 @@ class _FeedbackIconButtonCompState extends State<FeedbackIconButtonComp> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user == null || !mounted) return;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
showFeedbackShortcut = user.showFeedbackShortcut;
|
||||
showFeedbackShortcut = userService.currentUser.showFeedbackShortcut;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
68
lib/src/visual/views/critical_error.view.dart
Normal file
68
lib/src/visual/views/critical_error.view.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/recover.view.dart';
|
||||
import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart';
|
||||
|
||||
class CriticalErrorView extends StatelessWidget {
|
||||
const CriticalErrorView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
size: 80,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Critical Error',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Please try restarting twonly. If the error persists, please contact our support and upload your debug log so we can troubleshoot the issue.\n\nYou can restore your account using the button below. If you have forgotten your password, you will need to reinstall twonly and then register with a new account.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await Restart.restartApp(
|
||||
notificationTitle: 'App restarted',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
forceKill: true,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Try Again'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await context.navPush(const BackupRecoveryView());
|
||||
},
|
||||
icon: const Icon(Icons.backup_rounded),
|
||||
label: const Text('Recovery from backup'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.navPush(const ContactUsView()),
|
||||
child: const Text('Contact Support'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,13 +70,6 @@ class HomeViewState extends State<HomeView> {
|
|||
unawaited(_mainCameraController.selectCamera(0, true));
|
||||
unawaited(_initAsync());
|
||||
|
||||
handleIntentUrl(
|
||||
context,
|
||||
Uri.parse(
|
||||
'https://me.twonly.eu/qr/#EAAauAEIgLDN0Nm7oKh0EghoYWhoaGhoaBohBRZQ8w_zpm1v7SRTdc8GEOMAxuf1caGDlBa-v0ZiTw9qIiEF05juEs1c3yw0STiSwQR7lowDX5hBaxN4YFR0HhkopGIoudTO5wIyQFQRtU1aO7P7O5s2ekB1ppAost3iQQizwhFObjOLgHQnpwcnwEONXZzSADYqCeEoNcvyE45w0v21z1Imhozk3Q44oI0GQhA9U_chIJwwZ7J9fpeXODZF',
|
||||
),
|
||||
);
|
||||
|
||||
// Subscribe to all events (initial link and further)
|
||||
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
||||
if (!mounted) return;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/src/constants/routes.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
import 'package:twonly/src/services/backup/restore.backup.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||
import 'package:twonly/src/visual/views/settings/backup/backup_server.view.dart';
|
||||
|
||||
class BackupRecoveryView extends StatefulWidget {
|
||||
const BackupRecoveryView({super.key});
|
||||
|
|
@ -140,9 +139,11 @@ class _BackupRecoveryViewState extends State<BackupRecoveryView> {
|
|||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
backupServer = await context.push(
|
||||
Routes.settingsBackupServer,
|
||||
);
|
||||
backupServer =
|
||||
await context.navPush(
|
||||
const BackupServerView(),
|
||||
)
|
||||
as BackupServer?;
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(context.lang.backupExpertSettings),
|
||||
|
|
|
|||
|
|
@ -264,15 +264,8 @@ $debugLogToken
|
|||
final fullMessage = await _getFeedbackText();
|
||||
if (!context.mounted || fullMessage == null) return;
|
||||
|
||||
final feedbackSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SubmitMessage(
|
||||
fullMessage: fullMessage,
|
||||
);
|
||||
},
|
||||
),
|
||||
final feedbackSend = await context.navPush(
|
||||
SubmitMessage(fullMessage: fullMessage),
|
||||
);
|
||||
|
||||
if (feedbackSend == true && context.mounted) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:twonly/src/constants/secure_storage.keys.dart';
|
|||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
||||
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/secure_storage.dart';
|
||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||
|
|
@ -56,21 +55,18 @@ class _NotificationViewState extends State<NotificationView> {
|
|||
);
|
||||
|
||||
if (run) {
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
final pushData = await encryptPushNotification(
|
||||
user.userId,
|
||||
userService.currentUser.userId,
|
||||
PushNotification(
|
||||
messageId: uuid.v4(),
|
||||
kind: PushKind.TEST_NOTIFICATION,
|
||||
),
|
||||
);
|
||||
await apiService.sendTextMessage(
|
||||
user.userId,
|
||||
userService.currentUser.userId,
|
||||
Uint8List(0),
|
||||
pushData,
|
||||
);
|
||||
}
|
||||
_troubleshootingDidRun = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue