diff --git a/frontend/pweb/lib/app/router/payout_routes.dart b/frontend/pweb/lib/app/router/payout_routes.dart new file mode 100644 index 0000000..3c2bb09 --- /dev/null +++ b/frontend/pweb/lib/app/router/payout_routes.dart @@ -0,0 +1,53 @@ +import 'package:pweb/app/router/pages.dart'; + +import 'package:pweb/widgets/sidebar/destinations.dart'; + + +const _payoutBasePath = Pages.dashboard; + +String payoutPath(PayoutDestination destination) { + final base = routerPage(_payoutBasePath); + switch (destination) { + case PayoutDestination.dashboard: + return base; + case PayoutDestination.recipients: + return '$base/recipients'; + case PayoutDestination.addrecipient: + return '$base/recipients/add'; + case PayoutDestination.payment: + return '$base/payment'; + case PayoutDestination.settings: + return '$base/settings'; + case PayoutDestination.reports: + return '$base/reports'; + case PayoutDestination.methods: + return '$base/methods'; + case PayoutDestination.editwallet: + return '$base/methods/edit'; + case PayoutDestination.sendPayout: + return '$base/send'; + } +} + +PayoutDestination payoutDestinationFromLocation(String location) { + final path = Uri.parse(location).path; + + // Check longer paths first to avoid prefix collisions. + for (final entry in _orderedRoutes) { + if (path.startsWith(entry.value)) return entry.key; + } + + return PayoutDestination.dashboard; +} + +final List> _orderedRoutes = [ + MapEntry(PayoutDestination.editwallet, payoutPath(PayoutDestination.editwallet)), + MapEntry(PayoutDestination.addrecipient, payoutPath(PayoutDestination.addrecipient)), + MapEntry(PayoutDestination.payment, payoutPath(PayoutDestination.payment)), + MapEntry(PayoutDestination.recipients, payoutPath(PayoutDestination.recipients)), + MapEntry(PayoutDestination.methods, payoutPath(PayoutDestination.methods)), + MapEntry(PayoutDestination.reports, payoutPath(PayoutDestination.reports)), + MapEntry(PayoutDestination.settings, payoutPath(PayoutDestination.settings)), + MapEntry(PayoutDestination.sendPayout, payoutPath(PayoutDestination.sendPayout)), + MapEntry(PayoutDestination.dashboard, payoutPath(PayoutDestination.dashboard)), +]; diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart new file mode 100644 index 0000000..de960b9 --- /dev/null +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/app/router/payout_routes.dart'; +import 'package:pweb/pages/address_book/form/page.dart'; +import 'package:pweb/pages/address_book/page/page.dart'; +import 'package:pweb/pages/dashboard/dashboard.dart'; +import 'package:pweb/pages/payout_page/page.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/page.dart'; +import 'package:pweb/pages/payment_methods/page.dart'; +import 'package:pweb/pages/report/page.dart'; +import 'package:pweb/pages/settings/profile/page.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; +import 'package:pweb/widgets/sidebar/page.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +RouteBase payoutShellRoute() => ShellRoute( + pageBuilder: (context, state, child) => NoTransitionPage( + child: PageSelector( + selected: payoutDestinationFromLocation(state.uri.toString()), + child: child, + ), + ), + routes: [ + GoRoute( + name: Pages.dashboard.name, + path: routerPage(Pages.dashboard), + pageBuilder: (context, state) { + final selector = context.read(); + return NoTransitionPage( + child: DashboardPage( + onRecipientSelected: (recipient) { + selector.selectRecipient(recipient); + context.go(payoutPath(PayoutDestination.payment)); + }, + onGoToPaymentWithoutRecipient: (type) { + selector.startPaymentWithoutRecipient(type); + context.go(payoutPath(PayoutDestination.payment)); + }, + ), + ); + }, + ), + GoRoute( + name: 'payoutRecipients', + path: payoutPath(PayoutDestination.recipients), + pageBuilder: (context, state) { + final selector = context.read(); + return NoTransitionPage( + child: RecipientAddressBookPage( + onRecipientSelected: (recipient) { + selector.selectRecipient(recipient, fromList: true); + context.go(payoutPath(PayoutDestination.payment)); + }, + onAddRecipient: () { + selector.goToAddRecipient(); + context.go(payoutPath(PayoutDestination.addrecipient)); + }, + onEditRecipient: (recipient) { + selector.editRecipient(recipient, fromList: true); + context.go(payoutPath(PayoutDestination.addrecipient)); + }, + ), + ); + }, + ), + GoRoute( + name: 'payoutAddRecipient', + path: payoutPath(PayoutDestination.addrecipient), + pageBuilder: (context, state) { + final selector = context.read(); + final recipient = selector.recipientProvider?.selectedRecipient; + return NoTransitionPage( + child: AdressBookRecipientForm( + recipient: recipient, + onSaved: (_) { + selector.selectPage(PayoutDestination.recipients); + context.go(payoutPath(PayoutDestination.recipients)); + }, + ), + ); + }, + ), + GoRoute( + name: 'payoutPayment', + path: payoutPath(PayoutDestination.payment), + pageBuilder: (context, state) { + final selector = context.read(); + return NoTransitionPage( + child: PaymentPage( + onBack: (_) { + final destination = selector.goBackFromPayment(); + context.go(payoutPath(destination)); + }, + ), + ); + }, + ), + GoRoute( + name: 'payoutSettings', + path: payoutPath(PayoutDestination.settings), + pageBuilder: (context, state) => NoTransitionPage( + child: const ProfileSettingsPage(), + ), + ), + GoRoute( + name: 'payoutReports', + path: payoutPath(PayoutDestination.reports), + pageBuilder: (context, state) => NoTransitionPage( + child: const OperationHistoryPage(), + ), + ), + GoRoute( + name: 'payoutMethods', + path: payoutPath(PayoutDestination.methods), + pageBuilder: (context, state) { + final selector = context.read(); + return NoTransitionPage( + child: PaymentConfigPage( + onWalletTap: (wallet) { + selector.selectWallet(wallet); + context.go(payoutPath(PayoutDestination.editwallet)); + }, + ), + ); + }, + ), + GoRoute( + name: 'payoutEditWallet', + path: payoutPath(PayoutDestination.editwallet), + pageBuilder: (context, state) { + final selector = context.read(); + final wallet = selector.walletsProvider?.selectedWallet; + if (wallet == null) { + final loc = AppLocalizations.of(context)!; + return NoTransitionPage( + child: Center(child: Text(loc.noWalletSelected)), + ); + } + + return NoTransitionPage( + child: WalletEditPage( + onBack: () { + selector.goBackFromWalletEdit(); + context.go(payoutPath(PayoutDestination.methods)); + }, + ), + ); + }, + ), + ], +); diff --git a/frontend/pweb/lib/app/router/router.dart b/frontend/pweb/lib/app/router/router.dart index 2a693e5..add8568 100644 --- a/frontend/pweb/lib/app/router/router.dart +++ b/frontend/pweb/lib/app/router/router.dart @@ -1,17 +1,11 @@ -import 'package:provider/provider.dart'; - import 'package:go_router/go_router.dart'; -import 'package:pshared/provider/organizations.dart'; - import 'package:pweb/app/router/pages.dart'; -import 'package:pweb/app/router/page_params.dart'; import 'package:pweb/pages/2fa/page.dart'; -import 'package:pweb/pages/signup/page.dart'; -import 'package:pweb/pages/verification/page.dart'; -import 'package:pweb/widgets/sidebar/page.dart'; -import 'package:pweb/pages/login/page.dart'; import 'package:pweb/pages/errors/not_found.dart'; +import 'package:pweb/pages/login/page.dart'; +import 'package:pweb/pages/signup/page.dart'; +import 'package:pweb/app/router/payout_shell.dart'; GoRouter createRouter() => GoRouter( @@ -27,32 +21,29 @@ GoRouter createRouter() => GoRouter( path: routerPage(Pages.login), builder: (_, _) => const LoginPage(), ), - GoRoute( - name: Pages.dashboard.name, - path: routerPage(Pages.dashboard), - builder: (_, _) => const PageSelector(), - ), GoRoute( name: Pages.sfactor.name, path: routerPage(Pages.sfactor), - builder: (context, _) => TwoFactorCodePage( - onVerificationSuccess: () { - // trigger organization load - context.read().load(); - context.goNamed(Pages.dashboard.name); - }, - ), + builder: (context, state) { + final isFromSignup = state.uri.queryParameters['from'] == 'signup'; + + return TwoFactorCodePage( + onVerificationSuccess: () { + if (isFromSignup) { + context.goNamed(Pages.login.name); + } else { + context.goNamed(Pages.dashboard.name); + } + }, + ); + }, ), GoRoute( name: Pages.signup.name, path: routerPage(Pages.signup), builder: (_, _) => const SignUpPage(), ), - GoRoute( - name: Pages.verify.name, - path: '${routerPage(Pages.verify)}${routerAddParam(PageParams.token)}', - builder: (_, state) => AccountVerificationPage(token: state.pathParameters[PageParams.token.name]!), - ), + payoutShellRoute(), ], ), ], diff --git a/frontend/pweb/lib/pages/payment_methods/add/wallet.dart b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart index ad25650..b872cc2 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/wallet.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/wallet.dart @@ -49,7 +49,8 @@ class _WalletFormState extends State { return; } - if (newData != null && newData != oldData) { + if (newData != null && + newData.walletId != _walletIdController.text) { _walletIdController.text = newData.walletId; } } @@ -67,4 +68,4 @@ class _WalletFormState extends State { validator: (val) => (val?.isEmpty ?? true) ? l10n.enterWalletId : null, ); } -} +} \ 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 4232719..b875087 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,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; +import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/providers/wallets.dart'; +import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -26,6 +30,7 @@ class SendPayoutButton extends StatelessWidget { if (wallet != null) { pageSelectorProvider.startPaymentFromWallet(wallet); + context.go(payoutPath(PayoutDestination.payment)); } }, child: Text(loc.payoutNavSendPayout), diff --git a/frontend/pweb/lib/providers/page_selector.dart b/frontend/pweb/lib/providers/page_selector.dart index c113eff..473675f 100644 --- a/frontend/pweb/lib/providers/page_selector.dart +++ b/frontend/pweb/lib/providers/page_selector.dart @@ -45,6 +45,7 @@ class PageSelectorProvider extends ChangeNotifier { } void selectPage(PayoutDestination dest) { + if (_selected == dest) return; _selected = dest; notifyListeners(); } @@ -95,7 +96,7 @@ class PageSelectorProvider extends ChangeNotifier { notifyListeners(); } - void goBackFromPayment() { + PayoutDestination goBackFromPayment() { _selected = _previousDestination ?? (_cameFromRecipientList ? PayoutDestination.recipients @@ -104,6 +105,7 @@ class PageSelectorProvider extends ChangeNotifier { _previousDestination = null; _cameFromRecipientList = false; notifyListeners(); + return _selected; } void goBackFromWalletEdit() { @@ -186,4 +188,4 @@ class PageSelectorProvider extends ChangeNotifier { Recipient? get selectedRecipient => recipientProvider?.selectedRecipient; Wallet? get selectedWallet => walletsProvider?.selectedWallet; -} +} \ 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 315ff99..6175218 100644 --- a/frontend/pweb/lib/widgets/sidebar/page.dart +++ b/frontend/pweb/lib/widgets/sidebar/page.dart @@ -1,44 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; -import 'package:pshared/models/resources.dart'; -import 'package:pshared/provider/account.dart'; -import 'package:pshared/provider/permissions.dart'; - -import 'package:pweb/pages/address_book/form/page.dart'; -import 'package:pweb/pages/address_book/page/page.dart'; -import 'package:pweb/pages/payment_methods/page.dart'; -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/pages/dashboard/dashboard.dart'; -import 'package:pweb/providers/page_selector.dart'; import 'package:pweb/app/router/pages.dart'; +import 'package:pweb/app/router/payout_routes.dart'; +import 'package:pweb/providers/account.dart'; +import 'package:pweb/providers/page_selector.dart'; +import 'package:pweb/providers/permissions.dart'; import 'package:pweb/widgets/appbar/app_bar.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}); + final PayoutDestination selected; + final Widget child; - void _logout(BuildContext context) { + const PageSelector({ + super.key, + required this.selected, + required this.child, + }); + + void _handleLogout(BuildContext context) { context.read().logout(); + context.read().clear(); navigateAndReplace(context, Pages.login); } + void _goToDestination(BuildContext context, PayoutDestination destination) { + context.go(payoutPath(destination)); + } + @override Widget build(BuildContext context) { final provider = context.watch(); final permissions = context.watch(); - final loc = AppLocalizations.of(context)!; + final account = context.watch().account; - final bool restrictedAccess = !permissions.canRead(ResourceType.chainWallets); - final allowedDestinations = restrictedAccess + final allowedDestinations = permissions.isRecipient ? { PayoutDestination.settings, PayoutDestination.methods, @@ -46,78 +48,23 @@ class PageSelector extends StatelessWidget { } : PayoutDestination.values.toSet(); - final selected = allowedDestinations.contains(provider.selected) - ? provider.selected - : (restrictedAccess ? PayoutDestination.settings : PayoutDestination.dashboard); + final safeSelected = allowedDestinations.contains(selected) + ? selected + : (permissions.isRecipient ? PayoutDestination.settings : PayoutDestination.dashboard); - if (selected != provider.selected) { - WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(selected)); + if (safeSelected != selected) { + WidgetsBinding.instance.addPostFrameCallback((_) => _goToDestination(context, safeSelected)); } - Widget content; - switch (selected) { - case PayoutDestination.dashboard: - content = DashboardPage( - onRecipientSelected: (recipient) => provider.selectRecipient(recipient), - onGoToPaymentWithoutRecipient: provider.startPaymentWithoutRecipient, - ); - break; - - case PayoutDestination.recipients: - content = RecipientAddressBookPage( - onRecipientSelected: (recipient) => - provider.selectRecipient(recipient, fromList: true), - onAddRecipient: provider.goToAddRecipient, - onEditRecipient: provider.editRecipient, - ); - break; - - case PayoutDestination.addrecipient: - final recipient = provider.recipientProvider?.selectedRecipient; - content = AdressBookRecipientForm( - recipient: recipient, - onSaved: (_) => provider.selectPage(PayoutDestination.recipients), - ); - break; - - case PayoutDestination.payment: - content = PaymentPage( - onBack: (_) => provider.goBackFromPayment(), - ); - break; - - case PayoutDestination.settings: - content = ProfileSettingsPage(); - break; - - case PayoutDestination.reports: - content = OperationHistoryPage(); - break; - - case PayoutDestination.methods: - content = PaymentConfigPage( - onWalletTap: provider.selectWallet, - ); - break; - - case PayoutDestination.editwallet: - final wallet = provider.walletsProvider?.selectedWallet; - content = wallet != null - ? WalletEditPage( - onBack: provider.goBackFromWalletEdit, - ) - : Center(child: Text(loc.noWalletSelected)); - break; - - default: - content = Text(selected.name); + if (provider.selected != safeSelected) { + WidgetsBinding.instance.addPostFrameCallback((_) => provider.selectPage(safeSelected)); } return Scaffold( appBar: PayoutAppBar( - title: Text(selected.localizedLabel(context)), + title: Text(safeSelected.localizedLabel(context)), onAddFundsPressed: () {}, - onLogout: () => _logout(context), + onLogout: () => _handleLogout(context), ), body: Padding( padding: const EdgeInsets.only(left: 200, top: 40, right: 200), @@ -126,11 +73,15 @@ class PageSelector extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ PayoutSidebar( - selected: selected, - onSelected: provider.selectPage, - onLogout: () => _logout(context), + selected: safeSelected, + onSelected: (destination) => _goToDestination(context, destination), + onLogout: () => _handleLogout(context), + userName: account?.name, + items: permissions.isRecipient + ? const [PayoutDestination.settings, PayoutDestination.methods] + : null, ), - Expanded(child: content), + Expanded(child: child), ], ), ),