Merge pull request 'name ui fix and removed parts of the app that are not ready' (#422) from SEND043 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #422
This commit was merged in pull request #422.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/api/responses/error/server.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/account.dart';
|
||||
import 'package:pshared/utils/exception.dart';
|
||||
|
||||
|
||||
class EmailVerificationProvider extends ChangeNotifier {
|
||||
Resource<bool> _resource = Resource(data: null, isLoading: false);
|
||||
String? _token;
|
||||
@@ -13,6 +13,11 @@ class EmailVerificationProvider extends ChangeNotifier {
|
||||
bool get isLoading => _resource.isLoading;
|
||||
bool get isSuccess => _resource.data == true;
|
||||
Exception? get error => _resource.error;
|
||||
int? get errorCode => _resource.error is ErrorResponse
|
||||
? (_resource.error as ErrorResponse).code
|
||||
: null;
|
||||
bool get canResendVerification =>
|
||||
errorCode == 400 || errorCode == 410 || errorCode == 500;
|
||||
|
||||
Future<void> verify(String token) async {
|
||||
final trimmed = token.trim();
|
||||
@@ -33,12 +38,12 @@ class EmailVerificationProvider extends ChangeNotifier {
|
||||
await AccountService.verifyEmail(trimmed);
|
||||
_setResource(Resource(data: true, isLoading: false));
|
||||
} catch (e) {
|
||||
if (e is ErrorResponse && e.code == 404) {
|
||||
_setResource(Resource(data: true, isLoading: false));
|
||||
return;
|
||||
}
|
||||
_setResource(
|
||||
Resource(
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: toException(e),
|
||||
),
|
||||
Resource(data: null, isLoading: false, error: toException(e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"payoutNavReports": "Reports",
|
||||
"payoutNavSettings": "Settings",
|
||||
"payoutNavLogout": "Logout",
|
||||
"payoutNavMethods": "Payouts",
|
||||
"payoutNavMethods": "Payout Methods",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse",
|
||||
"pageTitleRecipients": "Recipient address book",
|
||||
@@ -421,7 +421,7 @@
|
||||
"paymentType": "Payment Method Type",
|
||||
"selectPaymentType": "Please select a payment method type",
|
||||
|
||||
"paymentTypeCard": "Credit Card",
|
||||
"paymentTypeCard": "Russian bank card",
|
||||
"paymentTypeBankAccount": "Russian Bank Account",
|
||||
"paymentTypeIban": "IBAN",
|
||||
"paymentTypeWallet": "Wallet",
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"payoutNavReports": "Отчеты",
|
||||
"payoutNavSettings": "Настройки",
|
||||
"payoutNavLogout": "Выйти",
|
||||
"payoutNavMethods": "Выплаты",
|
||||
"payoutNavMethods": "Способы выплат",
|
||||
"expand": "Развернуть",
|
||||
"collapse": "Свернуть",
|
||||
"pageTitleRecipients": "Адресная книга получателей",
|
||||
@@ -421,7 +421,7 @@
|
||||
"paymentType": "Тип способа оплаты",
|
||||
"selectPaymentType": "Пожалуйста, выберите тип способа оплаты",
|
||||
|
||||
"paymentTypeCard": "Кредитная карта",
|
||||
"paymentTypeCard": "Российская банковская карта",
|
||||
"paymentTypeBankAccount": "Российский банковский счет",
|
||||
"paymentTypeIban": "IBAN",
|
||||
"paymentTypeWallet": "Кошелек",
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/filter.dart';
|
||||
|
||||
|
||||
class RecipientFilterButton extends StatelessWidget {
|
||||
final String text;
|
||||
final RecipientFilter filter;
|
||||
final RecipientFilter selected;
|
||||
final ValueChanged<RecipientFilter> onTap;
|
||||
|
||||
const RecipientFilterButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.filter,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSelected = selected == filter;
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: () => onTap(filter),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
shadowColor: WidgetStateProperty.all(Colors.transparent),
|
||||
elevation: WidgetStateProperty.all(0),
|
||||
),
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: isSelected
|
||||
? theme.onPrimaryContainer
|
||||
: theme.onPrimaryContainer.withAlpha(60),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 2,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.primary
|
||||
: theme.onPrimaryContainer.withAlpha(60),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,9 @@ 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:pshared/provider/recipient/provider.dart';
|
||||
import 'package:pweb/pages/address_book/page/empty.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';
|
||||
import 'package:pweb/pages/address_book/page/search.dart';
|
||||
@@ -42,7 +40,6 @@ class RecipientAddressBookPage extends StatefulWidget {
|
||||
class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
late final TextEditingController _searchController;
|
||||
late final FocusNode _searchFocusNode;
|
||||
RecipientFilter _selectedFilter = RecipientFilter.all;
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
@@ -65,19 +62,12 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _setFilter(RecipientFilter filter) {
|
||||
setState(() {
|
||||
_selectedFilter = filter;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final provider = context.watch<RecipientsProvider>();
|
||||
final filteredRecipients = filterRecipients(
|
||||
recipients: provider.recipients,
|
||||
filter: _selectedFilter,
|
||||
query: _query,
|
||||
);
|
||||
|
||||
@@ -100,34 +90,6 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
|
||||
onChanged: _setQuery,
|
||||
),
|
||||
const SizedBox(height: RecipientAddressBookPage._bigBox),
|
||||
Row(
|
||||
children: [
|
||||
RecipientFilterButton(
|
||||
text: loc.allStatus,
|
||||
filter: RecipientFilter.all,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.readyStatus,
|
||||
filter: RecipientFilter.ready,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.registeredStatus,
|
||||
filter: RecipientFilter.registered,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
RecipientFilterButton(
|
||||
text: loc.notRegisteredStatus,
|
||||
filter: RecipientFilter.notRegistered,
|
||||
selected: _selectedFilter,
|
||||
onTap: _setFilter,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: RecipientAddressBookPage._expandedHeight,
|
||||
child: Padding(
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/actions.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/info_column.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/payment_row.dart';
|
||||
import 'package:pweb/pages/address_book/page/recipient/status.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
|
||||
|
||||
|
||||
@@ -18,7 +17,6 @@ class RecipientAddressBookItem extends StatefulWidget {
|
||||
final double borderRadius;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double spacingDotAvatar;
|
||||
final double spacingAvatarInfo;
|
||||
final double spacingBottom;
|
||||
final double avatarRadius;
|
||||
@@ -32,7 +30,6 @@ class RecipientAddressBookItem extends StatefulWidget {
|
||||
this.borderRadius = 12,
|
||||
this.elevation = 4,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
this.spacingDotAvatar = 8,
|
||||
this.spacingAvatarInfo = 16,
|
||||
this.spacingBottom = 10,
|
||||
this.avatarRadius = 24,
|
||||
@@ -65,8 +62,6 @@ class _RecipientAddressBookItemState extends State<RecipientAddressBookItem> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
RecipientStatusDot(status: recipient.status),
|
||||
SizedBox(width: widget.spacingDotAvatar),
|
||||
RecipientAvatar(
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/status.dart';
|
||||
|
||||
|
||||
class RecipientStatusDot extends StatelessWidget {
|
||||
final RecipientStatus status;
|
||||
|
||||
const RecipientStatusDot({super.key, required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color color;
|
||||
switch (status) {
|
||||
case RecipientStatus.ready:
|
||||
color = Colors.green;
|
||||
break;
|
||||
case RecipientStatus.notRegistered:
|
||||
color = Theme.of(context).colorScheme.error;
|
||||
break;
|
||||
case RecipientStatus.registered:
|
||||
color = Colors.yellow;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -70,16 +70,17 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||
icon: Icons.person_add,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.small),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: TransactionRefButton(
|
||||
onTap: () => _setActive(false),
|
||||
isActive: _showContainerMultiple,
|
||||
label: l10n.sendMultiple,
|
||||
icon: Icons.group_add,
|
||||
),
|
||||
),
|
||||
//TODO bring back multiple payouts
|
||||
// const SizedBox(width: AppSpacing.small),
|
||||
// Expanded(
|
||||
// flex: 0,
|
||||
// child: TransactionRefButton(
|
||||
// onTap: () => _setActive(false),
|
||||
// isActive: _showContainerMultiple,
|
||||
// label: l10n.sendMultiple,
|
||||
// icon: Icons.group_add,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.medium),
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/models/control_state.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/new_recipient/type.dart';
|
||||
import 'package:pweb/utils/payment/availability.dart';
|
||||
|
||||
|
||||
class SinglePayout extends StatelessWidget {
|
||||
final void Function(PaymentType type) onGoToPayment;
|
||||
|
||||
static const double _cardPadding = 30.0;
|
||||
static const double _dividerPaddingVertical = 12.0;
|
||||
static const double _cardBorderRadius = 12.0;
|
||||
static const double _dividerThickness = 1.0;
|
||||
|
||||
const SinglePayout({super.key, required this.onGoToPayment});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final paymentTypes = visiblePaymentTypes;
|
||||
final dividerColor = Theme.of(context).dividerColor;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(_cardBorderRadius),
|
||||
),
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(_cardPadding),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < paymentTypes.length; i++) ...[
|
||||
PaymentTypeTile(
|
||||
type: paymentTypes[i],
|
||||
onSelected: onGoToPayment,
|
||||
state: disabledPaymentTypes.contains(paymentTypes[i])
|
||||
? ControlState.disabled
|
||||
: ControlState.enabled,
|
||||
),
|
||||
if (i < paymentTypes.length - 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: _dividerPaddingVertical),
|
||||
child: Divider(thickness: _dividerThickness, color: dividerColor),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/models/control_state.dart';
|
||||
import 'package:pweb/pages/payment_methods/icon.dart';
|
||||
import 'package:pweb/utils/payment/label.dart';
|
||||
|
||||
|
||||
class PaymentTypeTile extends StatelessWidget {
|
||||
final PaymentType type;
|
||||
final void Function(PaymentType type) onSelected;
|
||||
final ControlState state;
|
||||
|
||||
const PaymentTypeTile({
|
||||
super.key,
|
||||
required this.type,
|
||||
required this.onSelected,
|
||||
this.state = ControlState.enabled,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = getPaymentTypeLabel(context, type);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isEnabled = state == ControlState.enabled;
|
||||
final isDisabled = state == ControlState.disabled;
|
||||
final isLoading = state == ControlState.loading;
|
||||
final textColor = isDisabled
|
||||
? theme.colorScheme.onSurface.withValues(alpha: 0.55)
|
||||
: theme.colorScheme.onSurface;
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: isEnabled ? () => onSelected(type) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(iconForPaymentType(type), size: 24, color: textColor),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: textColor),
|
||||
),
|
||||
if (isLoading) ...[
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/payouts/single/address_book/widget.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/single/new_recipient/payout.dart';
|
||||
|
||||
|
||||
class SinglePayoutForm extends StatelessWidget {
|
||||
@@ -17,18 +16,13 @@ class SinglePayoutForm extends StatelessWidget {
|
||||
required this.onGoToPayment,
|
||||
});
|
||||
|
||||
static const double _spacingBetweenAddressAndForm = 20.0;
|
||||
static const double _bottomSpacing = 40.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AddressBookPayout(onSelected: onRecipientSelected),
|
||||
const SizedBox(height: _spacingBetweenAddressAndForm),
|
||||
SinglePayout(onGoToPayment: onGoToPayment),
|
||||
const SizedBox(height: _bottomSpacing),
|
||||
//TODO Add history of recent wallets/payments
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ class ErrorPage extends StatelessWidget {
|
||||
final String title;
|
||||
final String errorMessage;
|
||||
final String errorHint;
|
||||
final Widget? action;
|
||||
|
||||
const ErrorPage({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.errorMessage,
|
||||
required this.errorHint,
|
||||
this.action,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -26,19 +28,34 @@ class ErrorPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 72, color: Theme.of(context).colorScheme.error),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 72,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const VSpacer(),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(color: Theme.of(context).colorScheme.error),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const VSpacer(multiplier: 0.5),
|
||||
ListTile(
|
||||
title: Text(errorMessage, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge),
|
||||
subtitle: Text(errorHint, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall),
|
||||
title: Text(
|
||||
errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
subtitle: Text(
|
||||
errorHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
const VSpacer(multiplier: 1.5),
|
||||
if (action != null) ...[action!, const VSpacer(multiplier: 0.5)],
|
||||
TextButton(
|
||||
onPressed: () => navigate(context, Pages.root),
|
||||
child: Text(AppLocalizations.of(context)!.goToMainPage),
|
||||
@@ -54,8 +71,10 @@ Widget exceptionToErrorPage({
|
||||
required String title,
|
||||
required String errorMessage,
|
||||
required Object exception,
|
||||
Widget? action,
|
||||
}) => ErrorPage(
|
||||
title: title,
|
||||
errorMessage: errorMessage,
|
||||
errorHint: ErrorHandler.handleError(context, exception),
|
||||
action: action,
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ class _LoginFormState extends State<LoginForm> {
|
||||
builder: (context, isUsernameValid, child) => ValueListenableBuilder<bool>(
|
||||
valueListenable: _isPasswordAcceptable,
|
||||
builder: (context, isPasswordValid, child) => ButtonsRow(
|
||||
onSignUp: () => navigate(context, Pages.signup),
|
||||
onSignUp: () => navigateAndReplace(context, Pages.signup),
|
||||
login: () => _login(
|
||||
context,
|
||||
() => navigateAndReplace(context, Pages.dashboard),
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/providers/account_name.dart';
|
||||
|
||||
|
||||
class AccountNameEditingFields extends StatelessWidget {
|
||||
const AccountNameEditingFields({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
required this.lastNameHint,
|
||||
required this.inputWidth,
|
||||
required this.borderWidth,
|
||||
required this.state,
|
||||
});
|
||||
|
||||
final String hintText;
|
||||
final String lastNameHint;
|
||||
final double inputWidth;
|
||||
final double borderWidth;
|
||||
final AccountNameState state;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: state.firstNameController,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
autofocus: true,
|
||||
enabled: !state.isBusy,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
labelText: hintText,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: state.lastNameController,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
enabled: !state.isBusy,
|
||||
decoration: InputDecoration(
|
||||
hintText: lastNameHint,
|
||||
labelText: lastNameHint,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class AccountNameSingleLineText extends StatelessWidget {
|
||||
const AccountNameSingleLineText({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/settings/profile/account/name/editing.dart';
|
||||
import 'package:pweb/pages/settings/profile/account/name/view.dart';
|
||||
import 'package:pweb/providers/account_name.dart';
|
||||
|
||||
|
||||
@@ -22,63 +24,22 @@ class AccountNameText extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<AccountNameState>();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (state.isEditing) {
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: state.firstNameController,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
autofocus: true,
|
||||
enabled: !state.isBusy,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
labelText: hintText,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: state.lastNameController,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
enabled: !state.isBusy,
|
||||
decoration: InputDecoration(
|
||||
hintText: lastNameHint,
|
||||
labelText: lastNameHint,
|
||||
isDense: true,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return AccountNameEditingFields(
|
||||
hintText: hintText,
|
||||
lastNameHint: lastNameHint,
|
||||
borderWidth: borderWidth,
|
||||
inputWidth: inputWidth,
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
|
||||
final displayName = state.currentFullName.isNotEmpty ? state.currentFullName : hintText;
|
||||
return Text(
|
||||
displayName,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
return AccountNameViewText(
|
||||
hintText: hintText,
|
||||
inputWidth: inputWidth,
|
||||
firstName: state.currentFirstName,
|
||||
lastName: state.currentLastName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/settings/profile/account/name/line.dart';
|
||||
|
||||
|
||||
class AccountNameViewText extends StatelessWidget {
|
||||
const AccountNameViewText({
|
||||
super.key,
|
||||
required this.hintText,
|
||||
required this.inputWidth,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
});
|
||||
|
||||
final String hintText;
|
||||
final double inputWidth;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final trimmedFirstName = firstName.trim();
|
||||
final trimmedLastName = lastName.trim();
|
||||
final hasFirstName = trimmedFirstName.isNotEmpty;
|
||||
final hasLastName = trimmedLastName.isNotEmpty;
|
||||
|
||||
final firstLineStyle = theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
final secondLineStyle = theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
if (!hasFirstName && !hasLastName) {
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: AccountNameSingleLineText(
|
||||
text: hintText,
|
||||
style: firstLineStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasFirstName || !hasLastName) {
|
||||
final singleLineName = hasFirstName ? trimmedFirstName : trimmedLastName;
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: AccountNameSingleLineText(
|
||||
text: singleLineName,
|
||||
style: firstLineStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: inputWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountNameSingleLineText(
|
||||
text: trimmedFirstName,
|
||||
style: firstLineStyle,
|
||||
),
|
||||
AccountNameSingleLineText(
|
||||
text: trimmedLastName,
|
||||
style: secondLineStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class SignUpFormContent extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
onPressed: onLogin,
|
||||
icon: Icon(Icons.arrow_back),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -105,7 +105,7 @@ class SignUpFormState extends State<SignUpForm> {
|
||||
),
|
||||
);
|
||||
|
||||
void handleLogin() => navigate(context, Pages.login);
|
||||
void handleLogin() => navigateAndReplace(context, Pages.login);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
||||
111
frontend/pweb/lib/pages/verification/content.dart
Normal file
111
frontend/pweb/lib/pages/verification/content.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/widgets/locale.dart';
|
||||
|
||||
import 'package:pweb/app/router/pages.dart';
|
||||
import 'package:pweb/pages/errors/error.dart';
|
||||
import 'package:pweb/pages/status/success.dart';
|
||||
import 'package:pweb/pages/with_footer.dart';
|
||||
import 'package:pweb/pages/verification/controller.dart';
|
||||
import 'package:pweb/pages/verification/resend_dialog.dart';
|
||||
import 'package:pweb/utils/snackbar.dart';
|
||||
import 'package:pweb/widgets/error/snackbar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AccountVerificationContent extends StatefulWidget {
|
||||
const AccountVerificationContent();
|
||||
|
||||
@override
|
||||
State<AccountVerificationContent> createState() =>
|
||||
AccountVerificationContentState();
|
||||
}
|
||||
|
||||
class AccountVerificationContentState
|
||||
extends State<AccountVerificationContent> {
|
||||
Future<void> _resendVerificationEmail() async {
|
||||
final controller = context.read<AccountVerificationController>();
|
||||
if (controller.isResending) return;
|
||||
final locs = AppLocalizations.of(context)!;
|
||||
final email = await requestVerificationEmail(context, locs);
|
||||
if (!mounted || email == null) return;
|
||||
if (email.isEmpty) {
|
||||
notifyUser(context, locs.errorEmailMissing);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await controller.resendVerificationEmail(email);
|
||||
if (!mounted) return;
|
||||
await notifyUser(context, locs.signupConfirmationResent(email));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
await postNotifyUserOfErrorX(
|
||||
context: context,
|
||||
errorSituation: locs.signupConfirmationResendError,
|
||||
exception: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locs = AppLocalizations.of(context)!;
|
||||
final controller = context.watch<AccountVerificationController>();
|
||||
final action = OutlinedButton.icon(
|
||||
onPressed: () => navigateAndReplace(context, Pages.login),
|
||||
icon: const Icon(Icons.login),
|
||||
label: Text(locs.login),
|
||||
);
|
||||
|
||||
Widget content;
|
||||
if (controller.isLoading) {
|
||||
content = const Center(child: CircularProgressIndicator());
|
||||
} else if (controller.isSuccess) {
|
||||
content = StatusPageSuccess(
|
||||
successMessage: locs.accountVerified,
|
||||
successDescription: locs.accountVerifiedDescription,
|
||||
action: action,
|
||||
);
|
||||
} else {
|
||||
content = exceptionToErrorPage(
|
||||
context: context,
|
||||
title: locs.verificationFailed,
|
||||
errorMessage: locs.accountVerificationFailed,
|
||||
exception:
|
||||
controller.error ?? Exception(locs.accountVerificationFailed),
|
||||
action: controller.canResend
|
||||
? OutlinedButton.icon(
|
||||
onPressed: controller.isResending
|
||||
? null
|
||||
: _resendVerificationEmail,
|
||||
icon: controller.isResending
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.mark_email_unread_outlined),
|
||||
label: Text(locs.signupConfirmationResend),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
return PageWithFooter(
|
||||
appBar: AppBar(
|
||||
title: Text(locs.verifyAccount),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
const LocaleChangerDropdown(
|
||||
availableLocales: AppLocalizations.supportedLocales,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
64
frontend/pweb/lib/pages/verification/controller.dart
Normal file
64
frontend/pweb/lib/pages/verification/controller.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/email_verification.dart';
|
||||
|
||||
import 'package:pweb/models/flow_status.dart';
|
||||
|
||||
|
||||
class AccountVerificationController extends ChangeNotifier {
|
||||
AccountVerificationController({
|
||||
required AccountProvider accountProvider,
|
||||
required EmailVerificationProvider verificationProvider,
|
||||
}) : _accountProvider = accountProvider,
|
||||
_verificationProvider = verificationProvider {
|
||||
_verificationProvider.addListener(_onVerificationChanged);
|
||||
}
|
||||
|
||||
final AccountProvider _accountProvider;
|
||||
final EmailVerificationProvider _verificationProvider;
|
||||
|
||||
FlowStatus _resendStatus = FlowStatus.idle;
|
||||
String? _verificationToken;
|
||||
|
||||
bool get isLoading => _verificationProvider.isLoading;
|
||||
bool get isSuccess => _verificationProvider.isSuccess;
|
||||
Exception? get error => _verificationProvider.error;
|
||||
bool get canResend => _verificationProvider.canResendVerification;
|
||||
bool get isResending => _resendStatus == FlowStatus.resending;
|
||||
|
||||
void startVerification(String token) {
|
||||
final trimmed = token.trim();
|
||||
if (trimmed.isEmpty || trimmed == _verificationToken) return;
|
||||
_verificationToken = trimmed;
|
||||
_verificationProvider.verify(trimmed);
|
||||
}
|
||||
|
||||
Future<void> resendVerificationEmail(String email) async {
|
||||
final trimmed = email.trim();
|
||||
if (trimmed.isEmpty || isResending) return;
|
||||
_setResendStatus(FlowStatus.resending);
|
||||
try {
|
||||
await _accountProvider.resendVerificationEmail(trimmed);
|
||||
_setResendStatus(FlowStatus.idle);
|
||||
} catch (_) {
|
||||
_setResendStatus(FlowStatus.error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _onVerificationChanged() {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setResendStatus(FlowStatus status) {
|
||||
_resendStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_verificationProvider.removeListener(_onVerificationChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,81 +2,26 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/account.dart';
|
||||
import 'package:pshared/provider/email_verification.dart';
|
||||
import 'package:pshared/widgets/locale.dart';
|
||||
|
||||
import 'package:pweb/app/router/pages.dart';
|
||||
import 'package:pweb/pages/errors/error.dart';
|
||||
import 'package:pweb/pages/status/success.dart';
|
||||
import 'package:pweb/pages/with_footer.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/verification/content.dart';
|
||||
import 'package:pweb/pages/verification/controller.dart';
|
||||
|
||||
|
||||
class AccountVerificationPage extends StatefulWidget {
|
||||
class AccountVerificationPage extends StatelessWidget {
|
||||
final String token;
|
||||
|
||||
const AccountVerificationPage({super.key, required this.token});
|
||||
|
||||
@override
|
||||
State<AccountVerificationPage> createState() => _AccountVerificationPageState();
|
||||
}
|
||||
|
||||
class _AccountVerificationPageState extends State<AccountVerificationPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<EmailVerificationProvider>().verify(widget.token);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const _AccountVerificationContent();
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountVerificationContent extends StatelessWidget {
|
||||
const _AccountVerificationContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locs = AppLocalizations.of(context)!;
|
||||
final provider = context.watch<EmailVerificationProvider>();
|
||||
final action = OutlinedButton.icon(
|
||||
onPressed: () => navigateAndReplace(context, Pages.login),
|
||||
icon: const Icon(Icons.login),
|
||||
label: Text(locs.login),
|
||||
);
|
||||
|
||||
Widget content;
|
||||
if (provider.isLoading) {
|
||||
content = const Center(child: CircularProgressIndicator());
|
||||
} else if (provider.isSuccess) {
|
||||
content = StatusPageSuccess(
|
||||
successMessage: locs.accountVerified,
|
||||
successDescription: locs.accountVerifiedDescription,
|
||||
action: action,
|
||||
);
|
||||
} else {
|
||||
content = exceptionToErrorPage(
|
||||
context: context,
|
||||
title: locs.verificationFailed,
|
||||
errorMessage: locs.accountVerificationFailed,
|
||||
exception: provider.error ?? Exception(locs.accountVerificationFailed),
|
||||
);
|
||||
}
|
||||
|
||||
return PageWithFooter(
|
||||
appBar: AppBar(
|
||||
title: Text(locs.verifyAccount),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
const LocaleChangerDropdown(
|
||||
availableLocales: AppLocalizations.supportedLocales,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: content,
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => AccountVerificationController(
|
||||
accountProvider: context.read<AccountProvider>(),
|
||||
verificationProvider: context.read<EmailVerificationProvider>(),
|
||||
)..startVerification(token),
|
||||
child: AccountVerificationContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
frontend/pweb/lib/pages/verification/resend_dialog.dart
Normal file
41
frontend/pweb/lib/pages/verification/resend_dialog.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
Future<String?> requestVerificationEmail(
|
||||
BuildContext context,
|
||||
AppLocalizations locs,
|
||||
) async {
|
||||
final controller = TextEditingController();
|
||||
final email = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(locs.signupConfirmationResend),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
labelText: locs.username,
|
||||
hintText: locs.usernameHint,
|
||||
),
|
||||
onSubmitted: (_) =>
|
||||
Navigator.of(dialogContext).pop(controller.text.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(locs.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(dialogContext).pop(controller.text.trim()),
|
||||
child: Text(locs.signupConfirmationResend),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
return email?.trim();
|
||||
}
|
||||
@@ -46,7 +46,8 @@ class PayoutSidebar extends StatelessWidget {
|
||||
PayoutDestination.recipients,
|
||||
PayoutDestination.invitations,
|
||||
PayoutDestination.methods,
|
||||
PayoutDestination.reports,
|
||||
//PayoutDestination.reports,
|
||||
//TODO Add when ready
|
||||
];
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
@@ -57,7 +57,7 @@ class UserProfileCard extends StatelessWidget {
|
||||
child: Text(
|
||||
userName ?? loc.userNamePlaceholder,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
|
||||
Reference in New Issue
Block a user