ironing out erp bugs

This commit is contained in:
2025-09-27 01:09:05 +02:00
parent cbabff4c10
commit f1e2327732
41 changed files with 1440 additions and 516 deletions

View File

@@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class AbstractController extends ChangeNotifier {
late BuildContext context;
void showErrorSnackBar(String? message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message.toString()),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.deepOrange,
),
);
});
}
}

89
lib/firebase_options.dart Normal file
View File

@@ -0,0 +1,89 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyDqcGjTQTrZjhFoBvbhwsOuDaxvA4p8Ch4',
appId: '1:77433712483:web:89d62f69ed8ffb09ab6d56',
messagingSenderId: '77433712483',
projectId: 'peak-c376b',
authDomain: 'peak-c376b.firebaseapp.com',
storageBucket: 'peak-c376b.firebasestorage.app',
measurementId: 'G-B94Y2DVT96',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyD3XsySzfF9Tp84jbAc_T0AwI-RSDRawmI',
appId: '1:77433712483:ios:f21e81701d8a2740ab6d56',
messagingSenderId: '77433712483',
projectId: 'peak-c376b',
storageBucket: 'peak-c376b.firebasestorage.app',
iosBundleId: 'zw.co.qantra.qpay',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyDqcGjTQTrZjhFoBvbhwsOuDaxvA4p8Ch4',
appId: '1:77433712483:web:03b1df2872cd9723ab6d56',
messagingSenderId: '77433712483',
projectId: 'peak-c376b',
authDomain: 'peak-c376b.firebaseapp.com',
storageBucket: 'peak-c376b.firebasestorage.app',
measurementId: 'G-DMD0HQ4W18',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyD3XsySzfF9Tp84jbAc_T0AwI-RSDRawmI',
appId: '1:77433712483:ios:f21e81701d8a2740ab6d56',
messagingSenderId: '77433712483',
projectId: 'peak-c376b',
storageBucket: 'peak-c376b.firebasestorage.app',
iosBundleId: 'zw.co.qantra.qpay',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyARyIWVumzj4UWNwplKZRxxVYmw_H6hBKI',
appId: '1:77433712483:android:c6508ec8c6a23889ab6d56',
messagingSenderId: '77433712483',
projectId: 'peak-c376b',
storageBucket: 'peak-c376b.firebasestorage.app',
);
}

View File

@@ -17,7 +17,7 @@ class Http {
LogInterceptor(
request: true,
responseBody: false,
requestBody: false,
requestBody: true,
error: true,
),
);

View File

@@ -3,9 +3,12 @@ import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/confirm/confirm_screen.dart';
import 'package:qpay/screens/gateway/gateway_screen.dart';
import 'package:qpay/screens/onboarding/complete_screen.dart';
import 'package:qpay/screens/onboarding/landing/landing_screen.dart';
import 'package:qpay/screens/onboarding/login/login_screen.dart';
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
import 'package:qpay/screens/onboarding/password/password_screen.dart';
import 'package:qpay/screens/onboarding/phone/phone_screen.dart';
import 'package:qpay/screens/onboarding/register/register_screen.dart';
import 'package:qpay/screens/onboarding/verify/verify_screen.dart';
import 'package:qpay/screens/pay/pay_screen.dart';
@@ -18,15 +21,25 @@ import 'screens/profile_screen.dart';
import 'screens/splash_screen.dart';
import 'theme/app_theme.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
const String testEnv = "assets/.env";
const String liveEnv = "assets/.env";
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await dotenv.load(fileName: testEnv);
runApp(
ChangeNotifierProvider(
create: (context) => TransactionController(),
MultiProvider(
providers: [
ChangeNotifierProvider<TransactionController>(create: (_) => TransactionController()),
ChangeNotifierProvider<OnboardingController>(create: (_) => OnboardingController()),
],
child: const MyApp(),
),
);
@@ -124,6 +137,10 @@ class _MyAppState extends State<MyApp> {
path: '/onboarding/register',
builder: (context, state) => const RegisterScreen(),
),
GoRoute(
path: '/onboarding/phone',
builder: (context, state) => const PhoneScreen(),
),
GoRoute(
path: '/onboarding/verify',
builder: (context, state) => const VerifyScreen(),
@@ -136,6 +153,10 @@ class _MyAppState extends State<MyApp> {
path: '/onboarding/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/onboarding/complete',
builder: (context, state) => const CompleteScreen(),
),
],
),
],

View File

