From fcb5ab4f2c05724866a027c3aad4577cb1a825a8 Mon Sep 17 00:00:00 2001 From: Arseni Date: Tue, 25 Nov 2025 08:20:09 +0300 Subject: [PATCH 1/2] Added Localizations and ran small fixes --- frontend/.DS_Store | Bin 0 -> 8196 bytes frontend/pshared/.DS_Store | Bin 0 -> 6148 bytes frontend/pshared/lib/widgets/template.dart | 4 +- frontend/pweb/lib/config/constants.dart | 2 +- frontend/pweb/lib/l10n/en.arb | 33 ++++- frontend/pweb/lib/l10n/ru.arb | 33 ++++- .../pweb/lib/models/wallet_transaction.dart | 8 +- .../lib/pages/address_book/page/page.dart | 61 ++++++-- .../dashboard/buttons/balance/add_funds.dart | 5 +- .../dashboard/buttons/balance/balance.dart | 7 +- .../dashboard/payouts/multiple/history.dart | 2 +- .../payouts/single/adress_book/widget.dart | 7 +- .../payment_methods/add/method_selector.dart | 2 +- .../pweb/lib/pages/payment_methods/title.dart | 8 +- .../pages/payment_methods/widgets/card.dart | 67 +++++++++ .../widgets/payment_info_section.dart | 7 +- .../widgets/payment_page_body.dart | 6 +- .../widgets/recipient_section.dart | 132 +----------------- .../pages/payment_methods/widgets/search.dart | 71 ++++++++++ .../pages/payout_page/methods/controller.dart | 12 +- frontend/pweb/lib/pages/payout_page/page.dart | 8 +- .../payout_page/wallet/edit/buttons/save.dart | 13 +- .../payout_page/wallet/edit/buttons/send.dart | 7 +- .../wallet/edit/buttons/top_up.dart | 9 +- .../pages/payout_page/wallet/edit/header.dart | 16 ++- .../pages/payout_page/wallet/edit/page.dart | 5 +- .../payout_page/wallet/history/filters.dart | 12 +- .../payout_page/wallet/history/history.dart | 11 +- .../payout_page/wallet/history/table.dart | 28 ++-- .../lib/pages/report/charts/distribution.dart | 4 +- frontend/pweb/lib/pages/report/page.dart | 9 +- .../settings/profile/account/locale.dart | 11 +- .../pweb/lib/pages/settings/profile/page.dart | 4 +- .../pweb/lib/pages/settings/widgets/pick.dart | 23 +-- frontend/pweb/lib/services/auth.dart | 18 +++ frontend/pweb/lib/services/wallets.dart | 11 +- frontend/pweb/lib/widgets/drawer/avatar.dart | 7 +- .../lib/widgets/sidebar/destinations.dart | 2 +- frontend/pweb/lib/widgets/sidebar/page.dart | 5 +- frontend/pweb/lib/widgets/sidebar/user.dart | 7 +- frontend/pweb/resources/logo.png | Bin 27268 -> 2596 bytes 41 files changed, 444 insertions(+), 233 deletions(-) create mode 100644 frontend/.DS_Store create mode 100644 frontend/pshared/.DS_Store create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/card.dart create mode 100644 frontend/pweb/lib/pages/payment_methods/widgets/search.dart create mode 100644 frontend/pweb/lib/services/auth.dart diff --git a/frontend/.DS_Store b/frontend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fd8ce273139d7bc9d334354342044a7987768026 GIT binary patch literal 8196 zcmeHMOK;Oa5S}Tebtq6%LMd-cL7a*x4LhJpW*0daqDXbN;SwiL=+2Q9J%AnYf#93)d8AY&YjuEv%^8A^Mq?g4cu>J|f4 zIN2THPUvcEDOBNrDjZOsjJiWXc{;^8(jCxMXja33VW5`*(Yp^q9TK<=-tzvP`(%0P z*TW!5Cnnxu->UxA1B2F}HDui=+~&ZOe&@5R@|U(=k2ep~kZnSeD*c6O3SsTV{n zmH|P9k<0ZfK~&+*Ja0smM8+1B!OB>fl0CM#c;M*aap&NXLrde%;?V;W6U}ite4c2Zstd1VhQ$eCPk3?Gi(@aUvi!o0(DPY173F!F@|y4zyOnu# zF=k??VnDJ;D|h^};y34>Ye^~R?p7Kba!R1r$ZT8Eh`HByZb^AM@@g!T$+;J_$%4Hw z2~$vn8a#k!@El&iD|iEM;S+qp0nFka+=oZ-1fIq7co}D~igj$@JT}!1eeEm1Q}3t& zTO@SV^L!zWLLODgBBJ`K+MD#9%2(g1{`xxah>y?|2!(Ft9QMtCV_7^#7;6|NVdE zV52n*7zX|c2C)BR;bfjtX}?r;XC-=VhvqGsVhF#MLK#Afa1nahjuUo%FeKU0HdkXy YAz5hr_74F+|AuRx|MH5NUf+*2!QsW5&0wE9&)`Bspv4mDE0+!u&s4Lr@W_R0yNYb+& z{Q!OdzknALkA49?nRxf)2k_vVKMj`Z)fh9cdGmXH^WK}zZ?iLfgb>Ko%hw4p2_Xh5 z$JhW4zY#vqwM%-Grxy`KjfjL!GpsotbGo9nLo=Wm_}3WV?`|KlNr)dxI^W-SPR(<} zXlCX!^z_Eg^z|G4#-Op2d&-)*$d2k^#tydlw>)E>)3$AA!==rlnOtUmWV=2MM1otU z7_$Ay^~FycnMXigj*%`PoWS#yi?$5YnP((KHXH9xm-d>l8%Qqwoq zi~C;HXD=m*fM<@7A%$&tg+|Ao+)_iGN${n;r?2Kndsp#+!Lvi>hRu=0XfiRD7#|-` zo}ajIadPrf(HwQ`(tai2zjv8=EXa6ux>X6>a;|2VT)$F?=s}copVOk5I2F21%TG>R z`NJBVFwa|Kp&Pl(-+XKz3c0-M!U?1$>QOVG8R%qy|My2R z`Tc+U&;7p$(t~C|Gw^>h0I`+aN(T3)?$)kd^1IeT-9_cX<606$3J&Tz76!kHCs2jp aoW%{qw8EA|^q}ZJ0)hrTXa@c&13v+H1<|ws literal 0 HcmV?d00001 diff --git a/frontend/pshared/lib/widgets/template.dart b/frontend/pshared/lib/widgets/template.dart index 5692f67..9041c88 100644 --- a/frontend/pshared/lib/widgets/template.dart +++ b/frontend/pshared/lib/widgets/template.dart @@ -21,8 +21,8 @@ class ResourceContainer extends StatelessWidget { @override Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { if (provider.isLoading) return loading ?? Center(child: CircularProgressIndicator()); - if (provider.error != null) return error ?? Text('Error while loading data. Try again'); //TODO: need to implement localizations and add more details to the error - if (provider.isEmpty) return empty ?? Text('Empty data'); //TODO: need to implement localizations too + if (provider.error != null) return error ?? Text('Error while loading data. Try again'); + if (provider.isEmpty) return empty ?? Text('Empty data'); return builder(context, provider); }); } diff --git a/frontend/pweb/lib/config/constants.dart b/frontend/pweb/lib/config/constants.dart index d6b52bc..3e371cd 100644 --- a/frontend/pweb/lib/config/constants.dart +++ b/frontend/pweb/lib/config/constants.dart @@ -5,6 +5,6 @@ class Constants { class AppConfig { static const String appName = String.fromEnvironment( 'APP_NAME', - defaultValue: 'SendiCo', + defaultValue: 'sendico', ); } diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index b5d53d1..7a44b1b 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -433,5 +433,36 @@ "companyDescriptionHint": "Describe any of the fields of the Company's business", "optional": "optional", "ownerRole": "Organization Owner", - "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges" + "ownerRoleDescription": "This role is granted to the organization’s creator, providing full administrative privileges", + "save": "Save", + "editWallet": "Edit Wallet", + "userNamePlaceholder": "User Name", + "noWalletSelected": "No wallet selected", + "noWalletsAvailable": "No wallets available", + "walletActivity": "Wallet activity", + "reset": "Reset", + "failedToLoadHistory": "Failed to load history", + "retry": "Retry", + "walletName": "Wallet name", + "walletNameSaved": "Wallet name saved", + "topUpBalance": "Top Up Balance", + "addFunctionality": "Add functionality", + "walletHistoryEmpty": "No history yet", + "colType": "Type", + "colAmount": "Amount", + "colBalance": "Balance", + "colCounterparty": "Counterparty", + "colDate": "Date", + "colComment": "Comment", + "recipientNoPaymentDetails": "This recipient has no available payment details.", + "paymentInfo": "Payment info", + "recipient": "Recipient", + "chooseAnotherRecipient": "Choose another recipient", + "noRecipientsYet": "No recipients yet.", + "noRecipientsFound": "No recipients found for this query.", + "sourceOfFunds": "Source of funds", + "walletTopUp": "Top up", + "englishLanguage": "English", + "russianLanguage": "Russian", + "germanLanguage": "German" } \ No newline at end of file diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 5303005..79d873d 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -425,5 +425,36 @@ "noRecipientSelected": "Получатель не выбран", "ownerRole": "Владелец организации", - "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права" + "ownerRoleDescription": "Эта роль предоставляется создателю организации и даёт ему полные административные права", + "save": "Сохранить", + "editWallet": "Редактировать кошелек", + "userNamePlaceholder": "Имя пользователя", + "noWalletSelected": "Кошелек не выбран", + "noWalletsAvailable": "Кошельки отсутствуют", + "walletActivity": "Активность кошелька", + "reset": "Сбросить", + "failedToLoadHistory": "Не удалось загрузить историю", + "retry": "Повторить", + "walletName": "Название кошелька", + "walletNameSaved": "Название кошелька сохранено", + "topUpBalance": "Пополнить баланс", + "addFunctionality": "Добавить функциональность", + "walletHistoryEmpty": "История пуста", + "colType": "Тип", + "colAmount": "Сумма", + "colBalance": "Баланс", + "colCounterparty": "Контрагент", + "colDate": "Дата", + "colComment": "Комментарий", + "recipientNoPaymentDetails": "У этого получателя нет доступных платежных данных.", + "paymentInfo": "Платежная информация", + "recipient": "Получатель", + "chooseAnotherRecipient": "Выбрать другого получателя", + "noRecipientsYet": "Получателей пока нет.", + "noRecipientsFound": "Получатели по запросу не найдены.", + "sourceOfFunds": "Источник средств", + "walletTopUp": "Пополнение", + "englishLanguage": "Английский", + "russianLanguage": "Русский", + "germanLanguage": "Немецкий" } \ No newline at end of file diff --git a/frontend/pweb/lib/models/wallet_transaction.dart b/frontend/pweb/lib/models/wallet_transaction.dart index adcd036..b8a9f79 100644 --- a/frontend/pweb/lib/models/wallet_transaction.dart +++ b/frontend/pweb/lib/models/wallet_transaction.dart @@ -1,18 +1,22 @@ import 'package:flutter/widgets.dart'; + import 'package:pshared/models/payment/status.dart'; import 'package:pweb/models/currency.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + enum WalletTransactionType { topUp, payout } extension WalletTransactionTypeX on WalletTransactionType { String label(BuildContext context) { + final loc = AppLocalizations.of(context)!; switch (this) { case WalletTransactionType.topUp: - return 'Top up'; + return loc.walletTopUp; case WalletTransactionType.payout: - return 'Payout'; + return loc.payout; } } diff --git a/frontend/pweb/lib/pages/address_book/page/page.dart b/frontend/pweb/lib/pages/address_book/page/page.dart index e459bba..2ed2a90 100644 --- a/frontend/pweb/lib/pages/address_book/page/page.dart +++ b/frontend/pweb/lib/pages/address_book/page/page.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; - import 'package:pshared/models/recipient/filter.dart'; + import 'package:pweb/pages/address_book/page/filter_button.dart'; import 'package:pweb/pages/address_book/page/header.dart'; import 'package:pweb/pages/address_book/page/list.dart'; @@ -14,7 +14,7 @@ import 'package:pweb/providers/recipient.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class RecipientAddressBookPage extends StatelessWidget { +class RecipientAddressBookPage extends StatefulWidget { final ValueChanged onRecipientSelected; final VoidCallback onAddRecipient; final ValueChanged? onEditRecipient; @@ -31,32 +31,65 @@ class RecipientAddressBookPage extends StatelessWidget { static const double _bigBox = 30; static const double _smallBox = 20; + @override + State createState() => _RecipientAddressBookPageState(); +} + +class _RecipientAddressBookPageState extends State { + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + + @override + void initState() { + super.initState(); + final provider = context.read(); + _searchController = TextEditingController(text: provider.query); + _searchFocusNode = FocusNode(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + void _syncSearchField(RecipientProvider provider) { + final query = provider.query; + if (_searchController.text == query) return; + + _searchController.value = TextEditingValue( + text: query, + selection: TextSelection.collapsed(offset: query.length), + ); + } @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final provider = context.watch(); + _syncSearchField(provider); if (provider.isLoading) { - return const Center(child: CircularProgressIndicator()); //TODO This should be in the provider + return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecipientAddressBookHeader(onAddRecipient: onAddRecipient), - const SizedBox(height: _smallBox), + RecipientAddressBookHeader(onAddRecipient: widget.onAddRecipient), + const SizedBox(height: RecipientAddressBookPage._smallBox), RecipientSearchField( - controller: TextEditingController(text: provider.query), - focusNode: FocusNode(), + controller: _searchController, + focusNode: _searchFocusNode, onChanged: provider.setQuery, ), - const SizedBox(height: _bigBox), + const SizedBox(height: RecipientAddressBookPage._bigBox), Row( children: [ RecipientFilterButton( @@ -86,17 +119,17 @@ class RecipientAddressBookPage extends StatelessWidget { ], ), SizedBox( - height: _expandedHeight, + height: RecipientAddressBookPage._expandedHeight, child: Padding( - padding: const EdgeInsets.all(_paddingAll), + padding: const EdgeInsets.all(RecipientAddressBookPage._paddingAll), child: RecipientAddressBookList( filteredRecipients: provider.filteredRecipients, - onEdit: (recipient) => onEditRecipient?.call(recipient), - onSelected: onRecipientSelected, + onEdit: (recipient) => widget.onEditRecipient?.call(recipient), + onSelected: widget.onRecipientSelected, ), ), ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart index 49f4658..502f9c9 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/add_funds.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceAddFunds extends StatelessWidget { final VoidCallback onTopUp; @@ -19,6 +21,7 @@ class BalanceAddFunds extends StatelessWidget { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; + final loc = AppLocalizations.of(context)!; return InkWell( onTap: onTopUp, @@ -37,7 +40,7 @@ class BalanceAddFunds extends StatelessWidget { ), const SizedBox(width: _spacingMedium), Text( - 'Add funds', + loc.addFunds, style: textTheme.bodyMedium?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w500, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart index 6f2fb87..cb4f39b 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance.dart @@ -5,6 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceWidget extends StatelessWidget { const BalanceWidget({super.key}); @@ -12,6 +14,7 @@ class BalanceWidget extends StatelessWidget { @override Widget build(BuildContext context) { final walletsProvider = context.watch(); + final loc = AppLocalizations.of(context)!; if (walletsProvider.isLoading) { return const Center(child: CircularProgressIndicator()); @@ -20,7 +23,7 @@ class BalanceWidget extends StatelessWidget { final wallets = walletsProvider.wallets; if (wallets == null || wallets.isEmpty) { - return const Center(child: Text('No wallets available')); + return Center(child: Text(loc.noWalletsAvailable)); } return @@ -29,4 +32,4 @@ class BalanceWidget extends StatelessWidget { onWalletChanged: walletsProvider.selectWallet, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart index 0760430..056109d 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/history.dart @@ -23,7 +23,7 @@ class UploadHistorySection extends StatelessWidget { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Text("Error: ${provider.error}"); + return Text(l10.notificationError(provider.error ?? l10.noErrorInformation)); } final items = provider.data ?? []; diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart index 4974e05..bceda13 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/adress_book/widget.dart @@ -9,6 +9,8 @@ import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/long_l import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart'; import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class AdressBookPayout extends StatefulWidget { final ValueChanged onSelected; @@ -54,6 +56,7 @@ class _AdressBookPayoutState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; final provider = context.watch(); if (provider.isLoading) { @@ -61,7 +64,7 @@ class _AdressBookPayoutState extends State { } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return SizedBox( @@ -100,4 +103,4 @@ class _AdressBookPayoutState extends State { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart index 9411619..3e19495 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/method_selector.dart @@ -21,7 +21,7 @@ class PaymentMethodTypeSelector extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return DropdownButtonFormField( - value: value, + initialValue: value, decoration: InputDecoration(labelText: l10n.paymentType), items: PaymentType.values.map((type) { final label = getPaymentTypeLabel(context, type); diff --git a/frontend/pweb/lib/pages/payment_methods/title.dart b/frontend/pweb/lib/pages/payment_methods/title.dart index 54136fd..c14c93b 100644 --- a/frontend/pweb/lib/pages/payment_methods/title.dart +++ b/frontend/pweb/lib/pages/payment_methods/title.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:pweb/pages/payment_methods/icon.dart'; import 'package:pshared/models/payment/methods/type.dart'; +import 'package:pweb/pages/payment_methods/icon.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentMethodTile extends StatelessWidget { const PaymentMethodTile({ super.key, @@ -62,7 +64,7 @@ class PaymentMethodTile extends StatelessWidget { Widget _buildMakeMainButton(BuildContext context) { final theme = Theme.of(context); return IconButton( - tooltip: 'Make main', + tooltip: AppLocalizations.of(context)!.makeMain, icon: Icon( method.isMain ? Icons.star : Icons.star_outline, color: method.isMain ? theme.colorScheme.primary : null, @@ -97,4 +99,4 @@ class PaymentMethodTile extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/card.dart b/frontend/pweb/lib/pages/payment_methods/widgets/card.dart new file mode 100644 index 0000000..bc6edf2 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/card.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class SelectedRecipientCard extends StatelessWidget { + final AppDimensions dimensions; + final Recipient recipient; + final VoidCallback onClear; + + const SelectedRecipientCard({ + super.key, + required this.dimensions, + required this.recipient, + required this.onClear, + }); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: EdgeInsets.all(dimensions.paddingMedium), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SectionTitle(loc.recipient), + SizedBox(height: dimensions.paddingSmall), + Row( + children: [ + CircleAvatar( + child: Text(recipient.name.substring(0, 1).toUpperCase()), + ), + SizedBox(width: dimensions.paddingMedium), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(recipient.name, style: theme.textTheme.titleMedium), + if (recipient.email.isNotEmpty) + Text( + recipient.email, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + ], + ), + ), + TextButton( + onPressed: onClear, + child: Text(loc.chooseAnotherRecipient), + ), + ], + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart index 534a727..5ba43bd 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_info_section.dart @@ -10,6 +10,8 @@ import 'package:pweb/providers/payment_flow_provider.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/utils/payment/selector_type.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentInfoSection extends StatelessWidget { final AppDimensions dimensions; @@ -27,13 +29,14 @@ class PaymentInfoSection extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; final hasRecipient = recipient != null; final availableTypes = hasRecipient ? pageSelector.getAvailablePaymentTypes() : {for (final type in PaymentType.values) type: type}; if (hasRecipient && availableTypes.isEmpty) { - return const Text('This recipient has no available payment details.'); + return Text(loc.recipientNoPaymentDetails); } final selectedType = flowProvider.selectedType; @@ -41,7 +44,7 @@ class PaymentInfoSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionTitle('Payment info'), + SectionTitle(loc.paymentInfo), SizedBox(height: dimensions.paddingSmall), PaymentTypeSelector( availableTypes: availableTypes, diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart index 8c43c52..6479aac 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/payment_page_body.dart @@ -15,6 +15,7 @@ import 'package:pweb/providers/payment_flow_provider.dart'; import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentPageBody extends StatelessWidget { @@ -45,13 +46,14 @@ class PaymentPageBody extends StatelessWidget { final recipientProvider = context.watch(); final flowProvider = context.watch(); final recipient = pageSelector.selectedRecipient; + final loc = AppLocalizations.of(context)!; if (methodsProvider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (methodsProvider.error != null) { - return Center(child: Text('Error: ${methodsProvider.error}')); + return Center(child: Text(loc.notificationError(methodsProvider.error ?? loc.noErrorInformation))); } return Align( @@ -74,7 +76,7 @@ class PaymentPageBody extends StatelessWidget { PaymentHeader(), SizedBox(height: dimensions.paddingXXLarge), - const SectionTitle('Source of funds'), + SectionTitle(loc.sourceOfFunds), SizedBox(height: dimensions.paddingSmall), PaymentMethodSelector( methodsProvider: methodsProvider, diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart index 69c1c84..1d284fa 100644 --- a/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart +++ b/frontend/pweb/lib/pages/payment_methods/widgets/recipient_section.dart @@ -3,10 +3,14 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pweb/pages/address_book/page/search.dart'; +import 'package:pweb/pages/payment_methods/widgets/card.dart'; +import 'package:pweb/pages/payment_methods/widgets/search.dart'; import 'package:pweb/pages/payment_methods/widgets/section_title.dart'; import 'package:pweb/providers/recipient.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class RecipientSection extends StatelessWidget { final Recipient? recipient; @@ -32,6 +36,7 @@ class RecipientSection extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; if (recipient != null) { return SelectedRecipientCard( dimensions: dimensions, @@ -43,7 +48,7 @@ class RecipientSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionTitle('Recipient'), + SectionTitle(loc.recipient), SizedBox(height: dimensions.paddingSmall), RecipientSearchField( controller: searchController, @@ -61,127 +66,4 @@ class RecipientSection extends StatelessWidget { ], ); } -} - -class SelectedRecipientCard extends StatelessWidget { - final AppDimensions dimensions; - final Recipient recipient; - final VoidCallback onClear; - - const SelectedRecipientCard({ - super.key, - required this.dimensions, - required this.recipient, - required this.onClear, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: EdgeInsets.all(dimensions.paddingMedium), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant.withOpacity(0.4), - borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SectionTitle('Recipient'), - SizedBox(height: dimensions.paddingSmall), - Row( - children: [ - CircleAvatar( - child: Text(recipient.name.substring(0, 1).toUpperCase()), - ), - SizedBox(width: dimensions.paddingMedium), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(recipient.name, style: theme.textTheme.titleMedium), - if (recipient.email.isNotEmpty) - Text( - recipient.email, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.7), - ), - ), - ], - ), - ), - TextButton( - onPressed: onClear, - child: const Text('Choose another recipient'), - ), - ], - ), - ], - ), - ); - } -} - -class RecipientSearchResults extends StatelessWidget { - final AppDimensions dimensions; - final RecipientProvider recipientProvider; - final ValueChanged onRecipientSelected; - - const RecipientSearchResults({ - super.key, - required this.dimensions, - required this.recipientProvider, - required this.onRecipientSelected, - }); - - @override - Widget build(BuildContext context) { - if (recipientProvider.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - if (recipientProvider.error != null) { - return Text( - recipientProvider.error!, - style: TextStyle(color: Theme.of(context).colorScheme.error), - ); - } - - if (recipientProvider.recipients.isEmpty) { - return const Text('No recipients yet.'); - } - - final results = recipientProvider.filteredRecipients; - - if (results.isEmpty) { - return const Text('No recipients found for this query.'); - } - - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 240), - child: ListView.separated( - shrinkWrap: true, - itemCount: results.length, - separatorBuilder: (_, __) => SizedBox(height: dimensions.paddingSmall), - itemBuilder: (context, index) { - final recipient = results[index]; - return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - ), - tileColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.2), - leading: CircleAvatar( - child: Text(recipient.name.substring(0, 1).toUpperCase()), - ), - title: Text(recipient.name), - subtitle: Text(recipient.email), - trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16), - onTap: () => onRecipientSelected(recipient), - ); - }, - ), - ); - } -} +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/payment_methods/widgets/search.dart b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart new file mode 100644 index 0000000..c6acf25 --- /dev/null +++ b/frontend/pweb/lib/pages/payment_methods/widgets/search.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/recipient/recipient.dart'; + +import 'package:pweb/providers/recipient.dart'; +import 'package:pweb/utils/dimensions.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class RecipientSearchResults extends StatelessWidget { + final AppDimensions dimensions; + final RecipientProvider recipientProvider; + final ValueChanged onRecipientSelected; + + const RecipientSearchResults({ + super.key, + required this.dimensions, + required this.recipientProvider, + required this.onRecipientSelected, + }); + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; + if (recipientProvider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (recipientProvider.error != null) { + return Text( + loc.notificationError(recipientProvider.error ?? loc.noErrorInformation), + style: TextStyle(color: Theme.of(context).colorScheme.error), + ); + } + + if (recipientProvider.recipients.isEmpty) { + return Text(loc.noRecipientsYet); + } + + final results = recipientProvider.filteredRecipients; + + if (results.isEmpty) { + return Text(loc.noRecipientsFound); + } + + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 240), + child: ListView.separated( + shrinkWrap: true, + itemCount: results.length, + separatorBuilder: (_, _) => SizedBox(height: dimensions.paddingSmall), + itemBuilder: (context, index) { + final recipient = results[index]; + return ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), + ), + leading: CircleAvatar( + child: Text(recipient.name.substring(0, 1).toUpperCase()), + ), + title: Text(recipient.name), + subtitle: Text(recipient.email), + trailing: const Icon(Icons.arrow_forward_ios_rounded, size: 16), + onTap: () => onRecipientSelected(recipient), + ); + }, + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/methods/controller.dart b/frontend/pweb/lib/pages/payout_page/methods/controller.dart index e0496f4..f964b0a 100644 --- a/frontend/pweb/lib/pages/payout_page/methods/controller.dart +++ b/frontend/pweb/lib/pages/payout_page/methods/controller.dart @@ -21,11 +21,12 @@ class PaymentConfigController { } Future addMethod() async { + final methodsProvider = context.read(); await showDialog( context: context, builder: (_) => const AddPaymentMethodDialog(), ); - loadMethods(); + methodsProvider.loadMethods(); } Future editMethod(PaymentMethod method) async { @@ -33,19 +34,20 @@ class PaymentConfigController { } Future deleteMethod(PaymentMethod method) async { + final methodsProvider = context.read(); final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( context: context, - builder: (_) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(l10n.delete), content: Text(l10n.deletePaymentConfirmation), actions: [ TextButton( - onPressed: () => Navigator.pop(context, false), + onPressed: () => Navigator.pop(dialogContext, false), child: Text(l10n.cancel), ), ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.pop(dialogContext, true), child: Text(l10n.delete), ), ], @@ -53,7 +55,7 @@ class PaymentConfigController { ); if (confirmed == true) { - context.read().deleteMethod(method); + methodsProvider.deleteMethod(method); } } diff --git a/frontend/pweb/lib/pages/payout_page/page.dart b/frontend/pweb/lib/pages/payout_page/page.dart index 818f1c6..5ef4fbb 100644 --- a/frontend/pweb/lib/pages/payout_page/page.dart +++ b/frontend/pweb/lib/pages/payout_page/page.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; + import 'package:pweb/models/wallet.dart'; import 'package:pweb/pages/payout_page/methods/widget.dart'; import 'package:pweb/pages/payout_page/wallet/wigets.dart'; import 'package:pweb/providers/payment_methods.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentConfigPage extends StatelessWidget { final Function(Wallet) onWalletTap; @@ -16,13 +19,14 @@ class PaymentConfigPage extends StatelessWidget { @override Widget build(BuildContext context) { final provider = context.watch(); + final loc = AppLocalizations.of(context)!; if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { - return Center(child: Text('Error: ${provider.error}')); + return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); } return Column( @@ -34,4 +38,4 @@ class PaymentConfigPage extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart index 8d64fea..0529585 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/save.dart @@ -3,25 +3,28 @@ import 'package:flutter/material.dart'; import 'package:pweb/models/wallet.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class SaveWalletButton extends StatelessWidget { final Wallet wallet; final TextEditingController nameController; final TextEditingController balanceController; - final VoidCallback onSave; // Changed to VoidCallback + final VoidCallback onSave; const SaveWalletButton({ super.key, required this.wallet, required this.nameController, required this.balanceController, - required this.onSave, // Now matches _saveWallet signature + required this.onSave, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final dimensions = AppDimensions(); + final loc = AppLocalizations.of(context)!; return Center( child: SizedBox( @@ -29,7 +32,7 @@ class SaveWalletButton extends StatelessWidget { height: dimensions.buttonHeight, child: InkWell( borderRadius: BorderRadius.circular(dimensions.borderRadiusSmall), - onTap: onSave, // Directly use onSave now + onTap: onSave, child: Container( decoration: BoxDecoration( color: theme.colorScheme.primary, @@ -37,7 +40,7 @@ class SaveWalletButton extends StatelessWidget { ), child: Center( child: Text( - 'Save', + loc.save, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSecondary, fontWeight: FontWeight.w600, @@ -49,4 +52,4 @@ class SaveWalletButton extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart index 4cfaccb..4232719 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; + import 'package:provider/provider.dart'; + import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class SendPayoutButton extends StatelessWidget { const SendPayoutButton({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return ElevatedButton( style: ElevatedButton.styleFrom( shadowColor: null, @@ -23,7 +28,7 @@ class SendPayoutButton extends StatelessWidget { pageSelectorProvider.startPaymentFromWallet(wallet); } }, - child: Text('Send Payout'), + child: Text(loc.payoutNavSendPayout), ); } } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart index 898fdb9..7e9b69c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class TopUpButton extends StatelessWidget{ const TopUpButton({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return ElevatedButton( style: ElevatedButton.styleFrom( shadowColor: null, @@ -13,10 +16,10 @@ class TopUpButton extends StatelessWidget{ ), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Add functionality')), + SnackBar(content: Text(loc.addFunctionality)), ); }, - child: Text('Top Up Balance'), + child: Text(loc.topUpBalance), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart index d91e449..4d9b550 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart @@ -4,6 +4,8 @@ import 'package:provider/provider.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletEditHeader extends StatefulWidget { const WalletEditHeader({super.key}); @@ -32,7 +34,9 @@ class _WalletEditHeaderState extends State { Widget build(BuildContext context) { final provider = context.watch(); final wallet = provider.selectedWallet; - + final loc = AppLocalizations.of(context)!; + final messanger = ScaffoldMessenger.of(context); + if (wallet == null) { return SizedBox.shrink(); } @@ -74,10 +78,10 @@ class _WalletEditHeaderState extends State { Expanded( child: TextFormField( controller: _controller, - decoration: const InputDecoration( + decoration: InputDecoration( border: OutlineInputBorder(), isDense: true, - hintText: 'Wallet name', + hintText: loc.walletName, ), ), ), @@ -87,8 +91,8 @@ class _WalletEditHeaderState extends State { onPressed: () async { provider.updateName(wallet.id, _controller.text); await provider.updateWallet(wallet.copyWith(name: _controller.text)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Wallet name saved')), + messanger.showSnackBar( + SnackBar(content: Text(loc.walletNameSaved)), ); setState(() { _isEditing = false; @@ -110,4 +114,4 @@ class _WalletEditHeaderState extends State { ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart index 635b259..e95431c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart @@ -9,6 +9,8 @@ import 'package:pweb/pages/payout_page/wallet/history/history.dart'; import 'package:pweb/providers/wallets.dart'; import 'package:pweb/utils/dimensions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletEditPage extends StatelessWidget { final VoidCallback onBack; @@ -18,13 +20,14 @@ class WalletEditPage extends StatelessWidget { @override Widget build(BuildContext context) { final dimensions = AppDimensions(); + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { final wallet = provider.selectedWallet; if (wallet == null) { - return Center(child: Text('Кошелёк не выбран')); + return Center(child: Text(loc.noWalletSelected)); } return Align( diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart index 65639e4..7b80d4f 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/filters.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:pshared/models/payment/status.dart'; @@ -7,6 +6,8 @@ import 'package:pshared/utils/localization.dart'; import 'package:pweb/models/wallet_transaction.dart'; import 'package:pweb/providers/wallet_transactions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletHistoryFilters extends StatelessWidget { final WalletTransactionsProvider provider; @@ -21,6 +22,7 @@ class WalletHistoryFilters extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; return Card( elevation: 2, @@ -35,13 +37,13 @@ class WalletHistoryFilters extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Wallet activity', + loc.walletActivity, style: theme.textTheme.titleMedium, ), if (provider.hasFilters) TextButton( onPressed: provider.resetFilters, - child: const Text('Reset'), + child: Text(loc.reset), ), ], ), @@ -81,7 +83,7 @@ class WalletHistoryFilters extends StatelessWidget { icon: const Icon(Icons.date_range_outlined), label: Text( provider.dateRange == null - ? 'Select period' + ? loc.selectPeriod : '${dateToLocalFormat(context, provider.dateRange!.start)} – ${dateToLocalFormat(context, provider.dateRange!.end)}', ), ), @@ -91,4 +93,4 @@ class WalletHistoryFilters extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart index 1e64438..b6117d3 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart @@ -7,6 +7,8 @@ import 'package:pweb/pages/payout_page/wallet/history/filters.dart'; import 'package:pweb/pages/payout_page/wallet/history/table.dart'; import 'package:pweb/providers/wallet_transactions.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletHistory extends StatefulWidget { final Wallet wallet; @@ -64,6 +66,7 @@ class _WalletHistoryState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { @@ -81,16 +84,16 @@ class _WalletHistoryState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Failed to load history', + loc.failedToLoadHistory, style: theme.textTheme.titleMedium! .copyWith(color: theme.colorScheme.error), ), const SizedBox(height: 8), - Text(provider.error!), + Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), const SizedBox(height: 8), OutlinedButton( onPressed: _load, - child: const Text('Retry'), + child: Text(loc.retry), ), ], ), @@ -113,4 +116,4 @@ class _WalletHistoryState extends State { }, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart index 49cd9c0..43312fb 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:pweb/models/wallet_transaction.dart'; @@ -6,6 +5,8 @@ import 'package:pweb/pages/payout_page/wallet/history/chip.dart'; import 'package:pweb/pages/report/table/badge.dart'; import 'package:pweb/utils/currency.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class WalletTransactionsTable extends StatelessWidget { final List transactions; @@ -15,14 +16,15 @@ class WalletTransactionsTable extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; if (transactions.isEmpty) { return Card( color: theme.colorScheme.onSecondary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: const Padding( - padding: EdgeInsets.all(16), - child: Text('No history yet'), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text(loc.walletHistoryEmpty), ), ); } @@ -38,14 +40,14 @@ class WalletTransactionsTable extends StatelessWidget { child: DataTable( columnSpacing: 18, headingTextStyle: const TextStyle(fontWeight: FontWeight.w600), - columns: const [ - DataColumn(label: Text('Status')), - DataColumn(label: Text('Type')), - DataColumn(label: Text('Amount')), - DataColumn(label: Text('Balance')), - DataColumn(label: Text('Counterparty')), - DataColumn(label: Text('Date')), - DataColumn(label: Text('Comment')), + columns: [ + DataColumn(label: Text(loc.colStatus)), + DataColumn(label: Text(loc.colType)), + DataColumn(label: Text(loc.colAmount)), + DataColumn(label: Text(loc.colBalance)), + DataColumn(label: Text(loc.colCounterparty)), + DataColumn(label: Text(loc.colDate)), + DataColumn(label: Text(loc.colComment)), ], rows: List.generate( transactions.length, @@ -85,4 +87,4 @@ class WalletTransactionsTable extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/report/charts/distribution.dart b/frontend/pweb/lib/pages/report/charts/distribution.dart index b491985..db016b3 100644 --- a/frontend/pweb/lib/pages/report/charts/distribution.dart +++ b/frontend/pweb/lib/pages/report/charts/distribution.dart @@ -16,7 +16,7 @@ class PayoutDistributionChart extends StatelessWidget { // 1) Aggregate sums final sums = {}; for (var op in operations) { - final name = op.name ?? AppLocalizations.of(context)!.unknown; + final name = op.name; sums[name] = (sums[name] ?? 0) + op.amount; } if (sums.isEmpty) { @@ -32,7 +32,7 @@ class PayoutDistributionChart extends StatelessWidget { final palette = [ Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.secondary, - Theme.of(context).colorScheme.tertiary ?? Colors.grey, + Theme.of(context).colorScheme.tertiary, Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.secondaryContainer, ]; diff --git a/frontend/pweb/lib/pages/report/page.dart b/frontend/pweb/lib/pages/report/page.dart index 31b1477..3b14e66 100644 --- a/frontend/pweb/lib/pages/report/page.dart +++ b/frontend/pweb/lib/pages/report/page.dart @@ -8,6 +8,8 @@ import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/widget.dart'; import 'package:pweb/providers/operatioins.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class OperationHistoryPage extends StatefulWidget { const OperationHistoryPage({super.key}); @@ -48,6 +50,7 @@ class _OperationHistoryPageState extends State { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { if (provider.isLoading) { @@ -59,10 +62,10 @@ class _OperationHistoryPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Error: ${provider.error}'), + Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), ElevatedButton( onPressed: () => provider.loadOperations(), - child: const Text('Retry'), + child: Text(loc.retry), ), ], ), @@ -108,4 +111,4 @@ class _OperationHistoryPageState extends State { }, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/profile/account/locale.dart b/frontend/pweb/lib/pages/settings/profile/account/locale.dart index 31d5982..22ff7e1 100644 --- a/frontend/pweb/lib/pages/settings/profile/account/locale.dart +++ b/frontend/pweb/lib/pages/settings/profile/account/locale.dart @@ -4,9 +4,10 @@ import 'package:provider/provider.dart'; import 'package:pshared/provider/locale.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/services/amplitude.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class LocalePicker extends StatelessWidget { final String title; @@ -74,13 +75,13 @@ class LocalePicker extends StatelessWidget { String _localizedLocaleName(Locale locale, AppLocalizations loc) { switch (locale.languageCode) { case 'en': - return 'English'; + return loc.englishLanguage; case 'ru': - return 'Русский'; + return loc.russianLanguage; case 'de': - return 'Deutsch'; + return loc.germanLanguage; default: return locale.toString(); } } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/profile/page.dart b/frontend/pweb/lib/pages/settings/profile/page.dart index ce850f4..861ab09 100644 --- a/frontend/pweb/lib/pages/settings/profile/page.dart +++ b/frontend/pweb/lib/pages/settings/profile/page.dart @@ -37,7 +37,7 @@ class ProfileSettingsPage extends StatelessWidget { errorText: loc.avatarUpdateError, ), AccountName( - name: 'User Name', + name: loc.userNamePlaceholder, title: loc.accountName, hintText: loc.accountNameHint, errorText: loc.accountNameUpdateError, @@ -50,4 +50,4 @@ class ProfileSettingsPage extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/settings/widgets/pick.dart b/frontend/pweb/lib/pages/settings/widgets/pick.dart index 7bf7933..e607007 100644 --- a/frontend/pweb/lib/pages/settings/widgets/pick.dart +++ b/frontend/pweb/lib/pages/settings/widgets/pick.dart @@ -78,14 +78,21 @@ class SelectValueTile extends BaseEditTile { ), ), Flexible( - child: ListView( - shrinkWrap: true, - children: displayedOptions.map((o) => RadioListTile( - value: o, - groupValue: initial, - title: Text(labelBuilder(o)), - onChanged: isSaving ? null : (v) { if (v != null) onSave(v); }, - )).toList(), + child: RadioGroup( + groupValue: initial, + onChanged: (value) { + if (value != null && !isSaving) { + onSave(value); + } + }, + child: ListView( + shrinkWrap: true, + children: displayedOptions.map((o) => RadioListTile( + value: o, + title: Text(labelBuilder(o)), + enabled: !isSaving, + )).toList(), + ), ), ), const Divider(), diff --git a/frontend/pweb/lib/services/auth.dart b/frontend/pweb/lib/services/auth.dart new file mode 100644 index 0000000..7c0c099 --- /dev/null +++ b/frontend/pweb/lib/services/auth.dart @@ -0,0 +1,18 @@ +class AuthenticationService { + Future verifyTwoFactorCode(String code) async { + await Future.delayed(const Duration(seconds: 2)); + + if (code == '000000') { + return true; + } else { + throw const WrongCodeException(); + } + } +} + +class WrongCodeException implements Exception { + const WrongCodeException(); + + @override + String toString() => 'WrongCodeException'; +} diff --git a/frontend/pweb/lib/services/wallets.dart b/frontend/pweb/lib/services/wallets.dart index 7f5d033..24c8a4a 100644 --- a/frontend/pweb/lib/services/wallets.dart +++ b/frontend/pweb/lib/services/wallets.dart @@ -26,7 +26,7 @@ class MockWalletsService implements WalletsService { Future getWallet(String walletId) async { return _wallets.firstWhere( (wallet) => wallet.id == walletId, - orElse: () => throw Exception('Wallet not found'), + orElse: () => throw const WalletNotFoundException(), ); } @@ -38,4 +38,11 @@ class MockWalletsService implements WalletsService { @override Future> deleteWallet() async => []; -} \ No newline at end of file +} + +class WalletNotFoundException implements Exception { + const WalletNotFoundException(); + + @override + String toString() => 'WalletNotFoundException'; +} diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart index 11dc659..632ea53 100644 --- a/frontend/pweb/lib/widgets/drawer/avatar.dart +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -6,16 +6,19 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:pshared/provider/account.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class AccountAvatar extends StatelessWidget { const AccountAvatar({super.key}); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, _) => UserAccountsDrawerHeader( - accountName: Text(provider.account?.name ?? 'John Doe'), - accountEmail: Text(provider.account?.login ?? 'john.doe@acme.com'), + accountName: Text(provider.account?.name ?? loc.userNamePlaceholder), + accountEmail: Text(provider.account?.login ?? loc.usernameHint), currentAccountPicture: CircleAvatar( backgroundImage: (provider.account?.avatarUrl?.isNotEmpty ?? false) ? CachedNetworkImageProvider(provider.account!.avatarUrl!) diff --git a/frontend/pweb/lib/widgets/sidebar/destinations.dart b/frontend/pweb/lib/widgets/sidebar/destinations.dart index 62598c9..899e805 100644 --- a/frontend/pweb/lib/widgets/sidebar/destinations.dart +++ b/frontend/pweb/lib/widgets/sidebar/destinations.dart @@ -40,7 +40,7 @@ enum PayoutDestination { case PayoutDestination.addrecipient: return loc.addRecipient; case PayoutDestination.editwallet: - return 'Edit Wallet'; + return loc.editWallet; } } } diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index 84d9528..bfb3c37 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -15,6 +15,8 @@ import 'package:pweb/pages/dashboard/dashboard.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/widgets/sidebar/sidebar.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class PageSelector extends StatelessWidget { const PageSelector({super.key}); @@ -22,6 +24,7 @@ class PageSelector extends StatelessWidget { @override Widget build(BuildContext context) { final provider = context.watch(); + final loc = AppLocalizations.of(context)!; Widget content; switch (provider.selected) { @@ -76,7 +79,7 @@ class PageSelector extends StatelessWidget { ? WalletEditPage( onBack: provider.goBackFromWalletEdit, ) - : const Center(child: Text('No wallet selected')); //TODO Localize + : Center(child: Text(loc.noWalletSelected)); break; default: diff --git a/frontend/pweb/lib/widgets/sidebar/user.dart b/frontend/pweb/lib/widgets/sidebar/user.dart index a979614..0370bcf 100644 --- a/frontend/pweb/lib/widgets/sidebar/user.dart +++ b/frontend/pweb/lib/widgets/sidebar/user.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class UserProfileCard extends StatelessWidget { final ThemeData theme; @@ -21,6 +23,7 @@ class UserProfileCard extends StatelessWidget { @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context)!; bool isSelected = selected == PayoutDestination.settings; final backgroundColor = isSelected ? theme.colorScheme.primaryContainer @@ -52,7 +55,7 @@ class UserProfileCard extends StatelessWidget { const SizedBox(width: 8), Flexible( child: Text( - userName ?? 'User Name', + userName ?? loc.userNamePlaceholder, style: theme.textTheme.bodyLarge?.copyWith( fontSize: 20, fontWeight: FontWeight.w500, @@ -67,4 +70,4 @@ class UserProfileCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/resources/logo.png b/frontend/pweb/resources/logo.png index 52b4648f7f3e832d657a44953a1e05e3358c2e7e..bc5ab8c638e31fae4ab1900512729feb188c7df6 100644 GIT binary patch literal 2596 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 Date: Wed, 26 Nov 2025 13:03:52 +0300 Subject: [PATCH 2/2] Added account permissions and ui for recipient --- frontend/pweb/devtools_options.yaml | 3 + frontend/pweb/lib/l10n/en.arb | 4 +- frontend/pweb/lib/l10n/ru.arb | 3 +- frontend/pweb/lib/main.dart | 19 +-- frontend/pweb/lib/pages/loaders/account.dart | 9 +- .../pweb/lib/pages/loaders/permissions.dart | 13 +- frontend/pweb/lib/pages/login/form.dart | 2 +- frontend/pweb/lib/pages/login/login.dart | 2 +- .../pweb/lib/pages/signup/form/state.dart | 4 +- frontend/pweb/lib/providers/account.dart | 97 +++++++++++++++ frontend/pweb/lib/providers/permissions.dart | 82 ++++++++++++ frontend/pweb/lib/providers/two_factor.dart | 38 ++---- frontend/pweb/lib/services/accounts.dart | 105 ++++++++++++++++ frontend/pweb/lib/services/mock_ids.dart | 12 ++ frontend/pweb/lib/services/permissions.dart | 117 ++++++++++++++++++ frontend/pweb/lib/utils/error_handler.dart | 3 + frontend/pweb/lib/widgets/drawer/avatar.dart | 2 +- .../pweb/lib/widgets/drawer/tiles/logout.dart | 2 +- frontend/pweb/lib/widgets/drawer/widget.dart | 2 +- .../pweb/lib/widgets/protected/widget.dart | 4 +- frontend/pweb/lib/widgets/sidebar/page.dart | 43 ++++++- .../pweb/lib/widgets/sidebar/sidebar.dart | 9 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + 23 files changed, 507 insertions(+), 70 deletions(-) create mode 100644 frontend/pweb/devtools_options.yaml create mode 100644 frontend/pweb/lib/providers/account.dart create mode 100644 frontend/pweb/lib/providers/permissions.dart create mode 100644 frontend/pweb/lib/services/accounts.dart create mode 100644 frontend/pweb/lib/services/mock_ids.dart create mode 100644 frontend/pweb/lib/services/permissions.dart diff --git a/frontend/pweb/devtools_options.yaml b/frontend/pweb/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/frontend/pweb/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 7a44b1b..f731400 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -36,9 +36,9 @@ "signupError": "Failed to signup: {error}", "signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.", "connectivityError": "Cannot reach the server at {serverAddress}. Check your network and try again.", - "errorAccountExists": "Account already exists", "errorAccountNotVerified": "Your account hasn't been verified yet. Please check your email to complete the verification", "errorLoginUnauthorized": "Login or password is incorrect. Please try again", + "errorAccountExists": "Account with this login already exists", "errorInternalError": "An internal error occurred. We're aware of the issue and working to resolve it. Please try again later", "errorVerificationTokenNotFound": "Account for verification not found. Sign up again", "created": "Created", @@ -465,4 +465,4 @@ "englishLanguage": "English", "russianLanguage": "Russian", "germanLanguage": "German" -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 79d873d..e1f9032 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -38,6 +38,7 @@ "connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.", "errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации", "errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова", + "errorAccountExists": "Аккаунт с таким логином уже существует", "errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже", "errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова", "created": "Создано", @@ -457,4 +458,4 @@ "englishLanguage": "Английский", "russianLanguage": "Русский", "germanLanguage": "Немецкий" -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/main.dart b/frontend/pweb/lib/main.dart index 3354d40..e943a7e 100644 --- a/frontend/pweb/lib/main.dart +++ b/frontend/pweb/lib/main.dart @@ -8,7 +8,6 @@ import 'package:provider/provider.dart'; import 'package:logging/logging.dart'; import 'package:pshared/config/constants.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/organizations.dart'; @@ -16,7 +15,9 @@ import 'package:pweb/app/app.dart'; import 'package:pweb/app/timeago.dart'; import 'package:pweb/providers/carousel.dart'; import 'package:pweb/providers/mock_payment.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/providers/operatioins.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/payment_methods.dart'; import 'package:pweb/providers/recipient.dart'; @@ -31,6 +32,8 @@ import 'package:pweb/services/payments/upload_history.dart'; import 'package:pweb/services/recipient/recipient.dart'; import 'package:pweb/services/wallet_transactions.dart'; import 'package:pweb/services/wallets.dart'; +import 'package:pweb/services/accounts.dart'; +import 'package:pweb/services/permissions.dart'; void _setupLogging() { @@ -55,17 +58,15 @@ void main() async { MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => LocaleProvider(null)), - ChangeNotifierProvider(create: (_) => AccountProvider()), - ChangeNotifierProxyProvider( - create: (context) => TwoFactorProvider( - accountProvider: context.read(), - ), - update: (context, accountProvider, previous) => TwoFactorProvider( - accountProvider: accountProvider, + ChangeNotifierProvider(create: (_) => PermissionsProvider(service: PermissionsService())), + ChangeNotifierProvider( + create: (context) => AccountProvider( + accountsService: AccountsService(), + permissionsProvider: context.read(), ), ), + ChangeNotifierProvider(create: (_) => TwoFactorProvider()), ChangeNotifierProvider(create: (_) => OrganizationsProvider()), - ChangeNotifierProvider(create: (_) => AccountProvider()), ChangeNotifierProvider(create: (_) => CarouselIndexProvider()), ChangeNotifierProvider( diff --git a/frontend/pweb/lib/pages/loaders/account.dart b/frontend/pweb/lib/pages/loaders/account.dart index 8c78257..0e64ba1 100644 --- a/frontend/pweb/lib/pages/loaders/account.dart +++ b/frontend/pweb/lib/pages/loaders/account.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/error/snackbar.dart'; @@ -26,13 +26,10 @@ class AccountLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.account == null)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - provider.restore(); - }); + if (provider.account == null) { + WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login)); return const Center(child: CircularProgressIndicator()); } return child; }); } - diff --git a/frontend/pweb/lib/pages/loaders/permissions.dart b/frontend/pweb/lib/pages/loaders/permissions.dart index 1488048..1bf2098 100644 --- a/frontend/pweb/lib/pages/loaders/permissions.dart +++ b/frontend/pweb/lib/pages/loaders/permissions.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/account.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/error/snackbar.dart'; @@ -16,8 +17,10 @@ class PermissionsLoader extends StatelessWidget { const PermissionsLoader({super.key, required this.child}); @override - Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - if (provider.isLoading) return const Center(child: CircularProgressIndicator()); + Widget build(BuildContext context) => Consumer2(builder: (context, provider, accountProvider, _) { + if (provider.isLoading) { + return const Center(child: CircularProgressIndicator()); + } if (provider.error != null) { postNotifyUserOfErrorX( context: context, @@ -26,9 +29,9 @@ class PermissionsLoader extends StatelessWidget { ); navigateAndReplace(context, Pages.login); } - if ((provider.error == null) && (provider.permissions.isEmpty)) { + if (provider.error == null && !provider.hasLoaded && accountProvider.account != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - provider.load(); + provider.loadForAccount(accountProvider.account!.id); }); return const Center(child: CircularProgressIndicator()); } diff --git a/frontend/pweb/lib/pages/login/form.dart b/frontend/pweb/lib/pages/login/form.dart index bf230b1..ac5f506 100644 --- a/frontend/pweb/lib/pages/login/form.dart +++ b/frontend/pweb/lib/pages/login/form.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/login/buttons.dart'; diff --git a/frontend/pweb/lib/pages/login/login.dart b/frontend/pweb/lib/pages/login/login.dart index ba7c4e8..b1f804b 100644 --- a/frontend/pweb/lib/pages/login/login.dart +++ b/frontend/pweb/lib/pages/login/login.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/widgets/vspacer.dart'; diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index 38f651d..5632a91 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -8,8 +8,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/models/describable.dart'; -import 'package:pshared/provider/account.dart'; import 'package:pshared/provider/locale.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/signup/form/content.dart'; @@ -110,4 +110,4 @@ class SignUpFormState extends State { onSignUp: handleSignUp, onLogin: handleLogin, ); -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/providers/account.dart b/frontend/pweb/lib/providers/account.dart new file mode 100644 index 0000000..0aa352b --- /dev/null +++ b/frontend/pweb/lib/providers/account.dart @@ -0,0 +1,97 @@ +import 'package:flutter/foundation.dart'; + +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/pending_login.dart'; +import 'package:pshared/models/describable.dart'; + +import 'package:pweb/providers/permissions.dart'; +import 'package:pweb/services/accounts.dart'; + +class AccountProvider extends ChangeNotifier { + final AccountsService _accountsService; + final PermissionsProvider _permissionsProvider; + + AccountProvider({ + required AccountsService accountsService, + required PermissionsProvider permissionsProvider, + }) : _accountsService = accountsService, + _permissionsProvider = permissionsProvider; + + Account? _account; + bool _isLoading = false; + Object? _error; + + Account? get account => _account; + bool get isLoading => _isLoading; + Object? get error => _error; + bool get isLoggedIn => _account != null; + + PendingLogin? get pendingLogin => null; + + Future login({ + required String email, + required String password, + required String locale, + }) async { + _setLoading(true); + try { + final result = await _accountsService.login(email, password, locale: locale); + _account = result.account; + _error = null; + await _permissionsProvider.loadForAccount(result.account.id); + return LoginOutcome.completed(result.account); + } catch (e) { + _error = e; + rethrow; + } finally { + _setLoading(false); + } + } + + void completePendingLogin(Account account) { + _account = account; + _permissionsProvider.loadForAccount(account.id); + notifyListeners(); + } + + Future restore() async { + _setLoading(true); + _account = null; + _permissionsProvider.clear(); + _error = Exception('Сохраненная сессия отсутствует'); + _setLoading(false); + return null; + } + + Future signup({ + required AccountData account, + required Describable organization, + required String timezone, + required Describable ownerRole, + }) async { + _setLoading(true); + _error = null; + try { + await _accountsService.signup(account); + } catch (e) { + _error = e; + rethrow; + } finally { + _setLoading(false); + } + } + + Future logout() async { + _account = null; + _error = null; + _permissionsProvider.clear(); + notifyListeners(); + } + + void _setLoading(bool value) { + _isLoading = value; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/providers/permissions.dart b/frontend/pweb/lib/providers/permissions.dart new file mode 100644 index 0000000..eb725c5 --- /dev/null +++ b/frontend/pweb/lib/providers/permissions.dart @@ -0,0 +1,82 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/permissions/action.dart' as perm; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/effect.dart'; +import 'package:pshared/models/resources.dart'; + +import 'package:pweb/services/permissions.dart'; +import 'package:pweb/services/mock_ids.dart'; + +class PermissionsProvider extends ChangeNotifier { + final PermissionsService _service; + + PermissionsProvider({required PermissionsService service}) : _service = service; + + bool _isLoading = false; + Object? _error; + String? _accountRef; + bool _hasLoaded = false; + String? _roleRef; + List _permissions = []; + List _policyDescriptions = []; + + bool get isLoading => _isLoading; + Object? get error => _error; + bool get isReady => _hasLoaded && !_isLoading && _error == null; + List get permissions => List.unmodifiable(_permissions); + bool get hasLoaded => _hasLoaded; + + bool get isCompany => _roleRef == companyRoleId; + bool get isRecipient => _roleRef == recipientRoleId; + + Future loadForAccount(String accountRef) async { + _accountRef = accountRef; + _isLoading = true; + _error = null; + notifyListeners(); + + try { + final access = await _service.loadForAccount(accountRef); + _permissions = access.permissions.permissions; + _policyDescriptions = access.descriptions.policies; + _roleRef = access.permissions.roles.firstOrNull?.descriptionRef; + } catch (e) { + _permissions = []; + _policyDescriptions = []; + _error = e; + _roleRef = null; + } finally { + _hasLoaded = true; + _isLoading = false; + notifyListeners(); + } + } + + void clear() { + _accountRef = null; + _permissions = []; + _policyDescriptions = []; + _error = null; + _hasLoaded = false; + _roleRef = null; + notifyListeners(); + } + + bool canAccessResource(ResourceType resource, {perm.Action? action}) { + final policy = _policyDescriptions.firstWhereOrNull( + (policy) => (policy.resourceTypes?.contains(resource) ?? false), + ); + if (policy == null) return false; + + return _permissions.any( + (permission) => + permission.accountRef == _accountRef && + permission.policy.descriptionRef == policy.storable.id && + permission.policy.effect.effect == Effect.allow && + (action == null || permission.policy.effect.action == action), + ); + } +} diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index f23bb52..4a5cfdf 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -1,15 +1,13 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:pshared/models/auth/pending_login.dart'; -import 'package:pshared/provider/account.dart'; -import 'package:pshared/service/account.dart'; +import 'package:pweb/services/auth.dart'; class TwoFactorProvider extends ChangeNotifier { static final _logger = Logger('provider.two_factor'); - final AccountProvider _accountProvider; + final AuthenticationService _authService; - TwoFactorProvider({required AccountProvider accountProvider}) : _accountProvider = accountProvider; + TwoFactorProvider({AuthenticationService? authService}) : _authService = authService ?? AuthenticationService(); bool _isSubmitting = false; bool _hasError = false; @@ -20,7 +18,6 @@ class TwoFactorProvider extends ChangeNotifier { bool get hasError => _hasError; bool get verificationSuccess => _verificationSuccess; String? get errorMessage => _errorMessage; - PendingLogin? get pendingLogin => _accountProvider.pendingLogin; Future submitCode(String code) async { @@ -31,16 +28,8 @@ class TwoFactorProvider extends ChangeNotifier { notifyListeners(); try { - final pending = _accountProvider.pendingLogin; - if (pending == null) { - throw Exception('No pending login available'); - } - final account = await AccountService.confirmLoginCode( - pending: pending, - code: code, - ); - _accountProvider.completePendingLogin(account); - _verificationSuccess = true; + final isValid = await _authService.verifyTwoFactorCode(code); + _verificationSuccess = isValid; } catch (e) { _hasError = true; _errorMessage = e.toString(); @@ -52,18 +41,9 @@ class TwoFactorProvider extends ChangeNotifier { } Future resendCode() async { - final pending = _accountProvider.pendingLogin; - if (pending == null) { - _logger.warning('No pending login to resend code for'); - return; - } - try { - await AccountService.resendLoginCode(pending); - } catch (e) { - _logger.warning('Failed to resend login code', e); - _hasError = true; - _errorMessage = e.toString(); - notifyListeners(); - } + _logger.fine('Resending mock two-factor code'); + _hasError = false; + _errorMessage = null; + notifyListeners(); } } diff --git a/frontend/pweb/lib/services/accounts.dart b/frontend/pweb/lib/services/accounts.dart new file mode 100644 index 0000000..d840bbc --- /dev/null +++ b/frontend/pweb/lib/services/accounts.dart @@ -0,0 +1,105 @@ +import 'package:collection/collection.dart'; + +import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/models/account/account.dart'; +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/storable.dart'; + +import 'mock_ids.dart'; + +class InvalidCredentialsException implements Exception { + @override + String toString() => 'InvalidCredentialsException'; +} + +class DuplicateAccountException implements Exception { + @override + String toString() => 'DuplicateAccountException'; +} + +class AccountLoginResult { + final Account account; + final String roleId; + + const AccountLoginResult({ + required this.account, + required this.roleId, + }); +} + +class _AccountRecord { + final Account account; + final String password; + final String roleId; + + const _AccountRecord({ + required this.account, + required this.password, + required this.roleId, + }); +} + +class AccountsService { + final List<_AccountRecord> _accounts = [ + _AccountRecord( + account: Account( + storable: newStorable(id: companyAccountRef), + describable: newDescribable(name: 'Sendico Company'), + avatarUrl: null, + lastName: 'Owner', + login: 'company@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: companyRoleId, + ), + _AccountRecord( + account: Account( + storable: newStorable(id: recipientAccountRef), + describable: newDescribable(name: 'John Recipient'), + avatarUrl: null, + lastName: 'Doe', + login: 'recipient@sendico.com', + locale: 'ru', + ), + password: 'password123A', + roleId: recipientRoleId, + ), + ]; + + Future login(String email, String password, {String? locale}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = email.trim().toLowerCase(); + final record = _accounts.where((acc) => acc.account.login.toLowerCase() == normalized).singleOrNull; + + if (record == null || record.password != password) { + throw InvalidCredentialsException(); + } + + return AccountLoginResult(account: record.account, roleId: record.roleId); + } + + Future signup(AccountData data, {String roleId = recipientRoleId}) async { + await Future.delayed(const Duration(milliseconds: 300)); + + final normalized = data.login.trim().toLowerCase(); + if (_accounts.any((acc) => acc.account.login.toLowerCase() == normalized)) { + throw DuplicateAccountException(); + } + + final account = Account( + storable: newStorable(id: 'account-${_accounts.length + 1}'), + describable: newDescribable(name: data.name), + avatarUrl: null, + lastName: data.lastName, + login: normalized, + locale: data.locale, + ); + + _accounts.add(_AccountRecord(account: account, password: data.password, roleId: roleId)); + return AccountLoginResult(account: account, roleId: roleId); + } + + Account? getByRef(String accountRef) => _accounts.where((acc) => acc.account.id == accountRef).singleOrNull?.account; +} diff --git a/frontend/pweb/lib/services/mock_ids.dart b/frontend/pweb/lib/services/mock_ids.dart new file mode 100644 index 0000000..ffde7ff --- /dev/null +++ b/frontend/pweb/lib/services/mock_ids.dart @@ -0,0 +1,12 @@ +// Centralized identifiers for mock auth/permission data to keep the +// mock services in sync and make it easy to swap in a real API later. +const String mockOrganizationRef = 'org-sendico'; + +const String companyRoleId = 'role-company'; +const String recipientRoleId = 'role-recipient'; + +const String companyAccountRef = 'account-company'; +const String recipientAccountRef = 'account-recipient'; + +const String accountsPolicyDescriptionId = 'policy-accounts'; +const String rolesPolicyDescriptionId = 'policy-roles'; diff --git a/frontend/pweb/lib/services/permissions.dart b/frontend/pweb/lib/services/permissions.dart new file mode 100644 index 0000000..e7ab248 --- /dev/null +++ b/frontend/pweb/lib/services/permissions.dart @@ -0,0 +1,117 @@ +import 'package:pshared/models/describable.dart'; +import 'package:pshared/models/permissions/access.dart'; +import 'package:pshared/models/permissions/action.dart'; +import 'package:pshared/models/permissions/action_effect.dart'; +import 'package:pshared/models/permissions/data/permission.dart'; +import 'package:pshared/models/permissions/data/permissions.dart'; +import 'package:pshared/models/permissions/data/policy.dart'; +import 'package:pshared/models/permissions/data/role.dart'; +import 'package:pshared/models/permissions/descriptions/permissions.dart'; +import 'package:pshared/models/permissions/descriptions/policy.dart'; +import 'package:pshared/models/permissions/descriptions/role.dart'; +import 'package:pshared/models/permissions/effect.dart'; +import 'package:pshared/models/resources.dart'; +import 'package:pshared/models/storable.dart'; + +import 'mock_ids.dart'; + +class PermissionsService { + static const String _objectType = 'permissions'; + + Future loadForAccount(String accountRef) async { + await Future.delayed(const Duration(milliseconds: 200)); + final baseAccess = _buildMockUserAccess(); + + final roles = [...baseAccess.permissions.roles]; + final permissions = [...baseAccess.permissions.permissions]; + final policies = [...baseAccess.permissions.policies]; + + final hasAccount = roles.any((r) => r.accountRef == accountRef); + if (!hasAccount) { + roles.add(Role(accountRef: accountRef, descriptionRef: recipientRoleId, organizationRef: mockOrganizationRef)); + } + + final relevantRoleRefs = roles + .where((r) => r.accountRef == accountRef) + .map((r) => r.descriptionRef) + .toSet(); + + final filteredPolicies = permissions + .where((p) => p.accountRef == accountRef && relevantRoleRefs.contains(p.policy.roleDescriptionRef)) + .toList(); + + return UserAccess( + descriptions: baseAccess.descriptions, + permissions: PermissionsData( + roles: roles.where((r) => r.accountRef == accountRef).toList(), + policies: policies.where((p) => relevantRoleRefs.contains(p.roleDescriptionRef)).toList(), + permissions: filteredPolicies, + ), + ); + } + + UserAccess _buildMockUserAccess() { + final roleDescriptions = [ + RoleDescription( + storable: newStorable(id: companyRoleId), + describable: newDescribable(name: 'Компания'), + organizationRef: mockOrganizationRef, + ), + RoleDescription( + storable: newStorable(id: recipientRoleId), + describable: newDescribable(name: 'Получатель'), + organizationRef: mockOrganizationRef, + ), + ]; + + final policyDescriptions = [ + PolicyDescription( + storable: newStorable(id: accountsPolicyDescriptionId), + describable: newDescribable(name: 'Управление аккаунтами'), + resourceTypes: const [ResourceType.accounts], + organizationRef: mockOrganizationRef, + ), + PolicyDescription( + storable: newStorable(id: rolesPolicyDescriptionId), + describable: newDescribable(name: 'Управление ролями'), + resourceTypes: const [ResourceType.roles], + organizationRef: mockOrganizationRef, + ), + ]; + + final companyAccountsPolicy = Policy( + roleDescriptionRef: companyRoleId, + organizationRef: mockOrganizationRef, + descriptionRef: accountsPolicyDescriptionId, + objectRef: null, + effect: const ActionEffect(action: Action.read, effect: Effect.allow), + ); + + final companyRolesPolicy = Policy( + roleDescriptionRef: companyRoleId, + organizationRef: mockOrganizationRef, + descriptionRef: rolesPolicyDescriptionId, + objectRef: null, + effect: const ActionEffect(action: Action.read, effect: Effect.allow), + ); + + final roles = [ + Role(accountRef: companyAccountRef, descriptionRef: companyRoleId, organizationRef: mockOrganizationRef), + Role(accountRef: recipientAccountRef, descriptionRef: recipientRoleId, organizationRef: mockOrganizationRef), + ]; + + final permissions = [ + Permission(policy: companyAccountsPolicy, accountRef: companyAccountRef), + Permission(policy: companyRolesPolicy, accountRef: companyAccountRef), + ]; + + return UserAccess( + descriptions: PermissionsDescription(roles: roleDescriptions, policies: policyDescriptions), + permissions: PermissionsData( + roles: roles, + policies: [companyAccountsPolicy, companyRolesPolicy], + permissions: permissions, + ), + ); + } +} diff --git a/frontend/pweb/lib/utils/error_handler.dart b/frontend/pweb/lib/utils/error_handler.dart index 2313b4f..90625da 100644 --- a/frontend/pweb/lib/utils/error_handler.dart +++ b/frontend/pweb/lib/utils/error_handler.dart @@ -5,6 +5,7 @@ import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/config/constants.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/services/accounts.dart'; class ErrorHandler { @@ -48,6 +49,8 @@ class ErrorHandler { final errorHandlers = { ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse), ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError), + InvalidCredentialsException: (_) => locs.errorLoginUnauthorized, + DuplicateAccountException: (_) => locs.errorAccountExists, }; return errorHandlers[e.runtimeType]?.call(e) ?? e.toString(); diff --git a/frontend/pweb/lib/widgets/drawer/avatar.dart b/frontend/pweb/lib/widgets/drawer/avatar.dart index 632ea53..5d969f3 100644 --- a/frontend/pweb/lib/widgets/drawer/avatar.dart +++ b/frontend/pweb/lib/widgets/drawer/avatar.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; diff --git a/frontend/pweb/lib/widgets/drawer/tiles/logout.dart b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart index fd4561a..a136d75 100644 --- a/frontend/pweb/lib/widgets/drawer/tiles/logout.dart +++ b/frontend/pweb/lib/widgets/drawer/tiles/logout.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/provider/account.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/app/router/pages.dart'; diff --git a/frontend/pweb/lib/widgets/drawer/widget.dart b/frontend/pweb/lib/widgets/drawer/widget.dart index 5649bbc..4e157c0 100644 --- a/frontend/pweb/lib/widgets/drawer/widget.dart +++ b/frontend/pweb/lib/widgets/drawer/widget.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/resources.dart'; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/widgets/drawer/avatar.dart'; import 'package:pweb/widgets/drawer/tiles/dashboard.dart'; diff --git a/frontend/pweb/lib/widgets/protected/widget.dart b/frontend/pweb/lib/widgets/protected/widget.dart index 49ce816..5d85206 100644 --- a/frontend/pweb/lib/widgets/protected/widget.dart +++ b/frontend/pweb/lib/widgets/protected/widget.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/models/resources.dart'; import 'package:pshared/models/permissions/action.dart' as perm; -import 'package:pshared/provider/permissions.dart'; +import 'package:pweb/providers/permissions.dart'; T? protectedWidgetctx(BuildContext context, ResourceType resource, T child, {perm.Action? action}) { @@ -13,4 +13,4 @@ T? protectedWidgetctx(BuildContext context, ResourceType resou T? protectedWidget(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) { return provider.canAccessResource(resource, action: action) ? child : null; -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/sidebar/page.dart b/frontend/pweb/lib/widgets/sidebar/page.dart index bfb3c37..d5b0bc6 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -9,7 +9,10 @@ import 'package:pweb/pages/payout_page/page.dart'; import 'package:pweb/pages/payout_page/wallet/edit/page.dart'; import 'package:pweb/pages/report/page.dart'; import 'package:pweb/pages/settings/profile/page.dart'; +import 'package:pweb/providers/account.dart'; import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/permissions.dart'; +import 'package:pweb/app/router/pages.dart'; import 'package:pweb/widgets/appbar/app_bar.dart'; import 'package:pweb/pages/dashboard/dashboard.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; @@ -21,13 +24,37 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PageSelector extends StatelessWidget { const PageSelector({super.key}); + void _handleLogout(BuildContext context) { + context.read().logout(); + context.read().clear(); + navigateAndReplace(context, Pages.login); + } + @override Widget build(BuildContext context) { final provider = context.watch(); + final permissions = context.watch(); + final account = context.watch().account; final loc = AppLocalizations.of(context)!; + final allowedDestinations = permissions.isRecipient + ? { + PayoutDestination.settings, + PayoutDestination.methods, + PayoutDestination.editwallet, + } + : PayoutDestination.values.toSet(); + + final selected = allowedDestinations.contains(provider.selected) + ? provider.selected + : (permissions.isRecipient ? PayoutDestination.settings : PayoutDestination.dashboard); + + if (selected != provider.selected) { + WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(selected)); + } + Widget content; - switch (provider.selected) { + switch (selected) { case PayoutDestination.dashboard: content = DashboardPage( onRecipientSelected: (recipient) => @@ -83,14 +110,14 @@ class PageSelector extends StatelessWidget { break; default: - content = Text(provider.selected.name); + content = Text(selected.name); } return Scaffold( appBar: PayoutAppBar( - title: Text(provider.selected.localizedLabel(context)), + title: Text(selected.localizedLabel(context)), onAddFundsPressed: () {}, - onLogout: () => debugPrint('Logout clicked'), + onLogout: () => _handleLogout(context), ), body: Padding( padding: const EdgeInsets.only(left: 200, top: 40, right: 200), @@ -99,9 +126,13 @@ class PageSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PayoutSidebar( - selected: provider.selected, + selected: selected, onSelected: provider.selectPage, - onLogout: () => debugPrint('Logout clicked'), + onLogout: () => _handleLogout(context), + userName: account?.name, + items: permissions.isRecipient + ? const [PayoutDestination.settings, PayoutDestination.methods] + : null, ), Expanded(child: content), ], diff --git a/frontend/pweb/lib/widgets/sidebar/sidebar.dart b/frontend/pweb/lib/widgets/sidebar/sidebar.dart index d3dac81..b51e012 100644 --- a/frontend/pweb/lib/widgets/sidebar/sidebar.dart +++ b/frontend/pweb/lib/widgets/sidebar/sidebar.dart @@ -13,6 +13,7 @@ class PayoutSidebar extends StatelessWidget { this.onLogout, this.userName, this.avatarUrl, + this.items, }); final PayoutDestination selected; @@ -21,11 +22,13 @@ class PayoutSidebar extends StatelessWidget { final String? userName; final String? avatarUrl; + final List? items; @override Widget build(BuildContext context) { - final items = [ + final menuItems = items ?? + [ PayoutDestination.dashboard, PayoutDestination.recipients, PayoutDestination.methods, @@ -49,11 +52,11 @@ class PayoutSidebar extends StatelessWidget { theme: theme, avatarUrl: avatarUrl, userName: userName, - items: items, + items: menuItems, selected: selected, onSelected: onSelected, ), ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift index 33de092..79f5652 100644 --- a/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/pweb/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import amplitude_flutter import file_selector_macos import flutter_timezone +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))