mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 05:44:07 +00:00
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
307 lines
9.3 KiB
Dart
307 lines
9.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:app_links/app_links.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:flutter_sharing_intent/model/sharing_file.dart' show SharedFile;
|
|
import 'package:provider/provider.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/locator.dart';
|
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
|
import 'package:twonly/src/constants/routes.keys.dart';
|
|
import 'package:twonly/src/localization/generated/app_localizations.dart';
|
|
import 'package:twonly/src/model/json/onboarding_state.model.dart';
|
|
import 'package:twonly/src/providers/routing.provider.dart';
|
|
import 'package:twonly/src/providers/settings.provider.dart';
|
|
import 'package:twonly/src/services/intent/links.intent.dart';
|
|
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
|
import 'package:twonly/src/utils/keyvalue.dart';
|
|
import 'package:twonly/src/utils/log.dart';
|
|
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';
|
|
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
|
import 'package:twonly/src/visual/views/recovery_from_secure_storage.view.dart';
|
|
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
|
|
|
class App extends StatefulWidget {
|
|
const App({
|
|
required this.storageError,
|
|
required this.recoveryPossible,
|
|
super.key,
|
|
});
|
|
final bool storageError;
|
|
final bool recoveryPossible;
|
|
@override
|
|
State<App> createState() => _AppState();
|
|
}
|
|
|
|
class _AppState extends State<App> with WidgetsBindingObserver {
|
|
bool _wasPaused = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
AppState.isAppInBackground = false;
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
if (state == AppLifecycleState.resumed) {
|
|
if (_wasPaused) {
|
|
AppState.isAppInBackground = false;
|
|
twonlyDB.markUpdated();
|
|
unawaited(apiService.connect());
|
|
}
|
|
} else if (state == AppLifecycleState.paused) {
|
|
_wasPaused = true;
|
|
AppState.isAppInBackground = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: context.read<SettingsChangeProvider>(),
|
|
builder: (context, child) {
|
|
const localizationsDelegates = [
|
|
AppLocalizations.delegate,
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
];
|
|
|
|
const supportedLocales = [
|
|
Locale('en', ''),
|
|
Locale('de', ''),
|
|
];
|
|
|
|
if (widget.storageError) {
|
|
return MaterialApp(
|
|
localizationsDelegates: localizationsDelegates,
|
|
debugShowCheckedModeBanner: false,
|
|
supportedLocales: supportedLocales,
|
|
title: 'twonly',
|
|
theme: lightTheme,
|
|
darkTheme: darkTheme,
|
|
themeMode: context.read<SettingsChangeProvider>().themeMode,
|
|
home: const CriticalErrorView(),
|
|
);
|
|
}
|
|
|
|
if (widget.recoveryPossible) {
|
|
return MaterialApp(
|
|
localizationsDelegates: localizationsDelegates,
|
|
debugShowCheckedModeBanner: false,
|
|
supportedLocales: supportedLocales,
|
|
title: 'twonly',
|
|
theme: lightTheme,
|
|
darkTheme: darkTheme,
|
|
themeMode: context.read<SettingsChangeProvider>().themeMode,
|
|
home: const RecoveryView(),
|
|
);
|
|
}
|
|
|
|
return MaterialApp.router(
|
|
routerConfig: routerProvider,
|
|
localizationsDelegates: localizationsDelegates,
|
|
debugShowCheckedModeBanner: false,
|
|
supportedLocales: supportedLocales,
|
|
title: 'twonly',
|
|
theme: lightTheme,
|
|
darkTheme: darkTheme,
|
|
themeMode: context.read<SettingsChangeProvider>().themeMode,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class AppMainWidget extends StatefulWidget {
|
|
const AppMainWidget({
|
|
required this.initialPage,
|
|
super.key,
|
|
});
|
|
final int initialPage;
|
|
@override
|
|
State<AppMainWidget> createState() => _AppMainWidgetState();
|
|
}
|
|
|
|
class _AppMainWidgetState extends State<AppMainWidget> {
|
|
bool _showOnboarding = true;
|
|
bool _isLoaded = false;
|
|
bool _isTwonlyLocked = true;
|
|
bool _wasLogged = true;
|
|
late int _initialPage;
|
|
|
|
StreamSubscription<Uri>? _deepLinkSub;
|
|
StreamSubscription<List<SharedFile>>? _intentStreamSub;
|
|
StreamSubscription<String>? _emailTokenSub;
|
|
|
|
(Future<int>?, bool) _proofOfWork = (null, false);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initialPage = widget.initialPage;
|
|
Log.info('AppWidgetState: initState started');
|
|
initAsync();
|
|
|
|
void handleShareLink(Uri uri) {
|
|
routerProvider.go(Routes.home);
|
|
HomeViewState.streamHomeViewPageIndex.add(1);
|
|
HomeViewState.streamSharedLink.add(uri);
|
|
}
|
|
|
|
// Subscribe to all events (initial link and further)
|
|
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
|
if (!mounted) return;
|
|
Log.info('Got link via app links: ${uri.scheme}');
|
|
if (!await handleIntentUrl(context, uri)) {
|
|
if (uri.scheme.startsWith('http')) {
|
|
handleShareLink(uri);
|
|
}
|
|
}
|
|
});
|
|
|
|
_intentStreamSub = initIntentStreams(
|
|
context,
|
|
handleShareLink,
|
|
);
|
|
|
|
_emailTokenSub = PasswordlessRecoveryService.onEmailTokenReceived.stream
|
|
.listen((token) {
|
|
routerProvider.go(Routes.recoverPasswordless, extra: token);
|
|
});
|
|
}
|
|
|
|
Future<void> initAsync() async {
|
|
Log.info('AppWidgetState: initAsync started');
|
|
final onboardingState = await KeyValueStore.getModel<OnboardingState>(
|
|
KeyValueKeys.onboardingState,
|
|
);
|
|
_showOnboarding = !onboardingState.hasOnboardingFinished;
|
|
if (userService.isUserCreated) {
|
|
if (_initialPage != 0) {
|
|
final count = await twonlyDB.contactsDao.getContactsCount();
|
|
if (count == 0) {
|
|
_initialPage = 0;
|
|
}
|
|
}
|
|
try {
|
|
unawaited(FirebaseMessaging.instance.requestPermission());
|
|
} catch (e) {
|
|
Log.error(e);
|
|
}
|
|
if (_isTwonlyLocked) {
|
|
// do not change in case twonly was already unlocked at some point
|
|
_isTwonlyLocked = userService.currentUser.screenLockEnabled;
|
|
}
|
|
} else {
|
|
// This means the user is in the onboarding screen, so start with the Proof of Work.
|
|
|
|
final (proof, disabled) = await apiService.getProofOfWork();
|
|
if (proof != null) {
|
|
Log.info('Starting with proof of work calculation.');
|
|
_proofOfWork = (
|
|
calculatePoW(proof.prefix, proof.difficulty.toInt()),
|
|
false,
|
|
);
|
|
} else {
|
|
_proofOfWork = (null, disabled);
|
|
}
|
|
|
|
if (onboardingState.hasStartedPasswordlessRecovery ||
|
|
onboardingState.emailRecoveryRequested) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
routerProvider.push(Routes.recoverPasswordless);
|
|
});
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
_isLoaded = true;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_wasLogged) {
|
|
Log.info('AppWidgetState: build started (_isLoaded: $_isLoaded)');
|
|
if (_isLoaded) {
|
|
_wasLogged = true;
|
|
}
|
|
}
|
|
if (!_isLoaded) {
|
|
return Center(child: Container());
|
|
}
|
|
|
|
late Widget child;
|
|
|
|
if (userService.isUserCreated) {
|
|
if (_isTwonlyLocked) {
|
|
child = UnlockTwonlyView(
|
|
callbackOnSuccess: () => setState(() {
|
|
_isTwonlyLocked = false;
|
|
}),
|
|
);
|
|
} else if (!userService.currentUser.skipSetupPages &&
|
|
userService.currentUser.currentSetupPage != null) {
|
|
// This will only be shown in case the user have not skipped
|
|
child = SetupView(
|
|
onUpdate: () => setState(() {
|
|
// userService.currentUser has updated...
|
|
}),
|
|
);
|
|
} else {
|
|
child = HomeView(
|
|
initialPage: _initialPage,
|
|
);
|
|
}
|
|
} else if (_showOnboarding) {
|
|
child = OnboardingView(
|
|
callbackOnSuccess: () async {
|
|
await KeyValueStore.update<OnboardingState>(
|
|
key: KeyValueKeys.onboardingState,
|
|
update: (state) => state.hasOnboardingFinished = true,
|
|
);
|
|
if (mounted) setState(() => _showOnboarding = false);
|
|
},
|
|
);
|
|
} else {
|
|
child = RegisterView(
|
|
callbackOnSuccess: initAsync,
|
|
proofOfWork: _proofOfWork,
|
|
);
|
|
}
|
|
|
|
return Stack(
|
|
children: [
|
|
child,
|
|
const AppOutdatedComp(),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_deepLinkSub?.cancel();
|
|
_intentStreamSub?.cancel();
|
|
_emailTokenSub?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|