improving VMC flow
This commit is contained in:
@@ -40,7 +40,8 @@ flutter run --dart-define=APP_ENV=live
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter build web --dart-define=APP_ENV=live
|
flutter build web --dart-define=APP_ENV=live
|
||||||
flutter build apk --dart-define=APP_ENV=live
|
flutter build apk --dart-define=APP_ENV=live --dart-define=BASE_URL=https://payapi.velocityafrica.net/api
|
||||||
|
flutter build appdundle --dart-define=APP_ENV=live --dart-define=BASE_URL=https://payapi.velocityafrica.net/api
|
||||||
```
|
```
|
||||||
|
|
||||||
### Quick pre-release checks
|
### Quick pre-release checks
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Remove this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?android:colorBackground" />
|
<item android:drawable="@color/splash_background" />
|
||||||
|
|
||||||
<!-- App Logo -->
|
|
||||||
<item
|
|
||||||
android:width="100dp"
|
|
||||||
android:height="100dp"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<bitmap
|
|
||||||
android:gravity="fill"
|
|
||||||
android:src="@mipmap/ic_launcher"
|
|
||||||
android:mipMap="true"/>
|
|
||||||
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
<!-- Remove this file to customize your launch splash screen -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/splash_background" />
|
<item android:drawable="@color/splash_background" />
|
||||||
|
|
||||||
<!-- App Logo -->
|
|
||||||
<item
|
|
||||||
android:width="100dp"
|
|
||||||
android:height="100dp"
|
|
||||||
android:gravity="center">
|
|
||||||
|
|
||||||
<bitmap
|
|
||||||
android:gravity="fill"
|
|
||||||
android:src="@mipmap/ic_launcher"
|
|
||||||
android:mipMap="true"/>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ class ApiResponse<T> {
|
|||||||
final String? error;
|
final String? error;
|
||||||
final int? statusCode;
|
final int? statusCode;
|
||||||
|
|
||||||
const ApiResponse({
|
const ApiResponse({this.data, this.error, this.statusCode});
|
||||||
this.data,
|
|
||||||
this.error,
|
|
||||||
this.statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
bool get isSuccess => error == null && data != null;
|
bool get isSuccess => error == null && data != null;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import 'package:qpay/screens/accounts/account_provider.dart';
|
|||||||
import 'package:qpay/screens/confirm/confirm_controller.dart';
|
import 'package:qpay/screens/confirm/confirm_controller.dart';
|
||||||
import 'package:qpay/widgets/app_snack_bar.dart';
|
import 'package:qpay/widgets/app_snack_bar.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../transactions/transaction_model.dart';
|
||||||
|
|
||||||
class ConfirmScreen extends StatefulWidget {
|
class ConfirmScreen extends StatefulWidget {
|
||||||
const ConfirmScreen({super.key});
|
const ConfirmScreen({super.key});
|
||||||
@@ -122,7 +125,23 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
|||||||
.authType ==
|
.authType ==
|
||||||
"WEB") {
|
"WEB") {
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
context.push('/gateway-web');
|
final txData = TransactionModel.fromJson(response.data!);
|
||||||
|
// VMC is the only method that requires redirect for now
|
||||||
|
if (txData.targetUrl != null) {
|
||||||
|
final proceed = await _showLeavingSiteBottomSheet();
|
||||||
|
if (proceed != true) return;
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
await launchUrl(
|
||||||
|
Uri.parse(txData.targetUrl!),
|
||||||
|
mode: LaunchMode
|
||||||
|
.externalApplication, // always open in external browser
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sent user to another tab and navigate to polling page to wait for response
|
||||||
|
if (!mounted) return;
|
||||||
|
context.push('/poll/${txData.id}');
|
||||||
} else {
|
} else {
|
||||||
context.push('/gateway');
|
context.push('/gateway');
|
||||||
}
|
}
|
||||||
@@ -304,6 +323,134 @@ class _ConfirmScreenState extends State<ConfirmScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shows a bottom sheet informing the user they are about to leave this site.
|
||||||
|
/// Returns `true` if the user chooses to proceed, `null` if they dismiss or cancel.
|
||||||
|
Future<bool?> _showLeavingSiteBottomSheet() async {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return showModalBottomSheet<bool>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF1A1A2E) : Colors.white,
|
||||||
|
borderRadius: const BorderRadius.vertical(
|
||||||
|
top: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.open_in_new_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'You\'re leaving QPay',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'You are about to be redirected to an external payment page. If nothing happens after the redirect, please make sure pop-ups are allowed in your browser settings.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(sheetContext).pop(true);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Proceed',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(sheetContext).pop(null);
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
class GatewayModel {
|
class GatewayModel {
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
String status = '';
|
String status = 'PENDING';
|
||||||
|
String pollStatus = 'PENDING';
|
||||||
String? errorMessage;
|
String? errorMessage;
|
||||||
bool isCancelled = false;
|
bool isCancelled = false;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
int pollAttempt = 0;
|
||||||
|
int pollMaxAttempts = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GatewayController extends ChangeNotifier {
|
class GatewayController extends ChangeNotifier {
|
||||||
@@ -84,19 +87,26 @@ class GatewayController extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<ApiResponse<Map<String, dynamic>>> pollTransaction(String uid) async {
|
Future<ApiResponse<Map<String, dynamic>>> pollTransaction(String uid) async {
|
||||||
// poll up to 15 times (~45 seconds at 3-second intervals)
|
// poll up to 15 times (~45 seconds at 3-second intervals)
|
||||||
int maxAttempts = 15;
|
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
|
|
||||||
while (!model.isCancelled && attempt < maxAttempts) {
|
model.pollAttempt = 0;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
while (!model.isCancelled && attempt < model.pollMaxAttempts) {
|
||||||
attempt++;
|
attempt++;
|
||||||
|
model.pollAttempt = attempt;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
ApiResponse<Map<String, dynamic>> result = await poll(uid);
|
ApiResponse<Map<String, dynamic>> result = await poll(uid);
|
||||||
|
|
||||||
// Only stop on success or failure
|
// Only stop on success or failure
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
String status = model.status;
|
|
||||||
// If status is SUCCESS, we're done
|
// If status is SUCCESS, we're done
|
||||||
if (status == 'SUCCESS') {
|
if (model.status == 'SUCCESS') {
|
||||||
|
model.isCancelled = true;
|
||||||
|
model.pollStatus = model.status;
|
||||||
|
finishLoading();
|
||||||
|
notifyListeners();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,18 +117,15 @@ class GatewayController extends ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
await Future.delayed(const Duration(seconds: 3));
|
await Future.delayed(const Duration(seconds: 3));
|
||||||
if (model.isCancelled) break;
|
if (model.isCancelled) break;
|
||||||
|
model.status = 'PENDING';
|
||||||
// Network error — show failure UI and stop
|
|
||||||
model.isCancelled = true;
|
|
||||||
model.status = 'FAILED';
|
|
||||||
model.errorMessage = result.error;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timed out after max attempts
|
// Timed out after max attempts
|
||||||
model.isCancelled = true;
|
model.isCancelled = true;
|
||||||
model.status = 'FAILED';
|
model.status = 'FAILED';
|
||||||
|
model.pollStatus = 'FAILED';
|
||||||
model.errorMessage = 'We failed to check your transaction status';
|
model.errorMessage = 'We failed to check your transaction status';
|
||||||
finishLoading();
|
finishLoading();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ class _PayScreenState extends State<PayScreen> with TickerProviderStateMixin {
|
|||||||
.account_balance_wallet_outlined,
|
.account_balance_wallet_outlined,
|
||||||
label: "City Wallet Balance",
|
label: "City Wallet Balance",
|
||||||
value:
|
value:
|
||||||
'\$${cityWallet!.balance.toStringAsFixed(2)}',
|
'\$${cityWallet.balance.toStringAsFixed(2)}',
|
||||||
valueStyle: TextStyle(
|
valueStyle: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _PollScreenState extends State<PollScreen> {
|
|||||||
body: ListenableBuilder(
|
body: ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
if (controller.model.status == 'SUCCESS') {
|
if (controller.model.pollStatus == 'SUCCESS') {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.go('/integration');
|
context.go('/integration');
|
||||||
});
|
});
|
||||||
@@ -90,13 +90,17 @@ class _PollScreenState extends State<PollScreen> {
|
|||||||
subtitle:
|
subtitle:
|
||||||
'We\'re checking with our gateway if your transaction was '
|
'We\'re checking with our gateway if your transaction was '
|
||||||
'successful. This won\'t take long.',
|
'successful. This won\'t take long.',
|
||||||
status: controller.model.status,
|
status: controller.model.pollStatus,
|
||||||
isLoading: controller.model.isLoading,
|
isLoading: controller.model.isLoading,
|
||||||
errorMessage:
|
errorMessage:
|
||||||
controller.model.errorMessage ??
|
controller.model.errorMessage ??
|
||||||
'We failed to check your transaction status',
|
'We failed to check your transaction status',
|
||||||
|
pollAttempt: controller.model.pollAttempt,
|
||||||
|
pollMaxAttempts: controller.model.pollMaxAttempts,
|
||||||
onRetry: () {
|
onRetry: () {
|
||||||
controller.model.isCancelled = false;
|
controller.model.isCancelled = false;
|
||||||
|
controller.model.pollStatus = 'PENDING';
|
||||||
|
controller.model.pollAttempt = 0;
|
||||||
_startPolling();
|
_startPolling();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../onboarding/auth_provider.dart';
|
|
||||||
|
|
||||||
class RecipientsScreen extends StatefulWidget {
|
class RecipientsScreen extends StatefulWidget {
|
||||||
const RecipientsScreen({super.key});
|
const RecipientsScreen({super.key});
|
||||||
|
|
||||||
@@ -87,6 +85,26 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_applyRecipient(Recipient recipient) {
|
||||||
|
String account = recipient.account ?? '';
|
||||||
|
if (!transactionController.model.selectedProvider!.requiresAccount) {
|
||||||
|
account = recipient.phoneNumber ?? '';
|
||||||
|
}
|
||||||
|
transactionController.model.formData = transactionController.model.formData
|
||||||
|
.copyWith(
|
||||||
|
creditAccount: account,
|
||||||
|
creditName: recipient.name ?? '',
|
||||||
|
creditPhone: recipient.phoneNumber ?? '',
|
||||||
|
creditEmail: recipient.email ?? '',
|
||||||
|
providerLabel:
|
||||||
|
transactionController.model.formData.providerLabel.isEmpty
|
||||||
|
? recipient.latestProviderLabel ?? ''
|
||||||
|
: transactionController.model.formData.providerLabel,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
context.push("/make-payment");
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -330,32 +348,7 @@ class _RecipientsScreenState extends State<RecipientsScreen>
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () => _applyRecipient(recipient),
|
||||||
String account = recipient.account ?? '';
|
|
||||||
if (!transactionController
|
|
||||||
.model
|
|
||||||
.selectedProvider!
|
|
||||||
.requiresAccount) {
|
|
||||||
account = recipient.phoneNumber ?? '';
|
|
||||||
}
|
|
||||||
transactionController
|
|
||||||
.model
|
|
||||||
.formData = transactionController.model.formData.copyWith(
|
|
||||||
creditAccount: account,
|
|
||||||
creditName: recipient.name ?? '',
|
|
||||||
creditPhone: recipient.phoneNumber ?? '',
|
|
||||||
creditEmail: recipient.email ?? '',
|
|
||||||
providerLabel:
|
|
||||||
transactionController
|
|
||||||
.model
|
|
||||||
.formData
|
|
||||||
.providerLabel
|
|
||||||
.isEmpty
|
|
||||||
? recipient.latestProviderLabel ?? ''
|
|
||||||
: transactionController.model.formData.providerLabel,
|
|
||||||
);
|
|
||||||
context.push("/make-payment");
|
|
||||||
},
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class TransactionModel {
|
|||||||
final String? creditEmail;
|
final String? creditEmail;
|
||||||
final String? productUid;
|
final String? productUid;
|
||||||
final String? targetUrl;
|
final String? targetUrl;
|
||||||
|
final String? redirectUrl;
|
||||||
final List<Map<String, dynamic>>? additionalData;
|
final List<Map<String, dynamic>>? additionalData;
|
||||||
|
|
||||||
const TransactionModel({
|
const TransactionModel({
|
||||||
@@ -145,6 +146,7 @@ class TransactionModel {
|
|||||||
this.creditEmail,
|
this.creditEmail,
|
||||||
this.productUid,
|
this.productUid,
|
||||||
this.targetUrl,
|
this.targetUrl,
|
||||||
|
this.redirectUrl,
|
||||||
this.additionalData,
|
this.additionalData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ TransactionModel _$TransactionModelFromJson(Map<String, dynamic> json) =>
|
|||||||
creditEmail: json['creditEmail'] as String?,
|
creditEmail: json['creditEmail'] as String?,
|
||||||
productUid: json['productUid'] as String?,
|
productUid: json['productUid'] as String?,
|
||||||
targetUrl: json['targetUrl'] as String?,
|
targetUrl: json['targetUrl'] as String?,
|
||||||
|
redirectUrl: json['redirectUrl'] as String?,
|
||||||
additionalData: const AdditionalDataJsonConverter().fromJson(
|
additionalData: const AdditionalDataJsonConverter().fromJson(
|
||||||
json['additionalData'] as List?,
|
json['additionalData'] as List?,
|
||||||
),
|
),
|
||||||
@@ -120,6 +121,7 @@ Map<String, dynamic> _$TransactionModelToJson(
|
|||||||
'creditEmail': instance.creditEmail,
|
'creditEmail': instance.creditEmail,
|
||||||
'productUid': instance.productUid,
|
'productUid': instance.productUid,
|
||||||
'targetUrl': instance.targetUrl,
|
'targetUrl': instance.targetUrl,
|
||||||
|
'redirectUrl': instance.redirectUrl,
|
||||||
'additionalData': const AdditionalDataJsonConverter().toJson(
|
'additionalData': const AdditionalDataJsonConverter().toJson(
|
||||||
instance.additionalData,
|
instance.additionalData,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ class _WorkspaceScreenState extends State<WorkspaceScreen>
|
|||||||
_loadWorkspaces();
|
_loadWorkspaces();
|
||||||
} else {
|
} else {
|
||||||
// if not logged in then create temp workspace
|
// if not logged in then create temp workspace
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
if (prefs.getString("workspaceId") == null) {
|
||||||
prefs.setString('workspaceId', const Uuid().v4());
|
await prefs.setString("workspaceId", const Uuid().v4());
|
||||||
});
|
}
|
||||||
_navigateToHome();
|
_navigateToHome();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class StepLoadingWidget extends StatefulWidget {
|
|||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final VoidCallback? onRetry;
|
final VoidCallback? onRetry;
|
||||||
|
final int pollAttempt;
|
||||||
|
final int pollMaxAttempts;
|
||||||
|
|
||||||
const StepLoadingWidget({
|
const StepLoadingWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -21,6 +23,8 @@ class StepLoadingWidget extends StatefulWidget {
|
|||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.onRetry,
|
this.onRetry,
|
||||||
|
this.pollAttempt = 0,
|
||||||
|
this.pollMaxAttempts = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -144,7 +148,24 @@ class _StepLoadingWidgetState extends State<StepLoadingWidget>
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 50),
|
// Poll attempt counter
|
||||||
|
if (widget.pollMaxAttempts > 0 &&
|
||||||
|
widget.status != 'SUCCESS' &&
|
||||||
|
widget.status != 'FAILED')
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Text(
|
||||||
|
'Attempt ${widget.pollAttempt} of ${widget.pollMaxAttempts}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
|
||||||
// Error state with retry
|
// Error state with retry
|
||||||
if (widget.status == 'FAILED')
|
if (widget.status == 'FAILED')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 1.0.1+3
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
Reference in New Issue
Block a user