usability fixes

This commit is contained in:
2026-06-24 18:01:34 +02:00
parent 9e55ec1097
commit a6b4a04bd5
26 changed files with 1099 additions and 216 deletions

View File

@@ -35,6 +35,7 @@ abstract class PaymentProcessor with _$PaymentProcessor {
required String accountFieldName,
required String accountFieldLabel,
required String label,
required String currency,
required String authType,
}) = _PaymentProcessor;
@@ -176,15 +177,22 @@ class PayController extends ChangeNotifier {
}
}
Future<ApiResponse<List<PaymentProcessor>>> getPaymentProcessors() async {
Future<ApiResponse<List<PaymentProcessor>>> getPaymentProcessors(
String currency,
) async {
try {
model.isLoading = true;
model.paymentProcessors = getFakePaymentProcessors();
notifyListeners();
List<dynamic> response = await http.get('/public/payment-processors');
model.paymentProcessors = response
.map((e) => PaymentProcessor.fromJson(e))
final response = await http.get(
'/public/payment-processors?currency=$currency',
);
final List<dynamic> content = response is List
? response
: (response['content'] ?? []);
model.paymentProcessors = content
.map((e) => PaymentProcessor.fromJson(e as Map<String, dynamic>))
.toList();
model.isLoading = false;
@@ -238,6 +246,7 @@ class PayController extends ChangeNotifier {
accountFieldLabel: "EcoCash Phone Number",
label: "ECOCASH",
authType: "REMOTE",
currency: "USD",
),
];
}

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PaymentProcessor implements DiagnosticableTreeMixin {
String get name; String get description; String get image; String get accountFieldName; String get accountFieldLabel; String get label; String get authType;
String get name; String get description; String get image; String get accountFieldName; String get accountFieldLabel; String get label; String get currency; String get authType;
/// Create a copy of PaymentProcessor
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -29,21 +29,21 @@ $PaymentProcessorCopyWith<PaymentProcessor> get copyWith => _$PaymentProcessorCo
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'PaymentProcessor'))
..add(DiagnosticsProperty('name', name))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('image', image))..add(DiagnosticsProperty('accountFieldName', accountFieldName))..add(DiagnosticsProperty('accountFieldLabel', accountFieldLabel))..add(DiagnosticsProperty('label', label))..add(DiagnosticsProperty('authType', authType));
..add(DiagnosticsProperty('name', name))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('image', image))..add(DiagnosticsProperty('accountFieldName', accountFieldName))..add(DiagnosticsProperty('accountFieldLabel', accountFieldLabel))..add(DiagnosticsProperty('label', label))..add(DiagnosticsProperty('currency', currency))..add(DiagnosticsProperty('authType', authType));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PaymentProcessor&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.image, image) || other.image == image)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName)&&(identical(other.accountFieldLabel, accountFieldLabel) || other.accountFieldLabel == accountFieldLabel)&&(identical(other.label, label) || other.label == label)&&(identical(other.authType, authType) || other.authType == authType));
return identical(this, other) || (other.runtimeType == runtimeType&&other is PaymentProcessor&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.image, image) || other.image == image)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName)&&(identical(other.accountFieldLabel, accountFieldLabel) || other.accountFieldLabel == accountFieldLabel)&&(identical(other.label, label) || other.label == label)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.authType, authType) || other.authType == authType));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,description,image,accountFieldName,accountFieldLabel,label,authType);
int get hashCode => Object.hash(runtimeType,name,description,image,accountFieldName,accountFieldLabel,label,currency,authType);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'PaymentProcessor(name: $name, description: $description, image: $image, accountFieldName: $accountFieldName, accountFieldLabel: $accountFieldLabel, label: $label, authType: $authType)';
return 'PaymentProcessor(name: $name, description: $description, image: $image, accountFieldName: $accountFieldName, accountFieldLabel: $accountFieldLabel, label: $label, currency: $currency, authType: $authType)';
}
@@ -54,7 +54,7 @@ abstract mixin class $PaymentProcessorCopyWith<$Res> {
factory $PaymentProcessorCopyWith(PaymentProcessor value, $Res Function(PaymentProcessor) _then) = _$PaymentProcessorCopyWithImpl;
@useResult
$Res call({
String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String authType
String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String currency, String authType
});
@@ -71,7 +71,7 @@ class _$PaymentProcessorCopyWithImpl<$Res>
/// Create a copy of PaymentProcessor
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? description = null,Object? image = null,Object? accountFieldName = null,Object? accountFieldLabel = null,Object? label = null,Object? authType = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? description = null,Object? image = null,Object? accountFieldName = null,Object? accountFieldLabel = null,Object? label = null,Object? currency = null,Object? authType = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
@@ -79,6 +79,7 @@ as String,image: null == image ? _self.image : image // ignore: cast_nullable_to
as String,accountFieldName: null == accountFieldName ? _self.accountFieldName : accountFieldName // ignore: cast_nullable_to_non_nullable
as String,accountFieldLabel: null == accountFieldLabel ? _self.accountFieldLabel : accountFieldLabel // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
as String,authType: null == authType ? _self.authType : authType // ignore: cast_nullable_to_non_nullable
as String,
));
@@ -165,10 +166,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String authType)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String currency, String authType)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PaymentProcessor() when $default != null:
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.authType);case _:
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.currency,_that.authType);case _:
return orElse();
}
@@ -186,10 +187,10 @@ return $default(_that.name,_that.description,_that.image,_that.accountFieldName,
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String authType) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String currency, String authType) $default,) {final _that = this;
switch (_that) {
case _PaymentProcessor():
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.authType);case _:
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.currency,_that.authType);case _:
throw StateError('Unexpected subclass');
}
@@ -206,10 +207,10 @@ return $default(_that.name,_that.description,_that.image,_that.accountFieldName,
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String authType)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String currency, String authType)? $default,) {final _that = this;
switch (_that) {
case _PaymentProcessor() when $default != null:
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.authType);case _:
return $default(_that.name,_that.description,_that.image,_that.accountFieldName,_that.accountFieldLabel,_that.label,_that.currency,_that.authType);case _:
return null;
}
@@ -221,7 +222,7 @@ return $default(_that.name,_that.description,_that.image,_that.accountFieldName,
@JsonSerializable()
class _PaymentProcessor with DiagnosticableTreeMixin implements PaymentProcessor {
const _PaymentProcessor({required this.name, required this.description, required this.image, required this.accountFieldName, required this.accountFieldLabel, required this.label, required this.authType});
const _PaymentProcessor({required this.name, required this.description, required this.image, required this.accountFieldName, required this.accountFieldLabel, required this.label, required this.currency, required this.authType});
factory _PaymentProcessor.fromJson(Map<String, dynamic> json) => _$PaymentProcessorFromJson(json);
@override final String name;
@@ -230,6 +231,7 @@ class _PaymentProcessor with DiagnosticableTreeMixin implements PaymentProcessor
@override final String accountFieldName;
@override final String accountFieldLabel;
@override final String label;
@override final String currency;
@override final String authType;
/// Create a copy of PaymentProcessor
@@ -246,21 +248,21 @@ Map<String, dynamic> toJson() {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties
..add(DiagnosticsProperty('type', 'PaymentProcessor'))
..add(DiagnosticsProperty('name', name))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('image', image))..add(DiagnosticsProperty('accountFieldName', accountFieldName))..add(DiagnosticsProperty('accountFieldLabel', accountFieldLabel))..add(DiagnosticsProperty('label', label))..add(DiagnosticsProperty('authType', authType));
..add(DiagnosticsProperty('name', name))..add(DiagnosticsProperty('description', description))..add(DiagnosticsProperty('image', image))..add(DiagnosticsProperty('accountFieldName', accountFieldName))..add(DiagnosticsProperty('accountFieldLabel', accountFieldLabel))..add(DiagnosticsProperty('label', label))..add(DiagnosticsProperty('currency', currency))..add(DiagnosticsProperty('authType', authType));
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PaymentProcessor&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.image, image) || other.image == image)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName)&&(identical(other.accountFieldLabel, accountFieldLabel) || other.accountFieldLabel == accountFieldLabel)&&(identical(other.label, label) || other.label == label)&&(identical(other.authType, authType) || other.authType == authType));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PaymentProcessor&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.image, image) || other.image == image)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName)&&(identical(other.accountFieldLabel, accountFieldLabel) || other.accountFieldLabel == accountFieldLabel)&&(identical(other.label, label) || other.label == label)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.authType, authType) || other.authType == authType));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,description,image,accountFieldName,accountFieldLabel,label,authType);
int get hashCode => Object.hash(runtimeType,name,description,image,accountFieldName,accountFieldLabel,label,currency,authType);
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return 'PaymentProcessor(name: $name, description: $description, image: $image, accountFieldName: $accountFieldName, accountFieldLabel: $accountFieldLabel, label: $label, authType: $authType)';
return 'PaymentProcessor(name: $name, description: $description, image: $image, accountFieldName: $accountFieldName, accountFieldLabel: $accountFieldLabel, label: $label, currency: $currency, authType: $authType)';
}
@@ -271,7 +273,7 @@ abstract mixin class _$PaymentProcessorCopyWith<$Res> implements $PaymentProcess
factory _$PaymentProcessorCopyWith(_PaymentProcessor value, $Res Function(_PaymentProcessor) _then) = __$PaymentProcessorCopyWithImpl;
@override @useResult
$Res call({
String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String authType
String name, String description, String image, String accountFieldName, String accountFieldLabel, String label, String currency, String authType
});
@@ -288,7 +290,7 @@ class __$PaymentProcessorCopyWithImpl<$Res>
/// Create a copy of PaymentProcessor
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? description = null,Object? image = null,Object? accountFieldName = null,Object? accountFieldLabel = null,Object? label = null,Object? authType = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? description = null,Object? image = null,Object? accountFieldName = null,Object? accountFieldLabel = null,Object? label = null,Object? currency = null,Object? authType = null,}) {
return _then(_PaymentProcessor(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
@@ -296,6 +298,7 @@ as String,image: null == image ? _self.image : image // ignore: cast_nullable_to
as String,accountFieldName: null == accountFieldName ? _self.accountFieldName : accountFieldName // ignore: cast_nullable_to_non_nullable
as String,accountFieldLabel: null == accountFieldLabel ? _self.accountFieldLabel : accountFieldLabel // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,currency: null == currency ? _self.currency : currency // ignore: cast_nullable_to_non_nullable
as String,authType: null == authType ? _self.authType : authType // ignore: cast_nullable_to_non_nullable
as String,
));

View File

@@ -14,6 +14,7 @@ _PaymentProcessor _$PaymentProcessorFromJson(Map<String, dynamic> json) =>
accountFieldName: json['accountFieldName'] as String,
accountFieldLabel: json['accountFieldLabel'] as String,
label: json['label'] as String,
currency: json['currency'] as String,
authType: json['authType'] as String,
);
@@ -25,6 +26,7 @@ Map<String, dynamic> _$PaymentProcessorToJson(_PaymentProcessor instance) =>
'accountFieldName': instance.accountFieldName,
'accountFieldLabel': instance.accountFieldLabel,
'label': instance.label,
'currency': instance.currency,
'authType': instance.authType,
};

