feat: Add Check Location feature with validation and UI enhancements
This commit is contained in:
parent
b6a82438dd
commit
0da8d5621c
|
@ -6,10 +6,9 @@ import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/liveness_detection_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/check-location/check_location.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
|
@ -51,6 +50,11 @@ class AppPages {
|
||||||
page: () => const ForgotPasswordScreen(),
|
page: () => const ForgotPasswordScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.checkLocation,
|
||||||
|
page: () => const CheckLocationScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: AppRoutes.locationWarning,
|
name: AppRoutes.locationWarning,
|
||||||
page: () => const LocationWarningScreen(),
|
page: () => const LocationWarningScreen(),
|
||||||
|
|
|
@ -86,11 +86,18 @@ class AuthenticationRepository extends GetxController {
|
||||||
session?.user.userMetadata?['profile_status'] == 'completed';
|
session?.user.userMetadata?['profile_status'] == 'completed';
|
||||||
|
|
||||||
// Cek lokasi terlebih dahulu
|
// Cek lokasi terlebih dahulu
|
||||||
if (await _locationService.isLocationValidForFeature() == false) {
|
if (!isFirstTime) {
|
||||||
_navigateToRoute(AppRoutes.locationWarning);
|
bool isLocationValid =
|
||||||
return;
|
await _locationService.isLocationValidForFeature();
|
||||||
|
if (!isLocationValid) {
|
||||||
|
Logger().w('Location is invalid, redirecting to location warning');
|
||||||
|
_navigateToRoute(AppRoutes.locationWarning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger().d('Available session: $session');
|
||||||
|
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
if (!isEmailVerified) {
|
if (!isEmailVerified) {
|
||||||
_navigateToRoute(AppRoutes.emailVerification);
|
_navigateToRoute(AppRoutes.emailVerification);
|
||||||
|
@ -109,7 +116,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error in screenRedirect: $e');
|
Logger().e('Error in screenRedirect: $e');
|
||||||
_navigateToRoute(AppRoutes.signIn);
|
_navigateToRoute(AppRoutes.checkLocation);
|
||||||
} finally {
|
} finally {
|
||||||
_isRedirecting = false;
|
_isRedirecting = false;
|
||||||
Logger().d('Screen redirect completed');
|
Logger().d('Screen redirect completed');
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/cores/services/location_service.dart';
|
||||||
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
|
||||||
|
class CheckLocationController extends GetxController
|
||||||
|
with GetSingleTickerProviderStateMixin {
|
||||||
|
final VoidCallback? onSuccess;
|
||||||
|
final LocationService _locationService = LocationService.instance;
|
||||||
|
|
||||||
|
// Reactive variables
|
||||||
|
final RxInt currentPage = 0.obs;
|
||||||
|
final RxBool isLoading = false.obs;
|
||||||
|
final RxBool isLocationValid = false.obs;
|
||||||
|
final RxString errorMessage = ''.obs;
|
||||||
|
final RxBool showUIElements = true.obs;
|
||||||
|
final RxBool bottomSheetShown = false.obs;
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
late PageController pageController;
|
||||||
|
late AnimationController animController;
|
||||||
|
late Animation<double> scaleAnimation;
|
||||||
|
late Timer autoSlideTimer;
|
||||||
|
|
||||||
|
// Slide data
|
||||||
|
final List<Map<String, dynamic>> slides = [
|
||||||
|
{
|
||||||
|
'image': TImages.womanHuggingEarth,
|
||||||
|
'title': 'Location Verification',
|
||||||
|
'subtitle':
|
||||||
|
'We need to verify your location as this app is specifically designed for the Jember region',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'image': TImages.communication,
|
||||||
|
'title': 'Region-Specific Services',
|
||||||
|
'subtitle':
|
||||||
|
'Our emergency services and features are only available within Jember boundaries',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'image': TImages.callingHelp,
|
||||||
|
'title': 'Your Safety Matters',
|
||||||
|
'subtitle':
|
||||||
|
'Location validation ensures you receive appropriate assistance when needed',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
CheckLocationController({this.onSuccess});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
// Initialize controllers
|
||||||
|
pageController = PageController(viewportFraction: 0.8);
|
||||||
|
|
||||||
|
// Setup animation controllers
|
||||||
|
animController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
);
|
||||||
|
|
||||||
|
scaleAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: 60.0,
|
||||||
|
).animate(CurvedAnimation(parent: animController, curve: Curves.easeInOut));
|
||||||
|
|
||||||
|
// Add listeners
|
||||||
|
animController.addStatusListener(_handleAnimationStatusChanged);
|
||||||
|
scaleAnimation.addListener(_handleScaleAnimationChanged);
|
||||||
|
|
||||||
|
// Start auto slide timer
|
||||||
|
autoSlideTimer = Timer.periodic(Duration(seconds: 3), _handleAutoSlide);
|
||||||
|
|
||||||
|
// Show bottom sheet after a delay
|
||||||
|
Future.delayed(Duration(seconds: 1), showBottomSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
autoSlideTimer.cancel();
|
||||||
|
pageController.dispose();
|
||||||
|
animController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to communication slide (index 1) before zooming
|
||||||
|
void _navigateToCommunicationSlide() {
|
||||||
|
if (currentPage.value != 1) {
|
||||||
|
pageController.animateToPage(
|
||||||
|
1, // Index for TImages.communication
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAnimationStatusChanged(AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
// Instead of calling onSuccess callback directly, navigate to RoleSelectionScreen
|
||||||
|
Get.off(
|
||||||
|
() => RoleSignupPageView(),
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the onSuccess callback after navigation if provided
|
||||||
|
if (onSuccess != null) {
|
||||||
|
Timer(Duration(milliseconds: 100), onSuccess!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleScaleAnimationChanged() {
|
||||||
|
if (scaleAnimation.value > 1.2) {
|
||||||
|
showUIElements.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleAutoSlide(Timer timer) {
|
||||||
|
if (pageController.hasClients) {
|
||||||
|
final nextPage = (currentPage.value + 1) % slides.length;
|
||||||
|
pageController.animateToPage(
|
||||||
|
nextPage,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPageChanged(int index) {
|
||||||
|
currentPage.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkLocation() async {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
final result = await _locationService.validateLocationForPanicButton();
|
||||||
|
|
||||||
|
if (result['valid']) {
|
||||||
|
isLoading.value = false;
|
||||||
|
isLocationValid.value = true;
|
||||||
|
|
||||||
|
// Navigate to communication slide before zooming
|
||||||
|
_navigateToCommunicationSlide();
|
||||||
|
|
||||||
|
// Give time to see the success message before animating
|
||||||
|
await Future.delayed(Duration(milliseconds: 800));
|
||||||
|
animController
|
||||||
|
.forward(); // Start zoom-in animation which will trigger the navigation
|
||||||
|
} else {
|
||||||
|
isLoading.value = false;
|
||||||
|
isLocationValid.value = false;
|
||||||
|
errorMessage.value = result['message'] ?? 'Location is invalid';
|
||||||
|
|
||||||
|
// Force page to show error slide
|
||||||
|
pageController.jumpToPage(slides.length - 1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
isLocationValid.value = false;
|
||||||
|
errorMessage.value = 'Failed to check location. Please try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleButtonPress() {
|
||||||
|
if (isLocationValid.value) {
|
||||||
|
// Navigate to communication slide before zooming
|
||||||
|
_navigateToCommunicationSlide();
|
||||||
|
|
||||||
|
// Give a brief moment for the page transition to complete
|
||||||
|
Future.delayed(Duration(milliseconds: 300), () {
|
||||||
|
animController
|
||||||
|
.forward(); // This will trigger the navigation when animation completes
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isLoading.value = true;
|
||||||
|
checkLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showBottomSheet() {
|
||||||
|
bottomSheetShown.value = true;
|
||||||
|
// Actual bottom sheet will be shown from the UI
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,396 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/onboarding/presentasion/controllers/check_location_controller.dart';
|
||||||
|
|
||||||
|
class CheckLocationScreen extends StatefulWidget {
|
||||||
|
final VoidCallback? onSuccess;
|
||||||
|
|
||||||
|
const CheckLocationScreen({super.key, this.onSuccess});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CheckLocationScreen> createState() => _CheckLocationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CheckLocationScreenState extends State<CheckLocationScreen> {
|
||||||
|
// Controller
|
||||||
|
late CheckLocationController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller = Get.put(CheckLocationController(onSuccess: widget.onSuccess));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to show bottom sheet
|
||||||
|
void _showBottomSheet() {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
isDismissible: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(24),
|
||||||
|
topRight: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
height: 5,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
theme.brightness == Brightness.light
|
||||||
|
? Colors.grey[300]
|
||||||
|
: Colors.grey[700],
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Location Validation Required',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'This application is designed specifically for users in the Jember region. We need to validate your location to ensure all services function properly.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.touch_app, color: theme.primaryColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Press "Check Location" button to proceed',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: Size(200, 50),
|
||||||
|
backgroundColor: theme.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('Got it'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
// Show bottom sheet when controller indicates
|
||||||
|
// Using ever instead of direct call to avoid rebuilding issues
|
||||||
|
ever(controller.bottomSheetShown, (shown) {
|
||||||
|
if (shown && mounted) {
|
||||||
|
_showBottomSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Info icon - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
controller.showUIElements.value
|
||||||
|
? Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _showBottomSheet,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: theme.primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Card carousel with zooming effect - Modify to center the current card during zoom
|
||||||
|
Expanded(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: controller.scaleAnimation,
|
||||||
|
builder: (context, child) {
|
||||||
|
// Calculate current page as an integer for clean zooming on the correct card
|
||||||
|
final currentPageIndex = controller.currentPage.value;
|
||||||
|
|
||||||
|
// Ensure we're centered on current page during animation
|
||||||
|
if (controller.isLocationValid.value &&
|
||||||
|
controller.scaleAnimation.value > 1.0 &&
|
||||||
|
controller.pageController.hasClients &&
|
||||||
|
controller.pageController.page?.round() !=
|
||||||
|
currentPageIndex) {
|
||||||
|
controller.pageController.jumpToPage(currentPageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transform.scale(
|
||||||
|
scale:
|
||||||
|
controller.isLocationValid.value
|
||||||
|
? controller.scaleAnimation.value
|
||||||
|
: 1.0,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: controller.pageController,
|
||||||
|
onPageChanged: controller.onPageChanged,
|
||||||
|
itemCount: controller.slides.length,
|
||||||
|
physics:
|
||||||
|
controller.isLocationValid.value
|
||||||
|
? const NeverScrollableScrollPhysics() // Lock scrolling during animation
|
||||||
|
: null,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildCarouselCard(index, theme);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Page indicators - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
controller.showUIElements.value
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(
|
||||||
|
controller.slides.length,
|
||||||
|
(index) => Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color:
|
||||||
|
controller.currentPage.value == index
|
||||||
|
? theme.primaryColor
|
||||||
|
: theme.disabledColor.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Title and subtitle - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
controller.showUIElements.value
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.slides[controller
|
||||||
|
.currentPage
|
||||||
|
.value]['title'],
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
controller.slides[controller
|
||||||
|
.currentPage
|
||||||
|
.value]['subtitle'],
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color:
|
||||||
|
theme.brightness == Brightness.light
|
||||||
|
? Colors.grey[600]
|
||||||
|
: Colors.grey[300],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Loading indicator - Fixed Obx usage
|
||||||
|
// Obx(
|
||||||
|
// () =>
|
||||||
|
// (controller.showUIElements.value &&
|
||||||
|
// controller.isLoading.value)
|
||||||
|
// ? Padding(
|
||||||
|
// padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
// child: CircularProgressIndicator(
|
||||||
|
// color: theme.primaryColor,
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// : const SizedBox.shrink(),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Error message - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
(controller.showUIElements.value &&
|
||||||
|
!controller.isLoading.value &&
|
||||||
|
!controller.isLocationValid.value &&
|
||||||
|
controller.errorMessage.value.isNotEmpty)
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
controller.errorMessage.value,
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Success message - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
(controller.showUIElements.value &&
|
||||||
|
!controller.isLoading.value &&
|
||||||
|
controller.isLocationValid.value)
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
"Location validated successfully!",
|
||||||
|
style: TextStyle(color: Colors.green),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Button - Fixed Obx usage
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
controller.showUIElements.value
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32.0,
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.handleButtonPress,
|
||||||
|
child:
|
||||||
|
controller.isLoading.value
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.0,
|
||||||
|
valueColor:
|
||||||
|
AlwaysStoppedAnimation<Color>(
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'Checking Location...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
controller.isLocationValid.value
|
||||||
|
? 'Next'
|
||||||
|
: 'Check Location',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCarouselCard(int index, ThemeData theme) {
|
||||||
|
final slide = controller.slides[index];
|
||||||
|
|
||||||
|
// Fix: Removed nested Obx and directly use controller values
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: index == controller.currentPage.value ? 10 : 30,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
theme.brightness == Brightness.light
|
||||||
|
? Colors.grey[200]
|
||||||
|
: Colors.grey[800],
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow:
|
||||||
|
index == controller.currentPage.value
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: SvgPicture.asset(slide['image'], fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InfoPopup extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final IconData iconData;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const InfoPopup({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.iconData = Icons.arrow_forward,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(iconData, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
class SlidingCard extends StatelessWidget {
|
||||||
|
final String imagePath;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final double scale;
|
||||||
|
final bool isError;
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
const SlidingCard({
|
||||||
|
super.key,
|
||||||
|
required this.imagePath,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.isError = false,
|
||||||
|
this.errorMessage = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: scale,
|
||||||
|
child: Container(
|
||||||
|
margin: EdgeInsets.symmetric(vertical: 20),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
imagePath,
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
if (isError)
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.7),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Text(
|
||||||
|
isError ? errorMessage : subtitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: isError ? Colors.red : Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,5 +23,5 @@ class AppRoutes {
|
||||||
static const String selfieVerification = '/selfie-verification';
|
static const String selfieVerification = '/selfie-verification';
|
||||||
static const String livenessDetection = '/liveness-detection';
|
static const String livenessDetection = '/liveness-detection';
|
||||||
static const String capturedSelfie = '/captured-selfie';
|
static const String capturedSelfie = '/captured-selfie';
|
||||||
|
static const String checkLocation = '/check-location';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue