Added Localizations and ran small fixes #6
3
frontend/pweb/devtools_options.yaml
Normal file
3
frontend/pweb/devtools_options.yaml
Normal file
@@ -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:
|
||||||
@@ -36,9 +36,9 @@
|
|||||||
"signupError": "Failed to signup: {error}",
|
"signupError": "Failed to signup: {error}",
|
||||||
"signupSuccess": "Email confirmation message has been sent to {email}. Please, open it and click link to activate your account.",
|
"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.",
|
"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",
|
"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",
|
"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",
|
"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",
|
"errorVerificationTokenNotFound": "Account for verification not found. Sign up again",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
@@ -465,4 +465,4 @@
|
|||||||
"englishLanguage": "English",
|
"englishLanguage": "English",
|
||||||
"russianLanguage": "Russian",
|
"russianLanguage": "Russian",
|
||||||
"germanLanguage": "German"
|
"germanLanguage": "German"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.",
|
"connectivityError": "Не удается связаться с сервером {serverAddress}. Проверьте ваше интернет-соединение и попробуйте снова.",
|
||||||
"errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации",
|
"errorAccountNotVerified": "Ваш аккаунт еще не подтвержден. Пожалуйста, проверьте вашу электронную почту для завершения верификации",
|
||||||
"errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова",
|
"errorLoginUnauthorized": "Неверный логин или пароль. Пожалуйста, попробуйте снова",
|
||||||
|
"errorAccountExists": "Аккаунт с таким логином уже существует",
|
||||||
"errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже",
|
"errorInternalError": "Произошла внутренняя ошибка. Мы в курсе проблемы и работаем над ее решением. Пожалуйста, попробуйте позже",
|
||||||
"errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова",
|
"errorVerificationTokenNotFound": "Аккаунт для верификации не найден. Зарегистрируйтесь снова",
|
||||||
"created": "Создано",
|
"created": "Создано",
|
||||||
@@ -457,4 +458,4 @@
|
|||||||
"englishLanguage": "Английский",
|
"englishLanguage": "Английский",
|
||||||
"russianLanguage": "Русский",
|
"russianLanguage": "Русский",
|
||||||
"germanLanguage": "Немецкий"
|
"germanLanguage": "Немецкий"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:pshared/config/constants.dart';
|
import 'package:pshared/config/constants.dart';
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
import 'package:pshared/provider/locale.dart';
|
import 'package:pshared/provider/locale.dart';
|
||||||
import 'package:pshared/provider/organizations.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/app/timeago.dart';
|
||||||
import 'package:pweb/providers/carousel.dart';
|
import 'package:pweb/providers/carousel.dart';
|
||||||
import 'package:pweb/providers/mock_payment.dart';
|
import 'package:pweb/providers/mock_payment.dart';
|
||||||
|
import 'package:pweb/providers/permissions.dart';
|
||||||
import 'package:pweb/providers/operatioins.dart';
|
import 'package:pweb/providers/operatioins.dart';
|
||||||
|
import 'package:pweb/providers/account.dart';
|
||||||
import 'package:pweb/providers/page_selector.dart';
|
import 'package:pweb/providers/page_selector.dart';
|
||||||
import 'package:pweb/providers/payment_methods.dart';
|
import 'package:pweb/providers/payment_methods.dart';
|
||||||
import 'package:pweb/providers/recipient.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/recipient/recipient.dart';
|
||||||
import 'package:pweb/services/wallet_transactions.dart';
|
import 'package:pweb/services/wallet_transactions.dart';
|
||||||
import 'package:pweb/services/wallets.dart';
|
import 'package:pweb/services/wallets.dart';
|
||||||
|
import 'package:pweb/services/accounts.dart';
|
||||||
|
import 'package:pweb/services/permissions.dart';
|
||||||
|
|
||||||
|
|
||||||
void _setupLogging() {
|
void _setupLogging() {
|
||||||
@@ -55,17 +58,15 @@ void main() async {
|
|||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
|
||||||
ChangeNotifierProvider(create: (_) => AccountProvider()),
|
ChangeNotifierProvider(create: (_) => PermissionsProvider(service: PermissionsService())),
|
||||||
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
|
ChangeNotifierProvider(
|
||||||
create: (context) => TwoFactorProvider(
|
create: (context) => AccountProvider(
|
||||||
accountProvider: context.read<AccountProvider>(),
|
accountsService: AccountsService(),
|
||||||
),
|
permissionsProvider: context.read<PermissionsProvider>(),
|
||||||
update: (context, accountProvider, previous) => TwoFactorProvider(
|
|
||||||
accountProvider: accountProvider,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProvider(create: (_) => TwoFactorProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => OrganizationsProvider()),
|
ChangeNotifierProvider(create: (_) => OrganizationsProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => AccountProvider()),
|
|
||||||
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
|
||||||
|
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.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/app/router/pages.dart';
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
import 'package:pweb/widgets/error/snackbar.dart';
|
||||||
@@ -26,13 +26,10 @@ class AccountLoader extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
navigateAndReplace(context, Pages.login);
|
navigateAndReplace(context, Pages.login);
|
||||||
}
|
}
|
||||||
if ((provider.error == null) && (provider.account == null)) {
|
if (provider.account == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
|
||||||
provider.restore();
|
|
||||||
});
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.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/app/router/pages.dart';
|
||||||
import 'package:pweb/widgets/error/snackbar.dart';
|
import 'package:pweb/widgets/error/snackbar.dart';
|
||||||
@@ -16,8 +17,10 @@ class PermissionsLoader extends StatelessWidget {
|
|||||||
const PermissionsLoader({super.key, required this.child});
|
const PermissionsLoader({super.key, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Consumer<PermissionsProvider>(builder: (context, provider, _) {
|
Widget build(BuildContext context) => Consumer2<PermissionsProvider, AccountProvider>(builder: (context, provider, accountProvider, _) {
|
||||||
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
|
if (provider.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
if (provider.error != null) {
|
if (provider.error != null) {
|
||||||
postNotifyUserOfErrorX(
|
postNotifyUserOfErrorX(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -26,9 +29,9 @@ class PermissionsLoader extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
navigateAndReplace(context, Pages.login);
|
navigateAndReplace(context, Pages.login);
|
||||||
}
|
}
|
||||||
if ((provider.error == null) && (provider.permissions.isEmpty)) {
|
if (provider.error == null && !provider.hasLoaded && accountProvider.account != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
provider.load();
|
provider.loadForAccount(accountProvider.account!.id);
|
||||||
});
|
});
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
import 'package:pshared/provider/locale.dart';
|
import 'package:pshared/provider/locale.dart';
|
||||||
|
import 'package:pweb/providers/account.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/pages.dart';
|
import 'package:pweb/app/router/pages.dart';
|
||||||
import 'package:pweb/pages/login/buttons.dart';
|
import 'package:pweb/pages/login/buttons.dart';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/provider/account.dart';
|
import 'package:pweb/providers/account.dart';
|
||||||
|
|
||||||
import 'package:pweb/widgets/vspacer.dart';
|
import 'package:pweb/widgets/vspacer.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/api/requests/login_data.dart';
|
import 'package:pshared/api/requests/login_data.dart';
|
||||||
import 'package:pshared/models/describable.dart';
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
import 'package:pshared/provider/locale.dart';
|
import 'package:pshared/provider/locale.dart';
|
||||||
|
import 'package:pweb/providers/account.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/router/pages.dart';
|
import 'package:pweb/app/router/pages.dart';
|
||||||
import 'package:pweb/pages/signup/form/content.dart';
|
import 'package:pweb/pages/signup/form/content.dart';
|
||||||
@@ -110,4 +110,4 @@ class SignUpFormState extends State<SignUpForm> {
|
|||||||
onSignUp: handleSignUp,
|
onSignUp: handleSignUp,
|
||||||
onLogin: handleLogin,
|
onLogin: handleLogin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
97
frontend/pweb/lib/providers/account.dart
Normal file
97
frontend/pweb/lib/providers/account.dart
Normal file
@@ -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<LoginOutcome> 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<Account?> restore() async {
|
||||||
|
_setLoading(true);
|
||||||
|
_account = null;
|
||||||
|
_permissionsProvider.clear();
|
||||||
|
_error = Exception('Сохраненная сессия отсутствует');
|
||||||
|
_setLoading(false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> logout() async {
|
||||||
|
_account = null;
|
||||||
|
_error = null;
|
||||||
|
_permissionsProvider.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setLoading(bool value) {
|
||||||
|
_isLoading = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
82
frontend/pweb/lib/providers/permissions.dart
Normal file
82
frontend/pweb/lib/providers/permissions.dart
Normal file
@@ -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<Permission> _permissions = [];
|
||||||
|
List<PolicyDescription> _policyDescriptions = [];
|
||||||
|
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
Object? get error => _error;
|
||||||
|
bool get isReady => _hasLoaded && !_isLoading && _error == null;
|
||||||
|
List<Permission> get permissions => List.unmodifiable(_permissions);
|
||||||
|
bool get hasLoaded => _hasLoaded;
|
||||||
|
|
||||||
|
bool get isCompany => _roleRef == companyRoleId;
|
||||||
|
bool get isRecipient => _roleRef == recipientRoleId;
|
||||||
|
|
||||||
|
Future<void> 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/auth/pending_login.dart';
|
import 'package:pweb/services/auth.dart';
|
||||||
import 'package:pshared/provider/account.dart';
|
|
||||||
import 'package:pshared/service/account.dart';
|
|
||||||
|
|
||||||
class TwoFactorProvider extends ChangeNotifier {
|
class TwoFactorProvider extends ChangeNotifier {
|
||||||
static final _logger = Logger('provider.two_factor');
|
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 _isSubmitting = false;
|
||||||
bool _hasError = false;
|
bool _hasError = false;
|
||||||
@@ -20,7 +18,6 @@ class TwoFactorProvider extends ChangeNotifier {
|
|||||||
bool get hasError => _hasError;
|
bool get hasError => _hasError;
|
||||||
bool get verificationSuccess => _verificationSuccess;
|
bool get verificationSuccess => _verificationSuccess;
|
||||||
String? get errorMessage => _errorMessage;
|
String? get errorMessage => _errorMessage;
|
||||||
PendingLogin? get pendingLogin => _accountProvider.pendingLogin;
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> submitCode(String code) async {
|
Future<void> submitCode(String code) async {
|
||||||
@@ -31,16 +28,8 @@ class TwoFactorProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final pending = _accountProvider.pendingLogin;
|
final isValid = await _authService.verifyTwoFactorCode(code);
|
||||||
if (pending == null) {
|
_verificationSuccess = isValid;
|
||||||
throw Exception('No pending login available');
|
|
||||||
}
|
|
||||||
final account = await AccountService.confirmLoginCode(
|
|
||||||
pending: pending,
|
|
||||||
code: code,
|
|
||||||
);
|
|
||||||
_accountProvider.completePendingLogin(account);
|
|
||||||
_verificationSuccess = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_hasError = true;
|
_hasError = true;
|
||||||
_errorMessage = e.toString();
|
_errorMessage = e.toString();
|
||||||
@@ -52,18 +41,9 @@ class TwoFactorProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resendCode() async {
|
Future<void> resendCode() async {
|
||||||
final pending = _accountProvider.pendingLogin;
|
_logger.fine('Resending mock two-factor code');
|
||||||
if (pending == null) {
|
_hasError = false;
|
||||||
_logger.warning('No pending login to resend code for');
|
_errorMessage = null;
|
||||||
return;
|
notifyListeners();
|
||||||
}
|
|
||||||
try {
|
|
||||||
await AccountService.resendLoginCode(pending);
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning('Failed to resend login code', e);
|
|
||||||
_hasError = true;
|
|
||||||
_errorMessage = e.toString();
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
frontend/pweb/lib/services/accounts.dart
Normal file
105
frontend/pweb/lib/services/accounts.dart
Normal file
@@ -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<AccountLoginResult> 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<AccountLoginResult> 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;
|
||||||
|
}
|
||||||
12
frontend/pweb/lib/services/mock_ids.dart
Normal file
12
frontend/pweb/lib/services/mock_ids.dart
Normal file
@@ -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';
|
||||||
117
frontend/pweb/lib/services/permissions.dart
Normal file
117
frontend/pweb/lib/services/permissions.dart
Normal file
@@ -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<UserAccess> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:pshared/api/responses/error/server.dart';
|
|||||||
import 'package:pshared/config/constants.dart';
|
import 'package:pshared/config/constants.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
import 'package:pweb/services/accounts.dart';
|
||||||
|
|
||||||
|
|
||||||
class ErrorHandler {
|
class ErrorHandler {
|
||||||
@@ -48,6 +49,8 @@ class ErrorHandler {
|
|||||||
final errorHandlers = <Type, String Function(Object)>{
|
final errorHandlers = <Type, String Function(Object)>{
|
||||||
ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse),
|
ErrorResponse: (ex) => _handleErrorResponseLocs(locs, ex as ErrorResponse),
|
||||||
ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError),
|
ConnectivityError: (ex) => _handleConnectivityErrorLocs(locs, ex as ConnectivityError),
|
||||||
|
InvalidCredentialsException: (_) => locs.errorLoginUnauthorized,
|
||||||
|
DuplicateAccountException: (_) => locs.errorAccountExists,
|
||||||
};
|
};
|
||||||
|
|
||||||
return errorHandlers[e.runtimeType]?.call(e) ?? e.toString();
|
return errorHandlers[e.runtimeType]?.call(e) ?? e.toString();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.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';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.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/app/router/pages.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/resources.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/avatar.dart';
|
||||||
import 'package:pweb/widgets/drawer/tiles/dashboard.dart';
|
import 'package:pweb/widgets/drawer/tiles/dashboard.dart';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/resources.dart';
|
import 'package:pshared/models/resources.dart';
|
||||||
import 'package:pshared/models/permissions/action.dart' as perm;
|
import 'package:pshared/models/permissions/action.dart' as perm;
|
||||||
import 'package:pshared/provider/permissions.dart';
|
import 'package:pweb/providers/permissions.dart';
|
||||||
|
|
||||||
|
|
||||||
T? protectedWidgetctx<T extends Widget>(BuildContext context, ResourceType resource, T child, {perm.Action? action}) {
|
T? protectedWidgetctx<T extends Widget>(BuildContext context, ResourceType resource, T child, {perm.Action? action}) {
|
||||||
@@ -13,4 +13,4 @@ T? protectedWidgetctx<T extends Widget>(BuildContext context, ResourceType resou
|
|||||||
|
|
||||||
T? protectedWidget<T extends Widget>(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) {
|
T? protectedWidget<T extends Widget>(PermissionsProvider provider, ResourceType resource, T child, {perm.Action? action}) {
|
||||||
return provider.canAccessResource(resource, action: action) ? child : null;
|
return provider.canAccessResource(resource, action: action) ? child : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/payout_page/wallet/edit/page.dart';
|
||||||
import 'package:pweb/pages/report/page.dart';
|
import 'package:pweb/pages/report/page.dart';
|
||||||
import 'package:pweb/pages/settings/profile/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/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/widgets/appbar/app_bar.dart';
|
||||||
import 'package:pweb/pages/dashboard/dashboard.dart';
|
import 'package:pweb/pages/dashboard/dashboard.dart';
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
@@ -21,13 +24,37 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
class PageSelector extends StatelessWidget {
|
class PageSelector extends StatelessWidget {
|
||||||
const PageSelector({super.key});
|
const PageSelector({super.key});
|
||||||
|
|
||||||
|
void _handleLogout(BuildContext context) {
|
||||||
|
context.read<AccountProvider>().logout();
|
||||||
|
context.read<PermissionsProvider>().clear();
|
||||||
|
navigateAndReplace(context, Pages.login);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = context.watch<PageSelectorProvider>();
|
final provider = context.watch<PageSelectorProvider>();
|
||||||
|
final permissions = context.watch<PermissionsProvider>();
|
||||||
|
final account = context.watch<AccountProvider>().account;
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final allowedDestinations = permissions.isRecipient
|
||||||
|
? <PayoutDestination>{
|
||||||
|
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;
|
Widget content;
|
||||||
switch (provider.selected) {
|
switch (selected) {
|
||||||
case PayoutDestination.dashboard:
|
case PayoutDestination.dashboard:
|
||||||
content = DashboardPage(
|
content = DashboardPage(
|
||||||
onRecipientSelected: (recipient) =>
|
onRecipientSelected: (recipient) =>
|
||||||
@@ -83,14 +110,14 @@ class PageSelector extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
content = Text(provider.selected.name);
|
content = Text(selected.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: PayoutAppBar(
|
appBar: PayoutAppBar(
|
||||||
title: Text(provider.selected.localizedLabel(context)),
|
title: Text(selected.localizedLabel(context)),
|
||||||
onAddFundsPressed: () {},
|
onAddFundsPressed: () {},
|
||||||
onLogout: () => debugPrint('Logout clicked'),
|
onLogout: () => _handleLogout(context),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.only(left: 200, top: 40, right: 200),
|
padding: const EdgeInsets.only(left: 200, top: 40, right: 200),
|
||||||
@@ -99,9 +126,13 @@ class PageSelector extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
PayoutSidebar(
|
PayoutSidebar(
|
||||||
selected: provider.selected,
|
selected: selected,
|
||||||
onSelected: provider.selectPage,
|
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),
|
Expanded(child: content),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class PayoutSidebar extends StatelessWidget {
|
|||||||
this.onLogout,
|
this.onLogout,
|
||||||
this.userName,
|
this.userName,
|
||||||
this.avatarUrl,
|
this.avatarUrl,
|
||||||
|
this.items,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PayoutDestination selected;
|
final PayoutDestination selected;
|
||||||
@@ -21,11 +22,13 @@ class PayoutSidebar extends StatelessWidget {
|
|||||||
|
|
||||||
final String? userName;
|
final String? userName;
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
|
final List<PayoutDestination>? items;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final items = [
|
final menuItems = items ??
|
||||||
|
<PayoutDestination>[
|
||||||
PayoutDestination.dashboard,
|
PayoutDestination.dashboard,
|
||||||
PayoutDestination.recipients,
|
PayoutDestination.recipients,
|
||||||
PayoutDestination.methods,
|
PayoutDestination.methods,
|
||||||
@@ -49,11 +52,11 @@ class PayoutSidebar extends StatelessWidget {
|
|||||||
theme: theme,
|
theme: theme,
|
||||||
avatarUrl: avatarUrl,
|
avatarUrl: avatarUrl,
|
||||||
userName: userName,
|
userName: userName,
|
||||||
items: items,
|
items: menuItems,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
onSelected: onSelected,
|
onSelected: onSelected,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Foundation
|
|||||||
import amplitude_flutter
|
import amplitude_flutter
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_timezone
|
import flutter_timezone
|
||||||
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin"))
|
AmplitudeFlutterPlugin.register(with: registry.registrar(forPlugin: "AmplitudeFlutterPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||||
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
|
|||||||
Reference in New Issue
Block a user