View File

@@ -4,6 +4,7 @@ import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/accounts/account_provider.dart';
import 'package:qpay/screens/pay/pay_controller.dart';
import 'package:qpay/screens/transaction_controller.dart';
import 'package:qpay/widgets/app_snack_bar.dart';
@@ -64,7 +65,6 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
super.initState();
controller = PayController(context);
controller.getPaymentProcessors();
controller.getProducts(controller.model.selectedProvider?.id ?? "");
transactionController = Provider.of<TransactionController>(
@@ -72,6 +72,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
listen: false,
);
controller.getPaymentProcessors(transactionController.model.currency);
_setupData();
_controller = AnimationController(
@@ -234,38 +235,71 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTransactionDetailsCard(
children: [
_buildDetailRow(
icon: Icons.business_outlined,
label: "Provider",
value:
controller
Builder(
builder: (context) {
// Look up City Wallet balance matching the
// transaction currency from the shared
// AccountProvider.
final accountProvider = context
.watch<AccountProvider>();
final currency =
transactionController.model.currency;
final cityWallet = accountProvider
.balanceForCurrency(currency);
final hasCityWallet =
cityWallet != null &&
cityWallet.name == 'City Wallet';
return _buildTransactionDetailsCard(
children: [
_buildDetailRow(
icon: Icons.business_outlined,
label: "Provider",
value:
controller
.model
.selectedProvider
?.name ??
"",
),
_buildDetailRow(
icon: Icons.account_balance_outlined,
label:
controller
.model
.selectedProvider
?.accountFieldName ??
"",
value: transactionController
.model
.selectedProvider
?.name ??
"",
),
_buildDetailRow(
icon: Icons.account_balance_outlined,
label:
controller
.formData
.creditAccount,
),
_buildDetailRow(
icon: Icons.monetization_on,
label: "Currency",
value: transactionController
.model
.selectedProvider
?.accountFieldName ??
"",
value: transactionController
.model
.formData
.creditAccount,
),
_buildDetailRow(
icon: Icons.monetization_on,
label: "Currency",
value:
transactionController.model.currency,
),
],
.currency,
),
if (hasCityWallet)
_buildDetailRow(
icon: Icons
.account_balance_wallet_outlined,
label: "City Wallet Balance",
value:
'\$${cityWallet!.balance.toStringAsFixed(2)}',
valueStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Theme.of(
context,
).colorScheme.primary,
),
),
],
);
},
),
SizedBox(height: 20),
InkWell(
@@ -748,6 +782,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
required IconData icon,
required String label,
required String value,
TextStyle? valueStyle,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
@@ -762,11 +797,13 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
const Spacer(),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
style:
valueStyle ??
TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),