prototype complete

This commit is contained in:
2025-07-14 01:04:53 +02:00
parent 03d0f77ac2
commit 44d21b3f14
69 changed files with 5033 additions and 765 deletions

View File

@@ -8,7 +8,7 @@ part 'home_controller.freezed.dart';
part 'home_controller.g.dart';
class HomeScreenModel {
List<dynamic> transactions = [];
List<Map<String, dynamic>> transactions = [];
List<Category> categories = [];
List<BillProvider> providers = [];
List<BillProvider> filterableProviders = [];
@@ -51,6 +51,7 @@ abstract class BillProvider with _$BillProvider {
required String image,
required String label,
required String category,
required String? accountFieldName,
}) = _BillProvider;
factory BillProvider.fromJson(Map<String, dynamic> json) =>
@@ -60,11 +61,26 @@ abstract class BillProvider with _$BillProvider {
class HomeController extends ChangeNotifier {
final HomeScreenModel model = HomeScreenModel();
final Http http = Http();
BuildContext context;
HomeController(this.context);
void _showErrorSnackBar(String? message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message.toString()),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.deepOrange,
),
);
});
}
Future<void> getProviders() async {
model.providers = getFakeProviders();
try {
model.isLoading = true;
notifyListeners();
List<dynamic> response = await http.get('/providers');
model.providers = response.map((e) => BillProvider.fromJson(e)).toList();
@@ -75,6 +91,9 @@ class HomeController extends ChangeNotifier {
}
} catch (e) {
logger.e(e);
_showErrorSnackBar(
"Problem fetching providers, are you connected to the internet?",
);
model.errorMessage = e.toString();
model.providers = [];
model.filterableProviders = [];
@@ -92,12 +111,39 @@ class HomeController extends ChangeNotifier {
notifyListeners();
}
Future<void> getTransactions(String userId) async {
model.isLoading = true;
notifyListeners();
try {
List<dynamic> response = await http.get(
'/transaction?userId=$userId&type=REQUEST',
);
model.transactions =
response.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
logger.e(e);
_showErrorSnackBar(
"Problem fetching transactions, are you connected to the internet?",
);
model.errorMessage = e.toString();
model.transactions = [];
} finally {
model.isLoading = false;
notifyListeners();
}
}
Future<void> getCategories() async {
// activate skeletonizer
model.categories = getFakeCategoryData();
model.transactions = getFakeTransactions();
// model.filterableProviders = getFakeProviders();
model.isLoading = true;
notifyListeners();
// Simulate fetching categories
try {
model.isLoading = true;
List<dynamic> response = await http.get('/categories');
model.categories = response.map((e) => Category.fromJson(e)).toList();
@@ -106,6 +152,9 @@ class HomeController extends ChangeNotifier {
}
} on Exception catch (e) {
logger.e(e);
_showErrorSnackBar(
"Problem fetching categories, are you connected to the internet?",
);
model.errorMessage = e.toString();
model.categories = [];
} finally {
@@ -131,6 +180,37 @@ class HomeController extends ChangeNotifier {
notifyListeners();
}
List<Map<String, dynamic>> getFakeTransactions() {
return [
{
"id": "77b2c479-b803-49fc-b5c3-acbf52cba633",
"trace": "22a3044b-8f53-4e43-b4f5-c77dcec48d2d",
"amount": 25.0,
"charge": 0.0,
"gatewayCharge": 0.25,
"tax": 0.5,
"totalAmount": 25.75,
"reference": "c838136c-c015-4a0b-9c7c-52f127c8c109",
"paymentProcessorLabel": "MPGS",
"paymentProcessorImage": "visa-mastercard.png",
"debitPhone": "",
"debitAccount": "",
"debitCurrency": "USD",
"debitRef": "peak",
"creditPhone": "",
"creditAccount": "07088597534",
"billClientId": "powertel_zesa",
"billName": "Zesa",
"type": "REQUEST",
"authType": "WEB",
"errorMessage": "",
"responseCode": "00",
"status": "SUCCESS",
"sdkActionId": "0aa0d90d-6132-48a7-a5d8-e796f757c73f",
},
];
}
List<BillProvider> getFakeProviders() {
return [
BillProvider(
@@ -148,6 +228,7 @@ class HomeController extends ChangeNotifier {
image: 'econet.png',
label: '',
category: '',
accountFieldName: '',
),
];
}
@@ -156,17 +237,11 @@ class HomeController extends ChangeNotifier {
List<Category> getFakeCategoryData() {
return [
Category(
name: 'Electricity',
name: 'Airtime',
description: 'Pay your electricity bills easily',
image: 'assets/images/electricity.png',
label: 'Electricity',
),
Category(
name: 'Internet',
description: 'Pay your internet bills easily',
image: 'assets/images/internet.png',
label: 'Internet',
),
];
}
}

View File

@@ -158,7 +158,7 @@ as String,
/// @nodoc
mixin _$BillProvider {
String get clientId; String get name; String get description; bool get requiresAccount; bool get requiresAmount; bool get requiresAmountFromMerchant; bool get requiresPhone; bool get requiresReversal; String? get additionalDataString; String get processorType; String get uid; String get image; String get label; String get category;
String get clientId; String get name; String get description; bool get requiresAccount; bool get requiresAmount; bool get requiresAmountFromMerchant; bool get requiresPhone; bool get requiresReversal; String? get additionalDataString; String get processorType; String get uid; String get image; String get label; String get category; String? get accountFieldName;
/// Create a copy of BillProvider
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -171,16 +171,16 @@ $BillProviderCopyWith<BillProvider> get copyWith => _$BillProviderCopyWithImpl<B
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BillProvider&&(identical(other.clientId, clientId) || other.clientId == clientId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.requiresAccount, requiresAccount) || other.requiresAccount == requiresAccount)&&(identical(other.requiresAmount, requiresAmount) || other.requiresAmount == requiresAmount)&&(identical(other.requiresAmountFromMerchant, requiresAmountFromMerchant) || other.requiresAmountFromMerchant == requiresAmountFromMerchant)&&(identical(other.requiresPhone, requiresPhone) || other.requiresPhone == requiresPhone)&&(identical(other.requiresReversal, requiresReversal) || other.requiresReversal == requiresReversal)&&(identical(other.additionalDataString, additionalDataString) || other.additionalDataString == additionalDataString)&&(identical(other.processorType, processorType) || other.processorType == processorType)&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.image, image) || other.image == image)&&(identical(other.label, label) || other.label == label)&&(identical(other.category, category) || other.category == category));
return identical(this, other) || (other.runtimeType == runtimeType&&other is BillProvider&&(identical(other.clientId, clientId) || other.clientId == clientId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.requiresAccount, requiresAccount) || other.requiresAccount == requiresAccount)&&(identical(other.requiresAmount, requiresAmount) || other.requiresAmount == requiresAmount)&&(identical(other.requiresAmountFromMerchant, requiresAmountFromMerchant) || other.requiresAmountFromMerchant == requiresAmountFromMerchant)&&(identical(other.requiresPhone, requiresPhone) || other.requiresPhone == requiresPhone)&&(identical(other.requiresReversal, requiresReversal) || other.requiresReversal == requiresReversal)&&(identical(other.additionalDataString, additionalDataString) || other.additionalDataString == additionalDataString)&&(identical(other.processorType, processorType) || other.processorType == processorType)&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.image, image) || other.image == image)&&(identical(other.label, label) || other.label == label)&&(identical(other.category, category) || other.category == category)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientId,name,description,requiresAccount,requiresAmount,requiresAmountFromMerchant,requiresPhone,requiresReversal,additionalDataString,processorType,uid,image,label,category);
int get hashCode => Object.hash(runtimeType,clientId,name,description,requiresAccount,requiresAmount,requiresAmountFromMerchant,requiresPhone,requiresReversal,additionalDataString,processorType,uid,image,label,category,accountFieldName);
@override
String toString() {
return 'BillProvider(clientId: $clientId, name: $name, description: $description, requiresAccount: $requiresAccount, requiresAmount: $requiresAmount, requiresAmountFromMerchant: $requiresAmountFromMerchant, requiresPhone: $requiresPhone, requiresReversal: $requiresReversal, additionalDataString: $additionalDataString, processorType: $processorType, uid: $uid, image: $image, label: $label, category: $category)';
return 'BillProvider(clientId: $clientId, name: $name, description: $description, requiresAccount: $requiresAccount, requiresAmount: $requiresAmount, requiresAmountFromMerchant: $requiresAmountFromMerchant, requiresPhone: $requiresPhone, requiresReversal: $requiresReversal, additionalDataString: $additionalDataString, processorType: $processorType, uid: $uid, image: $image, label: $label, category: $category, accountFieldName: $accountFieldName)';
}
@@ -191,7 +191,7 @@ abstract mixin class $BillProviderCopyWith<$Res> {
factory $BillProviderCopyWith(BillProvider value, $Res Function(BillProvider) _then) = _$BillProviderCopyWithImpl;
@useResult
$Res call({
String clientId, String name, String description, bool requiresAccount, bool requiresAmount, bool requiresAmountFromMerchant, bool requiresPhone, bool requiresReversal, String? additionalDataString, String processorType, String uid, String image, String label, String category
String clientId, String name, String description, bool requiresAccount, bool requiresAmount, bool requiresAmountFromMerchant, bool requiresPhone, bool requiresReversal, String? additionalDataString, String processorType, String uid, String image, String label, String category, String? accountFieldName
});
@@ -208,7 +208,7 @@ class _$BillProviderCopyWithImpl<$Res>
/// Create a copy of BillProvider
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? clientId = null,Object? name = null,Object? description = null,Object? requiresAccount = null,Object? requiresAmount = null,Object? requiresAmountFromMerchant = null,Object? requiresPhone = null,Object? requiresReversal = null,Object? additionalDataString = freezed,Object? processorType = null,Object? uid = null,Object? image = null,Object? label = null,Object? category = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? clientId = null,Object? name = null,Object? description = null,Object? requiresAccount = null,Object? requiresAmount = null,Object? requiresAmountFromMerchant = null,Object? requiresPhone = null,Object? requiresReversal = null,Object? additionalDataString = freezed,Object? processorType = null,Object? uid = null,Object? image = null,Object? label = null,Object? category = null,Object? accountFieldName = freezed,}) {
return _then(_self.copyWith(
clientId: null == clientId ? _self.clientId : clientId // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -224,7 +224,8 @@ as String,uid: null == uid ? _self.uid : uid // ignore: cast_nullable_to_non_nul
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,category: null == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as String,
as String,accountFieldName: freezed == accountFieldName ? _self.accountFieldName : accountFieldName // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -235,7 +236,7 @@ as String,
@JsonSerializable()
class _BillProvider implements BillProvider {
const _BillProvider({required this.clientId, required this.name, required this.description, required this.requiresAccount, required this.requiresAmount, required this.requiresAmountFromMerchant, required this.requiresPhone, required this.requiresReversal, required this.additionalDataString, required this.processorType, required this.uid, required this.image, required this.label, required this.category});
const _BillProvider({required this.clientId, required this.name, required this.description, required this.requiresAccount, required this.requiresAmount, required this.requiresAmountFromMerchant, required this.requiresPhone, required this.requiresReversal, required this.additionalDataString, required this.processorType, required this.uid, required this.image, required this.label, required this.category, required this.accountFieldName});
factory _BillProvider.fromJson(Map<String, dynamic> json) => _$BillProviderFromJson(json);
@override final String clientId;
@@ -252,6 +253,7 @@ class _BillProvider implements BillProvider {
@override final String image;
@override final String label;
@override final String category;
@override final String? accountFieldName;
/// Create a copy of BillProvider
/// with the given fields replaced by the non-null parameter values.
@@ -266,16 +268,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BillProvider&&(identical(other.clientId, clientId) || other.clientId == clientId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.requiresAccount, requiresAccount) || other.requiresAccount == requiresAccount)&&(identical(other.requiresAmount, requiresAmount) || other.requiresAmount == requiresAmount)&&(identical(other.requiresAmountFromMerchant, requiresAmountFromMerchant) || other.requiresAmountFromMerchant == requiresAmountFromMerchant)&&(identical(other.requiresPhone, requiresPhone) || other.requiresPhone == requiresPhone)&&(identical(other.requiresReversal, requiresReversal) || other.requiresReversal == requiresReversal)&&(identical(other.additionalDataString, additionalDataString) || other.additionalDataString == additionalDataString)&&(identical(other.processorType, processorType) || other.processorType == processorType)&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.image, image) || other.image == image)&&(identical(other.label, label) || other.label == label)&&(identical(other.category, category) || other.category == category));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BillProvider&&(identical(other.clientId, clientId) || other.clientId == clientId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.requiresAccount, requiresAccount) || other.requiresAccount == requiresAccount)&&(identical(other.requiresAmount, requiresAmount) || other.requiresAmount == requiresAmount)&&(identical(other.requiresAmountFromMerchant, requiresAmountFromMerchant) || other.requiresAmountFromMerchant == requiresAmountFromMerchant)&&(identical(other.requiresPhone, requiresPhone) || other.requiresPhone == requiresPhone)&&(identical(other.requiresReversal, requiresReversal) || other.requiresReversal == requiresReversal)&&(identical(other.additionalDataString, additionalDataString) || other.additionalDataString == additionalDataString)&&(identical(other.processorType, processorType) || other.processorType == processorType)&&(identical(other.uid, uid) || other.uid == uid)&&(identical(other.image, image) || other.image == image)&&(identical(other.label, label) || other.label == label)&&(identical(other.category, category) || other.category == category)&&(identical(other.accountFieldName, accountFieldName) || other.accountFieldName == accountFieldName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientId,name,description,requiresAccount,requiresAmount,requiresAmountFromMerchant,requiresPhone,requiresReversal,additionalDataString,processorType,uid,image,label,category);
int get hashCode => Object.hash(runtimeType,clientId,name,description,requiresAccount,requiresAmount,requiresAmountFromMerchant,requiresPhone,requiresReversal,additionalDataString,processorType,uid,image,label,category,accountFieldName);
@override
String toString() {
return 'BillProvider(clientId: $clientId, name: $name, description: $description, requiresAccount: $requiresAccount, requiresAmount: $requiresAmount, requiresAmountFromMerchant: $requiresAmountFromMerchant, requiresPhone: $requiresPhone, requiresReversal: $requiresReversal, additionalDataString: $additionalDataString, processorType: $processorType, uid: $uid, image: $image, label: $label, category: $category)';
return 'BillProvider(clientId: $clientId, name: $name, description: $description, requiresAccount: $requiresAccount, requiresAmount: $requiresAmount, requiresAmountFromMerchant: $requiresAmountFromMerchant, requiresPhone: $requiresPhone, requiresReversal: $requiresReversal, additionalDataString: $additionalDataString, processorType: $processorType, uid: $uid, image: $image, label: $label, category: $category, accountFieldName: $accountFieldName)';
}
@@ -286,7 +288,7 @@ abstract mixin class _$BillProviderCopyWith<$Res> implements $BillProviderCopyWi
factory _$BillProviderCopyWith(_BillProvider value, $Res Function(_BillProvider) _then) = __$BillProviderCopyWithImpl;
@override @useResult
$Res call({
String clientId, String name, String description, bool requiresAccount, bool requiresAmount, bool requiresAmountFromMerchant, bool requiresPhone, bool requiresReversal, String? additionalDataString, String processorType, String uid, String image, String label, String category
String clientId, String name, String description, bool requiresAccount, bool requiresAmount, bool requiresAmountFromMerchant, bool requiresPhone, bool requiresReversal, String? additionalDataString, String processorType, String uid, String image, String label, String category, String? accountFieldName
});
@@ -303,7 +305,7 @@ class __$BillProviderCopyWithImpl<$Res>
/// Create a copy of BillProvider
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? clientId = null,Object? name = null,Object? description = null,Object? requiresAccount = null,Object? requiresAmount = null,Object? requiresAmountFromMerchant = null,Object? requiresPhone = null,Object? requiresReversal = null,Object? additionalDataString = freezed,Object? processorType = null,Object? uid = null,Object? image = null,Object? label = null,Object? category = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? clientId = null,Object? name = null,Object? description = null,Object? requiresAccount = null,Object? requiresAmount = null,Object? requiresAmountFromMerchant = null,Object? requiresPhone = null,Object? requiresReversal = null,Object? additionalDataString = freezed,Object? processorType = null,Object? uid = null,Object? image = null,Object? label = null,Object? category = null,Object? accountFieldName = freezed,}) {
return _then(_BillProvider(
clientId: null == clientId ? _self.clientId : clientId // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -319,7 +321,8 @@ as String,uid: null == uid ? _self.uid : uid // ignore: cast_nullable_to_non_nul
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,category: null == category ? _self.category : category // ignore: cast_nullable_to_non_nullable
as String,
as String,accountFieldName: freezed == accountFieldName ? _self.accountFieldName : accountFieldName // ignore: cast_nullable_to_non_nullable
as String?,
));
}

