diff --git a/lib/ui/settings/device_settings_list.dart b/lib/ui/settings/device_settings_list.dart new file mode 100644 index 000000000..dd2d1ace6 --- /dev/null +++ b/lib/ui/settings/device_settings_list.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/data/models/entities.dart'; +import 'package:invoiceninja_flutter/ui/settings/device_settings_list_vm.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class DeviceSettingsList extends StatelessWidget { + const DeviceSettingsList({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final DeviceSettingsListVM viewModel; + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + SwitchListTile( + title: Text(AppLocalization.of(context).darkMode), + value: viewModel.enableDarkMode, + onChanged: (value) => viewModel.onDarkModeChanged(context, value), + secondary: Icon(FontAwesomeIcons.moon), + activeColor: Theme.of(context).accentColor, + ), + FutureBuilder( + future: viewModel.authenticationSupported, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData && snapshot.data == true) { + return SwitchListTile( + title: + Text(AppLocalization.of(context).biometricAuthentication), + value: viewModel.requireAuthentication, + onChanged: (value) => + viewModel.onRequireAuthenticationChanged(context, value), + secondary: Icon(viewModel.requireAuthentication + ? FontAwesomeIcons.lock + : FontAwesomeIcons.unlockAlt), + activeColor: Theme.of(context).accentColor, + ); + } else { + return SizedBox(); + } + }, + ), + viewModel.state.selectedCompany.isModuleEnabled(EntityType.task) + ? SwitchListTile( + title: Text(AppLocalization.of(context).autoStartTasks), + value: viewModel.autoStartTasks, + onChanged: (value) => + viewModel.onAutoStartTasksChanged(context, value), + secondary: Icon(FontAwesomeIcons.clock), + activeColor: Theme.of(context).accentColor, + ) + : SizedBox(), + ListTile( + leading: Icon(FontAwesomeIcons.syncAlt), + title: Text(AppLocalization.of(context).refreshData), + onTap: () { + viewModel.onRefreshTap(context); + }, + ), + ListTile( + leading: Icon(FontAwesomeIcons.powerOff), + title: Text(AppLocalization.of(context).logout), + onTap: () { + viewModel.onLogoutTap(context); + }, + ), + ], + ); + } +} diff --git a/lib/ui/settings/device_settings_list_vm.dart b/lib/ui/settings/device_settings_list_vm.dart new file mode 100644 index 000000000..e4b9c334d --- /dev/null +++ b/lib/ui/settings/device_settings_list_vm.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/loading_dialog.dart'; +import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/utils/platforms.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:redux/redux.dart'; +import 'package:invoiceninja_flutter/ui/settings/device_settings_list.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class DeviceSettingsListBuilder extends StatelessWidget { + const DeviceSettingsListBuilder({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return StoreConnector( + converter: DeviceSettingsListVM.fromStore, + builder: (context, viewModel) { + return DeviceSettingsList(viewModel: viewModel); + }, + ); + } +} + +class DeviceSettingsListVM { + DeviceSettingsListVM({ + @required this.state, + @required this.onLogoutTap, + @required this.onRefreshTap, + @required this.onDarkModeChanged, + @required this.enableDarkMode, + @required this.autoStartTasks, + @required this.onAutoStartTasksChanged, + @required this.onRequireAuthenticationChanged, + @required this.requireAuthentication, + @required this.authenticationSupported, + }); + + static DeviceSettingsListVM fromStore(Store store) { + void _refreshData(BuildContext context) async { + final completer = snackBarCompleter( + context, AppLocalization.of(context).refreshComplete, + shouldPop: true); + store.dispatch(RefreshData( + platform: getPlatform(context), + completer: completer, + )); + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => SimpleDialog( + children: [LoadingDialog()], + )); + AppBuilder.of(context).rebuild(); + store.dispatch(LoadDashboard()); + } + + void _confirmLogout(BuildContext context) { + final localization = AppLocalization.of(context); + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + semanticLabel: localization.areYouSure, + title: Text(localization.areYouSure), + actions: [ + new FlatButton( + child: Text(localization.cancel.toUpperCase()), + onPressed: () { + Navigator.pop(context); + }), + new FlatButton( + child: Text(localization.ok.toUpperCase()), + onPressed: () { + store.dispatch(UserLogout(context)); + }) + ], + ), + ); + } + + return DeviceSettingsListVM( + state: store.state, + onLogoutTap: (BuildContext context) => _confirmLogout(context), + onRefreshTap: (BuildContext context) => _refreshData(context), + onDarkModeChanged: (BuildContext context, bool value) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefEnableDarkMode, value); + store.dispatch(UserSettingsChanged(enableDarkMode: value)); + AppBuilder.of(context).rebuild(); + }, + onAutoStartTasksChanged: (BuildContext context, bool value) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefAutoStartTasks, value); + store.dispatch(UserSettingsChanged(autoStartTasks: value)); + AppBuilder.of(context).rebuild(); + }, + onRequireAuthenticationChanged: (BuildContext context, bool value) async { + bool authenticated = false; + try { + authenticated = await LocalAuthentication() + .authenticateWithBiometrics( + localizedReason: + AppLocalization.of(context).authenticateToChangeSetting, + useErrorDialogs: true, + stickyAuth: false); + } catch (e) { + print(e); + } + if (authenticated) { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool(kSharedPrefRequireAuthentication, value); + store.dispatch(UserSettingsChanged(requireAuthentication: value)); + } else {} + }, + autoStartTasks: store.state.uiState.autoStartTasks, + enableDarkMode: store.state.uiState.enableDarkMode, + requireAuthentication: store.state.uiState.requireAuthentication, + //authenticationSupported: LocalAuthentication().canCheckBiometrics, + // TODO remove this once issue is resolved: + // https://github.com/flutter/flutter/issues/24339 + authenticationSupported: Future(() async { + bool enable = false; + try { + enable = await LocalAuthentication().canCheckBiometrics; + } catch (e) { + // do nothing + } + return enable; + }), + ); + } + + final AppState state; + final Function(BuildContext context) onLogoutTap; + final Function(BuildContext context) onRefreshTap; + final Function(BuildContext context, bool value) onDarkModeChanged; + final Function(BuildContext context, bool value) onAutoStartTasksChanged; + final bool enableDarkMode; + final bool autoStartTasks; + final Function(BuildContext context, bool value) + onRequireAuthenticationChanged; + final bool requireAuthentication; + final Future authenticationSupported; +} diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index e106f2d83..c7c3f90af 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -1,8 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/ui/settings/settings_list_vm.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class SettingsList extends StatelessWidget { @@ -15,58 +13,12 @@ class SettingsList extends StatelessWidget { @override Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + return ListView( children: [ - SwitchListTile( - title: Text(AppLocalization.of(context).darkMode), - value: viewModel.enableDarkMode, - onChanged: (value) => viewModel.onDarkModeChanged(context, value), - secondary: Icon(FontAwesomeIcons.moon), - activeColor: Theme.of(context).accentColor, - ), - FutureBuilder( - future: viewModel.authenticationSupported, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData && snapshot.data == true) { - return SwitchListTile( - title: - Text(AppLocalization.of(context).biometricAuthentication), - value: viewModel.requireAuthentication, - onChanged: (value) => - viewModel.onRequireAuthenticationChanged(context, value), - secondary: Icon(viewModel.requireAuthentication - ? FontAwesomeIcons.lock - : FontAwesomeIcons.unlockAlt), - activeColor: Theme.of(context).accentColor, - ); - } else { - return SizedBox(); - } - }, - ), - viewModel.state.selectedCompany.isModuleEnabled(EntityType.task) - ? SwitchListTile( - title: Text(AppLocalization.of(context).autoStartTasks), - value: viewModel.autoStartTasks, - onChanged: (value) => - viewModel.onAutoStartTasksChanged(context, value), - secondary: Icon(FontAwesomeIcons.clock), - activeColor: Theme.of(context).accentColor, - ) - : SizedBox(), ListTile( - leading: Icon(FontAwesomeIcons.syncAlt), - title: Text(AppLocalization.of(context).refreshData), - onTap: () { - viewModel.onRefreshTap(context); - }, - ), - ListTile( - leading: Icon(FontAwesomeIcons.powerOff), - title: Text(AppLocalization.of(context).logout), - onTap: () { - viewModel.onLogoutTap(context); - }, + title: Text(localization.companyDetails), ), ], ); diff --git a/lib/ui/settings/settings_list_vm.dart b/lib/ui/settings/settings_list_vm.dart index 74c76c7e3..5be030f05 100644 --- a/lib/ui/settings/settings_list_vm.dart +++ b/lib/ui/settings/settings_list_vm.dart @@ -6,12 +6,13 @@ import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/loading_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/app_builder.dart'; +import 'package:invoiceninja_flutter/ui/settings/settings_list.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:local_auth/local_auth.dart'; import 'package:redux/redux.dart'; -import 'package:invoiceninja_flutter/ui/settings/settings_list.dart'; +import 'package:invoiceninja_flutter/ui/settings/device_settings_list.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/auth/auth_actions.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -33,120 +34,13 @@ class SettingsListBuilder extends StatelessWidget { class SettingsListVM { SettingsListVM({ @required this.state, - @required this.onLogoutTap, - @required this.onRefreshTap, - @required this.onDarkModeChanged, - @required this.enableDarkMode, - @required this.autoStartTasks, - @required this.onAutoStartTasksChanged, - @required this.onRequireAuthenticationChanged, - @required this.requireAuthentication, - @required this.authenticationSupported, }); static SettingsListVM fromStore(Store store) { - void _refreshData(BuildContext context) async { - final completer = snackBarCompleter( - context, AppLocalization.of(context).refreshComplete, - shouldPop: true); - store.dispatch(RefreshData( - platform: getPlatform(context), - completer: completer, - )); - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) => SimpleDialog( - children: [LoadingDialog()], - )); - AppBuilder.of(context).rebuild(); - store.dispatch(LoadDashboard()); - } - - void _confirmLogout(BuildContext context) { - final localization = AppLocalization.of(context); - showDialog( - context: context, - builder: (BuildContext context) => AlertDialog( - semanticLabel: localization.areYouSure, - title: Text(localization.areYouSure), - actions: [ - new FlatButton( - child: Text(localization.cancel.toUpperCase()), - onPressed: () { - Navigator.pop(context); - }), - new FlatButton( - child: Text(localization.ok.toUpperCase()), - onPressed: () { - store.dispatch(UserLogout(context)); - }) - ], - ), - ); - } - return SettingsListVM( state: store.state, - onLogoutTap: (BuildContext context) => _confirmLogout(context), - onRefreshTap: (BuildContext context) => _refreshData(context), - onDarkModeChanged: (BuildContext context, bool value) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool(kSharedPrefEnableDarkMode, value); - store.dispatch(UserSettingsChanged(enableDarkMode: value)); - AppBuilder.of(context).rebuild(); - }, - onAutoStartTasksChanged: (BuildContext context, bool value) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool(kSharedPrefAutoStartTasks, value); - store.dispatch(UserSettingsChanged(autoStartTasks: value)); - AppBuilder.of(context).rebuild(); - }, - onRequireAuthenticationChanged: (BuildContext context, bool value) async { - bool authenticated = false; - try { - authenticated = await LocalAuthentication() - .authenticateWithBiometrics( - localizedReason: - AppLocalization.of(context).authenticateToChangeSetting, - useErrorDialogs: true, - stickyAuth: false); - } catch (e) { - print(e); - } - if (authenticated) { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool(kSharedPrefRequireAuthentication, value); - store.dispatch(UserSettingsChanged(requireAuthentication: value)); - } else {} - }, - autoStartTasks: store.state.uiState.autoStartTasks, - enableDarkMode: store.state.uiState.enableDarkMode, - requireAuthentication: store.state.uiState.requireAuthentication, - //authenticationSupported: LocalAuthentication().canCheckBiometrics, - // TODO remove this once issue is resolved: - // https://github.com/flutter/flutter/issues/24339 - authenticationSupported: Future(() async { - bool enable = false; - try { - enable = await LocalAuthentication().canCheckBiometrics; - } catch (e) { - // do nothing - } - return enable; - }), ); } final AppState state; - final Function(BuildContext context) onLogoutTap; - final Function(BuildContext context) onRefreshTap; - final Function(BuildContext context, bool value) onDarkModeChanged; - final Function(BuildContext context, bool value) onAutoStartTasksChanged; - final bool enableDarkMode; - final bool autoStartTasks; - final Function(BuildContext context, bool value) - onRequireAuthenticationChanged; - final bool requireAuthentication; - final Future authenticationSupported; } diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index f923a0f75..dfa2055df 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/ui/app/app_drawer_vm.dart'; +import 'package:invoiceninja_flutter/ui/settings/device_settings_list_vm.dart'; import 'package:invoiceninja_flutter/ui/settings/settings_list_vm.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index df75b03fe..ba11694ce 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -14,6 +14,20 @@ abstract class LocaleCodeAware { mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { + 'company_details': 'Company Details', + 'user_details': 'User Details', + 'localization': 'Localization', + 'online_payments': 'Online Payments', + 'tax_rates': 'Tax Rates', + 'notifications': 'Notifications', + 'import_export': 'Import | Export', + 'invoice_settings': 'Invoice Settings', + 'invoice_design': 'Invoice Design', + 'buy_now_buttons': 'Buy Now Buttons', + 'email_settings': 'Email Settings', + 'templates_and_reminders': 'Templates & Reminders', + 'credit_cards_and_banks': 'Credit Cards & Banks', + 'data_visualizations': 'Data Visualizations', 'price': 'Price', 'email_sign_up': 'Email Sign Up', 'google_sign_up': 'Google Sign Up', @@ -14613,6 +14627,38 @@ mixin LocalizationsProvider on LocaleCodeAware { String get price => _localizedValues[localeCode]['price']; + String get companyDetails => _localizedValues[localeCode]['company_details']; + + String get userDetails => _localizedValues[localeCode]['user_details']; + + String get localization => _localizedValues[localeCode]['localization']; + + String get onlinePayments => _localizedValues[localeCode]['online_payments']; + + String get taxRates => _localizedValues[localeCode]['tax_rates']; + + String get notifications => _localizedValues[localeCode]['notifications']; + + String get importExport => _localizedValues[localeCode]['import_export']; + + String get invoiceSettings => + _localizedValues[localeCode]['invoice_settings']; + + String get invoiceDesign => _localizedValues[localeCode]['invoice_design']; + + String get buyNowButtons => _localizedValues[localeCode]['buy_now_buttons']; + + String get emailSettings => _localizedValues[localeCode]['email_settings']; + + String get templatesAndReminders => + _localizedValues[localeCode]['templates_and_reminders']; + + String get creditCardsAndBanks => + _localizedValues[localeCode]['credit_cards_and_banks']; + + String get dataVisualizations => + _localizedValues[localeCode]['data_visualizations']; + String lookup(String key) { final lookupKey = toSnakeCase(key); return _localizedValues[localeCode][lookupKey] ??