@@ -280,7 +280,7 @@ class HomeController extends ChangeNotifier {
description: 'USD Wallet',
image: 'united-states.png',
currency: 'USD',
balance: '4.02',
balance: '47655.02',
),
Account(
name: 'Vusumuzi Khoza',

View File

@@ -21,13 +21,12 @@ class _HomeScreenState extends State<HomeScreen>
late AnimationController _controller;
late Animation<AlignmentGeometry> _alignAnimation;
late TransactionController transactionController;
late bool _loading = true;
late SharedPreferences prefs;
late HomeController homeController;
late final router = GoRouter.of(context);
late String initials;
Timer? _loadingTimer;
bool get _isLoggedIn => false;
bool _isLoggedIn = false;
String _selectedFilter = 'All';
final List<Animation<Offset>> _animations = [];
@@ -36,15 +35,6 @@ class _HomeScreenState extends State<HomeScreen>
@override
void initState() {
super.initState();
router.routerDelegate.addListener(_onRouteChanged);
transactionController = Provider.of<TransactionController>(
context,
listen: false,
);
// Reset the transaction controller model
transactionController.model = TransactionModel();
_controller = AnimationController(
duration: const Duration(milliseconds: 600),
@@ -69,18 +59,19 @@ class _HomeScreenState extends State<HomeScreen>
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
setupDataAndTransitions();
// Simulate a delay to mimic loading state
_loadingTimer = Timer(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_loading = false;
});
}
});
}
Future<void> setupDataAndTransitions() async {
router.routerDelegate.addListener(_onRouteChanged);
transactionController = Provider.of<TransactionController>(
context,
listen: false,
);
// Reset the transaction controller model
transactionController.model = TransactionModel();
homeController = HomeController(context);
await homeController.getAccounts();
await homeController.getCategories();
@@ -103,8 +94,11 @@ class _HomeScreenState extends State<HomeScreen>
prefs = await SharedPreferences.getInstance();
if (prefs.getString("userId") == null) {
prefs.setString("userId", Uuid().v4());
if (prefs.getString("token") != null) {
setState(() {
_isLoggedIn = true;
initials = prefs.getString("initials")!;
});
}
await homeController.getTransactions(prefs.getString("userId")!);
@@ -126,16 +120,21 @@ class _HomeScreenState extends State<HomeScreen>
);
}
void _onRouteChanged() {
void _onRouteChanged() async {
final location = router.routerDelegate.currentConfiguration.uri.toString();
if (location.startsWith('/home')) {
setupDataAndTransitions();
prefs = await SharedPreferences.getInstance();
if (prefs.getString("userId") == null) {
prefs.setString("userId", Uuid().v4());
}
await homeController.getTransactions(prefs.getString("userId")!);
}
}
@override
void dispose() {
_loadingTimer?.cancel();
_controller.dispose();
homeController.dispose();
router.routerDelegate.removeListener(_onRouteChanged);
@@ -159,45 +158,83 @@ class _HomeScreenState extends State<HomeScreen>
surfaceTintColor: Colors.transparent,
floating: true,
title: Image(image: AssetImage('assets/peak.png'), width: 85),
leading: IconButton(
icon: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border.all(color: Colors.black87.withAlpha(20)),
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
Theme.of(
context,
).colorScheme.tertiary.withValues(alpha: 0.05),
],
stops: const [0.3, 0.7],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
leading: _isLoggedIn ? Container(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
border: Border.all(color: Colors.black87.withAlpha(20)),
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
Theme.of(
context,
).colorScheme.tertiary.withValues(alpha: 0.05),
],
stops: const [0.3, 0.7],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
),
child: Center(
child: Text(
'VK',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
child: Center(
child: Text(
initials,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
onPressed: () {},
),
onPressed: () {},
),
// actions: [
// IconButton(
// icon: const Icon(Icons.notifications_outlined),
// onPressed: () {},
// ),
// ],
) : SizedBox(),
actions: [
if(_isLoggedIn)
IconButton(
icon: const Icon(Icons.exit_to_app_sharp),
onPressed: () {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Confirm Logout'),
content: const Text('Are you sure you want to log out?'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(dialogContext).pop(); // Dismiss the dialog
},
),
TextButton(
child: const Text('Logout'),
onPressed: () {
Navigator.of(dialogContext).pop(); // Dismiss the dialog
prefs.remove("token");
setState(() {
_isLoggedIn = false;
});
// It's generally not recommended to call initState directly.
// Consider moving the logic from initState that needs to be rerun
// into a separate method and call that method here.
// For example, if you have a method like _reloadData(), call it here.
// _reloadData();
setupDataAndTransitions(); // Or specific parts of it if not all is needed
},
),
],
);
},
);
},
),
],
),
SliverToBoxAdapter(
child: Padding(
@@ -209,7 +246,7 @@ class _HomeScreenState extends State<HomeScreen>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!_isLoggedIn) _buildLogin(homeController),
if (_isLoggedIn) _buildAccounts(homeController),
// if (_isLoggedIn) _buildAccounts(homeController),
_buildFilterChips(homeController),
SizedBox(height: 10),
Column(
@@ -361,7 +398,7 @@ class _HomeScreenState extends State<HomeScreen>
// context.push("/wallet");
},
child: Container(
width: 150,
width: 170,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
@@ -388,11 +425,11 @@ class _HomeScreenState extends State<HomeScreen>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset('assets/${e.image}', width: 20),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Image.asset('assets/${e.image}', width: 20),
SizedBox(width: 10),
Text(
e.currency,
style: TextStyle(
@@ -402,12 +439,15 @@ class _HomeScreenState extends State<HomeScreen>
),
),
SizedBox(width: 5),
Text(
e.balance,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: Colors.black,
Expanded(
child: Text(
e.balance,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: Colors.black,
overflow: TextOverflow.ellipsis,
),
),
),
],

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/onboarding/login/login_controller.dart';
class CompleteScreen extends StatefulWidget {
const CompleteScreen({super.key});
@override
State<CompleteScreen> createState() => _CompleteScreenState();
}
class _CompleteScreenState extends State<CompleteScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 75),
Text(
'Registration Complete',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
SizedBox(height: 5),
Text(
'Thank you for registering with Peak payments',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Image.asset('assets/landing.png', width: 300),
Text(
'Tap Login to get started',
style: TextStyle(fontSize: 18),
),
SizedBox(height: 10),
],
),
),
),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
context.go('/onboarding/login');
},
child: Text('Go to Login', style: TextStyle(fontSize: 16)),
),
),
],
),
),
SizedBox(height: 5),
],
),
),
);
}
}

View File

