completed first web iteration
This commit is contained in:
25
.metadata
25
.metadata
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
||||
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,26 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: android
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: ios
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: linux
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: macos
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
- platform: web
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
- platform: windows
|
||||
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
71
Dockerfile
Normal file
71
Dockerfile
Normal file
@@ -0,0 +1,71 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
###############################################
|
||||
# Build stage: compile Flutter web artifacts. #
|
||||
###############################################
|
||||
FROM ubuntu:22.04 AS build
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
FLUTTER_CHANNEL=stable \
|
||||
FLUTTER_VERSION=3.24.3 \
|
||||
FLUTTER_HOME=/opt/flutter
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
unzip \
|
||||
xz-utils \
|
||||
zip \
|
||||
libglu1-mesa && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz" \
|
||||
| tar -xJ -C /opt && \
|
||||
ln -s ${FLUTTER_HOME} /usr/local/flutter
|
||||
|
||||
ENV PATH="${FLUTTER_HOME}/bin:${FLUTTER_HOME}/bin/cache/dart-sdk/bin:${PATH}"
|
||||
|
||||
RUN flutter config --enable-web && \
|
||||
flutter doctor -v
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pubspec.yaml pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
|
||||
COPY . .
|
||||
RUN flutter build web --release
|
||||
|
||||
##########################################
|
||||
# Runtime stage: serve assets via nginx. #
|
||||
##########################################
|
||||
FROM ubuntu:22.04 AS runner
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nginx && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Remove default site definition to avoid duplicate configs.
|
||||
RUN rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Provide a minimal nginx site config for the Flutter web assets.
|
||||
RUN printf 'server {\n\
|
||||
listen 80 default_server;\n\
|
||||
listen [::]:80 default_server;\n\
|
||||
root /var/www/html;\n\
|
||||
index index.html;\n\
|
||||
location / {\n\
|
||||
try_files $uri $uri/ /index.html;\n\
|
||||
}\n\
|
||||
}\n' > /etc/nginx/conf.d/flutter.conf
|
||||
|
||||
COPY --from=build /app/build/web /var/www/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<application
|
||||
android:label="Peak"
|
||||
android:label="Velocity"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:forceDarkAllowed="false">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Peak</string>
|
||||
<string name="app_name">Velocity</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
BASE_URL=https://peakapi.qantra.co.zw/api
|
||||
; BASE_URL=http://192.168.100.26:6950/api
|
||||
; BASE_URL=http://10.69.5.204:6950/api
|
||||
; BASE_URL=http://173.212.247.232:6950/api
|
||||
; BASE_URL=http://192.168.1.164:6950/api
|
||||
; BASE_URL=http://192.168.120.160:6950/api
|
||||
; BASE_URL=http://10.10.2.92:6950/api
|
||||
@@ -8,3 +8,4 @@ BASE_URL=https://peakapi.qantra.co.zw/api
|
||||
; BASE_URL=http://10.10.3.92:6950/api
|
||||
CLIENTID=77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com
|
||||
SIMULATE_PAYMENT_SUCCESS=false
|
||||
; SIMULATE_PAYMENT_SUCCESS=true
|
||||
|
||||
BIN
assets/favicon.png
Normal file
BIN
assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/velocity-animation.gif
Normal file
BIN
assets/velocity-animation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/velocity.png
Normal file
BIN
assets/velocity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
5
devtools_options.yaml
Normal file
5
devtools_options.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Peak</string>
|
||||
<string>Velocity</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Peak</string>
|
||||
<string>Velocity</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
7
lib/interop.dart
Normal file
7
lib/interop.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
@JS('configure')
|
||||
external void configure(String sessionID);
|
||||
|
||||
@JS('showEmbeddedPage')
|
||||
external void showEmbeddedPage();
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/navbar.dart';
|
||||
import 'package:qpay/screens/confirm/confirm_screen.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_screen.dart';
|
||||
import 'package:qpay/screens/integration/integration_screen.dart';
|
||||
@@ -13,9 +14,11 @@ import 'package:qpay/screens/onboarding/phone/phone_screen.dart';
|
||||
import 'package:qpay/screens/onboarding/register/register_screen.dart';
|
||||
import 'package:qpay/screens/onboarding/verify/verify_screen.dart';
|
||||
import 'package:qpay/screens/pay/pay_screen.dart';
|
||||
import 'package:qpay/screens/poll/poll_screen.dart';
|
||||
import 'package:qpay/screens/receipt/receipt_screen.dart';
|
||||
import 'package:qpay/screens/recipient/recipients_screen.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'screens/gateway/gateway_web_screen.dart';
|
||||
import 'screens/home/home_screen.dart';
|
||||
import 'screens/history/history_screen.dart';
|
||||
import 'screens/profile_screen.dart';
|
||||
@@ -66,7 +69,7 @@ class _MyAppState extends State<MyApp> {
|
||||
Widget build(BuildContext context) {
|
||||
if (_showSplash) {
|
||||
return MaterialApp(
|
||||
title: 'Peak',
|
||||
title: 'Velocity',
|
||||
theme: AppTheme.lightTheme,
|
||||
themeMode: ThemeMode.light,
|
||||
home: SplashScreen(onSplashComplete: _onSplashComplete),
|
||||
@@ -75,7 +78,7 @@ class _MyAppState extends State<MyApp> {
|
||||
}
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'Peak',
|
||||
title: 'Velocity',
|
||||
theme: AppTheme.lightTheme,
|
||||
themeMode: ThemeMode.light, // Force light mode regardless of OS setting
|
||||
debugShowCheckedModeBanner: false,
|
||||
@@ -112,6 +115,14 @@ class _MyAppState extends State<MyApp> {
|
||||
path: '/gateway',
|
||||
builder: (context, state) => const GatewayScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/gateway-web',
|
||||
builder: (context, state) => const GatewayWebScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/poll',
|
||||
builder: (context, state) => const PollScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/receipt',
|
||||
builder: (context, state) => const ReceiptScreen(),
|
||||
@@ -167,65 +178,3 @@ class _MyAppState extends State<MyApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
const ScaffoldWithNavBar({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
onDestinationSelected: (index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/home');
|
||||
break;
|
||||
// case 1:
|
||||
// context.go('/recipients');
|
||||
// break;
|
||||
case 1:
|
||||
context.go('/history');
|
||||
break;
|
||||
// case 2:
|
||||
// context.go('/profile');
|
||||
// break;
|
||||
}
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
// NavigationDestination(
|
||||
// icon: Icon(Icons.contacts_outlined),
|
||||
// selectedIcon: Icon(Icons.contacts),
|
||||
// label: 'Recipients',
|
||||
// ),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'History',
|
||||
),
|
||||
// NavigationDestination(
|
||||
// icon: Icon(Icons.person_outline),
|
||||
// selectedIcon: Icon(Icons.person),
|
||||
// label: 'Profile',
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
// if (location.startsWith('/recipients')) return 1;
|
||||
if (location.startsWith('/history')) return 1;
|
||||
// if (location.startsWith('/profile')) return 2;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
5
lib/models/responsive_policy.dart
Normal file
5
lib/models/responsive_policy.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
class ResponsivePolicy {
|
||||
static const int sm = 400;
|
||||
static const int md = 600;
|
||||
static const int lg = 950;
|
||||
}
|
||||
209
lib/navbar.dart
Normal file
209
lib/navbar.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'models/responsive_policy.dart';
|
||||
|
||||
class ScaffoldWithNavBar extends StatefulWidget {
|
||||
const ScaffoldWithNavBar({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
|
||||
}
|
||||
|
||||
class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
|
||||
bool _isLoggedIn = false;
|
||||
late String initials;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
init();
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
var prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (prefs.getString("token") != null) {
|
||||
setState(() {
|
||||
_isLoggedIn = true;
|
||||
initials = prefs.getString("initials")!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > ResponsivePolicy.md) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
extended: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0, bottom: 10),
|
||||
child: Image(
|
||||
image: AssetImage('assets/velocity.png'),
|
||||
width: 150
|
||||
),
|
||||
),
|
||||
trailing: _buildLogin(context),
|
||||
labelType: NavigationRailLabelType.none,
|
||||
indicatorColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withAlpha(30),
|
||||
onDestinationSelected: (index) {
|
||||
_handleTap(index, context);
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: _buildNavigationRailDestinations(),
|
||||
),
|
||||
VerticalDivider(
|
||||
thickness: 1,
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||
),
|
||||
Expanded(child: widget.child),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
body: widget.child,
|
||||
bottomNavigationBar: NavigationBar(
|
||||
indicatorColor: Theme.of(context).colorScheme.primary.withAlpha(30),
|
||||
onDestinationSelected: (index) {
|
||||
_handleTap(index, context);
|
||||
},
|
||||
selectedIndex: _calculateSelectedIndex(context),
|
||||
destinations: _buildNavigationDestinations(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<NavigationRailDestination> _buildNavigationRailDestinations() {
|
||||
List<NavigationRailDestination> destinations = [];
|
||||
for (var destination in _buildDestinationData()) {
|
||||
destinations.add(
|
||||
NavigationRailDestination(
|
||||
icon: destination["icon"] as Widget,
|
||||
selectedIcon: destination["selectedIcon"] as Widget,
|
||||
label: Text(destination["label"], style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
return destinations;
|
||||
}
|
||||
|
||||
List<NavigationDestination> _buildNavigationDestinations() {
|
||||
List<NavigationDestination> destinations = [];
|
||||
for (var destination in _buildDestinationData()) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: destination["icon"] as Widget,
|
||||
selectedIcon: destination["selectedIcon"] as Widget,
|
||||
label: destination["label"] as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
return destinations;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _buildDestinationData() {
|
||||
return [
|
||||
{
|
||||
"icon": Icon(Icons.home_outlined),
|
||||
"selectedIcon": Icon(Icons.home),
|
||||
"label": 'Home',
|
||||
},
|
||||
{
|
||||
"icon": Icon(Icons.history_outlined),
|
||||
"selectedIcon": Icon(Icons.history),
|
||||
"label": 'History',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildLogin(BuildContext context) {
|
||||
if(_isLoggedIn) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 220,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Text(
|
||||
'Register or Login for more features',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.push('/onboarding/landing');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('Get Started', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap(int index, BuildContext context) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.go('/home');
|
||||
break;
|
||||
// case 1:
|
||||
// context.go('/recipients');
|
||||
// break;
|
||||
case 1:
|
||||
context.go('/history');
|
||||
break;
|
||||
// case 2:
|
||||
// context.go('/profile');
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateSelectedIndex(BuildContext context) {
|
||||
final String location = GoRouterState.of(context).uri.path;
|
||||
// if (location.startsWith('/recipients')) return 1;
|
||||
if (location.startsWith('/history')) return 1;
|
||||
// if (location.startsWith('/profile')) return 2;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -81,10 +82,13 @@ class ConfirmController extends ChangeNotifier {
|
||||
|
||||
transactionController.updateReceiptData(response);
|
||||
|
||||
if (transactionController.model.selectedPaymentProcessor.authType ==
|
||||
"WEB") {
|
||||
if (transactionController.model.selectedPaymentProcessor.authType == "WEB") {
|
||||
if (context.mounted) {
|
||||
context.push('/gateway');
|
||||
if(kIsWeb){
|
||||
context.push('/gateway-web');
|
||||
}else {
|
||||
context.push('/gateway');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await pollTransaction(transactionController.model.receiptData?['id']);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/confirm/confirm_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -88,255 +89,266 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1.0,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
), // Adjust spacing
|
||||
child: Text(
|
||||
"PROVIDER DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
...controller
|
||||
.transactionController
|
||||
.model
|
||||
.confirmationData["additionalData"]
|
||||
.map<Widget>((data) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
"PROVIDER DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
...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: [
|
||||
Text(
|
||||
data["name"] ?? "",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
data["value"] ?? "",
|
||||
style: TextStyle(fontSize: 16),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Our Charge",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller
|
||||
.transactionController
|
||||
.model
|
||||
.confirmationData["charge"]
|
||||
.toString(),
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
})
|
||||
.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
|
||||
if (controller
|
||||
.transactionController
|
||||
.model
|
||||
.confirmationData["charge"]
|
||||
.toString(),
|
||||
style: TextStyle(fontSize: 16),
|
||||
.confirmationData["gatewayCharge"] !=
|
||||
0)
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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
|
||||
if (controller
|
||||
.transactionController
|
||||
.model
|
||||
.confirmationData["gatewayCharge"]
|
||||
.toString(),
|
||||
style: TextStyle(fontSize: 16),
|
||||
.confirmationData["tax"] !=
|
||||
0)
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
if (controller
|
||||
.transactionController
|
||||
.model
|
||||
.confirmationData["tax"] !=
|
||||
0)
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Tax",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
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,
|
||||
),
|
||||
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(
|
||||
child: OutlinedButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
_buildPaymentProcessorButton(),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
child: Text('Cancel'),
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -76,15 +76,22 @@ class GatewayController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> pollTransaction(String uid) async {
|
||||
// only poll for 5 minutes
|
||||
int max = 30;
|
||||
while (!model.isCancelled) {
|
||||
max++;
|
||||
// Check cancellation flag instead of true
|
||||
// Wait 5 seconds before the next poll
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await Future.delayed(const Duration(seconds: 10));
|
||||
|
||||
// Check again after delay in case it was cancelled during the delay
|
||||
if (model.isCancelled) break;
|
||||
|
||||
await poll(uid);
|
||||
|
||||
if(max > 30) {
|
||||
model.isCancelled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -17,6 +15,7 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@@ -24,63 +23,71 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
// https://na.gateway.mastercard.com/checkout/pay/SESSION0002220827459I8059951J88?checkoutVersion=1.0.0
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
_initWebView(url);
|
||||
|
||||
_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']!);
|
||||
if(simulate) {
|
||||
sleep(Duration(seconds: 10));
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(seconds: 10));
|
||||
String targetUrl = url.replaceAll("/pay/", "/receipt/");
|
||||
_webViewController.loadRequest(Uri.parse(targetUrl));
|
||||
}
|
||||
}
|
||||
|
||||
void _initWebView(String url) {
|
||||
_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!}");
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(url.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),
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(url));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,15 +101,14 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
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(),
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
@@ -112,9 +118,12 @@ class _GatewayScreenState extends State<GatewayScreen> {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return controller.model.isLoading
|
||||
? const Center(child: CircularProgressIndicator(strokeWidth: 5))
|
||||
: WebViewWidget(controller: _webViewController);
|
||||
: SizedBox.expand(
|
||||
child: WebViewWidget(controller: _webViewController),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
259
lib/screens/gateway/gateway_web_screen.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/interop.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
class GatewayWebScreen extends StatefulWidget {
|
||||
const GatewayWebScreen({super.key});
|
||||
|
||||
@override
|
||||
State<GatewayWebScreen> createState() => _GatewayWebScreenState();
|
||||
}
|
||||
|
||||
class _GatewayWebScreenState extends State<GatewayWebScreen> {
|
||||
late WebViewController _webViewController;
|
||||
late GatewayController controller;
|
||||
late String url;
|
||||
String? sessionID;
|
||||
|
||||
bool integrationStarted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = GatewayController(context);
|
||||
|
||||
url = controller.transactionController.model.receiptData?['targetUrl'];
|
||||
}
|
||||
|
||||
// A function to set up the ResizeObserver
|
||||
void setupAttachmentObserver(web.Element element) {
|
||||
final observer = web.ResizeObserver(
|
||||
(JSArray entries, JSAny observer) {
|
||||
// The element has been laid out (and thus attached to the DOM)
|
||||
onElementAttached(element);
|
||||
|
||||
// Disconnect the observer once the action is done
|
||||
(observer as web.ResizeObserver).disconnect();
|
||||
}.toJS,
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
}
|
||||
|
||||
// Called after `element` is attached to the DOM.
|
||||
void onElementAttached(web.Element element) {
|
||||
// Your code to execute after the element is in the DOM goes here.
|
||||
print('Element with ID ${element.id} is attached to the DOM.');
|
||||
// You can now safely query the DOM or call JavaScript functions that rely
|
||||
// on the element being present.
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
initIframeView();
|
||||
});
|
||||
}
|
||||
|
||||
void initIframeView() async {
|
||||
final uri = Uri.parse(url);
|
||||
// 2. The pathSegments property returns a list of the parts of the path.
|
||||
// For "checkout/pay/SESSION...", the segments are ["checkout", "pay", "SESSION..."].
|
||||
final pathSegments = uri.pathSegments;
|
||||
// 3. Check if there are any segments and return the last one,
|
||||
// which is the session ID in this URL structure.
|
||||
if (pathSegments.isNotEmpty) {
|
||||
sessionID = pathSegments.last;
|
||||
|
||||
configure(sessionID!);
|
||||
}
|
||||
showEmbeddedPage();
|
||||
|
||||
Future.delayed(const Duration(seconds: 10), () async {
|
||||
web.HTMLIFrameElement element = fetchIframeFromDom(
|
||||
"hc-comms-layer-iframe",
|
||||
)!;
|
||||
print(element);
|
||||
element.onload = (JSAny event) {
|
||||
// This is called every time a new page loads in the iframe
|
||||
print(event);
|
||||
|
||||
try {
|
||||
final newLocation = element.contentWindow?.location.href;
|
||||
print('Iframe navigated to: $newLocation');
|
||||
// Perform actions based on the new URL
|
||||
|
||||
_navigateWhenDone(newLocation!);
|
||||
} catch (e) {
|
||||
// Handle potential (though unlikely for same-origin) security errors
|
||||
print('Could not access iframe location: $e');
|
||||
}
|
||||
}.toJS;
|
||||
|
||||
bool simulate = bool.parse(dotenv.env['SIMULATE_PAYMENT_SUCCESS']!);
|
||||
if (simulate) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
_navigateWhenDone("/checkout/receipt/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
web.HTMLIFrameElement? fetchIframeFromDom(String id) {
|
||||
final element = web.window.document.querySelector('#$id');
|
||||
if (element is web.HTMLIFrameElement) {
|
||||
print('Found iframe in DOM: ${element.id}');
|
||||
print(element.toString());
|
||||
return element;
|
||||
}
|
||||
|
||||
print('Could not find iframe with id: $id in the DOM.');
|
||||
return null;
|
||||
}
|
||||
|
||||
void _navigateWhenDone(String url) {
|
||||
if (url.contains("/checkout/receipt/")) {
|
||||
if (integrationStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Payment was successful, please wait while we process your order.",
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.black87,
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
integrationStarted = true;
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Complete your payment'),
|
||||
leading: context.canPop()
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
)
|
||||
: SizedBox.shrink(),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
listenable: controller,
|
||||
builder: (context, child) {
|
||||
if (controller.model.status == 'SUCCESS') {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/integration');
|
||||
});
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: ResponsivePolicy.md.toDouble(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.black87.withAlpha(20),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Once done on this page, check transaction status',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.push('/poll');
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Check status',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: constraints.maxHeight + 200,
|
||||
child: HtmlElementView.fromTagName(
|
||||
tagName: 'div',
|
||||
onElementCreated: (element) {
|
||||
final divElement =
|
||||
element as web.HTMLDivElement;
|
||||
divElement.id = "embed-target";
|
||||
setupAttachmentObserver(element);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class HistoryController extends ChangeNotifier {
|
||||
"debitPhone": "",
|
||||
"debitAccount": "",
|
||||
"debitCurrency": "USD",
|
||||
"debitRef": "peak",
|
||||
"debitRef": "Velocity",
|
||||
"creditPhone": "",
|
||||
"creditAccount": "07088597534",
|
||||
"billClientId": "powertel_zesa",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -23,7 +24,6 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final Map<String, String> _queryParams = {};
|
||||
final List<Animation<Offset>> _alignListAnimations = [];
|
||||
final int _pageSize = 8;
|
||||
|
||||
@override
|
||||
@@ -38,21 +38,9 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
);
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
vsync: this,
|
||||
)..forward();
|
||||
|
||||
List tranListIndices = [0, 1];
|
||||
for (int i = 0; i < tranListIndices.length; i++) {
|
||||
_alignListAnimations.add(
|
||||
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.075 * i, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setupData() async {
|
||||
@@ -90,56 +78,78 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchField(controller),
|
||||
if (controller.model.transactions.isEmpty)
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
Text(
|
||||
'No transactions yet. Make a payment to see your recent activity.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (controller.model.transactions.isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
...controller.model.transactions.map(
|
||||
(e) => _buildTransactionItem(e, context),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(
|
||||
constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toString()
|
||||
: constraints.maxWidth.toString(),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchField(controller),
|
||||
SizedBox(height: 10),
|
||||
if (controller.model.transactions.isEmpty)
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
Text(
|
||||
'No transactions yet. Make a payment to see your recent activity.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (controller.model.pageableModel != null &&
|
||||
controller.model.pageableModel!.number <
|
||||
controller.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',
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (controller.model.transactions.isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
...controller.model.transactions.map(
|
||||
(e) =>
|
||||
_buildTransactionItem(e, context),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (controller.model.pageableModel !=
|
||||
null &&
|
||||
controller.model.pageableModel!.number <
|
||||
controller
|
||||
.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),
|
||||
),
|
||||
),
|
||||
suffixIcon:
|
||||
_searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_queryParams.clear();
|
||||
controller.getTransactions({
|
||||
'userId': prefs.getString("userId")!,
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_queryParams.clear();
|
||||
controller.getTransactions({
|
||||
'userId': prefs.getString("userId")!,
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.clear),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
// controller.updateCreditAccount(value);
|
||||
@@ -227,8 +236,17 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
}
|
||||
|
||||
Widget _buildTransactionItem(Map<String, dynamic> e, BuildContext context) {
|
||||
int index = controller.model.transactions.indexOf(e);
|
||||
Animation<Offset> animation =
|
||||
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.175 * index, 1.0, curve: Curves.easeIn),
|
||||
),
|
||||
);
|
||||
|
||||
return SlideTransition(
|
||||
position: _alignListAnimations[0],
|
||||
position: animation,
|
||||
child: Skeletonizer(
|
||||
enabled: controller.model.isLoading,
|
||||
child: InkWell(
|
||||
@@ -260,18 +278,17 @@ class _HistoryScreenState extends State<HistoryScreen>
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Icon(
|
||||
e['status'] == 'SUCCESS'
|
||||
e['integrationStatus'] == 'SUCCESS'
|
||||
? Icons.check_circle
|
||||
: e['status'] == 'PENDING'
|
||||
: e['integrationStatus'] == 'PENDING'
|
||||
? Icons.pending
|
||||
: Icons.cancel,
|
||||
size: 16,
|
||||
color:
|
||||
e['status'] == 'SUCCESS'
|
||||
? Colors.green
|
||||
: e['status'] == 'PENDING'
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
color: e['integrationStatus'] == 'SUCCESS'
|
||||
? Colors.green
|
||||
: e['integrationStatus'] == 'PENDING'
|
||||
? Colors.orange
|
||||
: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -104,6 +104,9 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
Future<void> getProviders() async {
|
||||
try {
|
||||
model.filterableProviders = getFakeProviders();
|
||||
model.transactions = getFakeTransactions();
|
||||
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -138,6 +141,7 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
Future<void> getTransactions(String userId) async {
|
||||
model.isLoading = true;
|
||||
// model.transactions = getFakeTransactions();
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
@@ -147,8 +151,9 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
PageableModel pageableModel = PageableModel.fromJson(response);
|
||||
|
||||
model.transactions =
|
||||
pageableModel.content.map((e) => e as Map<String, dynamic>).toList();
|
||||
model.transactions = pageableModel.content
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
_showErrorSnackBar(
|
||||
@@ -165,8 +170,6 @@ class HomeController extends ChangeNotifier {
|
||||
Future<void> getCategories() async {
|
||||
// activate skeletonizer
|
||||
model.categories = getFakeCategoryData();
|
||||
model.transactions = getFakeTransactions();
|
||||
// model.filterableProviders = getFakeProviders();
|
||||
model.isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -200,10 +203,9 @@ class HomeController extends ChangeNotifier {
|
||||
|
||||
model.selectedCategory = category;
|
||||
|
||||
model.filterableProviders =
|
||||
model.providers
|
||||
.where((element) => element.category == category.label)
|
||||
.toList();
|
||||
model.filterableProviders = model.providers
|
||||
.where((element) => element.category == category.label)
|
||||
.toList();
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -224,7 +226,7 @@ class HomeController extends ChangeNotifier {
|
||||
"debitPhone": "",
|
||||
"debitAccount": "",
|
||||
"debitCurrency": "USD",
|
||||
"debitRef": "peak",
|
||||
"debitRef": "Velocity",
|
||||
"creditPhone": "",
|
||||
"creditAccount": "07088597534",
|
||||
"billClientId": "powertel_zesa",
|
||||
@@ -242,21 +244,21 @@ class HomeController extends ChangeNotifier {
|
||||
List<BillProvider> getFakeProviders() {
|
||||
return [
|
||||
BillProvider(
|
||||
clientId: '1',
|
||||
name: 'ZESA',
|
||||
description: 'Pay your electricity bills easily',
|
||||
clientId: 'econet_airtime',
|
||||
name: 'Econet Airtime',
|
||||
description: 'Econet Airtime',
|
||||
requiresAccount: false,
|
||||
requiresAmount: false,
|
||||
requiresAmount: true,
|
||||
requiresAmountFromMerchant: false,
|
||||
requiresPhone: false,
|
||||
requiresReversal: false,
|
||||
requiresPhone: true,
|
||||
requiresReversal: true,
|
||||
additionalDataString: '',
|
||||
processorType: '',
|
||||
uid: '',
|
||||
processorType: 'AIRTIME',
|
||||
uid: '78f1f497-c9eb-401c-b22c-884756e68e40',
|
||||
image: 'econet.png',
|
||||
label: '',
|
||||
category: '',
|
||||
accountFieldName: '',
|
||||
label: 'ECONET',
|
||||
category: 'ECONET',
|
||||
accountFieldName: 'Phone Number',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,12 +63,14 @@ class IntegrationController extends ChangeNotifier {
|
||||
model.status = response['status'];
|
||||
transactionController.updateReceiptData(response);
|
||||
|
||||
gatewayController.poll(transactionController.model.confirmationData['id']);
|
||||
} else {
|
||||
model.status = response['status'];
|
||||
model.errorMessage = response['errorMessage'];
|
||||
_showErrorSnackBar(response['errorMessage']);
|
||||
await gatewayController.poll(transactionController.model.confirmationData['id']);
|
||||
}
|
||||
|
||||
// regardless of poll result we proceed to receipt
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go('/receipt');
|
||||
});
|
||||
|
||||
model.isLoading = false;
|
||||
notifyListeners();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -68,49 +69,55 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(50),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
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\'ve successfully collected your funds. We are now '
|
||||
'updating your billing account. This won\'t take long',
|
||||
style: TextStyle(fontSize: 20),
|
||||
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();
|
||||
},
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'We\'ve successfully collected your funds. We are now '
|
||||
'updating your billing account. 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 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:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
|
||||
class CompleteScreen extends StatefulWidget {
|
||||
@@ -16,56 +17,61 @@ class _CompleteScreenState extends State<CompleteScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Registration Complete',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Thank you for registering with Peak payments',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/landing.png', width: 300),
|
||||
Text(
|
||||
'Tap Login to get started',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
child: Text('Go to Login', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Registration Complete',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Thank you for registering with Velocity payments',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/landing.png', width: 300),
|
||||
Text(
|
||||
'Tap Login to get started',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
child: Text('Go to Login', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
|
||||
class LandingScreen extends StatefulWidget {
|
||||
@@ -16,73 +17,82 @@ class _LandingScreenState extends State<LandingScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Welcome to Peak',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'The fastest way to pay for local goods and services',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/landing.png', width: 300),
|
||||
Image.asset('assets/peak.png', width: 100),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Register or login to get started',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/register');
|
||||
},
|
||||
child: Text('Register', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Welcome to Velocity',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'The fastest way to pay for local goods and services',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/landing.png', width: 300),
|
||||
Image.asset('assets/velocity.png', width: 150),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Register or login to get started',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(height: 50),
|
||||
Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
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:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:google_sign_in_web/web_only.dart' as web;
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/login/login_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
@@ -17,6 +21,8 @@ class LoginScreen extends StatefulWidget {
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
late SharedPreferences prefs;
|
||||
late GoogleSignIn googleSignIn;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _loading = false;
|
||||
|
||||
@@ -30,22 +36,23 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loginController = LoginController(context);
|
||||
|
||||
googleSignIn = GoogleSignIn.instance;
|
||||
if(kIsWeb){
|
||||
googleSignIn.initialize();
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
|
||||
}else {
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async {
|
||||
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
|
||||
if (GoogleSignIn.instance.supportsAuthenticate()){
|
||||
|
||||
unawaited(GoogleSignIn.instance.authenticate().then((value) async {
|
||||
print(value);
|
||||
|
||||
saveUser(value);
|
||||
await _loginController.login(value.email, value.id);
|
||||
context.go('/');
|
||||
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
@@ -85,6 +92,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
?.authorizationClient
|
||||
.authorizationForScopes(scopes);
|
||||
// #enddocregion CheckAuthorization
|
||||
|
||||
print(user);
|
||||
saveUser(user as GoogleSignInAccount);
|
||||
|
||||
await _loginController.login(user.email, user.id);
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationError(Object e) async {
|
||||
@@ -105,100 +118,117 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Welcome back to Peak',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
Image.asset('assets/password.png', width: 300),
|
||||
Text(
|
||||
'Tap on your preferred social media platform to get started',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
// Username Field
|
||||
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),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Login',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
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: [
|
||||
SizedBox(height: 5),
|
||||
TextButton(
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGoogleButton(){
|
||||
return Skeletonizer(
|
||||
enabled: _loading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
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_contacts/flutter_contacts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/phone/phone_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -82,89 +83,94 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
||||
padding: const EdgeInsets.all(25.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Verify Your Device',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
'We will send you a verification code to this number',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/phone.png', width: 300),
|
||||
SizedBox(height: 20),
|
||||
_buildPhoneField(),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Skeletonizer(
|
||||
enabled: phoneController.model.isLoading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
String fullPhoneNumber =
|
||||
selectedCountryCode + _phoneController.text;
|
||||
|
||||
phoneController.updatePhone(fullPhoneNumber);
|
||||
await phoneController.register();
|
||||
if(phoneController.model.status != 'failed') {
|
||||
context.go('/onboarding/verify');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text('Send Code', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
},
|
||||
child: Text(
|
||||
'Go to Login',
|
||||
style: TextStyle(fontSize: 16),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Verify Your Device',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
'We will send you a verification code to this number',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/phone.png', width: 300),
|
||||
SizedBox(height: 20),
|
||||
_buildPhoneField(),
|
||||
SizedBox(height: 30),
|
||||
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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:google_sign_in_web/web_only.dart' as web;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/onboarding_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -17,6 +20,7 @@ class RegisterScreen extends StatefulWidget {
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
late OnboardingController onboardingController;
|
||||
late GoogleSignIn googleSignIn;
|
||||
|
||||
bool _loading = false;
|
||||
|
||||
@@ -32,17 +36,22 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
listen: false,
|
||||
);
|
||||
// onboardingController.resetState();
|
||||
|
||||
googleSignIn = GoogleSignIn.instance;
|
||||
if(kIsWeb){
|
||||
googleSignIn.initialize();
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
|
||||
}else {
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
}
|
||||
}
|
||||
|
||||
void signInWithGoogle() {
|
||||
final GoogleSignIn googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
unawaited(googleSignIn.initialize(serverClientId: dotenv.env['CLIENTID']));
|
||||
|
||||
if (GoogleSignIn.instance.supportsAuthenticate()){
|
||||
unawaited(GoogleSignIn.instance.authenticate().then((value) {
|
||||
print(value);
|
||||
onboardingController.updateGoogleUser(value);
|
||||
googleSignIn.authenticationEvents
|
||||
.listen(_handleAuthenticationEvent)
|
||||
.onError(_handleAuthenticationError);
|
||||
@@ -51,9 +60,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationEvent(
|
||||
GoogleSignInAuthenticationEvent event,
|
||||
) async {
|
||||
Future<void> _handleAuthenticationEvent(GoogleSignInAuthenticationEvent event) async {
|
||||
// #docregion CheckAuthorization
|
||||
final GoogleSignInAccount? user = // ...
|
||||
// #enddocregion CheckAuthorization
|
||||
@@ -68,6 +75,9 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
?.authorizationClient
|
||||
.authorizationForScopes(scopes);
|
||||
// #enddocregion CheckAuthorization
|
||||
print(user);
|
||||
onboardingController.updateGoogleUser(user as GoogleSignInAccount);
|
||||
|
||||
}
|
||||
|
||||
Future<void> _handleAuthenticationError(Object e) async {
|
||||
@@ -83,170 +93,184 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Registration',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
"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(
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
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,
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Registration',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Skeletonizer(
|
||||
enabled: _loading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
signInWithGoogle();
|
||||
},
|
||||
icon: Image.asset(
|
||||
'assets/google.png',
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
label: Text('Google', style: TextStyle(fontSize: 16)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
"Let's start by getting some of your online details to get you started",
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/social.png', width: 300),
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Social Login Buttons
|
||||
if(onboardingController.model.googleUser == null)
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Choose from the options below to continue',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: Colors.grey.shade300)),
|
||||
],
|
||||
),
|
||||
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: 20,),
|
||||
Text(
|
||||
"Welcome, ${onboardingController.model.googleUser?.displayName}",
|
||||
style: TextStyle(fontSize: 25),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
Text("Tap Proceed to continue", style: TextStyle(fontSize: 18),)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/');
|
||||
},
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
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:go_router/go_router.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/onboarding/verify/verify_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
@@ -59,169 +60,162 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Verify Your Account',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Enter the 6-digit code sent to your phone/email',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/verify.png', width: 300),
|
||||
// Verification Code Input
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(
|
||||
6,
|
||||
(index) => SizedBox(
|
||||
width: 50,
|
||||
child: TextFormField(
|
||||
controller: _codeControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 1,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth > ResponsivePolicy.md
|
||||
? ResponsivePolicy.md.toDouble()
|
||||
: constraints.maxWidth;
|
||||
return SizedBox(
|
||||
width: maxWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: 75),
|
||||
Text(
|
||||
'Verify Your Account',
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
'Enter the 6-digit code sent to your phone/email',
|
||||
style: TextStyle(fontSize: 18),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Image.asset('assets/verify.png', width: 300),
|
||||
// Verification Code Input
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(
|
||||
6,
|
||||
(index) => SizedBox(
|
||||
width: 50,
|
||||
child: TextFormField(
|
||||
controller: _codeControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 1,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
onChanged: (value) => _onCodeChanged(value, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
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
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Didn't receive the code? ",
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
verifyController.resendOtp();
|
||||
},
|
||||
child: Text(
|
||||
'Resend Code',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
SizedBox(height: 5),
|
||||
if(errorMessage != null)
|
||||
Text(errorMessage ?? '', style: TextStyle(color: Colors.red)),
|
||||
SizedBox(height: 30),
|
||||
// Resend Code Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Didn't receive the code? ",
|
||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 16),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
verifyController.resendOtp();
|
||||
},
|
||||
child: Text(
|
||||
'Resend Code',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// SizedBox(height: 20),
|
||||
// // Timer for resend (optional)
|
||||
// Text(
|
||||
// 'Resend available in 2:30',
|
||||
// style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||
// ),
|
||||
SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SizedBox(
|
||||
height: 130,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Skeletonizer(
|
||||
enabled: verifyController.model.isLoading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
showErrorMessage(null);
|
||||
String code = '';
|
||||
for (var controller in _codeControllers) {
|
||||
code += controller.text;
|
||||
}
|
||||
// SizedBox(height: 20),
|
||||
// // Timer for resend (optional)
|
||||
// Text(
|
||||
// 'Resend available in 2:30',
|
||||
// style: TextStyle(color: Colors.grey.shade500, fontSize: 14),
|
||||
// ),
|
||||
SizedBox(height: 40),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Skeletonizer(
|
||||
enabled: verifyController.model.isLoading,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
showErrorMessage(null);
|
||||
String code = '';
|
||||
for (var controller in _codeControllers) {
|
||||
code += controller.text;
|
||||
}
|
||||
|
||||
if(code.length != 6) {
|
||||
showErrorMessage('Invalid code');
|
||||
return;
|
||||
}
|
||||
verifyController.updateCode(code);
|
||||
await verifyController.verifyOtp();
|
||||
if(verifyController.model.status == 'done') {
|
||||
context.go('/onboarding/complete');
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
if(code.length != 6) {
|
||||
showErrorMessage('Invalid code');
|
||||
return;
|
||||
}
|
||||
verifyController.updateCode(code);
|
||||
await verifyController.verifyOtp();
|
||||
if(verifyController.model.status == 'done') {
|
||||
context.go('/onboarding/complete');
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Verify Code',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
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(width: 16),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/onboarding/login');
|
||||
context.go('/');
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Back to Login',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
child: Text('Continue as guest?', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
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(
|
||||
type: "CONFIRM",
|
||||
billClientId: model.selectedProvider?.clientId ?? '',
|
||||
debitRef: 'peak',
|
||||
debitRef: 'Velocity',
|
||||
debitCurrency: 'USD',
|
||||
creditAccount: transactionController.model.formData.creditAccount,
|
||||
creditPhone: transactionController.model.formData.creditPhone,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/pay/pay_controller.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@@ -178,105 +179,127 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
||||
position: _slideAnimation,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
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: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.accountFieldName ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transactionController
|
||||
.model
|
||||
.formData
|
||||
.creditAccount,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Divider(color: Theme.of(context).colorScheme.primary),
|
||||
InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showAdditionalRecipientDetails =
|
||||
!showAdditionalRecipientDetails;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
showAdditionalRecipientDetails
|
||||
? Icons.arrow_drop_up
|
||||
: Icons.arrow_drop_down,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
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: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
controller
|
||||
.model
|
||||
.selectedProvider
|
||||
?.accountFieldName ??
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
transactionController
|
||||
.model
|
||||
.formData
|
||||
.creditAccount,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 20),
|
||||
InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showAdditionalRecipientDetails =
|
||||
!showAdditionalRecipientDetails;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(
|
||||
showAdditionalRecipientDetails
|
||||
? Icons.arrow_drop_up
|
||||
: Icons.arrow_drop_down,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
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:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qpay/models/responsive_policy.dart';
|
||||
import 'package:qpay/screens/transaction_controller.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
||||
@@ -83,81 +84,107 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Provider", style: TextStyle(fontSize: 16)),
|
||||
Text(
|
||||
controller.model.selectedProvider?.name ?? "",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Divider(color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 20),
|
||||
_buildAccountField(controller),
|
||||
const SizedBox(height: 20),
|
||||
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,
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
|
||||
ResponsivePolicy.md.toString() :
|
||||
constraints.maxWidth.toString()),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black87.withAlpha(20)),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"TRANSACTIONS DETAILS",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(Icons.arrow_forward_ios, size: 12),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Provider",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
)),
|
||||
Text(
|
||||
controller.model.selectedProvider?.name ?? "",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Recent Recipients",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildRecipientItems(controller),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
_buildAccountField(controller),
|
||||
const SizedBox(height: 20),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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 share_plus
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
import webview_flutter_wkwebview
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
@@ -18,5 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||
}
|
||||
|
||||
52
pubspec.lock
52
pubspec.lock
@@ -153,6 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -377,7 +385,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
google_sign_in_web:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||
@@ -392,6 +400,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -861,6 +877,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.28"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.6"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -869,6 +909,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -950,7 +998,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: qpay
|
||||
description: "Peak payments solutions"
|
||||
description: "Velocity payments solutions"
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
@@ -55,6 +55,10 @@ dependencies:
|
||||
gif_view: ^0.4.0
|
||||
google_sign_in: ^7.1.1
|
||||
firebase_core: ^4.1.0
|
||||
url_launcher: ^6.3.2
|
||||
html: ^0.15.6
|
||||
web: ^1.1.1
|
||||
google_sign_in_web: ^1.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 85 KiB |
@@ -20,6 +20,7 @@
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="google-signin-client_id" content="77433712483-ng7pntvcpf6tnjccriuqm8dbna8vvp3b.apps.googleusercontent.com">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -30,8 +31,27 @@
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>qpay</title>
|
||||
<title>Velocity</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script src="https://na.gateway.mastercard.com/static/checkout/checkout.min.js">
|
||||
</script>
|
||||
<!-- <script src="https://test-gateway.mastercard.com/static/checkout/checkout.min.js">-->
|
||||
<!-- </script>-->
|
||||
|
||||
<script type="text/javascript">
|
||||
function configure(sessionID) {
|
||||
Checkout.configure({
|
||||
session: {
|
||||
id: sessionID
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEmbeddedPage() {
|
||||
Checkout.showEmbeddedPage('#embed-target');
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
|
||||
Reference in New Issue
Block a user