verification before payment and email fixes

This commit is contained in:
Arseni
2026-02-18 18:15:38 +03:00
parent 4dc182bfa2
commit e901ac3eb6
35 changed files with 1023 additions and 192 deletions

View File

@@ -23,6 +23,7 @@ class TwoFactorCodePage extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<TwoFactorProvider>(
builder: (context, provider, child) {
final email = provider.pendingLogin?.target ?? '';
if (provider.verificationSuccess) {
WidgetsBinding.instance.addPostFrameCallback((_) {
onVerificationSuccess();
@@ -36,7 +37,7 @@ class TwoFactorCodePage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const TwoFactorPromptText(),
TwoFactorPromptText(email: email),
const SizedBox(height: 32),
TwoFactorCodeInput(
onCompleted: (code) => provider.submitCode(code),
@@ -45,7 +46,12 @@ class TwoFactorCodePage extends StatelessWidget {
if (provider.isSubmitting)
const Center(child: CircularProgressIndicator())
else
const ResendCodeButton(),
ResendCodeButton(
onPressed: provider.resendCode,
isCooldownActive: provider.isCooldownActive,
isResending: provider.isResending,
cooldownRemainingSeconds: provider.cooldownRemainingSeconds,
),
if (provider.hasError) ...[
const SizedBox(height: 12),
ErrorMessage(error: AppLocalizations.of(context)!.twoFactorError),

View File

@@ -1,20 +1,19 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class TwoFactorPromptText extends StatelessWidget {
const TwoFactorPromptText({super.key});
final String email;
const TwoFactorPromptText({
super.key,
required this.email,
});
@override
Widget build(BuildContext context) => Text(
AppLocalizations.of(context)!.twoFactorPrompt(
context.watch<TwoFactorProvider>().pendingLogin?.target ?? '',
),
AppLocalizations.of(context)!.twoFactorPrompt(email),
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
);

View File

@@ -1,8 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/utils/cooldown_format.dart';
import 'package:pweb/widgets/resend_link.dart';
@@ -10,23 +7,33 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class ResendCodeButton extends StatelessWidget {
const ResendCodeButton({super.key});
final VoidCallback onPressed;
final bool isCooldownActive;
final bool isResending;
final int cooldownRemainingSeconds;
const ResendCodeButton({
super.key,
required this.onPressed,
required this.isCooldownActive,
required this.isResending,
required this.cooldownRemainingSeconds,
});
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final provider = context.watch<TwoFactorProvider>();
final isDisabled = provider.isCooldownActive || provider.isResending;
final isDisabled = isCooldownActive || isResending;
final label = provider.isCooldownActive
? '${localizations.twoFactorResend} (${formatCooldownSeconds(provider.cooldownRemainingSeconds)})'
final label = isCooldownActive
? '${localizations.twoFactorResend} (${formatCooldownSeconds(cooldownRemainingSeconds)})'
: localizations.twoFactorResend;
return ResendLink(
label: label,
onPressed: provider.resendCode,
onPressed: onPressed,
isDisabled: isDisabled,
isLoading: provider.isResending,
isLoading: isResending,
);
}

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pweb/utils/payment/payout_verification_flow.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
@@ -8,7 +12,14 @@ Future<void> handleMultiplePayoutSend(
BuildContext context,
MultiplePayoutsController controller,
) async {
final outcome = await controller.sendAndStorePayments();
final verificationController = context.read<PayoutVerificationController>();
final verified = await runPayoutVerification(
context: context,
controller: verificationController,
);
if (!verified) return;
final outcome = await controller.sendAndGetOutcome();
if (!context.mounted) return;

View File

@@ -3,13 +3,17 @@ import 'package:flutter/material.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
import 'package:pweb/pages/payment_methods/payment_page/send_button.dart';
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
import 'package:pweb/widgets/cooldown_hint.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
import 'package:provider/provider.dart';
class SourceQuotePanel extends StatelessWidget {
@@ -25,7 +29,10 @@ class SourceQuotePanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
final verificationController =
context.watch<PayoutVerificationController>();
final isCooldownActive = verificationController.isCooldownActive;
final canSend = controller.canSend && !isCooldownActive;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
@@ -51,22 +58,24 @@ class SourceQuotePanel extends StatelessWidget {
MultipleQuoteStatusCard(controller: controller),
const SizedBox(height: 12),
Center(
child: ElevatedButton(
onPressed: controller.canSend
? () => handleMultiplePayoutSend(context, controller)
: null,
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SendButton(
onPressed: () => handleMultiplePayoutSend(context, controller),
state: controller.isSending
? ControlState.loading
: canSend
? ControlState.enabled
: ControlState.disabled,
),
textStyle: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
child: Text(l10n.send),
if (isCooldownActive) ...[
const SizedBox(height: 8),
CooldownHint(
seconds: verificationController.cooldownRemainingSeconds,
),
],
],
),
),
],

View File

@@ -15,6 +15,7 @@ import 'package:pweb/widgets/password/hint/short.dart';
import 'package:pweb/widgets/password/password.dart';
import 'package:pweb/widgets/username.dart';
import 'package:pweb/widgets/vspacer.dart';
import 'package:pweb/controllers/email.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/services/posthog.dart';
@@ -31,12 +32,11 @@ class LoginForm extends StatefulWidget {
}
class _LoginFormState extends State<LoginForm> {
final TextEditingController _usernameController = TextEditingController();
final EmailFieldController _emailController = EmailFieldController();
final TextEditingController _passwordController = TextEditingController();
final _formKey = GlobalKey<FormState>();
// ValueNotifiers for validation state
final ValueNotifier<bool> _isUsernameAcceptable = ValueNotifier<bool>(false);
final ValueNotifier<bool> _isPasswordAcceptable = ValueNotifier<bool>(false);
@override
@@ -44,8 +44,7 @@ class _LoginFormState extends State<LoginForm> {
super.initState();
final initialEmail = widget.initialEmail?.trim();
if (initialEmail != null && initialEmail.isNotEmpty) {
_usernameController.text = initialEmail;
_isUsernameAcceptable.value = true;
_emailController.setText(initialEmail);
}
}
@@ -54,7 +53,7 @@ class _LoginFormState extends State<LoginForm> {
try {
final outcome = await provider.login(
email: _usernameController.text,
email: _emailController.text,
password: _passwordController.text,
locale: context.read<LocaleProvider>().locale.languageCode,
);
@@ -74,9 +73,8 @@ class _LoginFormState extends State<LoginForm> {
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_isUsernameAcceptable.dispose();
_isPasswordAcceptable.dispose();
super.dispose();
}
@@ -93,8 +91,7 @@ class _LoginFormState extends State<LoginForm> {
const LoginHeader(),
const VSpacer(multiplier: 1.5),
UsernameField(
controller: _usernameController,
onValid: (isValid) => _isUsernameAcceptable.value = isValid,
controller: _emailController,
),
VSpacer(),
defaulRulesPasswordField(
@@ -105,7 +102,7 @@ class _LoginFormState extends State<LoginForm> {
),
VSpacer(multiplier: 2.0),
ValueListenableBuilder<bool>(
valueListenable: _isUsernameAcceptable,
valueListenable: _emailController.isValid,
builder: (context, isUsernameValid, child) => ValueListenableBuilder<bool>(
valueListenable: _isPasswordAcceptable,
builder: (context, isPasswordValid, child) => ButtonsRow(

View File

@@ -17,6 +17,9 @@ import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
import 'package:pweb/controllers/payment_page.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pweb/utils/payment/payout_verification_flow.dart';
import 'package:pweb/models/control_state.dart';
class PaymentPage extends StatefulWidget {
@@ -98,8 +101,15 @@ class _PaymentPageState extends State<PaymentPage> {
final flowProvider = context.read<PaymentFlowProvider>();
final paymentProvider = context.read<PaymentProvider>();
final controller = context.read<PaymentPageController>();
final verificationController = context.read<PayoutVerificationController>();
if (paymentProvider.isLoading) return;
final verified = await runPayoutVerification(
context: context,
controller: verificationController,
);
if (!verified || !mounted) return;
final isSuccess = await controller.sendPayment();
if (!mounted) return;
@@ -117,11 +127,16 @@ class _PaymentPageState extends State<PaymentPage> {
Widget build(BuildContext context) {
final methodsProvider = context.watch<PaymentMethodsProvider>();
final recipientProvider = context.watch<RecipientsProvider>();
final verificationController =
context.watch<PayoutVerificationController>();
final recipient = recipientProvider.currentObject;
final filteredRecipients = filterRecipients(
recipients: recipientProvider.recipients,
query: _query,
);
final sendState = verificationController.isCooldownActive
? ControlState.disabled
: ControlState.enabled;
return PaymentPageBody(
onBack: widget.onBack,
@@ -132,6 +147,9 @@ class _PaymentPageState extends State<PaymentPage> {
searchQuery: _query,
filteredRecipients: filteredRecipients,
methodsProvider: methodsProvider,
sendState: sendState,
cooldownRemainingSeconds:
verificationController.cooldownRemainingSeconds,
onWalletSelected: context.read<WalletsController>().selectWallet,
searchController: _searchController,
searchFocusNode: _searchFocusNode,

View File

@@ -8,6 +8,7 @@ import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/pages/payment_methods/widgets/state_view.dart';
import 'package:pweb/pages/payment_methods/payment_page/page.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -20,6 +21,8 @@ class PaymentPageBody extends StatelessWidget {
final String searchQuery;
final List<Recipient> filteredRecipients;
final PaymentMethodsProvider methodsProvider;
final ControlState sendState;
final int cooldownRemainingSeconds;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final TextEditingController searchController;
@@ -38,6 +41,8 @@ class PaymentPageBody extends StatelessWidget {
required this.searchQuery,
required this.filteredRecipients,
required this.methodsProvider,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.onWalletSelected,
required this.fallbackDestination,
required this.searchController,
@@ -71,6 +76,8 @@ class PaymentPageBody extends StatelessWidget {
filteredRecipients: filteredRecipients,
onWalletSelected: onWalletSelected,
fallbackDestination: fallbackDestination,
sendState: sendState,
cooldownRemainingSeconds: cooldownRemainingSeconds,
searchController: searchController,
searchFocusNode: searchFocusNode,
onSearchChanged: onSearchChanged,

View File

@@ -13,7 +13,9 @@ import 'package:pweb/pages/payment_methods/widgets/payment_info_section.dart';
import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
import 'package:pweb/utils/dimensions.dart';
import 'package:pweb/widgets/cooldown_hint.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/models/control_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -27,6 +29,8 @@ class PaymentPageContent extends StatelessWidget {
final List<Recipient> filteredRecipients;
final ValueChanged<Wallet> onWalletSelected;
final PayoutDestination fallbackDestination;
final ControlState sendState;
final int cooldownRemainingSeconds;
final TextEditingController searchController;
final FocusNode searchFocusNode;
final ValueChanged<String> onSearchChanged;
@@ -44,6 +48,8 @@ class PaymentPageContent extends StatelessWidget {
required this.filteredRecipients,
required this.onWalletSelected,
required this.fallbackDestination,
required this.sendState,
required this.cooldownRemainingSeconds,
required this.searchController,
required this.searchFocusNode,
required this.onSearchChanged,
@@ -104,7 +110,20 @@ class PaymentPageContent extends StatelessWidget {
SizedBox(height: dimensions.paddingLarge),
const PaymentFormWidget(),
SizedBox(height: dimensions.paddingXXXLarge),
SendButton(onPressed: onSend),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SendButton(
onPressed: onSend,
state: sendState,
),
if (sendState == ControlState.disabled &&
cooldownRemainingSeconds > 0) ...[
SizedBox(height: dimensions.paddingSmall),
CooldownHint(seconds: cooldownRemainingSeconds),
],
],
),
SizedBox(height: dimensions.paddingLarge),
],
),

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/controllers/payout_verification.dart';
import 'package:pweb/pages/2fa/error_message.dart';
import 'package:pweb/pages/2fa/input.dart';
import 'package:pweb/pages/2fa/prompt.dart';
import 'package:pweb/pages/2fa/resend.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PayoutVerificationPage extends StatelessWidget {
const PayoutVerificationPage({super.key});
@override
Widget build(BuildContext context) {
return Consumer<PayoutVerificationController>(
builder: (context, controller, child) {
if (controller.verificationSuccess) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(true);
});
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.twoFactorTitle),
),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TwoFactorPromptText(email: controller.target),
const SizedBox(height: 32),
TwoFactorCodeInput(
onCompleted: controller.submitCode,
),
const SizedBox(height: 24),
if (controller.isSubmitting)
const Center(child: CircularProgressIndicator())
else
ResendCodeButton(
onPressed: controller.resendCode,
isCooldownActive: controller.isCooldownActive,
isResending: controller.isResending,
cooldownRemainingSeconds: controller.cooldownRemainingSeconds,
),
if (controller.hasError) ...[
const SizedBox(height: 12),
ErrorMessage(
error: AppLocalizations.of(context)!.twoFactorError,
),
],
],
),
),
);
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pweb/models/load_more_state.dart';
import 'package:pweb/pages/report/cards/items.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -10,11 +11,15 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class OperationsCardsList extends StatelessWidget {
final List<OperationItem> operations;
final ValueChanged<OperationItem>? onTap;
final LoadMoreState loadMoreState;
final VoidCallback? onLoadMore;
const OperationsCardsList({
super.key,
required this.operations,
this.onTap,
this.loadMoreState = LoadMoreState.hidden,
this.onLoadMore,
});
@override
@@ -26,18 +31,42 @@ class OperationsCardsList extends StatelessWidget {
onTap: onTap,
);
if (operations.isEmpty) {
return Expanded(
child: Center(
child: Text(
loc.reportPaymentsEmpty,
style: Theme.of(context).textTheme.bodyMedium,
),
),
);
}
final extraItems = loadMoreState == LoadMoreState.hidden ? 0 : 1;
return Expanded(
child: operations.isEmpty
? Center(
child: Text(
loc.reportPaymentsEmpty,
style: Theme.of(context).textTheme.bodyMedium,
child: ListView.builder(
itemCount: items.length + extraItems,
itemBuilder: (context, index) {
if (index < items.length) {
return items[index];
}
if (loadMoreState == LoadMoreState.loading) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: TextButton(
onPressed: onLoadMore,
child: Text(loc.loadMore),
),
)
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => items[index],
),
);
},
),
);
}

View File

@@ -4,16 +4,14 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/controllers/payment_details.dart';
import 'package:pweb/pages/report/details/content.dart';
import 'package:pweb/pages/report/details/states/error.dart';
import 'package:pweb/pages/report/details/states/not_found.dart';
import 'package:pweb/utils/report/download_act.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -26,39 +24,48 @@ class PaymentDetailsPage extends StatelessWidget {
required this.paymentId,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider<PaymentsProvider, PaymentDetailsController>(
create: (_) => PaymentDetailsController(paymentId: paymentId),
update: (_, payments, controller) => controller!
..update(payments, paymentId),
child: const _PaymentDetailsView(),
);
}
}
class _PaymentDetailsView extends StatelessWidget {
const _PaymentDetailsView();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Consumer<PaymentsProvider>(
builder: (context, provider, child) {
child: Consumer<PaymentDetailsController>(
builder: (context, controller, child) {
final loc = AppLocalizations.of(context)!;
if (provider.isLoading) {
if (controller.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
if (controller.error != null) {
return PaymentDetailsError(
message: provider.error?.toString() ?? loc.noErrorInformation,
onRetry: () => provider.refresh(),
message: controller.error?.toString() ?? loc.noErrorInformation,
onRetry: () => controller.refresh(),
);
}
final payment = _findPayment(provider.payments, paymentId);
final payment = controller.payment;
if (payment == null) {
return PaymentDetailsNotFound(onBack: () => _handleBack(context));
}
final status = statusFromPayment(payment);
final paymentRef = payment.paymentRef ?? '';
final canDownload = status == OperationStatus.success &&
paymentRef.trim().isNotEmpty;
return PaymentDetailsContent(
payment: payment,
onBack: () => _handleBack(context),
onDownloadAct: canDownload
? () => downloadPaymentAct(context, paymentRef)
onDownloadAct: controller.canDownload
? () => downloadPaymentAct(context, payment.paymentRef ?? '')
: null,
);
},
@@ -66,16 +73,6 @@ class PaymentDetailsPage extends StatelessWidget {
);
}
Payment? _findPayment(List<Payment> payments, String paymentId) {
final trimmed = paymentId.trim();
if (trimmed.isEmpty) return null;
for (final payment in payments) {
if (payment.paymentRef == trimmed) return payment;
if (payment.idempotencyKey == trimmed) return payment;
}
return null;
}
void _handleBack(BuildContext context) {
final router = GoRouter.of(context);
if (router.canPop()) {

View File

@@ -12,6 +12,7 @@ import 'package:pweb/controllers/report_operations.dart';
import 'package:pweb/pages/report/table/filters.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/app/router/payout_routes.dart';
import 'package:pweb/models/load_more_state.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -124,6 +125,11 @@ class _OperationHistoryView extends StatelessWidget {
OperationsCardsList(
operations: filteredOperations,
onTap: (operation) => _openPaymentDetails(context, operation),
loadMoreState: controller.loadMoreState,
onLoadMore: controller.loadMoreState ==
LoadMoreState.available
? () => controller.loadMore()
: null,
),
],
),

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:pweb/controllers/email.dart';
class SignUpFormControllers {
final TextEditingController companyName = TextEditingController();
final TextEditingController description = TextEditingController();
final TextEditingController firstName = TextEditingController();
final TextEditingController lastName = TextEditingController();
final TextEditingController email = TextEditingController();
final EmailFieldController email = EmailFieldController();
final TextEditingController password = TextEditingController();
final TextEditingController passwordConfirm = TextEditingController();

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:email_validator/email_validator.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
//TODO: check with /widgets/username.dart
class EmailField extends StatelessWidget {
final TextEditingController controller;
const EmailField({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.username,
hintText: AppLocalizations.of(context)!.usernameHint,
),
validator: (value) {
if (value == null || !EmailValidator.validate(value)) {
return AppLocalizations.of(context)!.usernameErrorInvalid;
}
return null;
},
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pweb/pages/signup/form/controllers.dart';
import 'package:pweb/pages/signup/form/description.dart';
import 'package:pweb/pages/signup/form/email.dart';
import 'package:pweb/widgets/username.dart';
import 'package:pweb/pages/signup/form/password_ui_controller.dart';
import 'package:pweb/pages/signup/header.dart';
import 'package:pweb/widgets/password/verify.dart';
@@ -45,7 +45,7 @@ class SignUpFormFields extends StatelessWidget {
error: AppLocalizations.of(context)!.enterLastName,
),
const VSpacer(),
EmailField(controller: controllers.email),
UsernameField(controller: controllers.email),
const VSpacer(),
SignUpPasswordUiController(controller: controllers.password),
const VSpacer(multiplier: 2.0),