completed first web iteration
This commit is contained in:
25
.metadata
25
.metadata
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
||||
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,26 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: android
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: ios
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: linux
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: macos
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
- platform: web
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: windows
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
###############################################
|
||||
# Build stage: compile Flutter web artifacts. #
|
||||
###############################################
|
||||
FROM ubuntu:22.04 AS build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
FLUTTER_CHANNEL=stable \
|
||||
FLUTTER_VERSION=3.24.3 \
|
||||
FLUTTER_HOME=/opt/flutter
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
xz-utils \
|
||||
zip \
|
||||
libglu1-mesa && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz" \
|
||||
| tar -xJ -C /opt && \
|
||||
ln -s ${FLUTTER_HOME} /usr/local/flutter
|
||||
|
||||
ENV PATH="${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin:${PATH}"
|
||||
|
||||
RUN flutter config --enable-web && \
|
||||
flutter doctor -v
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pubspec.yaml pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
|
||||
COPY . .
|
||||
RUN flutter build web --release
|
||||
|
||||
##########################################
|
||||
# Runtime stage: serve assets via nginx. #
|
||||
##########################################
|
||||
FROM ubuntu:22.04 AS runner
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nginx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Remove default site definition to avoid duplicate configs.
|
||||
RUN rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Provide a minimal nginx site config for the Flutter web assets.
|
||||
RUN printf 'server {\n\
|
||||
listen 80 default_server;\n\
|
||||
listen [::]:80 default_server;\n\
|
||||
root /var/www/html;\n\
|
||||
index index.html;\n\
|
||||
location / {\n\
|
||||
try_files $uri $uri/ /index.html;\n\
|
||||
}\n\
|
||||
}\n' > /etc/nginx/conf.d/flutter.conf
|
||||
|
||||
COPY --from=build /app/build/web /var/www/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<application
|
||||
android:label="Peak"
|
||||
android:label="Velocity"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:forceDarkAllowed="false">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Peak</string>
|
||||
<string name="app_name">Velocity</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
BASE_URL=https://peakapi.qantra.co.zw/api
|
||||
; BASE_URL=http://192.168.100.26:6950/api
|
||||
; BASE_URL=http://10.69.5.204:6950/api
|
||||
; BASE_URL=http://173.212.247.232:6950/api
|
||||
; BASE_URL=http://192.168.1.164:6950/api
|
||||
; BASE_URL=http://192.168.120.160:6950/api
|
||||
; BASE_URL=http://10.10.2.92:6950/api
|
||||
@@ -8,3 +8,4 @@ BASE_URL=https://peakapi.qantra.co.zw/api
|
||||
; BASE_URL=http://10.10.3.92:6950/api
|
||||
CLIENTID=77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com
|
||||
SIMULATE_PAYMENT_SUCCESS=false
|
||||
; SIMULATE_PAYMENT_SUCCESS=true
|
||||
|
||||
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/velocity-animation.gif
Normal file
BIN
assets/velocity-animation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/velocity.png
Normal file
BIN
assets/velocity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
5
devtools_options.yaml
Normal file
5
devtools_options.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Peak</string>
|
||||
<string>Velocity</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Peak</string>
|
||||
<string>Velocity</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
7
lib/interop.dart
Normal file
7
lib/interop.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS('configure')
|
||||
external void configure(String sessionID);
|
||||
|
||||
@JS('showEmbeddedPage')
|
||||
external void showEmbeddedPage();
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/navbar.dart';
|
||||
import 'package:qpay/screens/confirm/confirm_screen.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_screen.dart';
|
||||
import 'package:qpay/screens/integration/integration_screen.dart';
|
||||
@@ -13,9 +14,11 @@ 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';
|
||||
import 'package:qpay/screens/poll/poll_screen.dart';
|
||||
import 'package:qpay/screens/receipt/receipt_screen.dart';
|
||||
import 'package:qpay/screens/recipient/recipients_screen.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'screens/gateway/gateway_web_screen.dart';
|
||||
import 'screens/home/home_screen.dart';
|
||||
import 'screens/history/history_screen.dart';
|
||||
import 'screens/profile_screen.dart';
|
||||
@@ -66,7 +69,7 @@ class _MyAppState extends State<MyApp> {
|
||||
Widget build(BuildContext context) {
|
||||
if (_showSplash) {
|
||||
return MaterialApp(
|
||||
title: 'Peak',
|
||||
title: 'Velocity',
|
||||
theme: AppTheme.lightTheme,
|
||||
themeMode: ThemeMode.light,
|
||||
home: SplashScreen(onSplashComplete: _onSplashComplete),
|
||||
@@ -75,7 +78,7 @@ class _MyAppState extends State<MyApp> {
|
||||
}
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'Peak',
|
||||
title: 'Velocity',
|
||||
theme: AppTheme.lightTheme,
|
||||
themeMode: ThemeMode.light, // Force light mode regardless of OS setting
|
||||
debugShowCheckedModeBanner: false,
|
||||
@@ -112,6 +115,14 @@ class _MyAppState extends State<MyApp> {
|
||||
path: '/gateway',
|
||||
builder: (context, state) => const GatewayScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/gateway-web',
|
||||
builder: (context, state) => const GatewayWebScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/poll',
|
||||
builder: (context, state) => const PollScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/receipt',
|
||||
builder: (context, state) => const ReceiptScreen(),
|
||||
@@ -167,65 +178,3 @@ class _MyAppState extends State<MyApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
const ScaffoldWithNavBar({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/home');
|
||||
break;
|
||||
// case 1:
|
||||
// context.go('/recipients');
|
||||
// break;
|
||||
case 1:
|
||||
context.go('/history');
|
||||
break;
|
||||
// case 2:
|
||||
// context.go('/profile');
|
||||
// break;
|
||||
}
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
// NavigationDestination(
|
||||
// icon: Icon(Icons.contacts_outlined),
|
||||
// selectedIcon: Icon(Icons.contacts),
|
||||
// label: 'Recipients',
|
||||
// ),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'History',
|
||||
),
|
||||
// NavigationDestination(
|
||||
// icon: Icon(Icons.person_outline),
|
||||
// selectedIcon: Icon(Icons.person),
|
||||
// label: 'Profile',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
// if (location.startsWith('/recipients')) return 1;
|
||||
if (location.startsWith('/history')) return 1;
|
||||
// if (location.startsWith('/profile')) return 2;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
5
lib/models/responsive_policy.dart
Normal file
5
lib/models/responsive_policy.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
class ResponsivePolicy {
|
||||
static const int sm = 400;
|
||||
static const int md = 600;
|
||||
static const int lg = 950;
|
||||
}
|
||||
209
lib/navbar.dart
Normal file
209
lib/navbar.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'models/responsive_policy.dart';
|
||||
|
||||
class ScaffoldWithNavBar extends StatefulWidget {
|
||||
const ScaffoldWithNavBar({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
|
||||
}
|
||||
|
||||
class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
|
||||
bool _isLoggedIn = false;
|
||||
late String initials;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
init();
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (prefs.getString("token") != null) {
|
||||
setState(() {
|
||||
_isLoggedIn = true;
|
||||
initials = prefs.getString("initials")!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > ResponsivePolicy.md) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
extended: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0, bottom: 10),
|
||||
child: Image(
|
||||
image: AssetImage('assets/velocity.png'),
|
||||
width: 150
|
||||
),
|
||||
),
|
||||
trailing: _buildLogin(context),
|
||||
labelType: NavigationRailLabelType.none,
|
||||
indicatorColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withAlpha(30),
|
||||
onDestinationSelected: (index) {
|
||||
_handleTap(index, context);
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: _buildNavigationRailDestinations(),
|
||||
),
|
||||
VerticalDivider(
|
||||
thickness: 1,
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||
),
|
||||
Expanded(child: widget.child),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
indicatorColor: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||
onDestinationSelected: (index) {
|
||||
_handleTap(index, context);
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: _buildNavigationDestinations(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<NavigationRailDestination> _buildNavigationRailDestinations() {
|
||||
List<NavigationRailDestination> destinations = [];
|
||||
for (var destination in _buildDestinationData()) {
|
||||
destinations.add(
|
||||
NavigationRailDestination(
|
||||
icon: destination["icon"] as Widget,
|
||||
selectedIcon: destination["selectedIcon"] as Widget,
|
||||
label: Text(destination["label"], style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return destinations;
|
||||
}
|
||||
|
||||
List<NavigationDestination> _buildNavigationDestinations() {
|
||||
List<NavigationDestination> destinations = [];
|
||||
for (var destination in _buildDestinationData()) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: destination["icon"] as Widget,
|
||||
selectedIcon: destination["selectedIcon"] as Widget,
|
||||
label: destination["label"] as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
return destinations;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _buildDestinationData() {
|
||||
return [
|
||||
{
|
||||
"icon": Icon(Icons.home_outlined),
|
||||
"selectedIcon": Icon(Icons.home),
|
||||
"label": 'Home',
|
||||
},
|
||||
{
|
||||
"icon": Icon(Icons.history_outlined),
|
||||
"selectedIcon": Icon(Icons.history),
|
||||
"label": 'History',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildLogin(BuildContext context) {
|
||||
if(_isLoggedIn) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
'Register or Login for more features',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.push('/onboarding/landing');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('Get Started', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap(int index, BuildContext context) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/home');
|
||||
break;
|
||||
// case 1:
|
||||
// context.go('/recipients');
|
||||
// break;
|
||||
case 1:
|
||||
context.go('/history');
|
||||
break;
|
||||
// case 2:
|
||||
// context.go('/profile');
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
// if (location.startsWith('/recipients')) return 1;
|
||||
if (location.startsWith('/history')) return 1;
|
||||
// if (location.startsWith('/profile')) return 2;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -81,11 +82,14 @@ class ConfirmController extends ChangeNotifier {
|
||||
|
||||
transactionController.updateReceiptData(response);
|
||||
|
||||
if (transactionController.model.selectedPaymentProcessor.authType ==
|
||||
"WEB") {
|
||||
if (transactionController.model.selectedPaymentProcessor.authType == "WEB") {
|
||||
if (context.mounted) {
|
||||
if(kIsWeb){
|
||||
context.push('/gateway-web');
|
||||
}else {
|
||||
context.push('/gateway');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await pollTransaction(transactionController.model.receiptData?['id']);
|
||||
context.push('/integration');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/confirm/confirm_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -88,6 +89,13 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -338,6 +346,10 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -76,15 +76,22 @@ class GatewayController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> pollTransaction(String uid) async {
|
||||
// only poll for 5 minutes
|
||||
int max = 30;
|
||||
while (!model.isCancelled) {
|
||||
max++;
|
||||
// Check cancellation flag instead of true
|
||||
// Wait 5 seconds before the next poll
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
// Check again after delay in case it was cancelled during the delay
|
||||
if (model.isCancelled) break;
|
||||
|
||||
await poll(uid);
|
||||
|
||||
if(max > 30) {
|
||||
model.isCancelled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -17,6 +15,7 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@@ -24,20 +23,15 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
// https://na.gateway.mastercard.com/checkout/pay/SESSION0002220827459I8059951J88?checkoutVersion=1.0.0
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
_initWebView(url);
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if(simulate) {
|
||||
sleep(Duration(seconds: 10));
|
||||
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
||||
_webViewController.loadRequest(Uri.parse(targetUrl));
|
||||
}
|
||||
_initWebView();
|
||||
}
|
||||
|
||||
void _initWebView(String url) {
|
||||
_webViewController =
|
||||
WebViewController()
|
||||
void _initWebView() async {
|
||||
_webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
@@ -55,14 +49,31 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
onUrlChange: (UrlChange url) async {
|
||||
print("URL Changed to ${url.url!}");
|
||||
|
||||
if(url.url!.contains("/checkout/receipt/")){
|
||||
_navigateWhenDone(url.url!);
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(seconds: 10));
|
||||
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
||||
_webViewController.loadRequest(Uri.parse(targetUrl));
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Payment was successful, please wait while we process your order."),
|
||||
content: Text(
|
||||
"Payment was successful, please wait while we process your order.",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
@@ -78,10 +89,6 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -94,8 +101,7 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Complete your payment'),
|
||||
leading:
|
||||
context.canPop()
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
@@ -112,9 +118,12 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return controller.model.isLoading
|
||||
? const Center(child: CircularProgressIndicator(strokeWidth: 5))
|
||||
: WebViewWidget(controller: _webViewController);
|
||||
: SizedBox.expand(
|
||||
child: WebViewWidget(controller: _webViewController),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/interop.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
class GatewayWebScreen extends StatefulWidget {
|
||||
const GatewayWebScreen({super.key});
|
||||
|
||||
@override
|
||||
State<GatewayWebScreen> createState() => _GatewayWebScreenState();
|
||||
}
|
||||
|
||||
class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
}
|
||||
|
||||
// A function to set up the ResizeObserver
|
||||
void setupAttachmentObserver(web.Element element) {
|
||||
final observer = web.ResizeObserver(
|
||||
(JSArray entries, JSAny observer) {
|
||||
// The element has been laid out (and thus attached to the DOM)
|
||||
onElementAttached(element);
|
||||
|
||||
// Disconnect the observer once the action is done
|
||||
(observer as web.ResizeObserver).disconnect();
|
||||
}.toJS,
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
// Called after `element` is attached to the DOM.
|
||||
void onElementAttached(web.Element element) {
|
||||
// Your code to execute after the element is in the DOM goes here.
|
||||
print('Element with ID ${element.id} is attached to the DOM.');
|
||||
// You can now safely query the DOM or call JavaScript functions that rely
|
||||
// on the element being present.
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
initIframeView();
|
||||
});
|
||||
}
|
||||
|
||||
void initIframeView() async {
|
||||
final uri = Uri.parse(url);
|
||||
// 2. The pathSegments property returns a list of the parts of the path.
|
||||
// For "checkout/pay/SESSION...", the segments are ["checkout", "pay", "SESSION..."].
|
||||
final pathSegments = uri.pathSegments;
|
||||
// 3. Check if there are any segments and return the last one,
|
||||
// which is the session ID in this URL structure.
|
||||
if (pathSegments.isNotEmpty) {
|
||||
sessionID = pathSegments.last;
|
||||
|
||||
configure(sessionID!);
|
||||
}
|
||||
showEmbeddedPage();
|
||||
|
||||
Future.delayed(const Duration(seconds: 10), () async {
|
||||
web.HTMLIFrameElement element = fetchIframeFromDom(
|
||||
"hc-comms-layer-iframe",
|
||||
)!;
|
||||
print(element);
|
||||
element.onload = (JSAny event) {
|
||||
// This is called every time a new page loads in the iframe
|
||||
print(event);
|
||||
|
||||
try {
|
||||
final newLocation = element.contentWindow?.location.href;
|
||||
print('Iframe navigated to: $newLocation');
|
||||
// Perform actions based on the new URL
|
||||
|
||||
_navigateWhenDone(newLocation!);
|
||||
} catch (e) {
|
||||
// Handle potential (though unlikely for same-origin) security errors
|
||||
print('Could not access iframe location: $e');
|
||||
}
|
||||
}.toJS;
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
_navigateWhenDone("/checkout/receipt/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
web.HTMLIFrameElement? fetchIframeFromDom(String id) {
|
||||
final element = web.window.document.querySelector('#$id');
|
||||
if (element is web.HTMLIFrameElement) {
|
||||
print('Found iframe in DOM: ${element.id}');
|
||||
print(element.toString());
|
||||
return element;
|
||||
}
|
||||
|
||||
print('Could not find iframe with id: $id in the DOM.');
|
||||
return null;
|
||||
}
|
||||
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Payment was successful, please wait while we process your order.",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Complete your payment'),
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, child) {
|
||||
if (controller.model.status == 'SUCCESS') {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: ResponsivePolicy.md.toDouble(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.black87.withAlpha(20),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Once done on this page, check transaction status',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.push('/poll');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Check status',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: constraints.maxHeight + 200,
|
||||
child: HtmlElementView.fromTagName(
|
||||
tagName: 'div',
|
||||
onElementCreated: (element) {
|
||||
final divElement =
|
||||
element as web.HTMLDivElement;
|
||||
divElement.id = "embed-target";
|
||||
setupAttachmentObserver(element);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class HistoryController extends ChangeNotifier {
|
||||
"debitPhone": "",
|
||||
"debitAccount": "",
|
||||
"debitCurrency": "USD",
|
||||
"debitRef": "peak",
|
||||
"debitRef": "Velocity",
|
||||
"creditPhone": "",
|
||||
"creditAccount": "07088597534",
|
||||
"billClientId": "powertel_zesa",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -23,7 +24,6 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Map<String, String> _queryParams = {};
|
||||
final List<Animation<Offset>> _alignListAnimations = [];
|
||||
final int _pageSize = 8;
|
||||
|
||||
@override
|
||||
@@ -38,21 +38,9 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
);
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
vsync: this,
|
||||
)..forward();
|
||||
|
||||
List tranListIndices = [0, 1];
|
||||
for (int i = 0; i < tranListIndices.length; i++) {
|
||||
_alignListAnimations.add(
|
||||
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.075 * i, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setupData() async {
|
||||
@@ -90,9 +78,19 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(
|
||||
constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toString()
|
||||
: constraints.maxWidth.toString(),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchField(controller),
|
||||
SizedBox(height: 10),
|
||||
if (controller.model.transactions.isEmpty)
|
||||
Column(
|
||||
children: [
|
||||
@@ -109,22 +107,30 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
...controller.model.transactions.map(
|
||||
(e) => _buildTransactionItem(e, context),
|
||||
(e) =>
|
||||
_buildTransactionItem(e, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (controller.model.pageableModel != null &&
|
||||
if (controller.model.pageableModel !=
|
||||
null &&
|
||||
controller.model.pageableModel!.number <
|
||||
controller.model.pageableModel!.totalPages)
|
||||
controller
|
||||
.model
|
||||
.pageableModel!
|
||||
.totalPages)
|
||||
OutlinedButton(
|
||||
child: Text('Load more'),
|
||||
onPressed: () {
|
||||
controller.getTransactions({
|
||||
'userId': prefs.getString("userId")!,
|
||||
'userId': prefs.getString(
|
||||
"userId",
|
||||
)!,
|
||||
'page':
|
||||
(controller
|
||||
.model
|
||||
@@ -145,6 +151,10 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -191,8 +201,7 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
).colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
suffixIcon:
|
||||
_searchController.text.isNotEmpty
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
@@ -227,8 +236,17 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
}
|
||||
|
||||
Widget _buildTransactionItem(Map<String, dynamic> e, BuildContext context) {
|
||||
int index = controller.model.transactions.indexOf(e);
|
||||
Animation<Offset> animation =
|
||||
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.175 * index, 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
return SlideTransition(
|
||||
position: _alignListAnimations[0],
|
||||
position: animation,
|
||||
child: Skeletonizer(
|
||||
enabled: controller.model.isLoading,
|
||||
child: InkWell(
|
||||
@@ -260,16 +278,15 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Icon(
|
||||
e['status'] == 'SUCCESS'
|
||||
e['integrationStatus'] == 'SUCCESS'
|
||||
? Icons.check_circle
|
||||
: e['status'] == 'PENDING'
|
||||
: e['integrationStatus'] == 'PENDING'
|
||||
? Icons.pending
|
||||
: Icons.cancel,
|
||||
size: 16,
|
||||
color:
|
||||
e['status'] == 'SUCCESS'
|
||||
color: e['integrationStatus'] == 'SUCCESS'
|
||||
? Colors.green
|
||||
: e['status'] == 'PENDING'
|
||||
: e['integrationStatus'] == 'PENDING'
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
),
|
||||
|
||||
@@ -104,6 +104,9 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
Future<void> getProviders() async {
|
||||
try {
|
||||
model.filterableProviders = getFakeProviders();
|
||||
model.transactions = getFakeTransactions();
|
||||
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -138,6 +141,7 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
Future<void> getTransactions(String userId) async {
|
||||
model.isLoading = true;
|
||||
// model.transactions = getFakeTransactions();
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
@@ -147,8 +151,9 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
PageableModel pageableModel = PageableModel.fromJson(response);
|
||||
|
||||
model.transactions =
|
||||
pageableModel.content.map((e) => e as Map<String, dynamic>).toList();
|
||||
model.transactions = pageableModel.content
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
_showErrorSnackBar(
|
||||
@@ -165,8 +170,6 @@ class HomeController extends ChangeNotifier {
|
||||
Future<void> getCategories() async {
|
||||
// activate skeletonizer
|
||||
model.categories = getFakeCategoryData();
|
||||
model.transactions = getFakeTransactions();
|
||||
// model.filterableProviders = getFakeProviders();
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -200,8 +203,7 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
model.selectedCategory = category;
|
||||
|
||||
model.filterableProviders =
|
||||
model.providers
|
||||
model.filterableProviders = model.providers
|
||||
.where((element) => element.category == category.label)
|
||||
.toList();
|
||||
|
||||
@@ -224,7 +226,7 @@ class HomeController extends ChangeNotifier {
|
||||
"debitPhone": "",
|
||||
"debitAccount": "",
|
||||
"debitCurrency": "USD",
|
||||
"debitRef": "peak",
|
||||
"debitRef": "Velocity",
|
||||
"creditPhone": "",
|
||||
"creditAccount": "07088597534",
|
||||
"billClientId": "powertel_zesa",
|
||||
@@ -242,21 +244,21 @@ class HomeController extends ChangeNotifier {
|
||||
List<BillProvider> getFakeProviders() {
|
||||
return [
|
||||
BillProvider(
|
||||
clientId: '1',
|
||||
name: 'ZESA',
|
||||
description: 'Pay your electricity bills easily',
|
||||
clientId: 'econet_airtime',
|
||||
name: 'Econet Airtime',
|
||||
description: 'Econet Airtime',
|
||||
requiresAccount: false,
|
||||
requiresAmount: false,
|
||||
requiresAmount: true,
|
||||
requiresAmountFromMerchant: false,
|
||||
requiresPhone: false,
|
||||
requiresReversal: false,
|
||||
requiresPhone: true,
|
||||
requiresReversal: true,
|
||||
additionalDataString: '',
|
||||
processorType: '',
|
||||
uid: '',
|
||||
processorType: 'AIRTIME',
|
||||
uid: '78f1f497-c9eb-401c-b22c-884756e68e40',
|
||||
image: 'econet.png',
|
||||
label: '',
|
||||
category: '',
|
||||
accountFieldName: '',
|
||||
label: 'ECONET',
|
||||
category: 'ECONET',
|
||||
accountFieldName: 'Phone Number',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
import 'package:qpay/screens/home/home_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
@@ -19,7 +20,6 @@ class HomeScreen extends StatefulWidget {
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<AlignmentGeometry> _alignAnimation;
|
||||
late TransactionController transactionController;
|
||||
late SharedPreferences prefs;
|
||||
late HomeController homeController;
|
||||
@@ -29,35 +29,15 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
bool _isLoggedIn = false;
|
||||
String _selectedFilter = 'All';
|
||||
|
||||
final List<Animation<Offset>> _animations = [];
|
||||
final List<Animation<Offset>> _alignListAnimations = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
vsync: this,
|
||||
)..forward();
|
||||
|
||||
List tranListIndices = [0, 1];
|
||||
for (int i = 0; i < tranListIndices.length; i++) {
|
||||
_alignListAnimations.add(
|
||||
Tween<Offset>(begin: Offset(0, -0.25), 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();
|
||||
}
|
||||
|
||||
@@ -77,21 +57,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
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("token") != null) {
|
||||
@@ -147,38 +112,44 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
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),
|
||||
leading: _isLoggedIn ? Container(
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 30),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: (constraints.maxWidth > ResponsivePolicy.lg)
|
||||
? ResponsivePolicy.lg.toDouble()
|
||||
: constraints.maxWidth, // Set the maximum width here
|
||||
),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: (constraints.maxWidth < ResponsivePolicy.md)
|
||||
? Image(
|
||||
image: AssetImage('assets/velocity.png'),
|
||||
width: 150,
|
||||
) : SizedBox(),
|
||||
actions: [
|
||||
_isLoggedIn
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: IconButton(
|
||||
icon: Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
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),
|
||||
Theme.of(context).colorScheme.tertiary
|
||||
.withValues(alpha: 0.05),
|
||||
],
|
||||
stops: const [0.3, 0.7],
|
||||
begin: Alignment.topRight,
|
||||
@@ -197,8 +168,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
) : SizedBox(),
|
||||
actions: [
|
||||
)
|
||||
: SizedBox(),
|
||||
if (_isLoggedIn)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.turn_slight_right_sharp),
|
||||
@@ -208,18 +179,24 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Confirm Logout'),
|
||||
content: const Text('Are you sure you want to log out?'),
|
||||
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
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop(); // Dismiss the dialog
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Logout'),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(); // Dismiss the dialog
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop(); // Dismiss the dialog
|
||||
prefs.remove("token");
|
||||
setState(() {
|
||||
_isLoggedIn = false;
|
||||
@@ -240,95 +217,61 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await homeController.getTransactions(
|
||||
prefs.getString("userId")!,
|
||||
);
|
||||
},
|
||||
child: ListenableBuilder(
|
||||
listenable: homeController,
|
||||
builder: (context, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!_isLoggedIn) _buildLogin(homeController),
|
||||
// if (_isLoggedIn) _buildAccounts(homeController),
|
||||
_buildFilterChips(homeController),
|
||||
SizedBox(height: 10),
|
||||
Column(
|
||||
children: [
|
||||
...homeController.model.filterableProviders.map(
|
||||
(e) => _buildProviderItem(e, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Recent Activity',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
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)
|
||||
Column(
|
||||
children: [
|
||||
ListView(
|
||||
if (constraints.maxWidth > ResponsivePolicy.md) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 50,
|
||||
mainAxisSpacing: 50,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
...homeController.model.transactions.map(
|
||||
(e) => _buildTransactionItem(e, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (homeController.model.transactions.length >
|
||||
2)
|
||||
OutlinedButton(
|
||||
child: Text('View All'),
|
||||
onPressed: () {
|
||||
context.go('/history');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
children: [_buildProviders(), _buildTransactions()],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProviders(),
|
||||
SizedBox(height: 10),
|
||||
_buildTransactions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogin(HomeController controller) {
|
||||
Widget _buildLogin() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -350,7 +293,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Theme.of(context).colorScheme.primary,
|
||||
decorationColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
@@ -376,26 +321,40 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Divider(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAccounts(HomeController controller) {
|
||||
Widget _buildAccounts() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
...controller.model.accounts.map(
|
||||
(e) => Row(
|
||||
...homeController.model.accounts.map((e) => _buildAccountItem(e)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAccountItem(Account e) {
|
||||
int index = homeController.model.accounts.indexOf(e);
|
||||
Animation<Offset> animation =
|
||||
Tween<Offset>(begin: Offset(0, -0.20), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.40 * index.toDouble(), 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
Skeletonizer(
|
||||
enabled: controller.model.isLoading,
|
||||
enabled: homeController.model.isLoading,
|
||||
child: SlideTransition(
|
||||
position: _alignListAnimations[0],
|
||||
position: animation,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
@@ -408,9 +367,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.black87.withAlpha(20),
|
||||
),
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
@@ -468,8 +425,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary
|
||||
.withValues(alpha: 0.15),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
@@ -486,18 +444,73 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactions() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Recent Activity',
|
||||
style: TextStyle(fontSize: 16, 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)
|
||||
Column(
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
...homeController.model.transactions.map(
|
||||
(e) => _buildTransactionItem(e, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (homeController.model.transactions.length > 2)
|
||||
OutlinedButton(
|
||||
child: Text('View All'),
|
||||
onPressed: () {
|
||||
context.go('/history');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionItem(Map<String, dynamic> e, BuildContext context) {
|
||||
int index = homeController.model.transactions.indexOf(e);
|
||||
Animation<Offset> animation =
|
||||
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.175 * index, 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
return SlideTransition(
|
||||
position: _alignListAnimations[0],
|
||||
position: animation,
|
||||
child: Skeletonizer(
|
||||
enabled: homeController.model.isLoading,
|
||||
child: InkWell(
|
||||
@@ -535,8 +548,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
? Icons.pending
|
||||
: Icons.cancel,
|
||||
size: 16,
|
||||
color:
|
||||
e['integrationStatus'] == 'SUCCESS'
|
||||
color: e['integrationStatus'] == 'SUCCESS'
|
||||
? Colors.green
|
||||
: e['integrationStatus'] == 'PENDING'
|
||||
? Colors.orange
|
||||
@@ -569,13 +581,35 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProviders() {
|
||||
return Column(
|
||||
children: [
|
||||
if (!_isLoggedIn) _buildLogin(),
|
||||
SizedBox(height: 10),
|
||||
// if (_isLoggedIn) _buildAccounts(homeController),
|
||||
// _buildFilterChips(homeController),
|
||||
...homeController.model.filterableProviders.map(
|
||||
(e) => _buildProviderItem(e, context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Column _buildProviderItem(BillProvider e, BuildContext context) {
|
||||
int index = homeController.model.filterableProviders.indexOf(e);
|
||||
Animation<Offset> animation =
|
||||
Tween<Offset>(begin: Offset(0, -0.20), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.40 * index.toDouble(), 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
Skeletonizer(
|
||||
enabled: homeController.model.isLoading,
|
||||
child: SlideTransition(
|
||||
position: _animations[homeController.model.providers.indexOf(e)],
|
||||
position: animation,
|
||||
child: Container(
|
||||
decoration: getBoxDecoration(e.clientId),
|
||||
child: InkWell(
|
||||
@@ -676,11 +710,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
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)
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.secondary.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
@@ -698,6 +729,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 600),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -724,7 +757,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
).colorScheme.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Image(image: AssetImage("assets/peak.png"), width: 45),
|
||||
child: Image(
|
||||
image: AssetImage("assets/favicon.png"),
|
||||
width: 45,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
@@ -808,8 +844,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -829,6 +866,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -63,12 +63,14 @@ class IntegrationController extends ChangeNotifier {
|
||||
model.status = response['status'];
|
||||
transactionController.updateReceiptData(response);
|
||||
|
||||
gatewayController.poll(transactionController.model.confirmationData['id']);
|
||||
} else {
|
||||
model.status = response['status'];
|
||||
model.errorMessage = response['errorMessage'];
|
||||
_showErrorSnackBar(response['errorMessage']);
|
||||
await gatewayController.poll(transactionController.model.confirmationData['id']);
|
||||
}
|
||||
|
||||
// regardless of poll result we proceed to receipt
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/receipt');
|
||||
});
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -68,6 +69,12 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(50),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -82,11 +89,8 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
||||
style: TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/peak-animation.gif', width: 150),
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
SizedBox(height: 50,),
|
||||
Image.asset('assets/velocity-animation.gif', width: 150),
|
||||
SizedBox(height: 50),
|
||||
if(controller.model.status == 'FAILED')
|
||||
Column(
|
||||
@@ -112,6 +116,9 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
|
||||
class CompleteScreen extends StatefulWidget {
|
||||
@@ -16,6 +17,14 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -31,7 +40,7 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Thank you for registering with Peak payments',
|
||||
'Thank you for registering with Velocity payments',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -40,15 +49,7 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
||||
'Tap Login to get started',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
@@ -67,6 +68,11 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
||||
SizedBox(height: 5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
|
||||
class LandingScreen extends StatefulWidget {
|
||||
@@ -16,13 +17,21 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Welcome to Peak',
|
||||
'Welcome to Velocity',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -36,20 +45,14 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/landing.png', width: 300),
|
||||
Image.asset('assets/peak.png', width: 100),
|
||||
Image.asset('assets/velocity.png', width: 150),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Register or login to get started',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
SizedBox(height: 50),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
@@ -65,7 +68,7 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
@@ -75,7 +78,7 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
@@ -83,6 +86,13 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
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:google_sign_in_web/web_only.dart' as web;
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
@@ -17,6 +21,8 @@ class LoginScreen extends StatefulWidget {
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
late SharedPreferences prefs;
|
||||
late GoogleSignIn googleSignIn;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _loading = false;
|
||||
|
||||
@@ -30,22 +36,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loginController = LoginController(context);
|
||||
|
||||
googleSignIn = GoogleSignIn.instance;
|
||||
if(kIsWeb){
|
||||
googleSignIn.initialize();
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
|
||||
}else {
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -85,6 +92,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
?.authorizationClient
|
||||
.authorizationForScopes(scopes);
|
||||
// #enddocregion CheckAuthorization
|
||||
|
||||
print(user);
|
||||
saveUser(user as GoogleSignInAccount);
|
||||
|
||||
await _loginController.login(user.email, user.id);
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationError(Object e) async {
|
||||
@@ -105,6 +118,14 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -120,11 +141,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Welcome back to Peak',
|
||||
'Welcome back to Velocity',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
SizedBox(height: 30),
|
||||
Image.asset('assets/password.png', width: 300),
|
||||
Text(
|
||||
'Tap on your preferred social media platform to get started',
|
||||
@@ -154,13 +175,40 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
// Social Login Buttons
|
||||
Row(
|
||||
if(GoogleSignIn.instance.supportsAuthenticate())
|
||||
_buildGoogleButton()
|
||||
else ...<Widget>[
|
||||
if (kIsWeb)
|
||||
web.renderButton()
|
||||
],
|
||||
SizedBox(height: 40),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoogleButton(){
|
||||
return Skeletonizer(
|
||||
enabled: _loading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
await signInWithGoogle();
|
||||
|
||||
onPressed: () {
|
||||
signInWithGoogle();
|
||||
},
|
||||
icon: Image.asset(
|
||||
'assets/google.png',
|
||||
@@ -177,28 +225,10 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 5),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/phone/phone_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -82,6 +83,14 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -109,15 +118,6 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
SizedBox(height: 20),
|
||||
_buildPhoneField(),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Skeletonizer(
|
||||
@@ -143,7 +143,7 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
@@ -157,7 +157,7 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
@@ -166,6 +166,12 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
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:google_sign_in_web/web_only.dart' as web;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -17,6 +20,7 @@ class RegisterScreen extends StatefulWidget {
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
late OnboardingController onboardingController;
|
||||
late GoogleSignIn googleSignIn;
|
||||
|
||||
bool _loading = false;
|
||||
|
||||
@@ -32,17 +36,22 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
listen: false,
|
||||
);
|
||||
// onboardingController.resetState();
|
||||
|
||||
googleSignIn = GoogleSignIn.instance;
|
||||
if(kIsWeb){
|
||||
googleSignIn.initialize();
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
|
||||
}else {
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -51,9 +60,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationEvent(
|
||||
GoogleSignInAuthenticationEvent event,
|
||||
) async {
|
||||
Future<void> _handleAuthenticationEvent(GoogleSignInAuthenticationEvent event) async {
|
||||
// #docregion CheckAuthorization
|
||||
final GoogleSignInAccount? user = // ...
|
||||
// #enddocregion CheckAuthorization
|
||||
@@ -68,6 +75,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
?.authorizationClient
|
||||
.authorizationForScopes(scopes);
|
||||
// #enddocregion CheckAuthorization
|
||||
print(user);
|
||||
onboardingController.updateGoogleUser(user as GoogleSignInAccount);
|
||||
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationError(Object e) async {
|
||||
@@ -83,6 +93,14 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -98,7 +116,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
"Let's start by getting some of your details online to get you started",
|
||||
"Let's start by getting some of your online details to get you started",
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -127,34 +145,12 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
],
|
||||
),
|
||||
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),
|
||||
if(GoogleSignIn.instance.supportsAuthenticate())
|
||||
_buildGoogleButton()
|
||||
else ...<Widget>[
|
||||
if (kIsWeb)
|
||||
web.renderButton()
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if(onboardingController.model.googleUser != null)
|
||||
@@ -177,16 +173,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
Text("Tap Proceed to continue", style: TextStyle(fontSize: 18),)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 50),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
@@ -222,7 +209,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
@@ -235,18 +222,55 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoogleButton(){
|
||||
return 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/verify/verify_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -59,6 +60,14 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -146,14 +155,6 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
// 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(
|
||||
@@ -179,12 +180,6 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
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),
|
||||
@@ -197,13 +192,6 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
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),
|
||||
@@ -214,15 +202,21 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class PayController extends ChangeNotifier {
|
||||
transactionController.model.formData.copyWith(
|
||||
type: "CONFIRM",
|
||||
billClientId: model.selectedProvider?.clientId ?? '',
|
||||
debitRef: 'peak',
|
||||
debitRef: 'Velocity',
|
||||
debitCurrency: 'USD',
|
||||
creditAccount: transactionController.model.formData.creditAccount,
|
||||
creditPhone: transactionController.model.formData.creditPhone,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/pay/pay_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@@ -178,6 +179,22 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -188,7 +205,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -229,8 +246,10 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Divider(color: Theme.of(context).colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -278,6 +297,10 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
126
lib/screens/poll/poll_screen.dart
Normal file
126
lib/screens/poll/poll_screen.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class PollScreen extends StatefulWidget {
|
||||
const PollScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PollScreen> createState() => _PollScreenState();
|
||||
}
|
||||
|
||||
class _PollScreenState extends State<PollScreen> {
|
||||
late GatewayController controller;
|
||||
late TransactionController transactionController;
|
||||
|
||||
bool integrating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
transactionController = Provider.of<TransactionController>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
controller.pollTransaction(transactionController.model.confirmationData['id']);
|
||||
}
|
||||
|
||||
void _showErrorSnackBar(String? message) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message.toString()),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.deepOrange,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Completing your transaction'),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, child) {
|
||||
if (controller.model.status == 'SUCCESS') {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(50),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'We\'re checking with our gateway if your transaction was successful. This won\'t take long',
|
||||
style: TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 50,),
|
||||
Image.asset('assets/velocity-animation.gif', width: 150),
|
||||
SizedBox(height: 50),
|
||||
if(controller.model.status == 'FAILED')
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'We failed to check your transaction status',
|
||||
style: TextStyle(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 20,),
|
||||
Skeletonizer(
|
||||
enabled: controller.model.isLoading,
|
||||
child: OutlinedButton(
|
||||
child: Text('Retry'),
|
||||
onPressed: () {
|
||||
controller.pollTransaction(transactionController.model.confirmationData['id']);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -59,10 +60,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.1),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
_slideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.1), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
@@ -87,9 +86,13 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
|
||||
_retry() async {
|
||||
receiptController.setLoading(true);
|
||||
await receiptController.doIntegration(receiptController.model.receiptData?["id"]);
|
||||
await receiptController.doIntegration(
|
||||
receiptController.model.receiptData?["id"],
|
||||
);
|
||||
if (receiptController.model.status == "SUCCESS") {
|
||||
receiptController.getReceiptData(transactionController.model.receiptData?["id"]);
|
||||
receiptController.getReceiptData(
|
||||
receiptController.model.receiptData?["id"],
|
||||
);
|
||||
}
|
||||
receiptController.setLoading(false);
|
||||
}
|
||||
@@ -98,7 +101,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
receiptController.setLoading(true);
|
||||
await gatewayController.poll(receiptController.model.receiptData?["id"]);
|
||||
if (gatewayController.model.status == "SUCCESS") {
|
||||
receiptController.getReceiptData(transactionController.model.receiptData?["id"]);
|
||||
receiptController.getReceiptData(
|
||||
transactionController.model.receiptData?["id"],
|
||||
);
|
||||
if (transactionController.model.receiptData?["pollingStatus"]) {
|
||||
receiptController.setLoading(false);
|
||||
return; // no need to proceed if we've already polled successfully
|
||||
@@ -131,8 +136,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
expandedHeight: 50.0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
floating: true,
|
||||
leading:
|
||||
context.canPop()
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
@@ -158,11 +162,20 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width:
|
||||
constraints.maxWidth >
|
||||
ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Skeletonizer(
|
||||
enabled: receiptController.model.isLoading,
|
||||
enabled:
|
||||
receiptController.model.isLoading,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -170,21 +183,29 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.1),
|
||||
Theme.of(context).colorScheme.tertiary
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withOpacity(0.05),
|
||||
Theme.of(context).colorScheme.tertiary
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiary
|
||||
.withOpacity(0.15),
|
||||
],
|
||||
stops: [0.0, 0.5, 1.0],
|
||||
),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius:
|
||||
BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context)
|
||||
@@ -241,7 +262,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
@@ -249,9 +271,13 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["amount"]
|
||||
.toStringAsFixed(2) ??
|
||||
.toStringAsFixed(
|
||||
2,
|
||||
) ??
|
||||
"",
|
||||
style: TextStyle(fontSize: 26),
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -265,24 +291,36 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["billName"] ??
|
||||
"",
|
||||
style: TextStyle(fontSize: 16),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["createdAt"] != null ?
|
||||
DateFormat.yMMMd().add_jm().format(DateTime.parse(
|
||||
.receiptData?["createdAt"] !=
|
||||
null
|
||||
? DateFormat.yMMMd()
|
||||
.add_jm()
|
||||
.format(
|
||||
DateTime.parse(
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["createdAt"])) : "",
|
||||
style: TextStyle(fontSize: 15),
|
||||
.receiptData?["createdAt"],
|
||||
),
|
||||
)
|
||||
: "",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
MainAxisAlignment
|
||||
.spaceEvenly,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
@@ -290,13 +328,19 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
shape:
|
||||
BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -311,14 +355,13 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth:
|
||||
2.5,
|
||||
valueColor: AlwaysStoppedAnimation<
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<
|
||||
Color
|
||||
>(
|
||||
Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -331,13 +374,11 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.repeat,
|
||||
color:
|
||||
Theme.of(
|
||||
Icons
|
||||
.repeat,
|
||||
color: Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary,
|
||||
).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
@@ -347,7 +388,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
"Repeat",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight:
|
||||
FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -358,13 +400,19 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
shape:
|
||||
BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -377,7 +425,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
)
|
||||
.colorScheme
|
||||
.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
@@ -387,26 +437,40 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
"Share",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight:
|
||||
FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if(receiptController.model.receiptData?["integrationStatus"] != "SUCCESS" &&
|
||||
receiptController.model.receiptData?["paymentStatus"] == "SUCCESS")
|
||||
if (receiptController
|
||||
.model
|
||||
.receiptData?["integrationStatus"] !=
|
||||
"SUCCESS" &&
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["paymentStatus"] ==
|
||||
"SUCCESS")
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
shape: BoxShape
|
||||
.circle,
|
||||
color:
|
||||
Colors.white,
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -416,8 +480,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.redo,
|
||||
color:
|
||||
Theme.of(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
size: 24,
|
||||
@@ -429,25 +492,37 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
"Retry",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if(receiptController.model.receiptData?["pollingStatus"] != "SUCCESS")
|
||||
if (receiptController
|
||||
.model
|
||||
.receiptData?["pollingStatus"] !=
|
||||
"SUCCESS")
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
shape: BoxShape
|
||||
.circle,
|
||||
color:
|
||||
Colors.white,
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -456,9 +531,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
_check();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.price_check,
|
||||
color:
|
||||
Theme.of(
|
||||
Icons
|
||||
.price_check,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
size: 24,
|
||||
@@ -470,7 +545,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
"Check status",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -487,30 +564,34 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicator: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
color:
|
||||
Theme.of(
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
indicatorPadding: EdgeInsets.all(5),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding:
|
||||
EdgeInsets.all(5),
|
||||
indicatorSize:
|
||||
TabBarIndicatorSize.tab,
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
dividerColor: Colors.transparent,
|
||||
unselectedLabelColor:
|
||||
Colors.grey[600],
|
||||
dividerColor:
|
||||
Colors.transparent,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
"Provider Details",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -518,7 +599,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
child: Text(
|
||||
"Transaction Details",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -527,16 +609,20 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).size.height *
|
||||
MediaQuery.of(
|
||||
context,
|
||||
).size.height *
|
||||
0.5,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
10.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
@@ -552,7 +638,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -560,8 +647,10 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["integrationStatus"] ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style:
|
||||
TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -569,11 +658,17 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
if (transactionController
|
||||
.model
|
||||
.receiptData?["errorMessage"] != null &&
|
||||
transactionController.model.receiptData?["errorMessage"] != "")
|
||||
.receiptData?["errorMessage"] !=
|
||||
null &&
|
||||
transactionController
|
||||
.model
|
||||
.receiptData?["errorMessage"] !=
|
||||
"")
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
@@ -587,22 +682,24 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Text(
|
||||
"Message",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
transactionController
|
||||
.model
|
||||
.receiptData?["errorMessage"] ??
|
||||
transactionController.model.receiptData?["errorMessage"] ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
softWrap: true,
|
||||
textAlign:
|
||||
TextAlign.end,
|
||||
softWrap:
|
||||
true,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -610,11 +707,19 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
if(receiptController.model.receiptData?["integrationStatus"] != "SUCCESS" &&
|
||||
receiptController.model.receiptData?["paymentStatus"] == "SUCCESS")
|
||||
if (receiptController
|
||||
.model
|
||||
.receiptData?["integrationStatus"] !=
|
||||
"SUCCESS" &&
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["paymentStatus"] ==
|
||||
"SUCCESS")
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
@@ -628,22 +733,26 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Text(
|
||||
"Additional action",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("Billing account update failed, please tap on Retry to try again",
|
||||
child: Text(
|
||||
"Billing account update failed, please tap on Retry to try again",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
softWrap: true,
|
||||
|
||||
textAlign:
|
||||
TextAlign.end,
|
||||
softWrap:
|
||||
true,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -651,7 +760,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
SizedBox(height: 10),
|
||||
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
@@ -668,7 +777,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -676,14 +786,18 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["debitRef"] ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style:
|
||||
TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
@@ -699,7 +813,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -707,14 +822,18 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["debitPhone"] ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style:
|
||||
TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
@@ -730,7 +849,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -738,19 +858,62 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["creditAccount"] ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
style:
|
||||
TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Skeletonizer(
|
||||
enabled:
|
||||
receiptController
|
||||
.model
|
||||
.isLoading,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Processor",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
receiptController
|
||||
.model
|
||||
.receiptData?["paymentProcessorName"] ??
|
||||
"",
|
||||
style:
|
||||
TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Divider(
|
||||
color: Colors.grey[300],
|
||||
color:
|
||||
Colors.grey[300],
|
||||
thickness: 1,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (receiptController
|
||||
.model
|
||||
.receiptData?["additionalData"] ==
|
||||
@@ -762,7 +925,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.isLoading,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
"No details found",
|
||||
@@ -775,12 +940,12 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
.model
|
||||
.receiptData?["additionalData"] !=
|
||||
null)
|
||||
...receiptController
|
||||
.model
|
||||
.receiptData?["additionalData"]
|
||||
.map<Widget>((data) {
|
||||
...receiptController.model.receiptData?["additionalData"].map<
|
||||
Widget
|
||||
>((data) {
|
||||
return Skeletonizer(
|
||||
enabled: receiptController
|
||||
enabled:
|
||||
receiptController
|
||||
.model
|
||||
.isLoading,
|
||||
child: Column(
|
||||
@@ -797,15 +962,13 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
data["value"] ??
|
||||
"",
|
||||
style:
|
||||
TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
@@ -818,13 +981,15 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
10.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -837,7 +1002,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -854,7 +1020,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (transactionController
|
||||
.model
|
||||
.receiptData?["charge"] !=
|
||||
@@ -869,7 +1037,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Text(
|
||||
"Our Charge",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
@@ -884,7 +1053,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
) ??
|
||||
"0.00",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -908,7 +1078,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Text(
|
||||
"Gateway Charge",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
@@ -923,7 +1094,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
) ??
|
||||
"0.00",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -947,7 +1119,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
Text(
|
||||
"Tax",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold,
|
||||
@@ -962,7 +1135,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
) ??
|
||||
"0.00",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize:
|
||||
16,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -982,7 +1156,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
FontWeight
|
||||
.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -999,7 +1174,9 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1011,6 +1188,10 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
||||
@@ -83,6 +84,23 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -90,19 +108,24 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Provider", style: TextStyle(fontSize: 16)),
|
||||
Text("Provider",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
Text(
|
||||
controller.model.selectedProvider?.name ?? "",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Divider(color: Theme.of(context).colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildAccountField(controller),
|
||||
const SizedBox(height: 20),
|
||||
@@ -159,6 +182,10 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
||||
_buildRecipientItems(controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import google_sign_in_ios
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
||||
52
pubspec.lock
52
pubspec.lock
@@ -153,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -377,7 +385,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||
@@ -392,6 +400,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -861,6 +877,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.28"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.6"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -869,6 +909,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -950,7 +998,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: qpay
|
||||
description: "Peak payments solutions"
|
||||
description: "Velocity payments solutions"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
@@ -55,6 +55,10 @@ dependencies:
|
||||
gif_view: ^0.4.0
|
||||
google_sign_in: ^7.1.1
|
||||
firebase_core: ^4.1.0
|
||||
url_launcher: ^6.3.2
|
||||
html: ^0.15.6
|
||||
web: ^1.1.1
|
||||
google_sign_in_web: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 85 KiB |
@@ -20,6 +20,7 @@
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="google-signin-client_id" content="77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -30,8 +31,27 @@
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>qpay</title>
|
||||
<title>Velocity</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script src="https://na.gateway.mastercard.com/static/checkout/checkout.min.js">
|
||||
</script>
|
||||
<!-- <script src="https://test-gateway.mastercard.com/static/checkout/checkout.min.js">-->
|
||||
<!-- </script>-->
|
||||
|
||||
<script type="text/javascript">
|
||||
function configure(sessionID) {
|
||||
Checkout.configure({
|
||||
session: {
|
||||
id: sessionID
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEmbeddedPage() {
|
||||
Checkout.showEmbeddedPage('#embed-target');
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
||||
Reference in New Issue
Block a user