@@ -54,6 +54,8 @@ class LoginController extends ChangeNotifier {
prefs.setString('firstName', response['firstName']);
prefs.setString('lastName', response['lastName']);
prefs.setString('phone', response['phone']);
prefs.setString('userId', response['uuid']);
prefs.setString('initials', response['firstName'].substring(0, 1) + response['lastName'].substring(0, 1));
return model;
} else {
@@ -64,7 +66,7 @@ class LoginController extends ChangeNotifier {
}
} on DioException catch (e, s) {
logger.e(s);
_showErrorSnackBar("Network error. Please try again or contact support");
_showErrorSnackBar(e.response?.data['errors'][0] ?? "Unknown error");
} catch (e, s) {
logger.e(s);
_showErrorSnackBar(

View File

@@ -1,6 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:qpay/screens/onboarding/login/login_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -10,9 +16,13 @@ class LoginScreen extends StatefulWidget {
}
class _LoginScreenState extends State<LoginScreen> {
late SharedPreferences prefs;
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
List<String> scopes = <String>[
'https://www.googleapis.com/auth/contacts.readonly',
];
late LoginController _loginController;
@@ -22,6 +32,65 @@ class _LoginScreenState extends State<LoginScreen> {
_loginController = LoginController(context);
}
Future<void> signInWithGoogle() async {
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
if (GoogleSignIn.instance.supportsAuthenticate()){
unawaited(GoogleSignIn.instance.authenticate().then((value) async {
print(value);
saveUser(value);
await _loginController.login(value.email, value.id);
context.go('/');
googleSignIn.authenticationEvents
.listen(_handleAuthenticationEvent)
.onError(_handleAuthenticationError);
}));
}
}
void saveUser(GoogleSignInAccount user) async {
final Map<String, dynamic> data = <String, dynamic>{
'displayName': user.displayName,
'email': user.email,
'id': user.id,
'photoUrl': user.photoUrl,
};
prefs = await SharedPreferences.getInstance();
if (prefs.getString("googleUser") == null) {
prefs.setString("googleUser", jsonEncode(data));
}
}
Future<void> _handleAuthenticationEvent(
GoogleSignInAuthenticationEvent event,
) async {
// #docregion CheckAuthorization
final GoogleSignInAccount? user = // ...
// #enddocregion CheckAuthorization
switch (event) {
GoogleSignInAuthenticationEventSignIn() => event.user,
GoogleSignInAuthenticationEventSignOut() => null,
};
// Check for existing authorization.
// #docregion CheckAuthorization
final GoogleSignInClientAuthorization? authorization = await user
?.authorizationClient
.authorizationForScopes(scopes);
// #enddocregion CheckAuthorization
}
Future<void> _handleAuthenticationError(Object e) async {
print(e.toString());
}
@override
void dispose() {
_loginController.dispose();
@@ -56,55 +125,15 @@ class _LoginScreenState extends State<LoginScreen> {
textAlign: TextAlign.center,
),
SizedBox(height: 40),
Image.asset('assets/password.png', width: 300),
Text(
'Tap on your preferred social media platform to get started',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
// Username Field
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.person_outline),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a phone number';
}
return null;
},
),
SizedBox(height: 20),
// Password Field
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outline),
suffixIcon: Icon(Icons.visibility_outlined),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a password';
}
return null;
},
),
SizedBox(height: 20),
// Forgot Password Link
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// TODO: Add forgot password logic
},
child: Text(
'Forgot Password?',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14,
),
),
),
),
SizedBox(height: 40),
// Divider with "or" text
Row(
children: [
@@ -112,11 +141,12 @@ class _LoginScreenState extends State<LoginScreen> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'or continue with',
'choose from the options below to continue',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
Expanded(child: Divider(color: Colors.grey.shade300)),
@@ -128,8 +158,9 @@ class _LoginScreenState extends State<LoginScreen> {
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Add Google login logic
onPressed: () async {
await signInWithGoogle();
},
icon: Image.asset(
'assets/google.png',
@@ -146,27 +177,6 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
),
SizedBox(width: 16),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Add Apple login logic
},
icon: Image.asset(
'assets/apple.png',
width: 20,
height: 20,
),
label: Text('Apple', style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
SizedBox(height: 40),
@@ -179,43 +189,6 @@ class _LoginScreenState extends State<LoginScreen> {
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
// TODO: Add login logic
if (_formKey.currentState!.validate()) {
LoginScreenModel model = await _loginController.login(
_usernameController.text,
_passwordController.text,
);
if (model.status == 'SUCCESS') {
context.go('/');
}
}
},
child: Text('Login', style: TextStyle(fontSize: 16)),
),
),
SizedBox(width: 16),
Expanded(
child: TextButton(
onPressed: () {
context.go('/onboarding/register');
},
child: Text(
'Create Account',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
SizedBox(height: 5),
TextButton(
onPressed: () {

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:qpay/http/http.dart';
class OnboardingModel {
String? phone;
String? workflowId;
String? username;
GoogleSignInAccount? googleUser;
bool isLoading = false;
}
class OnboardingController extends ChangeNotifier {
OnboardingModel model = OnboardingModel();
void resetState() {
model = OnboardingModel();
}
void updateModel(Map mapModel) {
model.workflowId = mapModel['workflowId'];
model.phone = mapModel['phone'];
model.username = mapModel['username'];
notifyListeners();
}
updatePhone(String phone) {
model.phone = phone;
notifyListeners();
}
updateGoogleUser(GoogleSignInAccount googleUser) {
model.googleUser = googleUser;
notifyListeners();
}
}

View File

@@ -0,0 +1,73 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:qpay/abstracts/AbstractController.dart';
import 'package:qpay/http/http.dart';
import '../onboarding_controller.dart';
class PhoneModel {
bool isLoading = false;
String? errorMessage;
String? status = 'failed';
}
class PhoneController extends AbstractController {
PhoneModel model = PhoneModel();
@override
BuildContext context;
late OnboardingController onboardingController;
final Http http = Http();
PhoneController(this.context) {
onboardingController = Provider.of<OnboardingController>(
context,
listen: false,
);
}
void updatePhone(String phone) {
onboardingController.updatePhone(phone);
notifyListeners();
}
Future<void> register() async {
Map user = {
"username": onboardingController.model.googleUser?.email,
"email": onboardingController.model.googleUser?.email,
"firstName": onboardingController.model.googleUser?.displayName?.split(" ").first,
"lastName": onboardingController.model.googleUser?.displayName?.split(" ").last,
"password": onboardingController.model.googleUser?.id,
"photoUrl": onboardingController.model.googleUser?.photoUrl,
"phone": onboardingController.model.phone
};
try {
model.isLoading = true;
Map response = await http.post(
'/auth/register',
user
);
model.status = response['state'];
if(response['state'] == 'failed') {
model.errorMessage = response['message'];
showErrorSnackBar(model.errorMessage);
}
Map body = response['body'];
body['workflowId'] = response['workflowId'];
onboardingController.updateModel(body);
} catch (e) {
logger.e(e);
showErrorSnackBar(
"Problem during registration, please try again or contact support?",
);
}
model.isLoading = false;
notifyListeners();
}
}

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/onboarding/phone/phone_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
class PhoneScreen extends StatefulWidget {
const PhoneScreen({super.key});
@override
State<PhoneScreen> createState() => _PhoneScreenState();
}
class _PhoneScreenState extends State<PhoneScreen> {
late PhoneController phoneController;
final _formKey = GlobalKey<FormState>();
final TextEditingController _phoneController = TextEditingController();
String selectedCountryCode = '+263'; // Default to ZW
// Common country codes with flags and names
final List<Map<String, String>> countryCodes = [
{'code': '+263', 'name': 'ZW', 'flag': '🇿🇼'},
{'code': '+27', 'name': 'ZA', 'flag': '🇿🇦'},
{'code': '+1', 'name': 'US', 'flag': '🇺🇸'},
{'code': '+44', 'name': 'UK', 'flag': '🇬🇧'},
{'code': '+61', 'name': 'AU', 'flag': '🇦🇺'},
{'code': '+91', 'name': 'IN', 'flag': '🇮🇳'},
{'code': '+86', 'name': 'CN', 'flag': '🇨🇳'},
{'code': '+81', 'name': 'JP', 'flag': '🇯🇵'},
{'code': '+49', 'name': 'DE', 'flag': '🇩🇪'},
{'code': '+33', 'name': 'FR', 'flag': '🇫🇷'},
{'code': '+39', 'name': 'IT', 'flag': '🇮🇹'},
{'code': '+34', 'name': 'ES', 'flag': '🇪🇸'},
{'code': '+7', 'name': 'RU', 'flag': '🇷🇺'},
{'code': '+55', 'name': 'BR', 'flag': '🇧🇷'},
{'code': '+52', 'name': 'MX', 'flag': '🇲🇽'},
{'code': '+971', 'name': 'AE', 'flag': '🇦🇪'},
{'code': '+966', 'name': 'SA', 'flag': '🇸🇦'},
{'code': '+234', 'name': 'NG', 'flag': '🇳🇬'},
{'code': '+254', 'name': 'KE', 'flag': '🇰🇪'},
{'code': '+233', 'name': 'GH', 'flag': '🇬🇭'},
{'code': '+256', 'name': 'UG', 'flag': '🇺🇬'},
];
@override
void initState() {
super.initState();
phoneController = PhoneController(context);
}
Future<void> updatePhoneNumber(String phoneNumber) async {
String existingPhone = phoneNumber;
if (existingPhone.isNotEmpty) {
// Try to extract country code from existing phone number
for (var country in countryCodes) {
if (existingPhone.startsWith(country['code']!)) {
setState(() {
selectedCountryCode = country['code']!;
});
_phoneController.text = existingPhone.substring(
country['code']!.length,
);
break;
}
}
// If no country code found, use default and set the full number
if (_phoneController.text.isEmpty) {
_phoneController.text = existingPhone;
}
}
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: phoneController,
builder: (context, child) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(height: 75),
Text(
'Verify Your Device',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme
.of(context)
.colorScheme
.primary,
),
textAlign: TextAlign.center,
),
SizedBox(height: 10),
Text(
'We will send you a verification code to this number',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Image.asset('assets/phone.png', width: 300),
SizedBox(height: 20),
_buildPhoneField(),
SizedBox(height: 30),
],
),
),
),
),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Skeletonizer(
enabled: phoneController.model.isLoading,
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
String fullPhoneNumber =
selectedCountryCode + _phoneController.text;
phoneController.updatePhone(fullPhoneNumber);
await phoneController.register();
if(phoneController.model.status != 'failed') {
context.go('/onboarding/verify');
}
}
},
child: Text('Send Code', style: TextStyle(fontSize: 16)),
),
),
SizedBox(width: 16),
Expanded(
child: TextButton(
onPressed: () {
context.go('/onboarding/login');
},
child: Text(
'Go to Login',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
),
SizedBox(height: 5),
TextButton(
onPressed: () {
context.go('/');
},
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
),
],
),
),
);
}
);
}
Widget _buildPhoneField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Country code dropdown
Container(
width: 100,
height: 55,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCountryCode,
isExpanded: true,
icon: const Icon(Icons.arrow_drop_down),
padding: const EdgeInsets.symmetric(horizontal: 8),
items:
countryCodes.map((country) {
return DropdownMenuItem<String>(
value: country['code'],
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(country['flag']!),
const SizedBox(width: 4),
Text(
country['code']!,
style: const TextStyle(fontSize: 16),
),
],
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
selectedCountryCode = newValue;
});
}
},
),
),
),
const SizedBox(width: 8),
// Phone number field
Expanded(
child: TextFormField(
controller: _phoneController,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
hintText: 'Enter phone number',
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.secondary,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.2),
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a phone number';
}
// Remove any non-digit characters for validation
String digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
if (digitsOnly.length < 7) {
return 'Phone number must be at least 7 digits';
}
if (digitsOnly.length > 15) {
return 'Phone number is too long';
}
return null;
},
),
),
],
),
],
);
}
}

