import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/auth/auth_notifier.dart'; import '../../../core/auth/auth_providers_provider.dart'; import '../../../core/constants.dart'; import '../../../core/theme/halo_tokens.dart'; import '../../../core/theme/widgets/widgets.dart'; class RegisterScreen extends ConsumerStatefulWidget { const RegisterScreen({super.key}); @override ConsumerState createState() => _RegisterScreenState(); } class _RegisterScreenState extends ConsumerState { final _phoneController = TextEditingController(); ProviderSubscription>? _authSub; // Server-imposed lockout: when /otp/request returns 429, the backend // includes retry_after_seconds. We disable "Kirim OTP" for that window. int _lockoutSeconds = 0; Timer? _lockoutTimer; String? _errorMessage; @override void initState() { super.initState(); _authSub = ref.listenManual>(authProvider, (prev, next) { if (!mounted) return; final data = next.valueOrNull; if (data is AuthOtpSentData) { // Use go (replace) so re-submitting the phone form doesn't stack // multiple OtpScreen instances with active listeners. context.go('/auth/otp', extra: _phoneController.text.trim()); return; } if (next is AsyncError) { final err = next.error; setState(() => _errorMessage = err.toString()); if (err is AuthErrorInfo && err.retryAfterSeconds != null && (err.code == 'OTP_COOLDOWN' || err.code == 'OTP_RATE_LIMIT_PHONE' || err.code == 'OTP_RATE_LIMIT_IP')) { _startLockout(err.retryAfterSeconds!); } } else if (next is AsyncData) { if (_errorMessage != null) setState(() => _errorMessage = null); } }); } @override void dispose() { _authSub?.close(); _lockoutTimer?.cancel(); _phoneController.dispose(); super.dispose(); } void _startLockout(int seconds) { _lockoutTimer?.cancel(); setState(() => _lockoutSeconds = seconds); _lockoutTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) { timer.cancel(); return; } setState(() { if (_lockoutSeconds > 0) _lockoutSeconds--; if (_lockoutSeconds <= 0) timer.cancel(); }); }); } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); final isLoading = authState is AsyncLoading; final isLockedOut = _lockoutSeconds > 0; final canSubmit = !isLoading && !isLockedOut; final providersAsync = ref.watch(authProvidersProvider); final providers = providersAsync.valueOrNull ?? AuthProvidersConfig.fallback; return Scaffold( appBar: AppBar(title: const Text('Masuk / Daftar')), body: SafeArea( child: Padding( padding: const EdgeInsets.all(HaloSpacing.s24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (providers.hasAnySocial) ...[ if (providers.google) ...[ HaloButton( label: 'lanjut dengan Google', icon: const Icon(Icons.g_mobiledata), variant: HaloButtonVariant.secondary, fullWidth: true, onPressed: isLoading ? null : () => ref.read(authProvider.notifier).loginGoogle(), ), const SizedBox(height: HaloSpacing.s12), ], if (providers.apple) ...[ HaloButton( label: 'lanjut dengan Apple', icon: const Icon(Icons.apple), variant: HaloButtonVariant.secondary, fullWidth: true, onPressed: isLoading ? null : () => ref.read(authProvider.notifier).loginApple(), ), const SizedBox(height: HaloSpacing.s12), ], const Padding( padding: EdgeInsets.symmetric(vertical: HaloSpacing.s16), child: Row( children: [ Expanded(child: Divider(color: HaloTokens.border)), Padding( padding: EdgeInsets.symmetric(horizontal: HaloSpacing.s12), child: Text( 'atau', style: TextStyle( fontFamily: HaloTokens.fontBody, color: HaloTokens.inkMuted, fontSize: 13, ), ), ), Expanded(child: Divider(color: HaloTokens.border)), ], ), ), ], TextField( controller: _phoneController, decoration: const InputDecoration( labelText: 'Nomor HP', hintText: '+628xxxxxxxxxx', ), keyboardType: TextInputType.phone, ), const SizedBox(height: HaloSpacing.s16), HaloButton( label: isLoading ? 'memproses...' : isLockedOut ? 'coba lagi dalam ${formatCountdown(_lockoutSeconds)}' : 'kirim OTP', fullWidth: true, onPressed: canSubmit ? () { final phone = _phoneController.text.trim(); if (phone.isEmpty) return; ref.read(authProvider.notifier).requestOtp(phone); } : null, ), if (_errorMessage != null) ...[ const SizedBox(height: HaloSpacing.s12), Text( _errorMessage!, textAlign: TextAlign.center, style: const TextStyle( fontFamily: HaloTokens.fontBody, color: HaloTokens.danger, fontSize: 13, ), ), ], ], ), ), ), ); } }