From 8ded9632155190498d79543708be63619a8efa33 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 30 Jan 2022 09:47:13 +0200 Subject: [PATCH 1/5] Implement company limit --- lib/redux/app/app_state.dart | 5 +++-- lib/ui/app/menu_drawer.dart | 4 +--- lib/ui/app/menu_drawer_vm.dart | 10 ++++++++++ lib/utils/i18n.dart | 5 +++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index 571525ec284..f1407476479 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -772,8 +772,9 @@ abstract class AppState implements Built { //bool get isEnterprisePlan => isSelfHosted || account.plan == kPlanEnterprise; - bool get isPaidAccount => - isSelfHosted ? isWhiteLabeled : (isProPlan || isEnterprisePlan); + bool get isPaidAccount => isSelfHosted + ? isWhiteLabeled + : ((isProPlan || isEnterprisePlan) && !isTrial); bool get isUserConfirmed { if (isSelfHosted) { diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index cd9b1d428de..251c7b1c6fc 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -825,9 +825,7 @@ class SidebarFooter extends StatelessWidget { ), if (isHosted(context) && !isPaidAccount(context) && !isApple()) IconButton( - tooltip: isHosted(context) - ? localization.upgrade - : localization.purchaseLicense, + tooltip: localization.upgrade, icon: Icon(Icons.arrow_circle_up), color: Colors.green, onPressed: () async { diff --git a/lib/ui/app/menu_drawer_vm.dart b/lib/ui/app/menu_drawer_vm.dart index 6493d8ca594..063ad23a696 100644 --- a/lib/ui/app/menu_drawer_vm.dart +++ b/lib/ui/app/menu_drawer_vm.dart @@ -122,6 +122,16 @@ class MenuDrawerVM { }); }, onAddCompany: (BuildContext context) { + if (state.isHosted && + !state.isPaidAccount && + state.companies.length >= state.account.hostedCompanyCount) { + showMessageDialog( + context: context, + message: AppLocalization.of(context).requiresAPaidPlan, + ); + return; + } + confirmCallback( context: context, message: AppLocalization.of(context).addCompany, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index d7c2552c424..bc086c640da 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -16,6 +16,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'requires_a_paid_plan': 'Requires a paid plan', 'file_saved_in_downloads_folder': 'The file has been saved in the downloads folder', 'small': 'Small', @@ -73366,6 +73367,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['file_saved_in_downloads_folder'] ?? _localizedValues['en']['file_saved_in_downloads_folder']; + String get requiresAPaidPlan => + _localizedValues[localeCode]['requires_a_paid_plan'] ?? + _localizedValues['en']['requires_a_paid_plan']; + // STARTER: lang field - do not remove comment String lookup(String key) { From 113945d97a4819be8ef145015d36128718455e6c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 30 Jan 2022 11:26:46 +0200 Subject: [PATCH 2/5] Include contact name/email in filter --- lib/data/models/client_model.dart | 22 +++++++++++++++++-- lib/data/models/vendor_model.dart | 22 +++++++++++++++++-- lib/redux/credit/credit_selectors.dart | 2 +- lib/redux/expense/expense_selectors.dart | 4 ++-- lib/redux/invoice/invoice_selectors.dart | 2 +- lib/redux/payment/payment_selectors.dart | 2 +- lib/redux/project/project_selectors.dart | 2 +- lib/redux/quote/quote_selectors.dart | 2 +- .../recurring_expense_selectors.dart | 4 ++-- .../recurring_invoice_selectors.dart | 2 +- lib/redux/task/task_selectors.dart | 2 +- .../invoice/edit/invoice_item_selector.dart | 5 +++-- 12 files changed, 54 insertions(+), 17 deletions(-) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 3cb7517bae0..691e7c96867 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -549,8 +549,26 @@ abstract class ClientEntity extends Object return response; } - bool matchesName(String filter) => - displayName.toLowerCase().contains(filter.toLowerCase()); + bool matchesNameOrEmail(String filter) { + filter = filter.toLowerCase(); + + if (displayName.toLowerCase().contains(filter)) { + return true; + } + + for (var i = 0; i < contacts.length; i++) { + final contact = contacts[i]; + if (contact.fullName.toLowerCase().contains(filter)) { + return true; + } + + if (contact.email.toLowerCase().contains(filter)) { + return true; + } + } + + return false; + } @override bool matchesFilter(String filter) { diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index e8f927ee284..a6d02f5a16d 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -331,8 +331,26 @@ abstract class VendorEntity extends Object return response; } - bool matchesName(String filter) => - name.toLowerCase().contains(filter.toLowerCase()); + bool matchesNameOrEmail(String filter) { + filter = filter.toLowerCase(); + + if (name.toLowerCase().contains(filter)) { + return true; + } + + for (var i = 0; i < contacts.length; i++) { + final contact = contacts[i]; + if (contact.fullName.toLowerCase().contains(filter)) { + return true; + } + + if (contact.email.toLowerCase().contains(filter)) { + return true; + } + } + + return false; + } @override bool matchesFilter(String filter) { diff --git a/lib/redux/credit/credit_selectors.dart b/lib/redux/credit/credit_selectors.dart index 1ed79b06bdc..53456892c22 100644 --- a/lib/redux/credit/credit_selectors.dart +++ b/lib/redux/credit/credit_selectors.dart @@ -116,7 +116,7 @@ List filteredCreditsSelector( return false; } if (!credit.matchesFilter(creditListState.filter) && - !client.matchesName(creditListState.filter)) { + !client.matchesNameOrEmail(creditListState.filter)) { return false; } diff --git a/lib/redux/expense/expense_selectors.dart b/lib/redux/expense/expense_selectors.dart index 5dd7046334a..b1cb5ce1bca 100644 --- a/lib/redux/expense/expense_selectors.dart +++ b/lib/redux/expense/expense_selectors.dart @@ -196,8 +196,8 @@ List filteredExpensesSelector( return expense.matchesFilter(expenseListState.filter) || expenseCategory.matchesFilter(expenseListState.filter) || - client.matchesName(expenseListState.filter) || - vendor.matchesName(expenseListState.filter); + client.matchesNameOrEmail(expenseListState.filter) || + vendor.matchesNameOrEmail(expenseListState.filter); }).toList(); list.sort((expenseAId, expenseBId) { diff --git a/lib/redux/invoice/invoice_selectors.dart b/lib/redux/invoice/invoice_selectors.dart index 4d0139ef763..4a49a4e2240 100644 --- a/lib/redux/invoice/invoice_selectors.dart +++ b/lib/redux/invoice/invoice_selectors.dart @@ -170,7 +170,7 @@ List filteredInvoicesSelector( return false; } if (!invoice.matchesFilter(invoiceListState.filter) && - !client.matchesName(invoiceListState.filter)) { + !client.matchesNameOrEmail(invoiceListState.filter)) { return false; } if (invoiceListState.custom1Filters.isNotEmpty && diff --git a/lib/redux/payment/payment_selectors.dart b/lib/redux/payment/payment_selectors.dart index 1e20d4eaaa1..97c8a55eadc 100644 --- a/lib/redux/payment/payment_selectors.dart +++ b/lib/redux/payment/payment_selectors.dart @@ -140,7 +140,7 @@ List filteredPaymentsSelector( } if (!payment.matchesFilter(paymentListState.filter) && - !client.matchesName(paymentListState.filter)) { + !client.matchesNameOrEmail(paymentListState.filter)) { return false; } diff --git a/lib/redux/project/project_selectors.dart b/lib/redux/project/project_selectors.dart index a30af0aafd7..5b54753b6a3 100644 --- a/lib/redux/project/project_selectors.dart +++ b/lib/redux/project/project_selectors.dart @@ -174,7 +174,7 @@ List filteredProjectsSelector( } if (!project.matchesFilter(projectListState.filter) && - !client.matchesName(projectListState.filter)) { + !client.matchesNameOrEmail(projectListState.filter)) { return false; } diff --git a/lib/redux/quote/quote_selectors.dart b/lib/redux/quote/quote_selectors.dart index 359d8106103..81547a7bcc8 100644 --- a/lib/redux/quote/quote_selectors.dart +++ b/lib/redux/quote/quote_selectors.dart @@ -67,7 +67,7 @@ List filteredQuotesSelector( } else if (!quote.matchesStatuses(quoteListState.statusFilters)) { return false; } else if (!quote.matchesFilter(quoteListState.filter) && - !client.matchesName(quoteListState.filter)) { + !client.matchesNameOrEmail(quoteListState.filter)) { return false; } diff --git a/lib/redux/recurring_expense/recurring_expense_selectors.dart b/lib/redux/recurring_expense/recurring_expense_selectors.dart index 2ddb5385592..b60d1191dbc 100644 --- a/lib/redux/recurring_expense/recurring_expense_selectors.dart +++ b/lib/redux/recurring_expense/recurring_expense_selectors.dart @@ -166,8 +166,8 @@ List filteredRecurringExpensesSelector( return expense.matchesFilter(expenseListState.filter) || expenseCategory.matchesFilter(expenseListState.filter) || - client.matchesName(expenseListState.filter) || - vendor.matchesName(expenseListState.filter); + client.matchesNameOrEmail(expenseListState.filter) || + vendor.matchesNameOrEmail(expenseListState.filter); }).toList(); list.sort((expenseAId, expenseBId) { diff --git a/lib/redux/recurring_invoice/recurring_invoice_selectors.dart b/lib/redux/recurring_invoice/recurring_invoice_selectors.dart index ab395a4599c..c6eed5f1618 100644 --- a/lib/redux/recurring_invoice/recurring_invoice_selectors.dart +++ b/lib/redux/recurring_invoice/recurring_invoice_selectors.dart @@ -72,7 +72,7 @@ List filteredRecurringInvoicesSelector( return false; } if (!invoice.matchesFilter(invoiceListState.filter) && - !client.matchesName(invoiceListState.filter)) { + !client.matchesNameOrEmail(invoiceListState.filter)) { return false; } if (invoiceListState.custom1Filters.isNotEmpty && diff --git a/lib/redux/task/task_selectors.dart b/lib/redux/task/task_selectors.dart index bb007780eac..408add9baaf 100644 --- a/lib/redux/task/task_selectors.dart +++ b/lib/redux/task/task_selectors.dart @@ -273,7 +273,7 @@ List filteredTasksSelector( } if (!task.matchesFilter(taskListState.filter) && - !client.matchesName(taskListState.filter) && + !client.matchesNameOrEmail(taskListState.filter) && !project.matchesName(taskListState.filter)) { return false; } diff --git a/lib/ui/invoice/edit/invoice_item_selector.dart b/lib/ui/invoice/edit/invoice_item_selector.dart index ac365fed215..c10a7faa4f4 100644 --- a/lib/ui/invoice/edit/invoice_item_selector.dart +++ b/lib/ui/invoice/edit/invoice_item_selector.dart @@ -155,7 +155,7 @@ class _InvoiceItemSelectorState extends State if (widget.excluded != null && widget.excluded.contains(task)) { return false; } - return task.matchesFilter(_filter) || client.matchesName(_filter); + return task.matchesFilter(_filter) || client.matchesNameOrEmail(_filter); }).toList(); final expenses = memoizedClientExpenseList( @@ -167,7 +167,8 @@ class _InvoiceItemSelectorState extends State if (widget.excluded != null && widget.excluded.contains(expense)) { return false; } - return expense.matchesFilter(_filter) || client.matchesName(_filter); + return expense.matchesFilter(_filter) || + client.matchesNameOrEmail(_filter); }).toList(); Widget _productList() { From 93aa2fb17baadf7c529fd65253518b9c7ff4a9eb Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 30 Jan 2022 11:32:31 +0200 Subject: [PATCH 3/5] Include contact name/email in filter --- lib/data/models/client_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 691e7c96867..c7aed22cd60 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -552,7 +552,7 @@ abstract class ClientEntity extends Object bool matchesNameOrEmail(String filter) { filter = filter.toLowerCase(); - if (displayName.toLowerCase().contains(filter)) { + if (name.toLowerCase().contains(filter)) { return true; } From 4ea68e582395b3746f4cc9f41605bc743f1d3755 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 30 Jan 2022 11:53:44 +0200 Subject: [PATCH 4/5] Recurring vs. Recurring #382 --- lib/ui/app/menu_drawer.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index 251c7b1c6fc..0029928ddb4 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -654,16 +654,16 @@ class _DrawerTileState extends State { color: textColor, ), ), - title: Text( - widget.title, - key: ValueKey('menu_${widget.title}'), - overflow: TextOverflow.clip, - maxLines: 1, - style: Theme.of(context).textTheme.bodyText1.copyWith( - fontSize: 14, - color: textColor, + title: state.isMenuCollapsed + ? SizedBox() + : Text( + widget.title, + key: ValueKey('menu_${widget.title}'), + style: Theme.of(context).textTheme.bodyText1.copyWith( + fontSize: 14, + color: textColor, + ), ), - ), onTap: () { if (widget.entityType != null) { viewEntitiesByType( From 82ec28621a2487b417a1089d3a66e9dcdd17d0e7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 30 Jan 2022 16:01:36 +0200 Subject: [PATCH 5/5] Safari issue --- lib/ui/auth/login_view.dart | 826 ++++++++++++++++++------------------ 1 file changed, 415 insertions(+), 411 deletions(-) diff --git a/lib/ui/auth/login_view.dart b/lib/ui/auth/login_view.dart index 64b22d13b37..0b7e01ed33e 100644 --- a/lib/ui/auth/login_view.dart +++ b/lib/ui/auth/login_view.dart @@ -295,452 +295,456 @@ class _LoginState extends State { final double horizontalPadding = calculateLayout(context) == AppLayout.desktop ? 40 : 16; - return SafeArea( - child: ScrollableListView( - children: [ - if (isDesktopOS()) - AppTitleBar() - else - Container( - width: double.infinity, - height: 24, - color: state.accentColor, + final child = ScrollableListView( + children: [ + if (isDesktopOS()) + AppTitleBar() + else + Container( + width: double.infinity, + height: 24, + color: state.accentColor, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: Center( + child: InkWell( + // TODO correct this + child: Image.asset( + state.prefState.enableDarkMode + ? 'assets/images/logo_dark.png' + : 'assets/images/logo_light.png', + height: 50), + onTap: isApple() + ? null + : () { + launch(kSiteUrl, + forceSafariVC: false, forceWebView: false); + }, + onLongPress: () { + if (kReleaseMode) { + return; + } + + setState(() => _tokenLogin = !_tokenLogin); + }, ), - Padding( - padding: EdgeInsets.symmetric(vertical: 25), - child: Center( - child: InkWell( - // TODO correct this - child: Image.asset( - state.prefState.enableDarkMode - ? 'assets/images/logo_dark.png' - : 'assets/images/logo_light.png', - height: 50), - onTap: isApple() - ? null - : () { - launch(kSiteUrl, - forceSafariVC: false, forceWebView: false); - }, - onLongPress: () { - if (kReleaseMode) { - return; - } - - setState(() => _tokenLogin = !_tokenLogin); - }, + ), + ), + if (_tokenLogin) + FormCard( + forceNarrow: calculateLayout(context) != AppLayout.mobile, + children: [ + DecoratedFormField( + autofocus: true, + label: localization.token, + controller: _tokenController, + keyboardType: TextInputType.text, ), - ), + AppButton( + label: localization.submit.toUpperCase(), + onPressed: () { + final Completer completer = Completer(); + viewModel.onTokenLoginPressed(context, completer, + token: _tokenController.text); + }, + ) + ], ), - if (_tokenLogin) - FormCard( - forceNarrow: calculateLayout(context) != AppLayout.mobile, - children: [ - DecoratedFormField( - autofocus: true, - label: localization.token, - controller: _tokenController, - keyboardType: TextInputType.text, - ), - AppButton( - label: localization.submit.toUpperCase(), - onPressed: () { - final Completer completer = Completer(); - viewModel.onTokenLoginPressed(context, completer, - token: _tokenController.text); - }, - ) - ], - ), - AnimatedOpacity( - duration: Duration(milliseconds: 500), - opacity: viewModel.authState.isAuthenticated ? 0 : 1, - child: Form( - key: _formKey, - child: AutofillGroup( - child: FormCard( - elevation: 20, - forceNarrow: calculateLayout(context) != AppLayout.mobile, - internalPadding: const EdgeInsets.all(0), - children: [ - Column( - children: [ - if (!isApple() && - (!kIsWeb || !state.authState.isSelfHost)) - Row( - children: [ - Expanded( - child: Material( - color: _createAccount - ? state.accentColor - : Colors.transparent, - child: InkWell( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - localization.signUp, - style: Theme.of(context) - .textTheme - .headline6 - .copyWith( - fontWeight: FontWeight.w600, - fontSize: 18, - color: _createAccount - ? Colors.white - : null), - ), - )), - onTap: () { - setState(() { - _createAccount = true; - _isSelfHosted = false; - _loginError = ''; - }); - }, - ), + AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: viewModel.authState.isAuthenticated ? 0 : 1, + child: Form( + key: _formKey, + child: AutofillGroup( + child: FormCard( + elevation: 20, + forceNarrow: calculateLayout(context) != AppLayout.mobile, + internalPadding: const EdgeInsets.all(0), + children: [ + Column( + children: [ + if (!isApple() && + (!kIsWeb || !state.authState.isSelfHost)) + Row( + children: [ + Expanded( + child: Material( + color: _createAccount + ? state.accentColor + : Colors.transparent, + child: InkWell( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + localization.signUp, + style: Theme.of(context) + .textTheme + .headline6 + .copyWith( + fontWeight: FontWeight.w600, + fontSize: 18, + color: _createAccount + ? Colors.white + : null), + ), + )), + onTap: () { + setState(() { + _createAccount = true; + _isSelfHosted = false; + _loginError = ''; + }); + }, ), ), - Expanded( - child: Material( - color: _createAccount - ? Colors.transparent - : state.accentColor, - child: InkWell( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - localization.login, - style: Theme.of(context) - .textTheme - .headline6 - .copyWith( - fontWeight: FontWeight.w600, - fontSize: 18, - color: _createAccount - ? null - : Colors.white), - ), - )), - onTap: () { - setState(() { - _createAccount = false; - _loginError = ''; - }); - }, - ), + ), + Expanded( + child: Material( + color: _createAccount + ? Colors.transparent + : state.accentColor, + child: InkWell( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + localization.login, + style: Theme.of(context) + .textTheme + .headline6 + .copyWith( + fontWeight: FontWeight.w600, + fontSize: 18, + color: _createAccount + ? null + : Colors.white), + ), + )), + onTap: () { + setState(() { + _createAccount = false; + _loginError = ''; + }); + }, ), ), - ], - ), - SizedBox(height: 20), - if (!_recoverPassword) ...[ - if (!_createAccount && (!kIsWeb || !kReleaseMode)) ...[ - RuledText(localization.selectPlatform), - AppToggleButtons( - tabLabels: [ - localization.hosted, - localization.selfhosted, - ], - selectedIndex: _isSelfHosted ? 1 : 0, - onTabChanged: (index) { - setState(() { - _isSelfHosted = index == 1; - _loginError = ''; - if (index == 1) { - _emailLogin = true; - } - }); - }, - ), - ], - if (!_isSelfHosted && !_hideGoogle) ...[ - RuledText(localization.selectMethod), - AppToggleButtons( - tabLabels: - calculateLayout(context) == AppLayout.mobile - ? [ - 'Google', - localization.email, - ] - : [ - _createAccount - ? localization.googleSignUp - : localization.googleSignIn, - _createAccount - ? localization.emailSignUp - : localization.emailSignIn, - ], - selectedIndex: _emailLogin ? 1 : 0, - onTabChanged: (index) { - setState(() { - _emailLogin = index == 1; - _loginError = ''; - }); - }, ), ], + ), + SizedBox(height: 20), + if (!_recoverPassword) ...[ + if (!_createAccount && (!kIsWeb || !kReleaseMode)) ...[ + RuledText(localization.selectPlatform), + AppToggleButtons( + tabLabels: [ + localization.hosted, + localization.selfhosted, + ], + selectedIndex: _isSelfHosted ? 1 : 0, + onTabChanged: (index) { + setState(() { + _isSelfHosted = index == 1; + _loginError = ''; + if (index == 1) { + _emailLogin = true; + } + }); + }, + ), ], - Padding( - padding: - EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Column( - children: [ - if (_emailLogin) - DecoratedFormField( - controller: _emailController, - label: localization.email, - keyboardType: TextInputType.emailAddress, - autovalidate: _autoValidate, - validator: (val) => - val.isEmpty || val.trim().isEmpty - ? localization.pleaseEnterYourEmail - : null, - autofillHints: [AutofillHints.email], - autofocus: true, - onSavePressed: (_) => _submitForm(), - ), - if (_emailLogin && !_recoverPassword) - PasswordFormField( - controller: _passwordController, - autoValidate: false, - newPassword: _createAccount, - onSavePressed: (_) => _submitForm(), - ), - if (!_createAccount && !_recoverPassword) - DecoratedFormField( - controller: _oneTimePasswordController, - label: - '${localization.oneTimePassword} (${localization.optional})', - onSavePressed: (_) => _submitForm(), - keyboardType: TextInputType.number, - autofillHints: [AutofillHints.oneTimeCode], - ), - if (_isSelfHosted && !kIsWeb) - DecoratedFormField( - controller: _urlController, - label: localization.url, - validator: (val) => - val.isEmpty || val.trim().isEmpty - ? localization.pleaseEnterYourUrl - : null, - keyboardType: TextInputType.url, - onSavePressed: (_) => _submitForm(), - ), - if (_isSelfHosted && !_recoverPassword) - PasswordFormField( - labelText: - '${localization.secret} (${localization.optional})', - controller: _secretController, - autoValidate: _autoValidate, - validate: false, - onSavePressed: (_) => _submitForm(), - ), - if (_createAccount) - Padding( - padding: EdgeInsets.only(top: 10), - child: Column( - children: [ - CheckboxListTile( - onChanged: (value) => - setState(() => _termsChecked = value), - controlAffinity: - ListTileControlAffinity.leading, - activeColor: convertHexStringToColor( - kDefaultAccentColor), - value: _termsChecked, - title: RichText( - text: TextSpan( - children: [ - TextSpan( - style: aboutTextStyle, - text: localization.iAgreeToThe + - ' ', - ), - LinkTextSpan( - style: linkStyle, - url: kTermsOfServiceURL, - text: localization.termsOfService, - ), - ], - ), + if (!_isSelfHosted && !_hideGoogle) ...[ + RuledText(localization.selectMethod), + AppToggleButtons( + tabLabels: + calculateLayout(context) == AppLayout.mobile + ? [ + 'Google', + localization.email, + ] + : [ + _createAccount + ? localization.googleSignUp + : localization.googleSignIn, + _createAccount + ? localization.emailSignUp + : localization.emailSignIn, + ], + selectedIndex: _emailLogin ? 1 : 0, + onTabChanged: (index) { + setState(() { + _emailLogin = index == 1; + _loginError = ''; + }); + }, + ), + ], + ], + Padding( + padding: + EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Column( + children: [ + if (_emailLogin) + DecoratedFormField( + controller: _emailController, + label: localization.email, + keyboardType: TextInputType.emailAddress, + autovalidate: _autoValidate, + validator: (val) => + val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterYourEmail + : null, + autofillHints: [AutofillHints.email], + autofocus: true, + onSavePressed: (_) => _submitForm(), + ), + if (_emailLogin && !_recoverPassword) + PasswordFormField( + controller: _passwordController, + autoValidate: false, + newPassword: _createAccount, + onSavePressed: (_) => _submitForm(), + ), + if (!_createAccount && !_recoverPassword) + DecoratedFormField( + controller: _oneTimePasswordController, + label: + '${localization.oneTimePassword} (${localization.optional})', + onSavePressed: (_) => _submitForm(), + keyboardType: TextInputType.number, + autofillHints: [AutofillHints.oneTimeCode], + ), + if (_isSelfHosted && !kIsWeb) + DecoratedFormField( + controller: _urlController, + label: localization.url, + validator: (val) => + val.isEmpty || val.trim().isEmpty + ? localization.pleaseEnterYourUrl + : null, + keyboardType: TextInputType.url, + onSavePressed: (_) => _submitForm(), + ), + if (_isSelfHosted && !_recoverPassword) + PasswordFormField( + labelText: + '${localization.secret} (${localization.optional})', + controller: _secretController, + autoValidate: _autoValidate, + validate: false, + onSavePressed: (_) => _submitForm(), + ), + if (_createAccount) + Padding( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + CheckboxListTile( + onChanged: (value) => + setState(() => _termsChecked = value), + controlAffinity: + ListTileControlAffinity.leading, + activeColor: convertHexStringToColor( + kDefaultAccentColor), + value: _termsChecked, + title: RichText( + text: TextSpan( + children: [ + TextSpan( + style: aboutTextStyle, + text: localization.iAgreeToThe + + ' ', + ), + LinkTextSpan( + style: linkStyle, + url: kTermsOfServiceURL, + text: localization.termsOfService, + ), + ], ), ), - CheckboxListTile( - onChanged: (value) => setState( - () => _privacyChecked = value), - controlAffinity: - ListTileControlAffinity.leading, - activeColor: convertHexStringToColor( - kDefaultAccentColor), - value: _privacyChecked, - title: RichText( - text: TextSpan( - children: [ - TextSpan( - style: aboutTextStyle, - text: localization.iAgreeToThe + - ' ', - ), - LinkTextSpan( - style: linkStyle, - url: kPrivacyPolicyURL, - text: localization.privacyPolicy, - ), - ], - ), + ), + CheckboxListTile( + onChanged: (value) => setState( + () => _privacyChecked = value), + controlAffinity: + ListTileControlAffinity.leading, + activeColor: convertHexStringToColor( + kDefaultAccentColor), + value: _privacyChecked, + title: RichText( + text: TextSpan( + children: [ + TextSpan( + style: aboutTextStyle, + text: localization.iAgreeToThe + + ' ', + ), + LinkTextSpan( + style: linkStyle, + url: kPrivacyPolicyURL, + text: localization.privacyPolicy, + ), + ], ), ), - ], - ), - ), - ], - ), - ), - ], - ), - if (_loginError.isNotEmpty && - !_loginError.contains(OTP_ERROR)) - Container( - padding: EdgeInsets.only( - top: 20, - left: horizontalPadding, - right: horizontalPadding), - child: Row( - children: [ - Expanded( - child: SelectableText( - _loginError, - style: TextStyle( - color: Colors.red, + ), + ], ), ), - ), - IconButton( - icon: Icon(Icons.content_copy), - tooltip: localization.copyError, - onPressed: () { - Clipboard.setData( - ClipboardData(text: _loginError)); - }), ], ), ), - Padding( + ], + ), + if (_loginError.isNotEmpty && + !_loginError.contains(OTP_ERROR)) + Container( padding: EdgeInsets.only( - top: 20, bottom: 10, left: 16, right: 16), - child: RoundedLoadingButton( - height: 50, - borderRadius: 4, - width: 430, - controller: _buttonController, - color: state.accentColor, - onPressed: () => _submitForm(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_emailLogin) - Icon(Icons.mail, color: Colors.white) - else - ClipOval( - child: Image.asset( - 'assets/images/google_logo.png', - width: 30, - height: 30), + top: 20, + left: horizontalPadding, + right: horizontalPadding), + child: Row( + children: [ + Expanded( + child: SelectableText( + _loginError, + style: TextStyle( + color: Colors.red, ), - SizedBox(width: 10), - Text( - _recoverPassword - ? localization.recoverPassword - : _createAccount - ? (_emailLogin - ? localization.emailSignUp - : localization.googleSignUp) - : (_emailLogin - ? localization.emailSignIn - : localization.googleSignIn), - style: TextStyle(fontSize: 18, color: Colors.white), - ) - ], - ), + ), + ), + IconButton( + icon: Icon(Icons.content_copy), + tooltip: localization.copyError, + onPressed: () { + Clipboard.setData( + ClipboardData(text: _loginError)); + }), + ], ), ), - SizedBox(height: 4), - Flex( - direction: calculateLayout(context) == AppLayout.desktop - ? Axis.horizontal - : Axis.vertical, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!_createAccount && _emailLogin) - InkWell( - onTap: () { - setState(() { - _recoverPassword = !_recoverPassword; - }); - }, - child: Padding( - padding: const EdgeInsets.all(14), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!_recoverPassword) - Icon(MdiIcons.lock, size: 16), - SizedBox(width: 8), - Text(_recoverPassword - ? localization.cancel - : localization.recoverPassword), - ]), + Padding( + padding: EdgeInsets.only( + top: 20, bottom: 10, left: 16, right: 16), + child: RoundedLoadingButton( + height: 50, + borderRadius: 4, + width: 430, + controller: _buttonController, + color: state.accentColor, + onPressed: () => _submitForm(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_emailLogin) + Icon(Icons.mail, color: Colors.white) + else + ClipOval( + child: Image.asset( + 'assets/images/google_logo.png', + width: 30, + height: 30), ), - ), - if (!_recoverPassword && !_isSelfHosted) - InkWell( - onTap: () { - launch(kStatusCheckUrl); - }, - child: Padding( - padding: const EdgeInsets.all(14), - child: Row( + SizedBox(width: 10), + Text( + _recoverPassword + ? localization.recoverPassword + : _createAccount + ? (_emailLogin + ? localization.emailSignUp + : localization.googleSignUp) + : (_emailLogin + ? localization.emailSignIn + : localization.googleSignIn), + style: TextStyle(fontSize: 18, color: Colors.white), + ) + ], + ), + ), + ), + SizedBox(height: 4), + Flex( + direction: calculateLayout(context) == AppLayout.desktop + ? Axis.horizontal + : Axis.vertical, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!_createAccount && _emailLogin) + InkWell( + onTap: () { + setState(() { + _recoverPassword = !_recoverPassword; + }); + }, + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.security, size: 16), + children: [ + if (!_recoverPassword) + Icon(MdiIcons.lock, size: 16), SizedBox(width: 8), - Text(localization.checkStatus) - ], - ), + Text(_recoverPassword + ? localization.cancel + : localization.recoverPassword), + ]), + ), + ), + if (!_recoverPassword && !_isSelfHosted) + InkWell( + onTap: () { + launch(kStatusCheckUrl); + }, + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.security, size: 16), + SizedBox(width: 8), + Text(localization.checkStatus) + ], ), ), - if (!_recoverPassword && kIsWeb) - InkWell( - onTap: () => launch(getNativeAppUrl(platform)), - child: Padding( - padding: const EdgeInsets.all(14), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(getNativeAppIcon(platform), size: 16), - SizedBox(width: 8), - Text('$platform ${localization.app}') - ], - ), + ), + if (!_recoverPassword && kIsWeb) + InkWell( + onTap: () => launch(getNativeAppUrl(platform)), + child: Padding( + padding: const EdgeInsets.all(14), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(getNativeAppIcon(platform), size: 16), + SizedBox(width: 8), + Text('$platform ${localization.app}') + ], ), ), - ], - ), - SizedBox(height: 16), - ], - ), + ), + ], + ), + SizedBox(height: 16), + ], ), ), ), - ], - ), + ), + ], ); + + if (!kIsWeb) { + return SafeArea(child: child); + } else { + return child; + } } }