verification before payment and email fixes
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
|
||||
64
frontend/pweb/lib/pages/payout_verification/page.dart
Normal file
64
frontend/pweb/lib/pages/payout_verification/page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user