View File

@@ -1,5 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:provider/provider.dart';
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@@ -9,186 +16,237 @@ class RegisterScreen extends StatefulWidget {
}
class _RegisterScreenState extends State<RegisterScreen> {
late OnboardingController onboardingController;
bool _loading = false;
List<String> scopes = <String>[
'https://www.googleapis.com/auth/contacts.readonly',
];
@override
void initState() {
super.initState();
onboardingController = Provider.of<OnboardingController>(
context,
listen: false,
);
// onboardingController.resetState();
}
void signInWithGoogle() {
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
if (GoogleSignIn.instance.supportsAuthenticate()){
unawaited(GoogleSignIn.instance.authenticate().then((value) {
print(value);
onboardingController.updateGoogleUser(value);
googleSignIn.authenticationEvents
.listen(_handleAuthenticationEvent)
.onError(_handleAuthenticationError);
}));
}
}
Future<void> _handleAuthenticationEvent(
GoogleSignInAuthenticationEvent event,
) async {
// #docregion CheckAuthorization
final GoogleSignInAccount? user = // ...
// #enddocregion CheckAuthorization
switch (event) {
GoogleSignInAuthenticationEventSignIn() => event.user,
GoogleSignInAuthenticationEventSignOut() => null,
};
// Check for existing authorization.
// #docregion CheckAuthorization
final GoogleSignInClientAuthorization? authorization = await user
?.authorizationClient
.authorizationForScopes(scopes);
// #enddocregion CheckAuthorization
}
Future<void> _handleAuthenticationError(Object e) async {
print(e.toString());
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 75),
Text(
'Registration',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
SizedBox(height: 5),
Text(
'Join Peak and start making fast payments',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
SizedBox(height: 40),
// First Name Field
TextField(
decoration: InputDecoration(
labelText: 'First Name',
prefixIcon: Icon(Icons.person_outline),
),
),
SizedBox(height: 20),
// Last Name Field
TextField(
decoration: InputDecoration(
labelText: 'Last Name',
prefixIcon: Icon(Icons.person_outline),
),
),
SizedBox(height: 20),
// Phone Field
TextField(
decoration: InputDecoration(
labelText: 'Phone Number',
prefixIcon: Icon(Icons.phone_outlined),
),
keyboardType: TextInputType.phone,
),
SizedBox(height: 20),
// Email Field
TextField(
decoration: InputDecoration(
labelText: 'Email Address',
prefixIcon: Icon(Icons.email_outlined),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
// Password Field
TextField(
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outline),
suffixIcon: Icon(Icons.visibility_outlined),
),
obscureText: true,
),
SizedBox(height: 40),
// Divider with "or" text
Row(
return ListenableBuilder(
listenable: onboardingController,
builder: (context, child) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Divider(color: Colors.grey.shade300)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'or continue with',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
SizedBox(height: 75),
Text(
'Registration',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
Expanded(child: Divider(color: Colors.grey.shade300)),
],
),
SizedBox(height: 30),
// Social Login Buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Add Google login logic
},
icon: Image.asset(
'assets/google.png',
width: 20,
height: 20,
SizedBox(height: 5),
Text(
"Let's start by getting some of your details online to get you started",
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Image.asset('assets/social.png', width: 300),
SizedBox(height: 20),
// Social Login Buttons
if(onboardingController.model.googleUser == null)
Column(
children: [
Row(
children: [
Expanded(child: Divider(color: Colors.grey.shade300)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Choose from the options below to continue',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
Expanded(child: Divider(color: Colors.grey.shade300)),
],
),
label: Text('Google', style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
SizedBox(height: 20),
Skeletonizer(
enabled: _loading,
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
signInWithGoogle();
},
icon: Image.asset(
'assets/google.png',
width: 20,
height: 20,
),
label: Text('Google', style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
SizedBox(width: 16),
],
),
),
),
],
),
SizedBox(width: 16),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Add Apple login logic
},
icon: Image.asset(
'assets/apple.png',
width: 20,
height: 20,
),
label: Text('Apple', style: TextStyle(fontSize: 16)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
if(onboardingController.model.googleUser != null)
Column(
children: [
SizedBox(
width: 100,
child: Divider(
color: Colors.grey.shade300,
thickness: 1,
),
),
),
SizedBox(height: 20,),
Text(
"Welcome, ${onboardingController.model.googleUser?.displayName}",
style: TextStyle(fontSize: 25),
textAlign: TextAlign.center,
),
SizedBox(height: 10,),
Text("Tap Proceed to continue", style: TextStyle(fontSize: 18),)
],
),
),
SizedBox(height: 40),
],
),
SizedBox(height: 40),
],
),
),
),
),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
context.go('/onboarding/verify');
},
child: Text('Submit', style: TextStyle(fontSize: 16)),
),
),
SizedBox(width: 16),
Expanded(
child: TextButton(
onPressed: () {
context.go('/onboarding/login');
},
child: Text(
'Go to Login',
style: TextStyle(fontSize: 16),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
if (onboardingController.model.googleUser == null) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Sign-In Required'),
content: const Text(
"Please sign in with one of the social media platforms to proceed."),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(dialogContext).pop(); // Close the dialog
},
),
],
);
},
);
} else {
context.go('/onboarding/phone');
}
},
child: Text('Proceed', style: TextStyle(fontSize: 16)),
),
),
),
SizedBox(width: 16),
Expanded(
child: TextButton(
onPressed: () {
context.go('/onboarding/login');
},
child: Text(
'Go to Login',
style: TextStyle(fontSize: 16),
),
),
),
],
),
],
),
),
SizedBox(height: 5),
TextButton(
onPressed: () {
context.go('/');
},
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
),
],
),
SizedBox(height: 5),
TextButton(
onPressed: () {
context.go('/');
},
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
),
],
),
),
),
);
}
);
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:qpay/abstracts/AbstractController.dart';
import 'package:qpay/http/http.dart';
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
class VerifyModel {
String? code;
bool isLoading = false;
String? errorMessage;
String? status = 'failed';
}
class VerifyController extends AbstractController {
late OnboardingController onboardingController;
final Http http = Http();
BuildContext context;
VerifyModel model = VerifyModel();
VerifyController(this.context){
onboardingController = Provider.of<OnboardingController>(
context,
listen: false,
);
}
void updateCode(String code) {
model.code = code;
notifyListeners();
}
Future<void> verifyOtp () async {
Map user = {
"username": onboardingController.model.phone,
"otpCode": model.code,
"otpType": "REGISTRATION",
"workflowId": onboardingController.model.workflowId,
};
try {
model.isLoading = true;
notifyListeners();
Map response = await http.post(
'/auth/verify',
user
);
model.status = response['state'];
if(response['state'] == 'failed') {
model.errorMessage = response['message'];
showErrorSnackBar(model.errorMessage);
}
} catch (e) {
logger.e(e);
showErrorSnackBar(
"Problem during registration, please try again or contact support?",
);
}
model.isLoading = false;
notifyListeners();
}
Future<void> resendOtp () async {
Map user = {
"username": onboardingController.model.phone,
"phone": onboardingController.model.phone,
"workflowId": onboardingController.model.workflowId,
};
try {
model.isLoading = true;
notifyListeners();
Map response = await http.post(
'/auth/resend',
user
);
model.status = response['state'];
if(response['state'] == 'failed') {
model.errorMessage = response['message'];
showErrorSnackBar(model.errorMessage);
}
} catch (e) {
logger.e(e);
showErrorSnackBar(
"Problem during registration, please try again or contact support?",
);
}
model.isLoading = false;
notifyListeners();
}
}

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/onboarding/verify/verify_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
class VerifyScreen extends StatefulWidget {
const VerifyScreen({super.key});
@@ -9,6 +11,16 @@ class VerifyScreen extends StatefulWidget {
}
class _VerifyScreenState extends State<VerifyScreen> {
late VerifyController verifyController;
String? errorMessage;
@override
void initState() {
super.initState();
verifyController = VerifyController(context);
}
final List<TextEditingController> _codeControllers = List.generate(
6,
(index) => TextEditingController(),
@@ -26,6 +38,12 @@ class _VerifyScreenState extends State<VerifyScreen> {
super.dispose();
}
void showErrorMessage(String? message) {
setState(() {
errorMessage = message;
});
}
void _onCodeChanged(String value, int index) {
if (value.length == 1 && index < 5) {
_focusNodes[index + 1].requestFocus();
@@ -34,155 +52,180 @@ class _VerifyScreenState extends State<VerifyScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 75),
Text(
'Verify Your Account',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
SizedBox(height: 5),
Text(
'Enter the 6-digit code sent to your phone/email',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Image.asset('assets/verify.png', width: 300),
// Verification Code Input
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
6,
(index) => SizedBox(
width: 50,
child: TextField(
controller: _codeControllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
return ListenableBuilder(
listenable: verifyController,
builder: (context, child) {
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: 75),
Text(
'Verify Your Account',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
SizedBox(height: 5),
Text(
'Enter the 6-digit code sent to your phone/email',
style: TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Image.asset('assets/verify.png', width: 300),
// Verification Code Input
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
6,
(index) => SizedBox(
width: 50,
child: TextFormField(
controller: _codeControllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
onChanged: (value) => _onCodeChanged(value, index),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
),
),
),
SizedBox(height: 5),
if(errorMessage != null)
Text(errorMessage ?? '', style: TextStyle(color: Colors.red)),
SizedBox(height: 30),
// Resend Code Section
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Didn't receive the code? ",
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
),
TextButton(
onPressed: () {
verifyController.resendOtp();
},
child: Text(
'Resend Code',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
width: 2,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
onChanged: (value) => _onCodeChanged(value, index),
],
),
// SizedBox(height: 20),
// // Timer for resend (optional)
// Text(
// 'Resend available in 2:30',
// style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
// ),
SizedBox(height: 40),
],
),
),
),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Skeletonizer(
enabled: verifyController.model.isLoading,
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
showErrorMessage(null);
String code = '';
for (var controller in _codeControllers) {
code += controller.text;
}
if(code.length != 6) {
showErrorMessage('Invalid code');
return;
}
verifyController.updateCode(code);
await verifyController.verifyOtp();
if(verifyController.model.status == 'done') {
context.go('/onboarding/complete');
}
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Verify Code',
style: TextStyle(fontSize: 16),
),
),
),
SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: () {
context.go('/onboarding/login');
},
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Back to Login',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
),
),
SizedBox(height: 30),
// Resend Code Section
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Didn't receive the code? ",
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
),
TextButton(
onPressed: () {
// TODO: Add resend code logic
},
child: Text(
'Resend Code',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
),
SizedBox(height: 20),
// Timer for resend (optional)
Text(
'Resend available in 2:30',
style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
),
SizedBox(height: 40),
],
SizedBox(height: 20),
TextButton(
onPressed: () {
context.go('/');
},
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
),
],
),
),
),
),
bottomNavigationBar: SizedBox(
height: 130,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
context.go('/onboarding/password');
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Verify Code',
style: TextStyle(fontSize: 16),
),
),
),
SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: () {
context.go('/onboarding/login');
},
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Back to Login',
style: TextStyle(fontSize: 16),
),
),
),
],
),
),
SizedBox(height: 20),
TextButton(
onPressed: () {
context.go('/');
},
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
),
],
),
),
);
}
);
}
}