View File

@@ -36,6 +36,7 @@ _BillProvider _$BillProviderFromJson(Map<String, dynamic> json) =>
image: json['image'] as String,
label: json['label'] as String,
category: json['category'] as String,
accountFieldName: json['accountFieldName'] as String?,
);
Map<String, dynamic> _$BillProviderToJson(_BillProvider instance) =>
@@ -54,4 +55,5 @@ Map<String, dynamic> _$BillProviderToJson(_BillProvider instance) =>
'image': instance.image,
'label': instance.label,
'category': instance.category,
'accountFieldName': instance.accountFieldName,
};

View File

@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:qpay/screens/home/home_controller.dart';
import 'package:qpay/screens/pay/pay_controller.dart';
import 'package:qpay/screens/transaction_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -17,34 +20,35 @@ class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<AlignmentGeometry> _alignAnimation;
final List<Animation<Offset>> _animations = [];
final List<Animation<Offset>> _alignListAnimations = [];
late TransactionController transactionController;
late bool _loading = true;
Timer? _loadingTimer;
late SharedPreferences prefs;
late HomeController homeController;
bool get _isLoggedIn => true;
Timer? _loadingTimer;
bool get _isLoggedIn => false;
String _selectedFilter = 'All';
final HomeController homeController = HomeController();
final List<Animation<Offset>> _animations = [];
final List<Animation<Offset>> _alignListAnimations = [];
@override
void initState() {
super.initState();
homeController.getCategories();
homeController.getProviders();
transactionController = Provider.of<TransactionController>(
context,
listen: false,
);
// Reset the transaction controller model
transactionController.model = TransactionModel();
_controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)..forward();
_alignAnimation = Tween<AlignmentGeometry>(
begin: Alignment.centerLeft,
end: Alignment.center,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
List tranListIndices = [0, 1];
for (int i = 0; i < tranListIndices.length; i++) {
_alignListAnimations.add(
@@ -57,17 +61,12 @@ class _HomeScreenState extends State<HomeScreen>
);
}
List buttonIndices = [0, 1, 2];
for (int i = 0; i < buttonIndices.length; i++) {
_animations.add(
Tween<Offset>(begin: Offset(-0.15, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.075 * i, 1.0, curve: Curves.easeOut),
),
),
);
}
_alignAnimation = Tween<AlignmentGeometry>(
begin: Alignment.centerLeft,
end: Alignment.center,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
setupDataAndTransitions();
// Simulate a delay to mimic loading state
_loadingTimer = Timer(const Duration(seconds: 1), () {
@@ -79,6 +78,35 @@ class _HomeScreenState extends State<HomeScreen>
});
}
Future<void> setupDataAndTransitions() async {
homeController = HomeController(context);
await homeController.getCategories();
await homeController.getProviders();
List<int> buttonIndices = List.generate(
homeController.model.providers.length,
(index) => index,
);
for (int i = 0; i < buttonIndices.length; i++) {
_animations.add(
Tween<Offset>(begin: Offset(-0.15, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.055 * i, 1.0, curve: Curves.easeOut),
),
),
);
}
prefs = await SharedPreferences.getInstance();
if (prefs.getString("userId") == null) {
prefs.setString("userId", Uuid().v4());
}
await homeController.getTransactions(prefs.getString("userId")!);
}
getBoxDecoration(String? clientId) {
// BillProvider? provider = homeController.model.selectedProvider;
//
@@ -108,19 +136,11 @@ class _HomeScreenState extends State<HomeScreen>
);
}
void _showErrorSnackBar(String? message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message.toString()),
behavior: SnackBarBehavior.floating,
),
);
}
@override
void dispose() {
_loadingTimer?.cancel();
_controller.dispose();
homeController.dispose();
super.dispose();
}
@@ -128,184 +148,199 @@ class _HomeScreenState extends State<HomeScreen>
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
title: Image(image: AssetImage('assets/icon.png'), width: 85),
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListenableBuilder(
listenable: homeController,
builder: (context, child) {
if (homeController.model.errorMessage != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showErrorSnackBar(homeController.model.errorMessage);
});
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isLoggedIn)
AlignTransition(
alignment: _alignAnimation,
child: Skeletonizer(
enabled: _loading,
child: Column(
children: [
Text(
"Vusumuzi Khoza",
style: TextStyle(fontSize: 15),
),
Text(
"USD 4.02",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
child: RefreshIndicator(
onRefresh: () async {
await homeController.getTransactions(prefs.getString("userId")!);
},
child: CustomScrollView(
slivers: [
SliverAppBar(
stretch: true,
stretchTriggerOffset: 100.0,
expandedHeight: 50.0,
surfaceTintColor: Colors.transparent,
floating: true,
title: Image(image: AssetImage('assets/peak.png'), width: 85),
actions: [
// IconButton(
// icon: const Icon(Icons.notifications_outlined),
// onPressed: () {},
// ),
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: ListenableBuilder(
listenable: homeController,
builder: (context, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isLoggedIn)
AlignTransition(
alignment: _alignAnimation,
child: Skeletonizer(
enabled: _loading,
child: Column(
children: [
Text(
"Vusumuzi Khoza",
style: TextStyle(fontSize: 15),
),
),
const SizedBox(height: 20),
],
Text(
"USD 4.02",
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
],
),
),
),
_buildFilterChips(homeController),
SizedBox(height: 10),
Column(
children: [
...homeController.model.filterableProviders.map(
(e) => _buildProviderItem(e, context),
),
],
),
_buildFilterChips(homeController),
SizedBox(height: 10),
Column(
children: [
...homeController.model.filterableProviders.map(
(e) => _buildProviderItem(e, context),
),
],
),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recent Activity',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () {
context.go('/history');
},
child: const Text(
'View All',
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Recent Activity',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 20),
Skeletonizer(
enabled: _loading,
child: ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
SlideTransition(
position: _alignListAnimations[0],
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.electric_bolt,
color:
Theme.of(context).colorScheme.primary,
),
),
title: Text(
'Electricity Bill',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
'Paid USD 50.00',
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
trailing: Text(
'-USD 50.00',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
),
SlideTransition(
position: _alignListAnimations[1],
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.phone_android,
color:
Theme.of(context).colorScheme.primary,
),
),
title: Text(
'Airtime Purchase',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
'Purchased USD 10.00',
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
trailing: Text(
'-USD 10.00',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
TextButton(
onPressed: () {
context.go('/history');
},
child: Text(
'View All',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
);
},
const SizedBox(height: 5),
if (homeController.model.transactions.isEmpty)
Column(
children: [
SizedBox(height: 30),
Center(
child: Text(
'No transactions yet. Make a payment to see your recent activity.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
),
],
),
if (homeController.model.transactions.isNotEmpty)
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
...homeController.model.transactions.map(
(e) => _buildTransactionItem(e, context),
),
],
),
],
);
},
),
),
),
],
),
),
),
);
}
Widget _buildTransactionItem(Map<String, dynamic> e, BuildContext context) {
return SlideTransition(
position: _alignListAnimations[0],
child: Skeletonizer(
enabled: homeController.model.isLoading,
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onTap: () {
transactionController.updateReceiptData(e);
context.push("/receipt");
},
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Image(
image: AssetImage("assets/${e['paymentProcessorImage']}"),
),
),
],
title: Row(
children: [
Text(
e['billName'],
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(width: 10),
Icon(
e['status'] == 'SUCCESS'
? Icons.check_circle
: e['status'] == 'PENDING'
? Icons.pending
: Icons.cancel,
size: 16,
color:
e['status'] == 'SUCCESS'
? Colors.green
: e['status'] == 'PENDING'
? Colors.orange
: Colors.red,
),
],
),
subtitle: Text(
e['createdAt'] != null
? timeago.format(DateTime.parse(e['createdAt']))
: '',
style: TextStyle(fontSize: 12),
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
e['amount'].toStringAsFixed(2),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Text(
e['totalAmount'].toStringAsFixed(2),
style: TextStyle(fontSize: 12),
),
],
),
),
),
),
);
@@ -317,7 +352,7 @@ class _HomeScreenState extends State<HomeScreen>
Skeletonizer(
enabled: homeController.model.isLoading,
child: SlideTransition(
position: _alignListAnimations[1],
position: _animations[homeController.model.providers.indexOf(e)],
child: Container(
decoration: getBoxDecoration(e.clientId),
child: InkWell(
@@ -326,7 +361,8 @@ class _HomeScreenState extends State<HomeScreen>
),
onTap: () {
homeController.updateSelectedProvider(e);
context.go("/make-payment");
transactionController.updateSelectedProvider(e);
context.push("/recipients");
},
child: ListTile(
leading: Container(
@@ -345,7 +381,10 @@ class _HomeScreenState extends State<HomeScreen>
),
subtitle: Text(
e.description,
style: TextStyle(fontWeight: FontWeight.normal, fontSize: 10),
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 10,
),
),
trailing: Icon(
Icons.chevron_right,
@@ -383,29 +422,42 @@ class _HomeScreenState extends State<HomeScreen>
Category? category,
]) {
final isSelected = _selectedFilter == label;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: isSelected,
label: Text(label),
onSelected: (selected) {
setState(() {
_selectedFilter = label;
});
if (category != null) {
controller.updateSelectedCategory(category);
} else {
controller.updateSelectedCategory(null);
}
},
backgroundColor: Theme.of(context).colorScheme.surface,
selectedColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
checkmarkColor: Theme.of(context).colorScheme.primary,
labelStyle: TextStyle(
color:
isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
return Skeletonizer(
enabled: controller.model.isLoading,
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.2),
),
),
selected: isSelected,
label: Text(label),
onSelected: (selected) {
setState(() {
_selectedFilter = label;
});
if (category != null) {
controller.updateSelectedCategory(category);
} else {
controller.updateSelectedCategory(null);
}
},
backgroundColor: Theme.of(context).colorScheme.surface,
selectedColor: Theme.of(context).colorScheme.primary.withOpacity(0.2),
checkmarkColor: Theme.of(context).colorScheme.primary,
labelStyle: TextStyle(
color:
isSelected
? Theme.of(
context,
).colorScheme.secondary.withValues(alpha: 0.7)
: Theme.of(context).colorScheme.onSurface,
),
),
),
);