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/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/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/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/welcome/welcome_screen.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
|
@ -51,6 +50,11 @@ class AppPages {
|
|||
page: () => const ForgotPasswordScreen(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.checkLocation,
|
||||
page: () => const CheckLocationScreen(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.locationWarning,
|
||||
page: () => const LocationWarningScreen(),
|
||||
|
|
|
@ -86,11 +86,18 @@ class AuthenticationRepository extends GetxController {
|
|||
session?.user.userMetadata?['profile_status'] == 'completed';
|
||||
|
||||
// Cek lokasi terlebih dahulu
|
||||
if (await _locationService.isLocationValidForFeature() == false) {
|
||||
_navigateToRoute(AppRoutes.locationWarning);
|
||||
return;
|
||||
if (!isFirstTime) {
|
||||
bool isLocationValid =
|
||||
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 (!isEmailVerified) {
|
||||
_navigateToRoute(AppRoutes.emailVerification);
|
||||
|
@ -109,7 +116,7 @@ class AuthenticationRepository extends GetxController {
|
|||
}
|
||||
} catch (e) {
|
||||
Logger().e('Error in screenRedirect: $e');
|
||||
_navigateToRoute(AppRoutes.signIn);
|
||||
_navigateToRoute(AppRoutes.checkLocation);
|
||||
} finally {
|
||||
_isRedirecting = false;
|
||||
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 livenessDetection = '/liveness-detection';
|
||||
static const String capturedSelfie = '/captured-selfie';
|
||||
|
||||
static const String checkLocation = '/check-location';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue