From dbe74cf2d555aff607588fc4c8494a2ce9b1fe06 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Tue, 22 Nov 2022 21:46:28 +0800 Subject: [PATCH] Restore legacy sign in in debug mode for quick dev access --- .../legacy/app_password_exchange_bloc.dart | 136 +++++++ app/lib/legacy/connect.dart | 319 ++++++++++++++++ app/lib/legacy/sign_in.dart | 356 ++++++++++++++++++ app/lib/widget/my_app.dart | 17 + app/lib/widget/sign_in.dart | 14 + 5 files changed, 842 insertions(+) create mode 100644 app/lib/legacy/app_password_exchange_bloc.dart create mode 100644 app/lib/legacy/connect.dart create mode 100644 app/lib/legacy/sign_in.dart diff --git a/app/lib/legacy/app_password_exchange_bloc.dart b/app/lib/legacy/app_password_exchange_bloc.dart new file mode 100644 index 00000000..9c004d3d --- /dev/null +++ b/app/lib/legacy/app_password_exchange_bloc.dart @@ -0,0 +1,136 @@ +import 'dart:io'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/api/api.dart'; +import 'package:nc_photos/exception.dart'; + +abstract class AppPasswordExchangeBlocEvent { + const AppPasswordExchangeBlocEvent(); +} + +class AppPasswordExchangeBlocConnect extends AppPasswordExchangeBlocEvent { + const AppPasswordExchangeBlocConnect(this.account); + + @override + toString() { + return "$runtimeType {" + "account: $account, " + "}"; + } + + final Account account; +} + +abstract class AppPasswordExchangeBlocState { + const AppPasswordExchangeBlocState(); +} + +class AppPasswordExchangeBlocInit extends AppPasswordExchangeBlocState { + const AppPasswordExchangeBlocInit(); +} + +class AppPasswordExchangeBlocSuccess extends AppPasswordExchangeBlocState { + const AppPasswordExchangeBlocSuccess(this.password); + + @override + toString() { + return "$runtimeType {" + "password: ${kDebugMode ? password : '***'}, " + "}"; + } + + final String password; +} + +class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState { + const AppPasswordExchangeBlocFailure(this.exception); + + @override + toString() { + return "$runtimeType {" + "exception: $exception, " + "}"; + } + + final dynamic exception; +} + +/// Legacy sign in support, may be removed any time in the future +class AppPasswordExchangeBloc + extends Bloc { + AppPasswordExchangeBloc() : super(const AppPasswordExchangeBlocInit()) { + on(_onEvent); + } + + Future _onEvent(AppPasswordExchangeBlocEvent event, + Emitter emit) async { + _log.info("[_onEvent] $event"); + if (event is AppPasswordExchangeBlocConnect) { + await _onEventConnect(event, emit); + } + } + + Future _onEventConnect(AppPasswordExchangeBlocConnect ev, + Emitter emit) async { + final account = ev.account; + try { + final appPwd = await _exchangePassword(account); + emit(AppPasswordExchangeBlocSuccess(appPwd)); + } on InvalidBaseUrlException catch (e) { + _log.warning("[_onEventConnect] Invalid base url"); + emit(AppPasswordExchangeBlocFailure(e)); + } on HandshakeException catch (e) { + _log.info("[_onEventConnect] Self-signed cert"); + emit(AppPasswordExchangeBlocFailure(e)); + } catch (e, stacktrace) { + if (e is ApiException && e.response.statusCode == 401) { + // wrong password, normal + _log.warning("[_onEventConnect] Server response 401, wrong password?"); + } else { + _log.shout("[_onEventConnect] Failed while exchanging password", e, + stacktrace); + } + emit(AppPasswordExchangeBlocFailure(e)); + } + } + + /// Query the app password for [account] + static Future _exchangePassword(Account account) async { + final response = await Api(account).request( + "GET", + "ocs/v2.php/core/getapppassword", + header: { + "OCS-APIRequest": "true", + }, + ); + if (response.isGood) { + try { + final appPwdRegex = RegExp(r"(.*)"); + final appPwdMatch = appPwdRegex.firstMatch(response.body); + return appPwdMatch!.group(1)!; + } catch (_) { + // this happens when the address is not the base URL and so Nextcloud + // returned the login page + throw InvalidBaseUrlException(); + } + } else if (response.statusCode == 403) { + // If the client is authenticated with an app password a 403 will be + // returned + _log.info("[_exchangePassword] Already an app password"); + return account.password; + } else { + _log.severe( + "[_exchangePassword] Failed while requesting app password: $response"); + throw ApiException( + response: response, + message: + "Server responed with an error: HTTP ${response.statusCode}"); + } + } + + static final _log = + Logger("legacy.app_password_exchange_bloc.AppPasswordExchangeBloc"); +} diff --git a/app/lib/legacy/connect.dart b/app/lib/legacy/connect.dart new file mode 100644 index 00000000..00ecee02 --- /dev/null +++ b/app/lib/legacy/connect.dart @@ -0,0 +1,319 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/ci_string.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/exception.dart'; +import 'package:nc_photos/exception_util.dart' as exception_util; +import 'package:nc_photos/help_utils.dart' as help_util; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/legacy/app_password_exchange_bloc.dart'; +import 'package:nc_photos/mobile/self_signed_cert_manager.dart'; +import 'package:nc_photos/platform/features.dart' as features; +import 'package:nc_photos/snack_bar_manager.dart'; +import 'package:nc_photos/string_extension.dart'; +import 'package:nc_photos/url_launcher_util.dart'; +import 'package:nc_photos/use_case/ls_single_file.dart'; + +class ConnectArguments { + ConnectArguments(this.account); + + final Account account; +} + +/// Legacy sign in support, may be removed any time in the future +class Connect extends StatefulWidget { + static const routeName = "/connect-legacy"; + + static Route buildRoute(ConnectArguments args) => MaterialPageRoute( + builder: (context) => Connect.fromArgs(args), + ); + + const Connect({ + Key? key, + required this.account, + }) : super(key: key); + + Connect.fromArgs(ConnectArguments args, {Key? key}) + : this( + key: key, + account: args.account, + ); + + @override + createState() => _ConnectState(); + + final Account account; +} + +class _ConnectState extends State { + @override + initState() { + super.initState(); + _initBloc(); + } + + @override + build(BuildContext context) { + return Scaffold( + body: BlocListener( + bloc: _bloc, + listener: (context, state) => _onStateChange(context, state), + child: Builder(builder: (context) => _buildContent(context)), + ), + ); + } + + void _initBloc() { + _log.info("[_initBloc] Initialize bloc"); + _connect(); + } + + Widget _buildContent(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud, + size: 128, + color: Theme.of(context).colorScheme.primary, + ), + Text( + L10n.global().connectingToServer(widget.account.url), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headline6, + ) + ], + ), + ), + ); + } + + void _onStateChange( + BuildContext context, AppPasswordExchangeBlocState state) { + if (state is AppPasswordExchangeBlocSuccess) { + final newAccount = widget.account.copyWith(password: state.password); + _log.info("[_onStateChange] Password exchanged: $newAccount"); + _checkWebDavUrl(context, newAccount); + } else if (state is AppPasswordExchangeBlocFailure) { + if (features.isSupportSelfSignedCert && + state.exception is HandshakeException) { + _onSelfSignedCert(context); + } else if (state.exception is ApiException && + (state.exception as ApiException).response.statusCode == 401) { + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().errorWrongPassword), + duration: k.snackBarDurationNormal, + )); + Navigator.of(context).pop(null); + } else { + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(state.exception)), + duration: k.snackBarDurationNormal, + )); + Navigator.of(context).pop(null); + } + } + } + + void _onSelfSignedCert(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(L10n.global().serverCertErrorDialogTitle), + content: Text(L10n.global().serverCertErrorDialogContent), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(MaterialLocalizations.of(context).closeButtonLabel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(L10n.global().advancedButtonLabel), + ), + ], + ), + ).then((value) { + if (value != true) { + Navigator.of(context).pop(null); + return; + } + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(L10n.global().whitelistCertDialogTitle), + content: Text(L10n.global().whitelistCertDialogContent( + SelfSignedCertManager().getLastBadCertHost(), + SelfSignedCertManager().getLastBadCertFingerprint())), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(L10n.global().whitelistCertButtonLabel), + ), + ], + ), + ).then((value) { + if (value != true) { + Navigator.of(context).pop(null); + return; + } + SelfSignedCertManager().whitelistLastBadCert().then((value) { + Navigator.of(context).pop(null); + }); + }); + }); + } + + void _connect() { + _bloc.add(AppPasswordExchangeBlocConnect(widget.account)); + } + + Future _onCheckWebDavUrlFailed( + BuildContext context, Account account) async { + final userId = await _askWebDavUrl(context, account); + if (userId != null) { + final newAccount = account.copyWith( + userId: userId.toCi(), + ); + return _checkWebDavUrl(context, newAccount); + } + } + + Future _checkWebDavUrl(BuildContext context, Account account) async { + // check the WebDAV URL + try { + final c = KiwiContainer().resolve(); + await LsSingleFile(c.withRemoteFileRepo())( + account, file_util.unstripPath(account, "")); + _log.info("[_checkWebDavUrl] Account is good: $account"); + Navigator.of(context).pop(account); + } on ApiException catch (e) { + if (e.response.statusCode == 404) { + return _onCheckWebDavUrlFailed(context, account); + } + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + Navigator.of(context).pop(null); + } on StateError catch (_) { + // Nextcloud for some reason doesn't return HTTP error when listing home + // dir of other users + return _onCheckWebDavUrlFailed(context, account); + } catch (e, stackTrace) { + _log.shout("[_checkWebDavUrl] Failed", e, stackTrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(exception_util.toUserString(e)), + duration: k.snackBarDurationNormal, + )); + Navigator.of(context).pop(null); + } + } + + Future _askWebDavUrl(BuildContext context, Account account) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _WebDavUrlDialog(account: account), + ); + } + + final _bloc = AppPasswordExchangeBloc(); + + static final _log = Logger("widget.connect._ConnectState"); +} + +class _WebDavUrlDialog extends StatefulWidget { + const _WebDavUrlDialog({ + Key? key, + required this.account, + }) : super(key: key); + + @override + createState() => _WebDavUrlDialogState(); + + final Account account; +} + +class _WebDavUrlDialogState extends State<_WebDavUrlDialog> { + @override + build(BuildContext context) { + return AlertDialog( + title: Text(L10n.global().homeFolderNotFoundDialogTitle), + content: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(L10n.global().homeFolderNotFoundDialogContent), + const SizedBox(height: 16), + Text("${widget.account.url}/remote.php/dav/files/"), + TextFormField( + validator: (value) { + if (value?.trimAny("/").isNotEmpty == true) { + return null; + } + return L10n.global().homeFolderInputInvalidEmpty; + }, + onSaved: (value) { + _formValue.userId = value!.trimAny("/"); + }, + initialValue: widget.account.userId.toString(), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _onHelpPressed, + child: Text(L10n.global().helpButtonLabel), + ), + TextButton( + onPressed: _onOkPressed, + child: Text(MaterialLocalizations.of(context).okButtonLabel), + ), + ], + ); + } + + void _onOkPressed() { + if (_formKey.currentState?.validate() == true) { + _formKey.currentState!.save(); + Navigator.of(context).pop(_formValue.userId); + } + } + + void _onHelpPressed() { + launch(help_util.homeFolderNotFoundUrl); + } + + final _formKey = GlobalKey(); + final _formValue = _FormValue(); +} + +class _FormValue { + late String userId; +} diff --git a/app/lib/legacy/sign_in.dart b/app/lib/legacy/sign_in.dart new file mode 100644 index 00000000..5aa54819 --- /dev/null +++ b/app/lib/legacy/sign_in.dart @@ -0,0 +1,356 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:kiwi/kiwi.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/ci_string.dart'; +import 'package:nc_photos/di_container.dart'; +import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; +import 'package:nc_photos/help_utils.dart' as help_utils; +import 'package:nc_photos/iterable_extension.dart'; +import 'package:nc_photos/legacy/connect.dart'; +import 'package:nc_photos/platform/k.dart' as platform_k; +import 'package:nc_photos/pref.dart'; +import 'package:nc_photos/pref_util.dart' as pref_util; +import 'package:nc_photos/string_extension.dart'; +import 'package:nc_photos/theme.dart'; +import 'package:nc_photos/url_launcher_util.dart'; +import 'package:nc_photos/widget/home.dart'; +import 'package:nc_photos/widget/root_picker.dart'; + +class SignIn extends StatefulWidget { + static const routeName = "/sign-in-legacy"; + + const SignIn({ + Key? key, + }) : super(key: key); + + @override + createState() => _SignInState(); +} + +class _SignInState extends State { + @override + build(BuildContext context) { + return Scaffold( + body: Builder(builder: (context) => _buildContent(context)), + ); + } + + Widget _buildContent(BuildContext context) { + if (_isConnecting) { + return Stack( + children: const [ + Positioned( + left: 0, + right: 0, + bottom: 64, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(), + ), + ), + ), + ], + ); + } else { + return SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints viewportConstraints) { + return Form( + key: _formKey, + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Text( + L10n.global().signInHeaderText, + style: Theme.of(context).textTheme.headline5, + textAlign: TextAlign.center, + ), + ), + Align( + alignment: Alignment.center, + child: Container( + constraints: BoxConstraints( + maxWidth: + Theme.of(context).widthLimitedContentMaxWidth, + ), + padding: const EdgeInsets.symmetric(horizontal: 32), + child: _buildForm(context), + ), + ), + Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints( + maxWidth: + Theme.of(context).widthLimitedContentMaxWidth, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: InkWell( + onTap: () { + launch(help_utils.twoFactorAuthUrl); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 16), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.help_outline, size: 16), + const SizedBox(width: 4), + Expanded( + child: + Text(L10n.global().signIn2faHintText), + ), + ], + ), + ), + ), + ), + if (!platform_k.isWeb) Expanded(child: Container()), + Container( + constraints: BoxConstraints( + maxWidth: + Theme.of(context).widthLimitedContentMaxWidth, + ), + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!ModalRoute.of(context)!.isFirst) + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(MaterialLocalizations.of(context) + .cancelButtonLabel), + ) + else + Container(), + ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate() == + true) { + _connect(); + } + }, + child: Text(L10n.global().connectButtonLabel), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } + } + + Widget _buildForm(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Icon( + Icons.cloud, + color: Theme.of(context).colorScheme.primary, + size: 72, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + SizedBox( + width: 64, + child: DropdownButtonHideUnderline( + child: DropdownButtonFormField<_Scheme>( + value: _scheme, + items: [_Scheme.http, _Scheme.https] + .map((e) => DropdownMenuItem<_Scheme>( + value: e, + child: Text(e.toValueString()), + )) + .toList(), + onChanged: (newValue) { + setState(() { + _scheme = newValue!; + }); + }, + onSaved: (value) { + _formValue.scheme = value!.toValueString(); + }, + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text("://"), + ), + Expanded( + child: TextFormField( + decoration: InputDecoration( + hintText: L10n.global().serverAddressInputHint, + ), + keyboardType: TextInputType.url, + validator: (value) { + if (value!.trim().trimRightAny("/").isEmpty) { + return L10n.global().serverAddressInputInvalidEmpty; + } + return null; + }, + onSaved: (value) { + _formValue.address = value!.trim().trimRightAny("/"); + }, + ), + ), + ], + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + hintText: L10n.global().usernameInputHint, + ), + validator: (value) { + if (value!.trim().isEmpty) { + return L10n.global().usernameInputInvalidEmpty; + } + return null; + }, + onSaved: (value) { + _formValue.username = value!; + }, + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + hintText: L10n.global().passwordInputHint, + ), + obscureText: true, + validator: (value) { + if (value!.trim().isEmpty) { + return L10n.global().passwordInputInvalidEmpty; + } + return null; + }, + onSaved: (value) { + _formValue.password = value!; + }, + ), + ], + ); + } + + Future _connect() async { + _formKey.currentState!.save(); + Account? account = Account( + Account.newId(), + _formValue.scheme, + _formValue.address, + _formValue.username.toCi(), + _formValue.username, + _formValue.password, + [""], + ); + _log.info("[_connect] Try connecting with account: $account"); + account = await Navigator.pushNamed(context, Connect.routeName, + arguments: ConnectArguments(account)); + if (account == null) { + // connection failed + return; + } + account = await Navigator.pushNamed(context, RootPicker.routeName, + arguments: RootPickerArguments(account)); + if (account == null) { + // ??? + return; + } + // we've got a good account + setState(() { + _isConnecting = true; + }); + try { + await _persistAccount(account); + unawaited( + Navigator.pushNamedAndRemoveUntil( + context, Home.routeName, (route) => false, + arguments: HomeArguments(account)), + ); + } catch (_) { + setState(() { + _isConnecting = false; + }); + rethrow; + } + } + + Future _persistAccount(Account account) async { + final c = KiwiContainer().resolve(); + await c.sqliteDb.use((db) async { + await db.insertAccountOf(account); + }); + // only signing in with app password would trigger distinct + final accounts = (Pref().getAccounts3Or([])..add(account)).distinct(); + try { + AccountPref.setGlobalInstance( + account, await pref_util.loadAccountPref(account)); + } catch (e, stackTrace) { + _log.shout("[_connect] Failed reading pref for account: $account", e, + stackTrace); + } + unawaited(Pref().setAccounts3(accounts)); + unawaited(Pref().setCurrentAccountIndex(accounts.indexOf(account))); + } + + final _formKey = GlobalKey(); + var _scheme = _Scheme.https; + var _isConnecting = false; + + final _formValue = _FormValue(); + + static final _log = Logger("widget.sign_in._SignInState"); +} + +enum _Scheme { + http, + https, +} + +extension on _Scheme { + String toValueString() { + switch (this) { + case _Scheme.http: + return "http"; + + case _Scheme.https: + return "https"; + + default: + throw StateError("Unknown value: $this"); + } + } +} + +class _FormValue { + late String scheme; + late String address; + late String username; + late String password; +} diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index d7d528f8..b3eef987 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -4,6 +4,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/language_util.dart' as language_util; +import 'package:nc_photos/legacy/connect.dart' as legacy; +import 'package:nc_photos/legacy/sign_in.dart' as legacy; import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; @@ -137,6 +139,7 @@ class _MyAppState extends State Setup.routeName: (context) => const Setup(), SignIn.routeName: (context) => const SignIn(), Splash.routeName: (context) => const Splash(), + legacy.SignIn.routeName: (_) => const legacy.SignIn(), }; Route? _onGenerateRoute(RouteSettings settings) { @@ -145,6 +148,7 @@ class _MyAppState extends State route ??= _handleBasicRoute(settings); route ??= _handleViewerRoute(settings); route ??= _handleConnectRoute(settings); + route ??= _handleConnectLegacyRoute(settings); route ??= _handleHomeRoute(settings); route ??= _handleRootPickerRoute(settings); route ??= _handleAlbumBrowserRoute(settings); @@ -221,6 +225,19 @@ class _MyAppState extends State return null; } + Route? _handleConnectLegacyRoute(RouteSettings settings) { + try { + if (settings.name == legacy.Connect.routeName && + settings.arguments != null) { + final args = settings.arguments as legacy.ConnectArguments; + return legacy.Connect.buildRoute(args); + } + } catch (e) { + _log.severe("[_handleConnectLegacyRoute] Failed while handling route", e); + } + return null; + } + Route? _handleHomeRoute(RouteSettings settings) { try { if (settings.name == Home.routeName && settings.arguments != null) { diff --git a/app/lib/widget/sign_in.dart b/app/lib/widget/sign_in.dart index 500fbe88..2cce4662 100644 --- a/app/lib/widget/sign_in.dart +++ b/app/lib/widget/sign_in.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; @@ -8,6 +9,7 @@ import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql; import 'package:nc_photos/iterable_extension.dart'; +import 'package:nc_photos/legacy/sign_in.dart' as legacy; import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/pref_util.dart' as pref_util; @@ -191,6 +193,18 @@ class _SignInState extends State { ), ], ), + if (kDebugMode) ...[ + const SizedBox(height: 8), + InkWell( + onTap: () { + Navigator.pushReplacementNamed(context, legacy.SignIn.routeName); + }, + child: const Text( + "Legacy sign in", + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ], ], ); }