diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index f71b5dd433f..3539586175e 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.140" + automatic_release_tag: "v5.0.141" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 39a0ac36e43..6bdd8587421 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -50,6 +50,7 @@ + diff --git a/lib/constants.dart b/lib/constants.dart index 4604c6c18d0..cb9f24e6aa1 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -4,7 +4,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.140'; +const String kClientVersion = '5.0.141'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index fc028206c96..e7a0c3d0ef0 100644 --- a/lib/data/models/settings_model.dart +++ b/lib/data/models/settings_model.dart @@ -30,6 +30,21 @@ abstract class SettingsEntity clientSettings?.defaultPurchaseOrderDesignId ?? groupSettings?.defaultPurchaseOrderDesignId ?? companySettings?.defaultPurchaseOrderDesignId, + defaultStatementDesignId: clientSettings?.defaultStatementDesignId ?? + groupSettings?.defaultStatementDesignId ?? + companySettings?.defaultStatementDesignId, + defaultDeliveryNoteDesignId: + clientSettings?.defaultDeliveryNoteDesignId ?? + groupSettings?.defaultDeliveryNoteDesignId ?? + companySettings?.defaultDeliveryNoteDesignId, + defaultPaymentReceiptDesignId: + clientSettings?.defaultPaymentReceiptDesignId ?? + groupSettings?.defaultPaymentReceiptDesignId ?? + companySettings?.defaultPaymentReceiptDesignId, + defaultPaymentRefundDesignId: + clientSettings?.defaultPaymentRefundDesignId ?? + groupSettings?.defaultPaymentRefundDesignId ?? + companySettings?.defaultPaymentRefundDesignId, defaultInvoiceTerms: clientSettings?.defaultInvoiceTerms ?? groupSettings?.defaultInvoiceTerms ?? companySettings?.defaultInvoiceTerms, diff --git a/lib/redux/app/app_state.dart b/lib/redux/app/app_state.dart index d2a12e2f609..19803a0b860 100644 --- a/lib/redux/app/app_state.dart +++ b/lib/redux/app/app_state.dart @@ -850,6 +850,11 @@ abstract class AppState implements Built { bool get isUpdateAvailable => isSelfHosted && account.isUpdateAvailable && userCompany.isAdmin; + bool get isUsingPostmark => [ + if (isHosted) SettingsEntity.EMAIL_SENDING_METHOD_DEFAULT, + SettingsEntity.EMAIL_SENDING_METHOD_POSTMARK, + ].contains(company.settings.emailSendingMethod); + bool get isUserConfirmed { if (isSelfHosted) { return true; diff --git a/lib/ui/app/entities/entity_list_tile.dart b/lib/ui/app/entities/entity_list_tile.dart index 7c38e8e0b3a..610b7251803 100644 --- a/lib/ui/app/entities/entity_list_tile.dart +++ b/lib/ui/app/entities/entity_list_tile.dart @@ -117,7 +117,7 @@ class _EntityListTileState extends State { formatDate(entity.date, context); } else if (entity is ExpenseEntity) { defaultSubtitle = - formatNumber(entity.amount, context, clientId: entity.clientId)! + + formatNumber(entity.amount, context, currencyId: entity.currencyId)! + ' • ' + formatDate(entity.date, context); } else if (entity is TransactionEntity) { diff --git a/lib/ui/app/portal_links.dart b/lib/ui/app/portal_links.dart index 4d0091a7788..524ec357a7f 100644 --- a/lib/ui/app/portal_links.dart +++ b/lib/ui/app/portal_links.dart @@ -43,17 +43,9 @@ class PortalLinks extends StatelessWidget { viewLinkWithHash += '&client_hash=${client!.clientHash}'; } - var copyLinkWithHash = copyLink; - if (!copyLink.contains('?')) { - copyLinkWithHash += '?'; - } - if (client != null) { - copyLinkWithHash += '&client_hash=${client!.clientHash}'; - } - final viewLinkPressed = () => launchUrl(Uri.parse(viewLinkWithHash)); final copyLinkPressed = () { - Clipboard.setData(ClipboardData(text: copyLinkWithHash)); + Clipboard.setData(ClipboardData(text: copyLink)); showToast(localization!.copiedToClipboard.replaceFirst(':value ', '')); }; diff --git a/lib/ui/auth/login_view.dart b/lib/ui/auth/login_view.dart index 5ae1ff9b947..b720a259354 100644 --- a/lib/ui/auth/login_view.dart +++ b/lib/ui/auth/login_view.dart @@ -455,6 +455,7 @@ class _LoginState extends State { val.isEmpty || val.trim().isEmpty ? localization.pleaseEnterYourEmail : null, + autofillHints: [AutofillHints.username], ), if (_loginType == LOGIN_TYPE_EMAIL && !_recoverPassword) diff --git a/lib/ui/dashboard/dashboard_screen.dart b/lib/ui/dashboard/dashboard_screen.dart index 3cc1f6d1d27..5d42d0b398b 100644 --- a/lib/ui/dashboard/dashboard_screen.dart +++ b/lib/ui/dashboard/dashboard_screen.dart @@ -267,27 +267,32 @@ class _DashboardScreenState extends State child: IconButton( tooltip: localization!.enableReactApp, onPressed: () async { - final credentials = state.credentials; - final account = state.account - .rebuild((b) => b..setReactAsDefaultAP = true); - final url = '${credentials.url}/accounts/${account.id}'; - final data = serializers.serializeWith( - AccountEntity.serializer, account); - - store.dispatch(StartSaving()); - WebClient() - .put( - url, - credentials.token, - data: json.encode(data), - ) - .then((dynamic _) { - store.dispatch(StopSaving()); - WebUtils.reloadBrowser(); - }).catchError((Object error) { - store.dispatch(StopSaving()); - showErrorDialog(message: error as String?); - }); + confirmCallback( + context: context, + message: localization.enableReactApp, + callback: (_) { + final credentials = state.credentials; + final account = state.account + .rebuild((b) => b..setReactAsDefaultAP = true); + final url = '${credentials.url}/accounts/${account.id}'; + final data = serializers.serializeWith( + AccountEntity.serializer, account); + + store.dispatch(StartSaving()); + WebClient() + .put( + url, + credentials.token, + data: json.encode(data), + ) + .then((dynamic _) { + store.dispatch(StopSaving()); + WebUtils.reloadBrowser(); + }).catchError((Object error) { + store.dispatch(StopSaving()); + showErrorDialog(message: error as String?); + }); + }); }, icon: Icon(MdiIcons.react), ), diff --git a/lib/ui/invoice/edit/invoice_edit_contacts.dart b/lib/ui/invoice/edit/invoice_edit_contacts.dart index 05af4e8327b..066025d6531 100644 --- a/lib/ui/invoice/edit/invoice_edit_contacts.dart +++ b/lib/ui/invoice/edit/invoice_edit_contacts.dart @@ -6,12 +6,16 @@ import 'package:flutter_styled_toast/flutter_styled_toast.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/data/web_client.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_contacts_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:url_launcher/url_launcher.dart'; class InvoiceEditContacts extends StatelessWidget { const InvoiceEditContacts({ @@ -57,8 +61,11 @@ class InvoiceEditContacts extends StatelessWidget { return ScrollableListView( children: vendorContacts.map((contact) { final invitation = invoice.getInvitationForVendorContact(contact); - return _VendorContactListTile( - vendorContact: contact, + + return _ContactListTile( + fullName: contact.fullName, + email: contact.email, + hash: '', invoice: invoice, invitation: invitation, onTap: () => invitation == null @@ -92,8 +99,10 @@ class InvoiceEditContacts extends StatelessWidget { showScrollbar: true, children: clientContacts.map((contact) { final invitation = invoice.getInvitationForClientContact(contact); - return _ClientContactListTile( - clientContact: contact, + return _ContactListTile( + fullName: contact.fullName, + email: contact.email, + hash: client?.clientHash ?? '', invoice: invoice, invitation: invitation, onTap: () => invitation == null @@ -106,23 +115,37 @@ class InvoiceEditContacts extends StatelessWidget { } } -class _ClientContactListTile extends StatelessWidget { - const _ClientContactListTile({ - required this.clientContact, +class _ContactListTile extends StatefulWidget { + const _ContactListTile({ + required this.fullName, + required this.email, required this.invoice, + required this.hash, this.invitation, this.onTap, }); + final String fullName; + final String email; + final String hash; final InvoiceEntity invoice; - final ClientContactEntity clientContact; final InvitationEntity? invitation; final Function? onTap; + @override + State<_ContactListTile> createState() => _ContactListTileState(); +} + +class _ContactListTileState extends State<_ContactListTile> { + bool _showEmailError = true; + @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; final store = StoreProvider.of(context); + final state = store.state; + + /* final invitationButton = (invitation?.link ?? '').isNotEmpty ? IconButton( tooltip: localization.copyLink, @@ -134,100 +157,45 @@ class _ClientContactListTile extends StatelessWidget { }, ) : SizedBox(); + */ - return Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - Checkbox( - activeColor: Theme.of(context).colorScheme.secondary, - value: invitation != null, - onChanged: (value) => onTap!(), - ), - if (store.state.prefState.showPdfPreviewSideBySide) - invitationButton, - ], - ), - SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - clientContact.fullName.isNotEmpty - ? clientContact.fullName - : AppLocalization.of(context)!.blankContact, - style: Theme.of(context).textTheme.titleMedium, + final invitationButton = (widget.invitation?.link ?? '').isNotEmpty + ? PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: IconText( + text: localization.viewPortal, + icon: Icons.open_in_new, + ), + value: localization.viewPortal, ), - if (clientContact.email.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - clientContact.email, - style: Theme.of(context).textTheme.bodySmall, - ), + PopupMenuItem( + child: IconText( + text: localization.copyLink, + icon: Icons.copy, ), - if ((invitation?.emailStatus ?? '').isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 2), - child: Text( - localization.lookup(invitation!.latestEmailStatus) + - ' • ' + - formatDate( - invitation!.latestEmailStatusDate, context), - style: Theme.of(context).textTheme.bodySmall, - ), - ), - if ((invitation?.emailError ?? '').isNotEmpty && - invitation?.emailStatus != - InvitationEntity.EMAIL_STATUS_DELIVERED) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - invitation!.emailError, - style: Theme.of(context).textTheme.bodySmall, - ), - ), - SizedBox(height: 8), - ], - ], - ), - ), - if (!store.state.prefState.showPdfPreviewSideBySide) invitationButton, - ], - ), - ); - } -} - -class _VendorContactListTile extends StatelessWidget { - const _VendorContactListTile({ - required this.vendorContact, - required this.invoice, - this.invitation, - this.onTap, - }); - - final InvoiceEntity invoice; - final VendorContactEntity vendorContact; - final InvitationEntity? invitation; - final Function? onTap; + value: localization.copyLink, + ), + ]; + }, + onSelected: (String action) { + var viewLinkWithHash = widget.invitation!.silentLink; + if (!viewLinkWithHash.contains('?')) { + viewLinkWithHash += '?'; + } + viewLinkWithHash += '&client_hash=${widget.hash}'; - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context)!; - final store = StoreProvider.of(context); - final invitationButton = (invitation?.link ?? '').isNotEmpty - ? IconButton( - tooltip: localization.copyLink, - icon: Icon(Icons.copy), - onPressed: () { - Clipboard.setData(ClipboardData(text: invitation!.link)); - showToast(localization.copiedToClipboard.replaceFirst( - ':value', invitation!.link.substring(0, 40) + '...')); + if (action == localization.viewPortal) { + launchUrl(Uri.parse(viewLinkWithHash)); + } else if (action == localization.copyLink) { + Clipboard.setData(ClipboardData(text: widget.invitation!.link)); + showToast( + localization.copiedToClipboard.replaceFirst(':value ', '')); + } else if (action == localization.reactivateEmail) { + // + } }, ) : SizedBox(); @@ -241,8 +209,8 @@ class _VendorContactListTile extends StatelessWidget { children: [ Checkbox( activeColor: Theme.of(context).colorScheme.secondary, - value: invitation != null, - onChanged: (value) => onTap!(), + value: widget.invitation != null, + onChanged: (value) => widget.onTap!(), ), if (store.state.prefState.showPdfPreviewSideBySide) invitationButton, @@ -251,43 +219,66 @@ class _VendorContactListTile extends StatelessWidget { SizedBox(width: 8), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - vendorContact.fullName.isNotEmpty - ? vendorContact.fullName + widget.fullName.isNotEmpty + ? widget.fullName : AppLocalization.of(context)!.blankContact, style: Theme.of(context).textTheme.titleMedium, ), - if (vendorContact.email.isNotEmpty) ...[ + if (widget.email.isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(top: 4), child: Text( - vendorContact.email, + widget.email, style: Theme.of(context).textTheme.bodySmall, ), ), - if ((invitation?.emailStatus ?? '').isNotEmpty) + if ((widget.invitation?.emailStatus ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( - localization.lookup(invitation!.latestEmailStatus) + + localization + .lookup(widget.invitation!.latestEmailStatus) + ' • ' + - formatDate( - invitation!.latestEmailStatusDate, context), + formatDate(widget.invitation!.latestEmailStatusDate, + context), style: Theme.of(context).textTheme.bodySmall, ), ), - if ((invitation?.emailError ?? '').isNotEmpty && - invitation?.emailStatus != - InvitationEntity.EMAIL_STATUS_DELIVERED) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - invitation!.emailError, - style: Theme.of(context).textTheme.bodySmall, - ), + if ((widget.invitation?.emailError ?? '').isNotEmpty && + widget.invitation?.emailStatus != + InvitationEntity.EMAIL_STATUS_DELIVERED && + _showEmailError) ...[ + if (state.isUsingPostmark) ...[ + SizedBox(height: 16), + OutlinedButton( + onPressed: () { + final credentials = state.credentials; + store.dispatch(StartSaving()); + WebClient() + .post( + '${credentials.url}/reactivate_email/${widget.invitation!.messageId}', + credentials.token) + .then((value) { + store.dispatch(StopSaving()); + showToast(localization.emailReactivated); + setState(() { + _showEmailError = false; + }); + }).catchError((error) { + store.dispatch(StopSaving()); + }); + }, + child: Text(localization.reactivateEmail)), + ], + SizedBox(height: 16), + Text( + widget.invitation!.emailError, + style: Theme.of(context).textTheme.bodySmall, ), + ], SizedBox(height: 8), ], ], diff --git a/lib/ui/invoice/edit/invoice_item_selector.dart b/lib/ui/invoice/edit/invoice_item_selector.dart index 168fba53347..b111460818e 100644 --- a/lib/ui/invoice/edit/invoice_item_selector.dart +++ b/lib/ui/invoice/edit/invoice_item_selector.dart @@ -76,7 +76,7 @@ class _InvoiceItemSelectorState extends State final List items = []; final state = StoreProvider.of(context).state; final company = state.company; - String? projectId; + String projectId = ''; _selected.forEach((entity) { if (entity.entityType == EntityType.product) { @@ -91,7 +91,7 @@ class _InvoiceItemSelectorState extends State ); } else if (entity.entityType == EntityType.task) { final task = entity as TaskEntity; - projectId ??= task.projectId; + projectId = task.projectId; items.add(convertTaskToInvoiceItem(task: task, context: context)); } else if (entity.entityType == EntityType.expense) { final expense = entity as ExpenseEntity; diff --git a/lib/ui/transaction/view/transaction_view.dart b/lib/ui/transaction/view/transaction_view.dart index 032700732bd..be9512c56fd 100644 --- a/lib/ui/transaction/view/transaction_view.dart +++ b/lib/ui/transaction/view/transaction_view.dart @@ -163,9 +163,9 @@ class _MatchDepositsState extends State<_MatchDeposits> { TextEditingController? _invoiceFilterController; TextEditingController? _paymentFilterController; FocusNode? _focusNode; - late List _invoices; - late List _selectedInvoices; - late List _payments; + late List _invoices; + late List _selectedInvoices; + late List _payments; PaymentEntity? _selectedPayment; bool _matchExisting = false; @@ -200,21 +200,14 @@ class _MatchDepositsState extends State<_MatchDeposits> { void updateInvoiceList() { final state = widget.viewModel.state; final invoiceState = state.invoiceState; - final transactions = widget.viewModel.transactions; _invoices = invoiceState.map.values.where((invoice) { if (_selectedInvoices.isNotEmpty) { - if (invoice.clientId != _selectedInvoices.first!.clientId) { + if (invoice.clientId != _selectedInvoices.first.clientId) { return false; } } - if (transactions.isNotEmpty && - state.clientState.get(invoice.clientId).currencyId != - transactions.first.currencyId) { - return false; - } - if (invoice.isPaid || invoice.isDeleted!) { return false; } @@ -258,14 +251,13 @@ class _MatchDepositsState extends State<_MatchDeposits> { return true; }).toList(); _invoices.sort((invoiceA, invoiceB) { - return invoiceB!.date.compareTo(invoiceA!.date); + return invoiceB.date.compareTo(invoiceA.date); }); } void updatePaymentList() { final state = widget.viewModel.state; final paymentState = state.paymentState; - final transactions = widget.viewModel.transactions; _payments = paymentState.map.values.where((payment) { if (_selectedPayment != null) { @@ -278,12 +270,6 @@ class _MatchDepositsState extends State<_MatchDeposits> { return false; } - if (transactions.isNotEmpty && - state.clientState.get(payment.clientId).currencyId != - transactions.first.currencyId) { - return false; - } - final filter = _paymentFilterController!.text; if (filter.isNotEmpty) { @@ -323,7 +309,7 @@ class _MatchDepositsState extends State<_MatchDeposits> { return true; }).toList(); _payments.sort((paymentA, paymentB) { - return paymentB!.date.compareTo(paymentA!.date); + return paymentB.date.compareTo(paymentA.date); }); } @@ -355,16 +341,20 @@ class _MatchDepositsState extends State<_MatchDeposits> { final viewModel = widget.viewModel; final state = viewModel.state; - String? currencyId; - if (_selectedInvoices.isNotEmpty) { - currencyId = - state.clientState.get(_selectedInvoices.first!.clientId).currencyId; - } - - double totalSelected = 0; + final totalSelected = {}; _selectedInvoices.forEach((invoice) { - totalSelected += invoice!.balanceOrAmount; + final client = state.clientState.get(invoice.clientId); + final currencyId = client.currencyId ?? kCurrencyUSDollar; + if (!totalSelected.containsKey(currencyId)) { + totalSelected[currencyId] = 0; + } + totalSelected[currencyId] = + totalSelected[currencyId]! + invoice.balanceOrAmount; }); + final totalSelectedString = totalSelected.keys.map((currencyId) { + return formatNumber(totalSelected[currencyId], context, + currencyId: currencyId); + }).join(' | '); return Column( mainAxisSize: MainAxisSize.max, @@ -552,7 +542,7 @@ class _MatchDepositsState extends State<_MatchDeposits> { separatorBuilder: (context, index) => ListDivider(), itemCount: _payments.length, itemBuilder: (BuildContext context, int index) { - final payment = _payments[index]!; + final payment = _payments[index]; return PaymentListItem( payment: payment, showCheckbox: true, @@ -581,7 +571,7 @@ class _MatchDepositsState extends State<_MatchDeposits> { separatorBuilder: (context, index) => ListDivider(), itemCount: _invoices.length, itemBuilder: (BuildContext context, int index) { - final invoice = _invoices[index]!; + final invoice = _invoices[index]; return InvoiceListItem( invoice: invoice, showCheckbox: true, @@ -604,7 +594,7 @@ class _MatchDepositsState extends State<_MatchDeposits> { Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( - '${_selectedInvoices.length} ${localization.selected} • ${formatNumber(totalSelected, context, currencyId: currencyId)}', + '${_selectedInvoices.length} ${localization.selected} • $totalSelectedString', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), @@ -642,7 +632,7 @@ class _MatchDepositsState extends State<_MatchDeposits> { viewModel.onConvertToPayment( context, _selectedInvoices - .map((invoice) => invoice!.id) + .map((invoice) => invoice.id) .toList(), ); }, @@ -689,7 +679,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { late List _expenses; VendorEntity? _selectedVendor; ExpenseCategoryEntity? _selectedCategory; - late List _selectedExpenses; + late List _selectedExpenses; @override void initState() { @@ -793,18 +783,12 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { void updateExpenseList() { final state = widget.viewModel.state; final expenseState = state.expenseState; - final transactions = widget.viewModel.transactions; _expenses = expenseState.map.values.where((expense) { if (expense.transactionId.isNotEmpty || expense.isDeleted!) { return false; } - if (transactions.isNotEmpty && - expense.currencyId != transactions.first.currencyId) { - return false; - } - final filter = _expenseFilterController!.text; if (filter.isNotEmpty) { @@ -889,16 +873,18 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { final transaction = transactions.isNotEmpty ? transactions.first : TransactionEntity(); - String? currencyId; - if (_selectedExpenses.isNotEmpty) { - currencyId = - state.clientState.get(_selectedExpenses.first!.clientId!).currencyId; - } - - double totalSelected = 0; + final totalSelected = {}; _selectedExpenses.forEach((expense) { - totalSelected += expense!.grossAmount; + if (!totalSelected.containsKey(expense.currencyId)) { + totalSelected[expense.currencyId] = 0; + } + totalSelected[expense.currencyId] = + totalSelected[expense.currencyId]! + expense.grossAmount; }); + final totalSelectedString = totalSelected.keys.map((currencyId) { + return formatNumber(totalSelected[currencyId], context, + currencyId: currencyId); + }).join(' | '); return Column( mainAxisSize: MainAxisSize.max, @@ -1063,7 +1049,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { store.dispatch(SaveTransactionSuccess(transaction.rebuild( (b) => b ..pendingExpenseId = _selectedExpenses - .map((expense) => expense!.id) + .map((expense) => expense.id) .join(',')))); }), ); @@ -1258,7 +1244,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Text( - '${_selectedExpenses.length} ${localization.selected} • ${formatNumber(totalSelected, context, currencyId: currencyId)}', + '${_selectedExpenses.length} ${localization.selected} • $totalSelectedString', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), @@ -1283,7 +1269,7 @@ class _MatchWithdrawalsState extends State<_MatchWithdrawals> { viewModel.onLinkToExpense( context, _selectedExpenses - .map((expense) => expense!.id) + .map((expense) => expense.id) .join(','), ); }, diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index f88f95f2212..5c0af8ea823 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,8 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'reactivate_email': 'Reactivate Email', + 'email_reactivated': 'Successfully reactivated email', 'template_help': 'Enable using the design as a template', 'delivery_note_design': 'Delivery Note Design', 'statement_design': 'Statement Design', @@ -109970,6 +109972,14 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['template_help'] ?? _localizedValues['en']!['template_help']!; + String get reactivateEmail => + _localizedValues[localeCode]!['reactivate_email'] ?? + _localizedValues['en']!['reactivate_email']!; + + String get emailReactivated => + _localizedValues[localeCode]!['email_reactivated'] ?? + _localizedValues['en']!['email_reactivated']!; + // STARTER: lang field - do not remove comment String lookup(String? key) { diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index fc536eea54c..5cb3aa6d437 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.140+140 +version: 5.0.141+141 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index 869eb8cc8e0..d269b03c59d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.140+140 +version: 5.0.141+141 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3b969622b5f..2cd33b1cb63 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.140' +version: '5.0.141' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead