diff --git a/lib/presentation/pages/auth/otp/otp_page.dart b/lib/presentation/pages/auth/otp/otp_page.dart new file mode 100644 index 0000000..8a4d07a --- /dev/null +++ b/lib/presentation/pages/auth/otp/otp_page.dart @@ -0,0 +1,252 @@ +import 'dart:async'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/button/button.dart'; + +@RoutePage() +class OtpPage extends StatefulWidget { + const OtpPage({super.key}); + + @override + State createState() => _OtpPageState(); +} + +class _OtpPageState extends State { + final List _controllers = List.generate( + 6, + (index) => TextEditingController(), + ); + final List _focusNodes = List.generate(6, (index) => FocusNode()); + + Timer? _timer; + int _secondsRemaining = 18; + bool _canResend = false; + + @override + void initState() { + super.initState(); + _startTimer(); + } + + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_secondsRemaining > 0) { + setState(() { + _secondsRemaining--; + }); + } else { + setState(() { + _canResend = true; + }); + _timer?.cancel(); + } + }); + } + + void _resendCode() { + setState(() { + _secondsRemaining = 18; + _canResend = false; + }); + _startTimer(); + // Add your resend logic here + } + + String _formatTime(int seconds) { + int minutes = seconds ~/ 60; + int remainingSeconds = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + } + + void _onCodeChanged(String value, int index) { + if (value.isNotEmpty) { + // Move to next field + if (index < 5) { + _focusNodes[index + 1].requestFocus(); + } else { + // Last field, unfocus + _focusNodes[index].unfocus(); + _verifyCode(); + } + } else { + // Handle backspace - move to previous field + if (index > 0) { + _focusNodes[index - 1].requestFocus(); + } + } + } + + void _verifyCode() { + String code = _controllers.map((controller) => controller.text).join(); + if (code.length == 6) { + // Add your verification logic here + print('Verifying code: $code'); + } + } + + @override + void dispose() { + _timer?.cancel(); + for (var controller in _controllers) { + controller.dispose(); + } + for (var focusNode in _focusNodes) { + focusNode.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Verifikasi')), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + + // Title + Text( + 'Masukan Kode Verifikasi', + style: AppStyle.xl.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + + const SizedBox(height: 12), + + // Description + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Kami telah mengirimkan kode verifikasi melalui ', + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + height: 1.4, + ), + ), + TextSpan( + text: 'Whatsapp', + style: AppStyle.sm.copyWith( + color: AppColor.success, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + TextSpan( + text: ' ke ', + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + height: 1.4, + ), + ), + TextSpan( + text: '+6288976680234', + style: AppStyle.sm.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + height: 1.4, + ), + ), + ], + ), + ), + + const SizedBox(height: 6), + + // Hidden text fields for input + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(6, (index) { + return Expanded( + child: Padding( + padding: EdgeInsets.only(right: index == 5 ? 0 : 8.0), + child: TextFormField( + controller: _controllers[index], + focusNode: _focusNodes[index], + keyboardType: TextInputType.number, + maxLength: 1, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration(counterText: ''), + textAlign: TextAlign.center, + style: AppStyle.lg.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + cursorColor: AppColor.primary, + onChanged: (value) { + setState(() {}); + _onCodeChanged(value, index); + }, + ), + ), + ); + }), + ), + + const SizedBox(height: 40), + + // Timer and Resend Section + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Mohon tunggu untuk kirim ulang kode ', + style: AppStyle.xs.copyWith(color: AppColor.textSecondary), + ), + Text( + _formatTime(_secondsRemaining), + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + + if (_canResend) ...[ + const SizedBox(height: 12), + Center( + child: TextButton( + onPressed: _resendCode, + child: Text( + 'Kirim Ulang Kode', + style: AppStyle.sm.copyWith( + color: AppColor.success, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + + const Spacer(), + + // Continue Button + AppElevatedButton( + title: 'Verifikasi', + onPressed: () { + String code = _controllers + .map((controller) => controller.text) + .join(); + if (code.length == 6) { + _verifyCode(); + } + }, + ), + + const SizedBox(height: 24), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/auth/register/register_page.dart b/lib/presentation/pages/auth/register/register_page.dart index 8261f43..2df773d 100644 --- a/lib/presentation/pages/auth/register/register_page.dart +++ b/lib/presentation/pages/auth/register/register_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import '../../../components/button/button.dart'; +import '../../../router/app_router.gr.dart'; import 'widgets/name_field.dart'; import 'widgets/phone_field.dart'; @@ -30,7 +31,10 @@ class RegisterPage extends StatelessWidget { Spacer(), // Continue Button - AppElevatedButton(onPressed: () {}, title: 'Daftar & Lanjutkan'), + AppElevatedButton( + onPressed: () => context.router.push(const OtpRoute()), + title: 'Daftar & Lanjutkan', + ), const SizedBox(height: 24), ], diff --git a/lib/presentation/router/app_router.dart b/lib/presentation/router/app_router.dart index da35479..8c08e32 100644 --- a/lib/presentation/router/app_router.dart +++ b/lib/presentation/router/app_router.dart @@ -14,5 +14,6 @@ class AppRouter extends RootStackRouter { // Auth AutoRoute(page: LoginRoute.page), AutoRoute(page: RegisterRoute.page), + AutoRoute(page: OtpRoute.page), ]; } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index e51f8b8..3a98e12 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -9,23 +9,24 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i5; +import 'package:auto_route/auto_route.dart' as _i6; import 'package:enaklo/presentation/pages/auth/login/login_page.dart' as _i1; +import 'package:enaklo/presentation/pages/auth/otp/otp_page.dart' as _i3; import 'package:enaklo/presentation/pages/auth/register/register_page.dart' - as _i3; + as _i4; import 'package:enaklo/presentation/pages/onboarding/onboarding_page.dart' as _i2; -import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i4; +import 'package:enaklo/presentation/pages/splash/splash_page.dart' as _i5; /// generated route for /// [_i1.LoginPage] -class LoginRoute extends _i5.PageRouteInfo { - const LoginRoute({List<_i5.PageRouteInfo>? children}) +class LoginRoute extends _i6.PageRouteInfo { + const LoginRoute({List<_i6.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { return const _i1.LoginPage(); @@ -35,13 +36,13 @@ class LoginRoute extends _i5.PageRouteInfo { /// generated route for /// [_i2.OnboardingPage] -class OnboardingRoute extends _i5.PageRouteInfo { - const OnboardingRoute({List<_i5.PageRouteInfo>? children}) +class OnboardingRoute extends _i6.PageRouteInfo { + const OnboardingRoute({List<_i6.PageRouteInfo>? children}) : super(OnboardingRoute.name, initialChildren: children); static const String name = 'OnboardingRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { return const _i2.OnboardingPage(); @@ -50,33 +51,49 @@ class OnboardingRoute extends _i5.PageRouteInfo { } /// generated route for -/// [_i3.RegisterPage] -class RegisterRoute extends _i5.PageRouteInfo { - const RegisterRoute({List<_i5.PageRouteInfo>? children}) - : super(RegisterRoute.name, initialChildren: children); +/// [_i3.OtpPage] +class OtpRoute extends _i6.PageRouteInfo { + const OtpRoute({List<_i6.PageRouteInfo>? children}) + : super(OtpRoute.name, initialChildren: children); - static const String name = 'RegisterRoute'; + static const String name = 'OtpRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { - return const _i3.RegisterPage(); + return const _i3.OtpPage(); }, ); } /// generated route for -/// [_i4.SplashPage] -class SplashRoute extends _i5.PageRouteInfo { - const SplashRoute({List<_i5.PageRouteInfo>? children}) +/// [_i4.RegisterPage] +class RegisterRoute extends _i6.PageRouteInfo { + const RegisterRoute({List<_i6.PageRouteInfo>? children}) + : super(RegisterRoute.name, initialChildren: children); + + static const String name = 'RegisterRoute'; + + static _i6.PageInfo page = _i6.PageInfo( + name, + builder: (data) { + return const _i4.RegisterPage(); + }, + ); +} + +/// generated route for +/// [_i5.SplashPage] +class SplashRoute extends _i6.PageRouteInfo { + const SplashRoute({List<_i6.PageRouteInfo>? children}) : super(SplashRoute.name, initialChildren: children); static const String name = 'SplashRoute'; - static _i5.PageInfo page = _i5.PageInfo( + static _i6.PageInfo page = _i6.PageInfo( name, builder: (data) { - return const _i4.SplashPage(); + return const _i5.SplashPage(); }, ); }