View File

@@ -87,6 +87,7 @@ class ReceiptController extends ChangeNotifier {
creditPhone: model.receiptData?['creditPhone'] ?? '',
paymentProcessorLabel: model.receiptData?['paymentProcessorLabel'] ?? '',
paymentProcessorName: model.receiptData?['paymentProcessorName'] ?? '',
id: null
);
setLoading(false);

View File

@@ -15,7 +15,6 @@ class TransactionModel {
BillProvider? selectedProvider;
Map<String, dynamic>? receiptData;
FormData formData = FormData(
id: '',
type: '',
billClientId: '',
debitRef: '',
@@ -48,9 +47,9 @@ class TransactionModel {
@freezed
abstract class FormData with _$FormData {
const factory FormData({
String? id,
required String type, // CONFIRM, REQUEST, REVERSE
required String billClientId,
required String id,
required String debitRef,
required String debitCurrency,
required String amount,
@@ -117,7 +116,7 @@ class TransactionController extends ChangeNotifier {
clearFormData() {
model.formData = FormData(
id: '',
id: null,
type: '',
billClientId: '',
debitRef: '',

View File

@@ -16,8 +16,8 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$FormData {
String get type;// CONFIRM, REQUEST, REVERSE
String get billClientId; String get id; String get debitRef; String get debitCurrency; String get amount; String get creditAccount; String? get creditPhone; String? get creditName; String? get creditEmail; String get billName; String? get errorMessage; String get status; String get paymentProcessorLabel; String get paymentProcessorName; String get paymentProcessorImage; String get providerImage; String get providerLabel; String get userId; String? get debitPhone; String? get debitAccount; String? get productUid; String? get trace; String? get authType; String? get charge; String? get gatewayCharge; String? get tax; String? get totalAmount;
String? get id; String get type;// CONFIRM, REQUEST, REVERSE
String get billClientId; String get debitRef; String get debitCurrency; String get amount; String get creditAccount; String? get creditPhone; String? get creditName; String? get creditEmail; String get billName; String? get errorMessage; String get status; String get paymentProcessorLabel; String get paymentProcessorName; String get paymentProcessorImage; String get providerImage; String get providerLabel; String get userId; String? get debitPhone; String? get debitAccount; String? get productUid; String? get trace; String? get authType; String? get charge; String? get gatewayCharge; String? get tax; String? get totalAmount;
/// Create a copy of FormData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -30,16 +30,16 @@ $FormDataCopyWith<FormData> get copyWith => _$FormDataCopyWithImpl<FormData>(thi
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.id, id) || other.id == id)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount));
return identical(this, other) || (other.runtimeType == runtimeType&&other is FormData&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,type,billClientId,id,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]);
int get hashCode => Object.hashAll([runtimeType,id,type,billClientId,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]);
@override
String toString() {
return 'FormData(type: $type, billClientId: $billClientId, id: $id, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)';
return 'FormData(id: $id, type: $type, billClientId: $billClientId, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)';
}
@@ -50,7 +50,7 @@ abstract mixin class $FormDataCopyWith<$Res> {
factory $FormDataCopyWith(FormData value, $Res Function(FormData) _then) = _$FormDataCopyWithImpl;
@useResult
$Res call({
String type, String billClientId, String id, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount
String? id, String type, String billClientId, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount
});
@@ -67,11 +67,11 @@ class _$FormDataCopyWithImpl<$Res>
/// Create a copy of FormData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? billClientId = null,Object? id = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? type = null,Object? billClientId = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,billClientId: null == billClientId ? _self.billClientId : billClientId // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,debitRef: null == debitRef ? _self.debitRef : debitRef // ignore: cast_nullable_to_non_nullable
as String,debitCurrency: null == debitCurrency ? _self.debitCurrency : debitCurrency // ignore: cast_nullable_to_non_nullable
as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
@@ -108,13 +108,13 @@ as String?,
@JsonSerializable()
class _FormData implements FormData {
const _FormData({required this.type, required this.billClientId, required this.id, required this.debitRef, required this.debitCurrency, required this.amount, required this.creditAccount, required this.creditPhone, required this.creditName, required this.creditEmail, required this.billName, required this.errorMessage, required this.status, required this.paymentProcessorLabel, required this.paymentProcessorName, required this.paymentProcessorImage, required this.providerImage, required this.providerLabel, required this.userId, this.debitPhone, this.debitAccount, this.productUid, this.trace, this.authType, this.charge, this.gatewayCharge, this.tax, this.totalAmount});
const _FormData({this.id, required this.type, required this.billClientId, required this.debitRef, required this.debitCurrency, required this.amount, required this.creditAccount, required this.creditPhone, required this.creditName, required this.creditEmail, required this.billName, required this.errorMessage, required this.status, required this.paymentProcessorLabel, required this.paymentProcessorName, required this.paymentProcessorImage, required this.providerImage, required this.providerLabel, required this.userId, this.debitPhone, this.debitAccount, this.productUid, this.trace, this.authType, this.charge, this.gatewayCharge, this.tax, this.totalAmount});
factory _FormData.fromJson(Map<String, dynamic> json) => _$FormDataFromJson(json);
@override final String? id;
@override final String type;
// CONFIRM, REQUEST, REVERSE
@override final String billClientId;
@override final String id;
@override final String debitRef;
@override final String debitCurrency;
@override final String amount;
@@ -154,16 +154,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _FormData&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.id, id) || other.id == id)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _FormData&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.billClientId, billClientId) || other.billClientId == billClientId)&&(identical(other.debitRef, debitRef) || other.debitRef == debitRef)&&(identical(other.debitCurrency, debitCurrency) || other.debitCurrency == debitCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.creditAccount, creditAccount) || other.creditAccount == creditAccount)&&(identical(other.creditPhone, creditPhone) || other.creditPhone == creditPhone)&&(identical(other.creditName, creditName) || other.creditName == creditName)&&(identical(other.creditEmail, creditEmail) || other.creditEmail == creditEmail)&&(identical(other.billName, billName) || other.billName == billName)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.status, status) || other.status == status)&&(identical(other.paymentProcessorLabel, paymentProcessorLabel) || other.paymentProcessorLabel == paymentProcessorLabel)&&(identical(other.paymentProcessorName, paymentProcessorName) || other.paymentProcessorName == paymentProcessorName)&&(identical(other.paymentProcessorImage, paymentProcessorImage) || other.paymentProcessorImage == paymentProcessorImage)&&(identical(other.providerImage, providerImage) || other.providerImage == providerImage)&&(identical(other.providerLabel, providerLabel) || other.providerLabel == providerLabel)&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.debitPhone, debitPhone) || other.debitPhone == debitPhone)&&(identical(other.debitAccount, debitAccount) || other.debitAccount == debitAccount)&&(identical(other.productUid, productUid) || other.productUid == productUid)&&(identical(other.trace, trace) || other.trace == trace)&&(identical(other.authType, authType) || other.authType == authType)&&(identical(other.charge, charge) || other.charge == charge)&&(identical(other.gatewayCharge, gatewayCharge) || other.gatewayCharge == gatewayCharge)&&(identical(other.tax, tax) || other.tax == tax)&&(identical(other.totalAmount, totalAmount) || other.totalAmount == totalAmount));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([runtimeType,type,billClientId,id,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]);
int get hashCode => Object.hashAll([runtimeType,id,type,billClientId,debitRef,debitCurrency,amount,creditAccount,creditPhone,creditName,creditEmail,billName,errorMessage,status,paymentProcessorLabel,paymentProcessorName,paymentProcessorImage,providerImage,providerLabel,userId,debitPhone,debitAccount,productUid,trace,authType,charge,gatewayCharge,tax,totalAmount]);
@override
String toString() {
return 'FormData(type: $type, billClientId: $billClientId, id: $id, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)';
return 'FormData(id: $id, type: $type, billClientId: $billClientId, debitRef: $debitRef, debitCurrency: $debitCurrency, amount: $amount, creditAccount: $creditAccount, creditPhone: $creditPhone, creditName: $creditName, creditEmail: $creditEmail, billName: $billName, errorMessage: $errorMessage, status: $status, paymentProcessorLabel: $paymentProcessorLabel, paymentProcessorName: $paymentProcessorName, paymentProcessorImage: $paymentProcessorImage, providerImage: $providerImage, providerLabel: $providerLabel, userId: $userId, debitPhone: $debitPhone, debitAccount: $debitAccount, productUid: $productUid, trace: $trace, authType: $authType, charge: $charge, gatewayCharge: $gatewayCharge, tax: $tax, totalAmount: $totalAmount)';
}
@@ -174,7 +174,7 @@ abstract mixin class _$FormDataCopyWith<$Res> implements $FormDataCopyWith<$Res>
factory _$FormDataCopyWith(_FormData value, $Res Function(_FormData) _then) = __$FormDataCopyWithImpl;
@override @useResult
$Res call({
String type, String billClientId, String id, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount
String? id, String type, String billClientId, String debitRef, String debitCurrency, String amount, String creditAccount, String? creditPhone, String? creditName, String? creditEmail, String billName, String? errorMessage, String status, String paymentProcessorLabel, String paymentProcessorName, String paymentProcessorImage, String providerImage, String providerLabel, String userId, String? debitPhone, String? debitAccount, String? productUid, String? trace, String? authType, String? charge, String? gatewayCharge, String? tax, String? totalAmount
});
@@ -191,11 +191,11 @@ class __$FormDataCopyWithImpl<$Res>
/// Create a copy of FormData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? billClientId = null,Object? id = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? type = null,Object? billClientId = null,Object? debitRef = null,Object? debitCurrency = null,Object? amount = null,Object? creditAccount = null,Object? creditPhone = freezed,Object? creditName = freezed,Object? creditEmail = freezed,Object? billName = null,Object? errorMessage = freezed,Object? status = null,Object? paymentProcessorLabel = null,Object? paymentProcessorName = null,Object? paymentProcessorImage = null,Object? providerImage = null,Object? providerLabel = null,Object? userId = null,Object? debitPhone = freezed,Object? debitAccount = freezed,Object? productUid = freezed,Object? trace = freezed,Object? authType = freezed,Object? charge = freezed,Object? gatewayCharge = freezed,Object? tax = freezed,Object? totalAmount = freezed,}) {
return _then(_FormData(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as String,billClientId: null == billClientId ? _self.billClientId : billClientId // ignore: cast_nullable_to_non_nullable
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,debitRef: null == debitRef ? _self.debitRef : debitRef // ignore: cast_nullable_to_non_nullable
as String,debitCurrency: null == debitCurrency ? _self.debitCurrency : debitCurrency // ignore: cast_nullable_to_non_nullable
as String,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable

View File

@@ -7,9 +7,9 @@ part of 'transaction_controller.dart';
// **************************************************************************
_FormData _$FormDataFromJson(Map<String, dynamic> json) => _FormData(
id: json['id'] as String?,
type: json['type'] as String,
billClientId: json['billClientId'] as String,
id: json['id'] as String,
debitRef: json['debitRef'] as String,
debitCurrency: json['debitCurrency'] as String,
amount: json['amount'] as String,
@@ -38,9 +38,9 @@ _FormData _$FormDataFromJson(Map<String, dynamic> json) => _FormData(
);
Map<String, dynamic> _$FormDataToJson(_FormData instance) => <String, dynamic>{
'id': instance.id,
'type': instance.type,
'billClientId': instance.billClientId,
'id': instance.id,
'debitRef': instance.debitRef,
'debitCurrency': instance.debitCurrency,
'amount': instance.amount,

View File

@@ -38,7 +38,7 @@ class AppTheme {
const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
cardTheme: CardTheme(
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),