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.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
@@ -13,26 +13,11 @@ project_type: app
|
|||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
- 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
|
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||||
- platform: windows
|
|
||||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
|
||||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
|
||||||
|
|
||||||
# User provided section
|
# 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.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<application
|
<application
|
||||||
android:label="Peak"
|
android:label="Velocity"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:forceDarkAllowed="false">
|
android:forceDarkAllowed="false">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Peak</string>
|
<string name="app_name">Velocity</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
BASE_URL=https://peakapi.qantra.co.zw/api
|
BASE_URL=https://peakapi.qantra.co.zw/api
|
||||||
; BASE_URL=http://192.168.100.26:6950/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.1.164:6950/api
|
||||||
; BASE_URL=http://192.168.120.160:6950/api
|
; BASE_URL=http://192.168.120.160:6950/api
|
||||||
; BASE_URL=http://10.10.2.92: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
|
; BASE_URL=http://10.10.3.92:6950/api
|
||||||
CLIENTID=77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com
|
CLIENTID=77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com
|
||||||
SIMULATE_PAYMENT_SUCCESS=false
|
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>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Peak</string>
|
<string>Velocity</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Peak</string>
|
<string>Velocity</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:go_router/go_router.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/confirm/confirm_screen.dart';
|
||||||
import 'package:qpay/screens/gateway/gateway_screen.dart';
|
import 'package:qpay/screens/gateway/gateway_screen.dart';
|
||||||
import 'package:qpay/screens/integration/integration_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/register/register_screen.dart';
|
||||||
import 'package:qpay/screens/onboarding/verify/verify_screen.dart';
|
import 'package:qpay/screens/onboarding/verify/verify_screen.dart';
|
||||||
import 'package:qpay/screens/pay/pay_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/receipt/receipt_screen.dart';
|
||||||
import 'package:qpay/screens/recipient/recipients_screen.dart';
|
import 'package:qpay/screens/recipient/recipients_screen.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
|
import 'screens/gateway/gateway_web_screen.dart';
|
||||||
import 'screens/home/home_screen.dart';
|
import 'screens/home/home_screen.dart';
|
||||||
import 'screens/history/history_screen.dart';
|
import 'screens/history/history_screen.dart';
|
||||||
import 'screens/profile_screen.dart';
|
import 'screens/profile_screen.dart';
|
||||||
@@ -66,7 +69,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_showSplash) {
|
if (_showSplash) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Peak',
|
title: 'Velocity',
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
themeMode: ThemeMode.light,
|
themeMode: ThemeMode.light,
|
||||||
home: SplashScreen(onSplashComplete: _onSplashComplete),
|
home: SplashScreen(onSplashComplete: _onSplashComplete),
|
||||||
@@ -75,7 +78,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'Peak',
|
title: 'Velocity',
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
themeMode: ThemeMode.light, // Force light mode regardless of OS setting
|
themeMode: ThemeMode.light, // Force light mode regardless of OS setting
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
@@ -112,6 +115,14 @@ class _MyAppState extends State<MyApp> {
|
|||||||
path: '/gateway',
|
path: '/gateway',
|
||||||
builder: (context, state) => const GatewayScreen(),
|
builder: (context, state) => const GatewayScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/gateway-web',
|
||||||
|
builder: (context, state) => const GatewayWebScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/poll',
|
||||||
|
builder: (context, state) => const PollScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/receipt',
|
path: '/receipt',
|
||||||
builder: (context, state) => const ReceiptScreen(),
|
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:flutter/material.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -81,10 +82,13 @@ class ConfirmController extends ChangeNotifier {
|
|||||||
|
|
||||||
transactionController.updateReceiptData(response);
|
transactionController.updateReceiptData(response);
|
||||||
|
|
||||||
if (transactionController.model.selectedPaymentProcessor.authType ==
|
if (transactionController.model.selectedPaymentProcessor.authType == "WEB") {
|
||||||
"WEB") {
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.push('/gateway');
|
if(kIsWeb){
|
||||||
|
context.push('/gateway-web');
|
||||||
|
}else {
|
||||||
|
context.push('/gateway');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await pollTransaction(transactionController.model.receiptData?['id']);
|
await pollTransaction(transactionController.model.receiptData?['id']);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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:qpay/screens/confirm/confirm_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
@@ -88,255 +89,266 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
|||||||
position: _slideAnimation,
|
position: _slideAnimation,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Container(
|
return SizedBox(
|
||||||
padding: EdgeInsets.all(15),
|
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||||
decoration: BoxDecoration(
|
ResponsivePolicy.md.toString() :
|
||||||
border: Border.all(
|
constraints.maxWidth.toString()),
|
||||||
color: Theme.of(
|
child: Column(
|
||||||
context,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
).colorScheme.primary.withOpacity(0.3),
|
children: [
|
||||||
),
|
Container(
|
||||||
borderRadius: BorderRadius.circular(10),
|
padding: EdgeInsets.all(15),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
child: Column(
|
color: Theme.of(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
context,
|
||||||
children: [
|
).colorScheme.primary.withOpacity(0.3),
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
width: 1.0,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
padding: EdgeInsets.only(
|
child: Column(
|
||||||
bottom: 4.0,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
), // Adjust spacing
|
children: [
|
||||||
child: Text(
|
Container(
|
||||||
"PROVIDER DETAILS",
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
border: Border(
|
||||||
fontSize: 16,
|
bottom: BorderSide(
|
||||||
fontWeight: FontWeight.bold,
|
width: 1.0,
|
||||||
),
|
color:
|
||||||
),
|
Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
...controller
|
),
|
||||||
.transactionController
|
),
|
||||||
.model
|
padding: EdgeInsets.only(
|
||||||
.confirmationData["additionalData"]
|
bottom: 4.0,
|
||||||
.map<Widget>((data) {
|
), // Adjust spacing
|
||||||
return Column(
|
child: Text(
|
||||||
children: [
|
"PROVIDER DETAILS",
|
||||||
const SizedBox(height: 10),
|
style: TextStyle(
|
||||||
Row(
|
fontSize: 16,
|
||||||
mainAxisAlignment:
|
fontWeight: FontWeight.bold,
|
||||||
MainAxisAlignment.spaceBetween,
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["additionalData"]
|
||||||
|
.map<Widget>((data) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
data["name"] ?? "",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
data["value"] ?? "",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
width: 1.0,
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: 4.0,
|
||||||
|
), // Adjust spacing
|
||||||
|
child: Text(
|
||||||
|
"TRANSACTIONS DETAILS",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Amount",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["amount"]
|
||||||
|
.toString(),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
if (controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["charge"] !=
|
||||||
|
0)
|
||||||
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
data["name"] ?? "",
|
mainAxisAlignment:
|
||||||
style: TextStyle(
|
MainAxisAlignment.spaceBetween,
|
||||||
fontSize: 16,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
Text(
|
||||||
),
|
"Our Charge",
|
||||||
),
|
style: TextStyle(
|
||||||
Text(
|
fontSize: 16,
|
||||||
data["value"] ?? "",
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(fontSize: 16),
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["charge"]
|
||||||
|
.toString(),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
if (controller
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
width: 1.0,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: 4.0,
|
|
||||||
), // Adjust spacing
|
|
||||||
child: Text(
|
|
||||||
"TRANSACTIONS DETAILS",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Amount",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
controller
|
|
||||||
.transactionController
|
|
||||||
.model
|
|
||||||
.confirmationData["amount"]
|
|
||||||
.toString(),
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (controller
|
|
||||||
.transactionController
|
|
||||||
.model
|
|
||||||
.confirmationData["charge"] !=
|
|
||||||
0)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Our Charge",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
controller
|
|
||||||
.transactionController
|
.transactionController
|
||||||
.model
|
.model
|
||||||
.confirmationData["charge"]
|
.confirmationData["gatewayCharge"] !=
|
||||||
.toString(),
|
0)
|
||||||
style: TextStyle(fontSize: 16),
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Gateway Charge",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["gatewayCharge"]
|
||||||
|
.toString(),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
if (controller
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (controller
|
|
||||||
.transactionController
|
|
||||||
.model
|
|
||||||
.confirmationData["gatewayCharge"] !=
|
|
||||||
0)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Gateway Charge",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
controller
|
|
||||||
.transactionController
|
.transactionController
|
||||||
.model
|
.model
|
||||||
.confirmationData["gatewayCharge"]
|
.confirmationData["tax"] !=
|
||||||
.toString(),
|
0)
|
||||||
style: TextStyle(fontSize: 16),
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Tax",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["tax"]
|
||||||
|
.toString(),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
mainAxisAlignment:
|
||||||
const SizedBox(height: 10),
|
MainAxisAlignment.spaceBetween,
|
||||||
],
|
children: [
|
||||||
),
|
Text(
|
||||||
if (controller
|
"Total Amount",
|
||||||
.transactionController
|
style: TextStyle(
|
||||||
.model
|
fontSize: 16,
|
||||||
.confirmationData["tax"] !=
|
fontWeight: FontWeight.bold,
|
||||||
0)
|
),
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Tax",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
controller
|
||||||
|
.transactionController
|
||||||
|
.model
|
||||||
|
.confirmationData["totalAmount"]
|
||||||
|
.toString(),
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
"NB: Your payment provider may charge additional fees.",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
controller
|
],
|
||||||
.transactionController
|
|
||||||
.model
|
|
||||||
.confirmationData["tax"]
|
|
||||||
.toString(),
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Total Amount",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
controller
|
|
||||||
.transactionController
|
|
||||||
.model
|
|
||||||
.confirmationData["totalAmount"]
|
|
||||||
.toString(),
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 5),
|
|
||||||
Text(
|
|
||||||
"NB: Your payment provider may charge additional fees.",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
],
|
_buildPaymentProcessorButton(),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
),
|
Center(
|
||||||
const SizedBox(height: 20),
|
child: OutlinedButton(
|
||||||
_buildPaymentProcessorButton(),
|
child: Text('Cancel'),
|
||||||
const SizedBox(height: 20),
|
onPressed: () {
|
||||||
Center(
|
context.go('/');
|
||||||
child: OutlinedButton(
|
},
|
||||||
child: Text('Cancel'),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
context.go('/');
|
],
|
||||||
},
|
),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -76,15 +76,22 @@ class GatewayController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pollTransaction(String uid) async {
|
Future<void> pollTransaction(String uid) async {
|
||||||
|
// only poll for 5 minutes
|
||||||
|
int max = 30;
|
||||||
while (!model.isCancelled) {
|
while (!model.isCancelled) {
|
||||||
|
max++;
|
||||||
// Check cancellation flag instead of true
|
// Check cancellation flag instead of true
|
||||||
// Wait 5 seconds before the next poll
|
// 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
|
// Check again after delay in case it was cancelled during the delay
|
||||||
if (model.isCancelled) break;
|
if (model.isCancelled) break;
|
||||||
|
|
||||||
await poll(uid);
|
await poll(uid);
|
||||||
|
|
||||||
|
if(max > 30) {
|
||||||
|
model.isCancelled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -17,6 +15,7 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
|||||||
late WebViewController _webViewController;
|
late WebViewController _webViewController;
|
||||||
late GatewayController controller;
|
late GatewayController controller;
|
||||||
late String url;
|
late String url;
|
||||||
|
String? sessionID;
|
||||||
|
|
||||||
bool integrationStarted = false;
|
bool integrationStarted = false;
|
||||||
|
|
||||||
@@ -24,63 +23,71 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = GatewayController(context);
|
controller = GatewayController(context);
|
||||||
|
// https://na.gateway.mastercard.com/checkout/pay/SESSION0002220827459I8059951J88?checkoutVersion=1.0.0
|
||||||
|
|
||||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||||
_initWebView(url);
|
|
||||||
|
_initWebView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initWebView() async {
|
||||||
|
_webViewController = WebViewController()
|
||||||
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
..setNavigationDelegate(
|
||||||
|
NavigationDelegate(
|
||||||
|
onProgress: (int progress) {
|
||||||
|
// Update loading bar.
|
||||||
|
},
|
||||||
|
onPageStarted: (String url) {
|
||||||
|
controller.startLoading();
|
||||||
|
},
|
||||||
|
onPageFinished: (String url) {
|
||||||
|
controller.finishLoading();
|
||||||
|
},
|
||||||
|
onHttpError: (HttpResponseError error) {},
|
||||||
|
onWebResourceError: (WebResourceError error) {},
|
||||||
|
onUrlChange: (UrlChange url) async {
|
||||||
|
print("URL Changed to ${url.url!}");
|
||||||
|
|
||||||
|
_navigateWhenDone(url.url!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..loadRequest(Uri.parse(url));
|
||||||
|
|
||||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||||
if(simulate) {
|
if (simulate) {
|
||||||
sleep(Duration(seconds: 10));
|
await Future.delayed(Duration(seconds: 10));
|
||||||
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
||||||
_webViewController.loadRequest(Uri.parse(targetUrl));
|
_webViewController.loadRequest(Uri.parse(targetUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initWebView(String url) {
|
void _navigateWhenDone(String url) {
|
||||||
_webViewController =
|
if (url.contains("/checkout/receipt/")) {
|
||||||
WebViewController()
|
if (integrationStarted) {
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
return;
|
||||||
..setNavigationDelegate(
|
}
|
||||||
NavigationDelegate(
|
|
||||||
onProgress: (int progress) {
|
|
||||||
// Update loading bar.
|
|
||||||
},
|
|
||||||
onPageStarted: (String url) {
|
|
||||||
controller.startLoading();
|
|
||||||
},
|
|
||||||
onPageFinished: (String url) {
|
|
||||||
controller.finishLoading();
|
|
||||||
},
|
|
||||||
onHttpError: (HttpResponseError error) {},
|
|
||||||
onWebResourceError: (WebResourceError error) {},
|
|
||||||
onUrlChange: (UrlChange url) async {
|
|
||||||
print("URL Changed to ${url.url!}");
|
|
||||||
|
|
||||||
if(url.url!.contains("/checkout/receipt/")){
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
if(integrationStarted) {
|
SnackBar(
|
||||||
return;
|
content: Text(
|
||||||
}
|
"Payment was successful, please wait while we process your order.",
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
backgroundColor: Colors.black87,
|
||||||
|
duration: const Duration(seconds: 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
integrationStarted = true;
|
||||||
content: Text("Payment was successful, please wait while we process your order."),
|
});
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: Colors.black87,
|
|
||||||
duration: const Duration(seconds: 10),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
setState(() {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
integrationStarted = true;
|
context.go('/integration');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
context.go('/integration');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..loadRequest(Uri.parse(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -94,15 +101,14 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Complete your payment'),
|
title: const Text('Complete your payment'),
|
||||||
leading:
|
leading: context.canPop()
|
||||||
context.canPop()
|
? IconButton(
|
||||||
? IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
context.pop();
|
||||||
context.pop();
|
},
|
||||||
},
|
icon: const Icon(Icons.arrow_back),
|
||||||
icon: const Icon(Icons.arrow_back),
|
)
|
||||||
)
|
: SizedBox.shrink(),
|
||||||
: SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
body: ListenableBuilder(
|
body: ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
@@ -112,9 +118,12 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
|||||||
context.go('/integration');
|
context.go('/integration');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return controller.model.isLoading
|
return controller.model.isLoading
|
||||||
? const Center(child: CircularProgressIndicator(strokeWidth: 5))
|
? 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": "",
|
"debitPhone": "",
|
||||||
"debitAccount": "",
|
"debitAccount": "",
|
||||||
"debitCurrency": "USD",
|
"debitCurrency": "USD",
|
||||||
"debitRef": "peak",
|
"debitRef": "Velocity",
|
||||||
"creditPhone": "",
|
"creditPhone": "",
|
||||||
"creditAccount": "07088597534",
|
"creditAccount": "07088597534",
|
||||||
"billClientId": "powertel_zesa",
|
"billClientId": "powertel_zesa",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -23,7 +24,6 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
|
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final Map<String, String> _queryParams = {};
|
final Map<String, String> _queryParams = {};
|
||||||
final List<Animation<Offset>> _alignListAnimations = [];
|
|
||||||
final int _pageSize = 8;
|
final int _pageSize = 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -38,21 +38,9 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
);
|
);
|
||||||
|
|
||||||
_controller = AnimationController(
|
_controller = AnimationController(
|
||||||
duration: const Duration(milliseconds: 600),
|
duration: const Duration(milliseconds: 3000),
|
||||||
vsync: this,
|
vsync: this,
|
||||||
)..forward();
|
)..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 {
|
void setupData() async {
|
||||||
@@ -90,56 +78,78 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Center(
|
||||||
children: [
|
child: LayoutBuilder(
|
||||||
_buildSearchField(controller),
|
builder: (context, constraints) {
|
||||||
if (controller.model.transactions.isEmpty)
|
return SizedBox(
|
||||||
Column(
|
width: double.parse(
|
||||||
children: [
|
constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 30),
|
? ResponsivePolicy.md.toString()
|
||||||
Text(
|
: constraints.maxWidth.toString(),
|
||||||
'No transactions yet. Make a payment to see your recent activity.',
|
),
|
||||||
style: TextStyle(fontSize: 16),
|
child: Column(
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
),
|
_buildSearchField(controller),
|
||||||
],
|
SizedBox(height: 10),
|
||||||
),
|
if (controller.model.transactions.isEmpty)
|
||||||
if (controller.model.transactions.isNotEmpty)
|
Column(
|
||||||
Column(
|
children: [
|
||||||
children: [
|
SizedBox(height: 30),
|
||||||
ListView(
|
Text(
|
||||||
shrinkWrap: true,
|
'No transactions yet. Make a payment to see your recent activity.',
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
style: TextStyle(fontSize: 16),
|
||||||
children: [
|
textAlign: TextAlign.center,
|
||||||
...controller.model.transactions.map(
|
),
|
||||||
(e) => _buildTransactionItem(e, context),
|
],
|
||||||
),
|
),
|
||||||
],
|
if (controller.model.transactions.isNotEmpty)
|
||||||
),
|
Column(
|
||||||
SizedBox(height: 10),
|
children: [
|
||||||
if (controller.model.pageableModel != null &&
|
ListView(
|
||||||
controller.model.pageableModel!.number <
|
shrinkWrap: true,
|
||||||
controller.model.pageableModel!.totalPages)
|
physics:
|
||||||
OutlinedButton(
|
const NeverScrollableScrollPhysics(),
|
||||||
child: Text('Load more'),
|
children: [
|
||||||
onPressed: () {
|
...controller.model.transactions.map(
|
||||||
controller.getTransactions({
|
(e) =>
|
||||||
'userId': prefs.getString("userId")!,
|
_buildTransactionItem(e, context),
|
||||||
'page':
|
),
|
||||||
(controller
|
],
|
||||||
.model
|
),
|
||||||
.pageableModel!
|
SizedBox(height: 10),
|
||||||
.number +
|
if (controller.model.pageableModel !=
|
||||||
1)
|
null &&
|
||||||
.toString(),
|
controller.model.pageableModel!.number <
|
||||||
'size': _pageSize.toString(),
|
controller
|
||||||
'sort': 'createdAt,desc',
|
.model
|
||||||
});
|
.pageableModel!
|
||||||
},
|
.totalPages)
|
||||||
),
|
OutlinedButton(
|
||||||
],
|
child: Text('Load more'),
|
||||||
),
|
onPressed: () {
|
||||||
],
|
controller.getTransactions({
|
||||||
|
'userId': prefs.getString(
|
||||||
|
"userId",
|
||||||
|
)!,
|
||||||
|
'page':
|
||||||
|
(controller
|
||||||
|
.model
|
||||||
|
.pageableModel!
|
||||||
|
.number +
|
||||||
|
1)
|
||||||
|
.toString(),
|
||||||
|
'size': _pageSize.toString(),
|
||||||
|
'sort': 'createdAt,desc',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -191,20 +201,19 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
).colorScheme.primary.withOpacity(0.2),
|
).colorScheme.primary.withOpacity(0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIcon:
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
_searchController.text.isNotEmpty
|
? IconButton(
|
||||||
? IconButton(
|
onPressed: () {
|
||||||
onPressed: () {
|
_searchController.clear();
|
||||||
_searchController.clear();
|
_queryParams.clear();
|
||||||
_queryParams.clear();
|
controller.getTransactions({
|
||||||
controller.getTransactions({
|
'userId': prefs.getString("userId")!,
|
||||||
'userId': prefs.getString("userId")!,
|
});
|
||||||
});
|
},
|
||||||
},
|
icon: Icon(Icons.clear),
|
||||||
icon: Icon(Icons.clear),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
)
|
||||||
)
|
: null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
// controller.updateCreditAccount(value);
|
// controller.updateCreditAccount(value);
|
||||||
@@ -227,8 +236,17 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTransactionItem(Map<String, dynamic> e, BuildContext context) {
|
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(
|
return SlideTransition(
|
||||||
position: _alignListAnimations[0],
|
position: animation,
|
||||||
child: Skeletonizer(
|
child: Skeletonizer(
|
||||||
enabled: controller.model.isLoading,
|
enabled: controller.model.isLoading,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -260,18 +278,17 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
),
|
),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Icon(
|
Icon(
|
||||||
e['status'] == 'SUCCESS'
|
e['integrationStatus'] == 'SUCCESS'
|
||||||
? Icons.check_circle
|
? Icons.check_circle
|
||||||
: e['status'] == 'PENDING'
|
: e['integrationStatus'] == 'PENDING'
|
||||||
? Icons.pending
|
? Icons.pending
|
||||||
: Icons.cancel,
|
: Icons.cancel,
|
||||||
size: 16,
|
size: 16,
|
||||||
color:
|
color: e['integrationStatus'] == 'SUCCESS'
|
||||||
e['status'] == 'SUCCESS'
|
? Colors.green
|
||||||
? Colors.green
|
: e['integrationStatus'] == 'PENDING'
|
||||||
: e['status'] == 'PENDING'
|
? Colors.orange
|
||||||
? Colors.orange
|
: Colors.red,
|
||||||
: Colors.red,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ class HomeController extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> getProviders() async {
|
Future<void> getProviders() async {
|
||||||
try {
|
try {
|
||||||
|
model.filterableProviders = getFakeProviders();
|
||||||
|
model.transactions = getFakeTransactions();
|
||||||
|
|
||||||
model.isLoading = true;
|
model.isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -138,6 +141,7 @@ class HomeController extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> getTransactions(String userId) async {
|
Future<void> getTransactions(String userId) async {
|
||||||
model.isLoading = true;
|
model.isLoading = true;
|
||||||
|
// model.transactions = getFakeTransactions();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -147,8 +151,9 @@ class HomeController extends ChangeNotifier {
|
|||||||
|
|
||||||
PageableModel pageableModel = PageableModel.fromJson(response);
|
PageableModel pageableModel = PageableModel.fromJson(response);
|
||||||
|
|
||||||
model.transactions =
|
model.transactions = pageableModel.content
|
||||||
pageableModel.content.map((e) => e as Map<String, dynamic>).toList();
|
.map((e) => e as Map<String, dynamic>)
|
||||||
|
.toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e(e);
|
logger.e(e);
|
||||||
_showErrorSnackBar(
|
_showErrorSnackBar(
|
||||||
@@ -165,8 +170,6 @@ class HomeController extends ChangeNotifier {
|
|||||||
Future<void> getCategories() async {
|
Future<void> getCategories() async {
|
||||||
// activate skeletonizer
|
// activate skeletonizer
|
||||||
model.categories = getFakeCategoryData();
|
model.categories = getFakeCategoryData();
|
||||||
model.transactions = getFakeTransactions();
|
|
||||||
// model.filterableProviders = getFakeProviders();
|
|
||||||
model.isLoading = true;
|
model.isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
@@ -200,10 +203,9 @@ class HomeController extends ChangeNotifier {
|
|||||||
|
|
||||||
model.selectedCategory = category;
|
model.selectedCategory = category;
|
||||||
|
|
||||||
model.filterableProviders =
|
model.filterableProviders = model.providers
|
||||||
model.providers
|
.where((element) => element.category == category.label)
|
||||||
.where((element) => element.category == category.label)
|
.toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -224,7 +226,7 @@ class HomeController extends ChangeNotifier {
|
|||||||
"debitPhone": "",
|
"debitPhone": "",
|
||||||
"debitAccount": "",
|
"debitAccount": "",
|
||||||
"debitCurrency": "USD",
|
"debitCurrency": "USD",
|
||||||
"debitRef": "peak",
|
"debitRef": "Velocity",
|
||||||
"creditPhone": "",
|
"creditPhone": "",
|
||||||
"creditAccount": "07088597534",
|
"creditAccount": "07088597534",
|
||||||
"billClientId": "powertel_zesa",
|
"billClientId": "powertel_zesa",
|
||||||
@@ -242,21 +244,21 @@ class HomeController extends ChangeNotifier {
|
|||||||
List<BillProvider> getFakeProviders() {
|
List<BillProvider> getFakeProviders() {
|
||||||
return [
|
return [
|
||||||
BillProvider(
|
BillProvider(
|
||||||
clientId: '1',
|
clientId: 'econet_airtime',
|
||||||
name: 'ZESA',
|
name: 'Econet Airtime',
|
||||||
description: 'Pay your electricity bills easily',
|
description: 'Econet Airtime',
|
||||||
requiresAccount: false,
|
requiresAccount: false,
|
||||||
requiresAmount: false,
|
requiresAmount: true,
|
||||||
requiresAmountFromMerchant: false,
|
requiresAmountFromMerchant: false,
|
||||||
requiresPhone: false,
|
requiresPhone: true,
|
||||||
requiresReversal: false,
|
requiresReversal: true,
|
||||||
additionalDataString: '',
|
additionalDataString: '',
|
||||||
processorType: '',
|
processorType: 'AIRTIME',
|
||||||
uid: '',
|
uid: '78f1f497-c9eb-401c-b22c-884756e68e40',
|
||||||
image: 'econet.png',
|
image: 'econet.png',
|
||||||
label: '',
|
label: 'ECONET',
|
||||||
category: '',
|
category: 'ECONET',
|
||||||
accountFieldName: '',
|
accountFieldName: 'Phone Number',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -63,12 +63,14 @@ class IntegrationController extends ChangeNotifier {
|
|||||||
model.status = response['status'];
|
model.status = response['status'];
|
||||||
transactionController.updateReceiptData(response);
|
transactionController.updateReceiptData(response);
|
||||||
|
|
||||||
gatewayController.poll(transactionController.model.confirmationData['id']);
|
await gatewayController.poll(transactionController.model.confirmationData['id']);
|
||||||
} else {
|
|
||||||
model.status = response['status'];
|
|
||||||
model.errorMessage = response['errorMessage'];
|
|
||||||
_showErrorSnackBar(response['errorMessage']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// regardless of poll result we proceed to receipt
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.go('/receipt');
|
||||||
|
});
|
||||||
|
|
||||||
model.isLoading = false;
|
model.isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
@@ -68,49 +69,55 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(50),
|
padding: EdgeInsets.all(50),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: LayoutBuilder(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
return SizedBox(
|
||||||
children: [
|
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||||
SizedBox(height: 75),
|
ResponsivePolicy.md.toString() :
|
||||||
|
constraints.maxWidth.toString()),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 75),
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'We\'ve successfully collected your funds. We are now '
|
'We\'ve successfully collected your funds. We are now '
|
||||||
'updating your billing account. This won\'t take long',
|
'updating your billing account. This won\'t take long',
|
||||||
style: TextStyle(fontSize: 20),
|
style: TextStyle(fontSize: 20),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
|
||||||
Image.asset('assets/peak-animation.gif', width: 150),
|
|
||||||
SizedBox(
|
|
||||||
width: 50,
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
),
|
|
||||||
SizedBox(height: 50),
|
|
||||||
if(controller.model.status == 'FAILED')
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'We failed to update your billing account',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
SizedBox(height: 20,),
|
|
||||||
Skeletonizer(
|
|
||||||
enabled: controller.model.isLoading,
|
|
||||||
child: OutlinedButton(
|
|
||||||
child: Text('Retry'),
|
|
||||||
onPressed: () {
|
|
||||||
controller.doIntegration();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
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 update your billing account',
|
||||||
|
style: TextStyle(fontSize: 20),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 20,),
|
||||||
|
Skeletonizer(
|
||||||
|
enabled: controller.model.isLoading,
|
||||||
|
child: OutlinedButton(
|
||||||
|
child: Text('Retry'),
|
||||||
|
onPressed: () {
|
||||||
|
controller.doIntegration();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||||
|
|
||||||
class CompleteScreen extends StatefulWidget {
|
class CompleteScreen extends StatefulWidget {
|
||||||
@@ -16,56 +17,61 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: [
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Registration Complete',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
),
|
SizedBox(height: 75),
|
||||||
SizedBox(height: 5),
|
Text(
|
||||||
Text(
|
'Registration Complete',
|
||||||
'Thank you for registering with Peak payments',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 18),
|
fontSize: 30,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Image.asset('assets/landing.png', width: 300),
|
),
|
||||||
Text(
|
),
|
||||||
'Tap Login to get started',
|
SizedBox(height: 5),
|
||||||
style: TextStyle(fontSize: 18),
|
Text(
|
||||||
),
|
'Thank you for registering with Velocity payments',
|
||||||
SizedBox(height: 10),
|
style: TextStyle(fontSize: 18),
|
||||||
],
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
Image.asset('assets/landing.png', width: 300),
|
||||||
),
|
Text(
|
||||||
bottomNavigationBar: SizedBox(
|
'Tap Login to get started',
|
||||||
height: 130,
|
style: TextStyle(fontSize: 18),
|
||||||
child: Column(
|
),
|
||||||
children: [
|
SizedBox(height: 30),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/onboarding/login');
|
context.go('/onboarding/login');
|
||||||
},
|
},
|
||||||
child: Text('Go to Login', style: TextStyle(fontSize: 16)),
|
child: Text('Go to Login', style: TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}
|
||||||
),
|
),
|
||||||
SizedBox(height: 5),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||||
|
|
||||||
class LandingScreen extends StatefulWidget {
|
class LandingScreen extends StatefulWidget {
|
||||||
@@ -16,73 +17,82 @@ class _LandingScreenState extends State<LandingScreen> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: [
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Welcome to Peak',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
),
|
SizedBox(height: 75),
|
||||||
SizedBox(height: 5),
|
Text(
|
||||||
Text(
|
'Welcome to Velocity',
|
||||||
'The fastest way to pay for local goods and services',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 18),
|
fontSize: 30,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Image.asset('assets/landing.png', width: 300),
|
),
|
||||||
Image.asset('assets/peak.png', width: 100),
|
),
|
||||||
SizedBox(height: 5),
|
SizedBox(height: 5),
|
||||||
Text(
|
Text(
|
||||||
'Register or login to get started',
|
'The fastest way to pay for local goods and services',
|
||||||
style: TextStyle(fontSize: 16),
|
style: TextStyle(fontSize: 18),
|
||||||
),
|
textAlign: TextAlign.center,
|
||||||
SizedBox(height: 10),
|
),
|
||||||
],
|
Image.asset('assets/landing.png', width: 300),
|
||||||
),
|
Image.asset('assets/velocity.png', width: 150),
|
||||||
),
|
SizedBox(height: 5),
|
||||||
),
|
Text(
|
||||||
bottomNavigationBar: SizedBox(
|
'Register or login to get started',
|
||||||
height: 130,
|
style: TextStyle(fontSize: 16),
|
||||||
child: Column(
|
),
|
||||||
children: [
|
SizedBox(height: 50),
|
||||||
Padding(
|
Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
children: [
|
||||||
child: Row(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: ElevatedButton(
|
children: [
|
||||||
onPressed: () {
|
Expanded(
|
||||||
context.go('/onboarding/register');
|
child: ElevatedButton(
|
||||||
},
|
onPressed: () {
|
||||||
child: Text('Register', style: TextStyle(fontSize: 16)),
|
context.go('/onboarding/register');
|
||||||
),
|
},
|
||||||
|
child: Text('Register', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/onboarding/login');
|
||||||
|
},
|
||||||
|
child: Text('Login', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/');
|
||||||
|
},
|
||||||
|
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 16),
|
);
|
||||||
Expanded(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/onboarding/login');
|
|
||||||
},
|
|
||||||
child: Text('Login', style: TextStyle(fontSize: 16)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/');
|
|
||||||
},
|
},
|
||||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_sign_in/google_sign_in.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:qpay/screens/onboarding/login/login_controller.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@@ -17,6 +21,8 @@ class LoginScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _LoginScreenState extends State<LoginScreen> {
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
late SharedPreferences prefs;
|
late SharedPreferences prefs;
|
||||||
|
late GoogleSignIn googleSignIn;
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
|
||||||
@@ -30,22 +36,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loginController = LoginController(context);
|
_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 {
|
Future<void> signInWithGoogle() async {
|
||||||
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
|
|
||||||
|
|
||||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
|
||||||
|
|
||||||
if (GoogleSignIn.instance.supportsAuthenticate()){
|
if (GoogleSignIn.instance.supportsAuthenticate()){
|
||||||
|
|
||||||
unawaited(GoogleSignIn.instance.authenticate().then((value) async {
|
unawaited(GoogleSignIn.instance.authenticate().then((value) async {
|
||||||
print(value);
|
|
||||||
|
|
||||||
saveUser(value);
|
|
||||||
await _loginController.login(value.email, value.id);
|
|
||||||
context.go('/');
|
|
||||||
|
|
||||||
googleSignIn.authenticationEvents
|
googleSignIn.authenticationEvents
|
||||||
.listen(_handleAuthenticationEvent)
|
.listen(_handleAuthenticationEvent)
|
||||||
.onError(_handleAuthenticationError);
|
.onError(_handleAuthenticationError);
|
||||||
@@ -85,6 +92,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
?.authorizationClient
|
?.authorizationClient
|
||||||
.authorizationForScopes(scopes);
|
.authorizationForScopes(scopes);
|
||||||
// #enddocregion CheckAuthorization
|
// #enddocregion CheckAuthorization
|
||||||
|
|
||||||
|
print(user);
|
||||||
|
saveUser(user as GoogleSignInAccount);
|
||||||
|
|
||||||
|
await _loginController.login(user.email, user.id);
|
||||||
|
context.go('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAuthenticationError(Object e) async {
|
Future<void> _handleAuthenticationError(Object e) async {
|
||||||
@@ -105,100 +118,117 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: [
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Login',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
),
|
SizedBox(height: 75),
|
||||||
SizedBox(height: 5),
|
Text(
|
||||||
Text(
|
'Login',
|
||||||
'Welcome back to Peak',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 18),
|
fontSize: 30,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
SizedBox(height: 40),
|
|
||||||
Image.asset('assets/password.png', width: 300),
|
|
||||||
Text(
|
|
||||||
'Tap on your preferred social media platform to get started',
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
// Username Field
|
|
||||||
SizedBox(height: 20),
|
|
||||||
// Forgot Password Link
|
|
||||||
// Divider with "or" text
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
'choose from the options below to continue',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 30),
|
|
||||||
// Social Login Buttons
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
await 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(height: 5),
|
||||||
|
Text(
|
||||||
|
'Welcome back to Velocity',
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 30),
|
||||||
|
Image.asset('assets/password.png', width: 300),
|
||||||
|
Text(
|
||||||
|
'Tap on your preferred social media platform to get started',
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
// Username Field
|
||||||
|
SizedBox(height: 20),
|
||||||
|
// Forgot Password Link
|
||||||
|
// Divider with "or" text
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(
|
||||||
|
'choose from the options below to continue',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 30),
|
||||||
|
// Social Login Buttons
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}
|
||||||
SizedBox(height: 40),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: SizedBox(
|
);
|
||||||
height: 130,
|
}
|
||||||
child: Column(
|
|
||||||
children: [
|
Widget _buildGoogleButton(){
|
||||||
SizedBox(height: 5),
|
return Skeletonizer(
|
||||||
TextButton(
|
enabled: _loading,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/');
|
signInWithGoogle();
|
||||||
},
|
},
|
||||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
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,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:go_router/go_router.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:qpay/screens/onboarding/phone/phone_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
@@ -82,89 +83,94 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
|||||||
padding: const EdgeInsets.all(25.0),
|
padding: const EdgeInsets.all(25.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: <Widget>[
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Verify Your Device',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
.of(context)
|
children: <Widget>[
|
||||||
.colorScheme
|
SizedBox(height: 75),
|
||||||
.primary,
|
Text(
|
||||||
),
|
'Verify Your Device',
|
||||||
textAlign: TextAlign.center,
|
style: TextStyle(
|
||||||
),
|
fontSize: 30,
|
||||||
SizedBox(height: 10),
|
fontWeight: FontWeight.bold,
|
||||||
Text(
|
color: Theme
|
||||||
'We will send you a verification code to this number',
|
.of(context)
|
||||||
style: TextStyle(fontSize: 18),
|
.colorScheme
|
||||||
textAlign: TextAlign.center,
|
.primary,
|
||||||
),
|
),
|
||||||
Image.asset('assets/phone.png', width: 300),
|
textAlign: TextAlign.center,
|
||||||
SizedBox(height: 20),
|
|
||||||
_buildPhoneField(),
|
|
||||||
SizedBox(height: 30),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: SizedBox(
|
|
||||||
height: 130,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: phoneController.model.isLoading,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
if (_formKey.currentState!.validate()) {
|
|
||||||
String fullPhoneNumber =
|
|
||||||
selectedCountryCode + _phoneController.text;
|
|
||||||
|
|
||||||
phoneController.updatePhone(fullPhoneNumber);
|
|
||||||
await phoneController.register();
|
|
||||||
if(phoneController.model.status != 'failed') {
|
|
||||||
context.go('/onboarding/verify');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('Send Code', style: TextStyle(fontSize: 16)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/onboarding/login');
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Go to Login',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'We will send you a verification code to this number',
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Image.asset('assets/phone.png', width: 300),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
_buildPhoneField(),
|
||||||
|
SizedBox(height: 30),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||||
|
child: Skeletonizer(
|
||||||
|
enabled: phoneController.model.isLoading,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
String fullPhoneNumber =
|
||||||
|
selectedCountryCode + _phoneController.text;
|
||||||
|
|
||||||
|
phoneController.updatePhone(fullPhoneNumber);
|
||||||
|
await phoneController.register();
|
||||||
|
if(phoneController.model.status != 'failed') {
|
||||||
|
context.go('/onboarding/verify');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Send Code', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/onboarding/login');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Go to Login',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/');
|
||||||
|
},
|
||||||
|
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 5),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/');
|
|
||||||
},
|
|
||||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_sign_in/google_sign_in.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:provider/provider.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
|
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ class RegisterScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _RegisterScreenState extends State<RegisterScreen> {
|
class _RegisterScreenState extends State<RegisterScreen> {
|
||||||
late OnboardingController onboardingController;
|
late OnboardingController onboardingController;
|
||||||
|
late GoogleSignIn googleSignIn;
|
||||||
|
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
|
||||||
@@ -32,17 +36,22 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
listen: false,
|
listen: false,
|
||||||
);
|
);
|
||||||
// onboardingController.resetState();
|
// 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() {
|
void signInWithGoogle() {
|
||||||
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
|
|
||||||
|
|
||||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
|
||||||
|
|
||||||
if (GoogleSignIn.instance.supportsAuthenticate()){
|
if (GoogleSignIn.instance.supportsAuthenticate()){
|
||||||
unawaited(GoogleSignIn.instance.authenticate().then((value) {
|
unawaited(GoogleSignIn.instance.authenticate().then((value) {
|
||||||
print(value);
|
|
||||||
onboardingController.updateGoogleUser(value);
|
|
||||||
googleSignIn.authenticationEvents
|
googleSignIn.authenticationEvents
|
||||||
.listen(_handleAuthenticationEvent)
|
.listen(_handleAuthenticationEvent)
|
||||||
.onError(_handleAuthenticationError);
|
.onError(_handleAuthenticationError);
|
||||||
@@ -51,9 +60,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAuthenticationEvent(
|
Future<void> _handleAuthenticationEvent(GoogleSignInAuthenticationEvent event) async {
|
||||||
GoogleSignInAuthenticationEvent event,
|
|
||||||
) async {
|
|
||||||
// #docregion CheckAuthorization
|
// #docregion CheckAuthorization
|
||||||
final GoogleSignInAccount? user = // ...
|
final GoogleSignInAccount? user = // ...
|
||||||
// #enddocregion CheckAuthorization
|
// #enddocregion CheckAuthorization
|
||||||
@@ -68,6 +75,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
?.authorizationClient
|
?.authorizationClient
|
||||||
.authorizationForScopes(scopes);
|
.authorizationForScopes(scopes);
|
||||||
// #enddocregion CheckAuthorization
|
// #enddocregion CheckAuthorization
|
||||||
|
print(user);
|
||||||
|
onboardingController.updateGoogleUser(user as GoogleSignInAccount);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAuthenticationError(Object e) async {
|
Future<void> _handleAuthenticationError(Object e) async {
|
||||||
@@ -83,170 +93,184 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: [
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Registration',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
Text(
|
|
||||||
"Let's start by getting some of your details online to get you started",
|
|
||||||
style: TextStyle(fontSize: 18),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
Image.asset('assets/social.png', width: 300),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Social Login Buttons
|
|
||||||
if(onboardingController.model.googleUser == null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
SizedBox(height: 75),
|
||||||
Padding(
|
Text(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
'Registration',
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
'Choose from the options below to continue',
|
fontSize: 30,
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey.shade600,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
SizedBox(height: 5),
|
||||||
],
|
Text(
|
||||||
),
|
"Let's start by getting some of your online details to get you started",
|
||||||
SizedBox(height: 20),
|
style: TextStyle(fontSize: 18),
|
||||||
Skeletonizer(
|
textAlign: TextAlign.center,
|
||||||
enabled: _loading,
|
),
|
||||||
child: Row(
|
Image.asset('assets/social.png', width: 300),
|
||||||
children: [
|
SizedBox(height: 20),
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
// Social Login Buttons
|
||||||
onPressed: () {
|
if(onboardingController.model.googleUser == null)
|
||||||
signInWithGoogle();
|
Column(
|
||||||
},
|
children: [
|
||||||
icon: Image.asset(
|
Row(
|
||||||
'assets/google.png',
|
children: [
|
||||||
width: 20,
|
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||||
height: 20,
|
Padding(
|
||||||
),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
label: Text('Google', style: TextStyle(fontSize: 16)),
|
child: Text(
|
||||||
style: OutlinedButton.styleFrom(
|
'Choose from the options below to continue',
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
style: TextStyle(
|
||||||
side: BorderSide(color: Colors.grey.shade300),
|
color: Colors.grey.shade600,
|
||||||
shape: RoundedRectangleBorder(
|
fontSize: 14,
|
||||||
borderRadius: BorderRadius.circular(12),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
if(GoogleSignIn.instance.supportsAuthenticate())
|
||||||
|
_buildGoogleButton()
|
||||||
|
else ...<Widget>[
|
||||||
|
if (kIsWeb)
|
||||||
|
web.renderButton()
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if(onboardingController.model.googleUser != null)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Divider(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
thickness: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 20,),
|
||||||
|
Text(
|
||||||
|
"Welcome, ${onboardingController.model.googleUser?.displayName}",
|
||||||
|
style: TextStyle(fontSize: 25),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 10,),
|
||||||
|
Text("Tap Proceed to continue", style: TextStyle(fontSize: 18),)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 50),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (onboardingController.model.googleUser == null) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Sign-In Required'),
|
||||||
|
content: const Text(
|
||||||
|
"Please sign in with one of the social media platforms to proceed."),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('OK'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop(); // Close the dialog
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
context.go('/onboarding/phone');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Proceed', style: TextStyle(fontSize: 16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/onboarding/login');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Go to Login',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(width: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if(onboardingController.model.googleUser != null)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 100,
|
|
||||||
child: Divider(
|
|
||||||
color: Colors.grey.shade300,
|
|
||||||
thickness: 1,
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 10),
|
||||||
SizedBox(height: 20,),
|
TextButton(
|
||||||
Text(
|
onPressed: () {
|
||||||
"Welcome, ${onboardingController.model.googleUser?.displayName}",
|
context.go('/');
|
||||||
style: TextStyle(fontSize: 25),
|
},
|
||||||
textAlign: TextAlign.center,
|
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||||
),
|
),
|
||||||
SizedBox(height: 10,),
|
|
||||||
Text("Tap Proceed to continue", style: TextStyle(fontSize: 18),)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 40),
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottomNavigationBar: SizedBox(
|
|
||||||
height: 130,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (onboardingController.model.googleUser == null) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext dialogContext) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Sign-In Required'),
|
|
||||||
content: const Text(
|
|
||||||
"Please sign in with one of the social media platforms to proceed."),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: const Text('OK'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(dialogContext).pop(); // Close the dialog
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
context.go('/onboarding/phone');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text('Proceed', style: TextStyle(fontSize: 16)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/onboarding/login');
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Go to Login',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 5),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/');
|
|
||||||
},
|
|
||||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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:qpay/screens/onboarding/verify/verify_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
|
||||||
@@ -59,169 +60,162 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: LayoutBuilder(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
builder: (context, constraints) {
|
||||||
children: [
|
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||||
SizedBox(height: 75),
|
? ResponsivePolicy.md.toDouble()
|
||||||
Text(
|
: constraints.maxWidth;
|
||||||
'Verify Your Account',
|
return SizedBox(
|
||||||
style: TextStyle(
|
width: maxWidth,
|
||||||
fontSize: 30,
|
child: Column(
|
||||||
fontWeight: FontWeight.bold,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
),
|
SizedBox(height: 75),
|
||||||
SizedBox(height: 5),
|
Text(
|
||||||
Text(
|
'Verify Your Account',
|
||||||
'Enter the 6-digit code sent to your phone/email',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 18),
|
fontSize: 30,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Image.asset('assets/verify.png', width: 300),
|
|
||||||
// Verification Code Input
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: List.generate(
|
|
||||||
6,
|
|
||||||
(index) => SizedBox(
|
|
||||||
width: 50,
|
|
||||||
child: TextFormField(
|
|
||||||
controller: _codeControllers[index],
|
|
||||||
focusNode: _focusNodes[index],
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
maxLength: 1,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
counterText: '',
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
SizedBox(height: 5),
|
||||||
borderSide: BorderSide(
|
Text(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
'Enter the 6-digit code sent to your phone/email',
|
||||||
width: 2,
|
style: TextStyle(fontSize: 18),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Image.asset('assets/verify.png', width: 300),
|
||||||
|
// Verification Code Input
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: List.generate(
|
||||||
|
6,
|
||||||
|
(index) => SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _codeControllers[index],
|
||||||
|
focusNode: _focusNodes[index],
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 1,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
counterText: '',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
onChanged: (value) => _onCodeChanged(value, index),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
|
||||||
),
|
),
|
||||||
onChanged: (value) => _onCodeChanged(value, index),
|
SizedBox(height: 5),
|
||||||
),
|
if(errorMessage != null)
|
||||||
),
|
Text(errorMessage ?? '', style: TextStyle(color: Colors.red)),
|
||||||
),
|
SizedBox(height: 30),
|
||||||
),
|
// Resend Code Section
|
||||||
SizedBox(height: 5),
|
Row(
|
||||||
if(errorMessage != null)
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Text(errorMessage ?? '', style: TextStyle(color: Colors.red)),
|
children: [
|
||||||
SizedBox(height: 30),
|
Text(
|
||||||
// Resend Code Section
|
"Didn't receive the code? ",
|
||||||
Row(
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
children: [
|
TextButton(
|
||||||
Text(
|
onPressed: () {
|
||||||
"Didn't receive the code? ",
|
verifyController.resendOtp();
|
||||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
|
},
|
||||||
),
|
child: Text(
|
||||||
TextButton(
|
'Resend Code',
|
||||||
onPressed: () {
|
style: TextStyle(
|
||||||
verifyController.resendOtp();
|
color: Theme.of(context).colorScheme.primary,
|
||||||
},
|
fontSize: 16,
|
||||||
child: Text(
|
fontWeight: FontWeight.w600,
|
||||||
'Resend Code',
|
),
|
||||||
style: TextStyle(
|
),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
),
|
||||||
fontSize: 16,
|
],
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
// SizedBox(height: 20),
|
||||||
),
|
// // Timer for resend (optional)
|
||||||
],
|
// Text(
|
||||||
),
|
// 'Resend available in 2:30',
|
||||||
// SizedBox(height: 20),
|
// style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||||
// // Timer for resend (optional)
|
// ),
|
||||||
// Text(
|
SizedBox(height: 40),
|
||||||
// 'Resend available in 2:30',
|
Padding(
|
||||||
// style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||||
// ),
|
child: Skeletonizer(
|
||||||
SizedBox(height: 40),
|
enabled: verifyController.model.isLoading,
|
||||||
],
|
child: Row(
|
||||||
),
|
children: [
|
||||||
),
|
Expanded(
|
||||||
),
|
child: ElevatedButton(
|
||||||
bottomNavigationBar: SizedBox(
|
onPressed: () async {
|
||||||
height: 130,
|
showErrorMessage(null);
|
||||||
child: Column(
|
String code = '';
|
||||||
children: [
|
for (var controller in _codeControllers) {
|
||||||
Padding(
|
code += controller.text;
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
}
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: verifyController.model.isLoading,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
showErrorMessage(null);
|
|
||||||
String code = '';
|
|
||||||
for (var controller in _codeControllers) {
|
|
||||||
code += controller.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(code.length != 6) {
|
if(code.length != 6) {
|
||||||
showErrorMessage('Invalid code');
|
showErrorMessage('Invalid code');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
verifyController.updateCode(code);
|
verifyController.updateCode(code);
|
||||||
await verifyController.verifyOtp();
|
await verifyController.verifyOtp();
|
||||||
if(verifyController.model.status == 'done') {
|
if(verifyController.model.status == 'done') {
|
||||||
context.go('/onboarding/complete');
|
context.go('/onboarding/complete');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
child: Text(
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
'Verify Code',
|
||||||
shape: RoundedRectangleBorder(
|
style: TextStyle(fontSize: 16),
|
||||||
borderRadius: BorderRadius.circular(12),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/onboarding/login');
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Back to Login',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
|
||||||
'Verify Code',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 10),
|
||||||
SizedBox(width: 16),
|
TextButton(
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/onboarding/login');
|
context.go('/');
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||||
padding: EdgeInsets.symmetric(vertical: 16),
|
|
||||||
side: BorderSide(color: Colors.grey.shade300),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Back to Login',
|
|
||||||
style: TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
),
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/');
|
|
||||||
},
|
|
||||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class PayController extends ChangeNotifier {
|
|||||||
transactionController.model.formData.copyWith(
|
transactionController.model.formData.copyWith(
|
||||||
type: "CONFIRM",
|
type: "CONFIRM",
|
||||||
billClientId: model.selectedProvider?.clientId ?? '',
|
billClientId: model.selectedProvider?.clientId ?? '',
|
||||||
debitRef: 'peak',
|
debitRef: 'Velocity',
|
||||||
debitCurrency: 'USD',
|
debitCurrency: 'USD',
|
||||||
creditAccount: transactionController.model.formData.creditAccount,
|
creditAccount: transactionController.model.formData.creditAccount,
|
||||||
creditPhone: transactionController.model.formData.creditPhone,
|
creditPhone: transactionController.model.formData.creditPhone,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.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/pay/pay_controller.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
@@ -178,105 +179,127 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
|||||||
position: _slideAnimation,
|
position: _slideAnimation,
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Text(
|
return SizedBox(
|
||||||
"TRANSACTIONS DETAILS",
|
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||||
style: TextStyle(
|
ResponsivePolicy.md.toString() :
|
||||||
fontSize: 16,
|
constraints.maxWidth.toString()),
|
||||||
fontWeight: FontWeight.bold,
|
child: Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
const SizedBox(height: 20),
|
Container(
|
||||||
Row(
|
decoration: BoxDecoration(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||||
children: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
Text(
|
),
|
||||||
"Provider",
|
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||||
style: TextStyle(
|
child: Column(
|
||||||
fontSize: 16,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fontWeight: FontWeight.bold,
|
children: [
|
||||||
),
|
Text(
|
||||||
),
|
"TRANSACTIONS DETAILS",
|
||||||
Text(
|
style: TextStyle(
|
||||||
controller.model.selectedProvider?.name ?? "",
|
fontSize: 16,
|
||||||
style: TextStyle(fontSize: 16),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
Row(
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
"Provider",
|
||||||
controller
|
style: TextStyle(
|
||||||
.model
|
fontSize: 16,
|
||||||
.selectedProvider
|
fontWeight: FontWeight.bold,
|
||||||
?.accountFieldName ??
|
),
|
||||||
"",
|
),
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 16,
|
controller.model.selectedProvider?.name ?? "",
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Text(
|
),
|
||||||
transactionController
|
const SizedBox(height: 10),
|
||||||
.model
|
Row(
|
||||||
.formData
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
.creditAccount,
|
children: [
|
||||||
style: TextStyle(fontSize: 16),
|
Text(
|
||||||
),
|
controller
|
||||||
],
|
.model
|
||||||
),
|
.selectedProvider
|
||||||
const SizedBox(height: 10),
|
?.accountFieldName ??
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
"",
|
||||||
InkWell(
|
style: TextStyle(
|
||||||
customBorder: RoundedRectangleBorder(
|
fontSize: 16,
|
||||||
borderRadius: BorderRadius.circular(12),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
onTap: () {
|
),
|
||||||
setState(() {
|
Text(
|
||||||
showAdditionalRecipientDetails =
|
transactionController
|
||||||
!showAdditionalRecipientDetails;
|
.model
|
||||||
});
|
.formData
|
||||||
},
|
.creditAccount,
|
||||||
child: Row(
|
style: TextStyle(fontSize: 16),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
),
|
||||||
children: [
|
],
|
||||||
IconButton(
|
),
|
||||||
onPressed: () {},
|
],
|
||||||
icon: Icon(
|
),
|
||||||
showAdditionalRecipientDetails
|
|
||||||
? Icons.arrow_drop_up
|
|
||||||
: Icons.arrow_drop_down,
|
|
||||||
),
|
),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
SizedBox(height: 20),
|
||||||
),
|
InkWell(
|
||||||
Text(
|
customBorder: RoundedRectangleBorder(
|
||||||
showAdditionalRecipientDetails
|
borderRadius: BorderRadius.circular(12),
|
||||||
? "Hide additional recipient details"
|
),
|
||||||
: "Show additional recipient details",
|
onTap: () {
|
||||||
style: TextStyle(fontSize: 16),
|
setState(() {
|
||||||
),
|
showAdditionalRecipientDetails =
|
||||||
],
|
!showAdditionalRecipientDetails;
|
||||||
),
|
});
|
||||||
),
|
},
|
||||||
if (showAdditionalRecipientDetails)
|
child: Row(
|
||||||
_buildAdditionalRecipientDetails(),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
const SizedBox(height: 7),
|
children: [
|
||||||
if (controller.model.products.isNotEmpty)
|
IconButton(
|
||||||
_buildBillProductSelector(controller),
|
onPressed: () {},
|
||||||
const SizedBox(height: 7),
|
icon: Icon(
|
||||||
_buildPhoneField(),
|
showAdditionalRecipientDetails
|
||||||
const SizedBox(height: 7),
|
? Icons.arrow_drop_up
|
||||||
_buildAmountField(),
|
: Icons.arrow_drop_down,
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
// _buildPayButton(controller),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
...controller.model.paymentProcessors.map(
|
),
|
||||||
(e) => _buildPaymentProcessorItem(e, context),
|
Text(
|
||||||
),
|
showAdditionalRecipientDetails
|
||||||
],
|
? "Hide additional recipient details"
|
||||||
|
: "Show additional recipient details",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showAdditionalRecipientDetails)
|
||||||
|
_buildAdditionalRecipientDetails(),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
if (controller.model.products.isNotEmpty)
|
||||||
|
_buildBillProductSelector(controller),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
_buildPhoneField(),
|
||||||
|
const SizedBox(height: 7),
|
||||||
|
_buildAmountField(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// _buildPayButton(controller),
|
||||||
|
...controller.model.paymentProcessors.map(
|
||||||
|
(e) => _buildPaymentProcessorItem(e, context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
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']);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
||||||
@@ -83,81 +84,107 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
|||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Center(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Text(
|
return SizedBox(
|
||||||
"TRANSACTIONS DETAILS",
|
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
ResponsivePolicy.md.toString() :
|
||||||
),
|
constraints.maxWidth.toString()),
|
||||||
const SizedBox(height: 20),
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Text("Provider", style: TextStyle(fontSize: 16)),
|
decoration: BoxDecoration(
|
||||||
Text(
|
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||||
controller.model.selectedProvider?.name ?? "",
|
borderRadius: BorderRadius.circular(12),
|
||||||
style: TextStyle(fontSize: 16),
|
),
|
||||||
),
|
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||||
],
|
|
||||||
),
|
child: Column(
|
||||||
const SizedBox(height: 20),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Divider(color: Theme.of(context).colorScheme.primary),
|
children: [
|
||||||
const SizedBox(height: 20),
|
Text(
|
||||||
_buildAccountField(controller),
|
"TRANSACTIONS DETAILS",
|
||||||
const SizedBox(height: 20),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
controller.model.creditAccount != null &&
|
|
||||||
controller.model.creditAccount!.isNotEmpty
|
|
||||||
? SlideTransition(
|
|
||||||
position: _alignListAnimations[0],
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
String phone = '';
|
|
||||||
if (!transactionController
|
|
||||||
.model
|
|
||||||
.selectedProvider!
|
|
||||||
.requiresAccount) {
|
|
||||||
phone = _accountController.text;
|
|
||||||
}
|
|
||||||
transactionController.model.formData =
|
|
||||||
transactionController.model.formData.copyWith(
|
|
||||||
creditAccount: _accountController.text,
|
|
||||||
creditPhone: phone,
|
|
||||||
);
|
|
||||||
context.push("/make-payment");
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor:
|
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Use as new recipient",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 10),
|
||||||
SizedBox(width: 5),
|
Row(
|
||||||
Icon(Icons.arrow_forward_ios, size: 12),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
],
|
children: [
|
||||||
|
Text("Provider",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
)),
|
||||||
|
Text(
|
||||||
|
controller.model.selectedProvider?.name ?? "",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
)
|
_buildAccountField(controller),
|
||||||
: SizedBox(),
|
const SizedBox(height: 20),
|
||||||
const SizedBox(height: 20),
|
controller.model.creditAccount != null &&
|
||||||
Text(
|
controller.model.creditAccount!.isNotEmpty
|
||||||
"Recent Recipients",
|
? SlideTransition(
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
position: _alignListAnimations[0],
|
||||||
),
|
child: ElevatedButton(
|
||||||
const SizedBox(height: 10),
|
onPressed: () {
|
||||||
_buildRecipientItems(controller),
|
String phone = '';
|
||||||
],
|
if (!transactionController
|
||||||
|
.model
|
||||||
|
.selectedProvider!
|
||||||
|
.requiresAccount) {
|
||||||
|
phone = _accountController.text;
|
||||||
|
}
|
||||||
|
transactionController.model.formData =
|
||||||
|
transactionController.model.formData.copyWith(
|
||||||
|
creditAccount: _accountController.text,
|
||||||
|
creditPhone: phone,
|
||||||
|
);
|
||||||
|
context.push("/make-payment");
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Use as new recipient",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Icon(Icons.arrow_forward_ios, size: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SizedBox(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
"Recent Recipients",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildRecipientItems(controller),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import google_sign_in_ios
|
|||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
import webview_flutter_wkwebview
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
52
pubspec.lock
52
pubspec.lock
@@ -153,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.6"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -377,7 +385,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.0"
|
||||||
google_sign_in_web:
|
google_sign_in_web:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: google_sign_in_web
|
name: google_sign_in_web
|
||||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||||
@@ -392,6 +400,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
html:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -861,6 +877,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -869,6 +909,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
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:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -950,7 +998,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.4"
|
version: "1.1.4"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: qpay
|
name: qpay
|
||||||
description: "Peak payments solutions"
|
description: "Velocity payments solutions"
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# 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
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
@@ -55,6 +55,10 @@ dependencies:
|
|||||||
gif_view: ^0.4.0
|
gif_view: ^0.4.0
|
||||||
google_sign_in: ^7.1.1
|
google_sign_in: ^7.1.1
|
||||||
firebase_core: ^4.1.0
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
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 content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project.">
|
||||||
<meta name="color-scheme" content="light">
|
<meta name="color-scheme" content="light">
|
||||||
|
<meta name="google-signin-client_id" content="77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -30,8 +31,27 @@
|
|||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>qpay</title>
|
<title>Velocity</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user