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/social_auth_enabled.dart'; import '../../../core/constants.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(); // Listener registered once in initState — keeps it independent of the // build cycle so it doesn't accumulate (see feedback_riverpod_listen_in_build). _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; return Scaffold( appBar: AppBar(title: const Text('Masuk / Daftar')), body: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (kSocialAuthEnabled) ...[ ElevatedButton.icon( icon: const Icon(Icons.g_mobiledata), onPressed: isLoading ? null : () => ref.read(authProvider.notifier).loginGoogle(), label: const Text('Lanjut dengan Google'), ), const SizedBox(height: 12), ElevatedButton.icon( icon: const Icon(Icons.apple), onPressed: isLoading ? null : () => ref.read(authProvider.notifier).loginApple(), label: const Text('Lanjut dengan Apple'), ), const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Row(children: [ Expanded(child: Divider()), Padding(padding: EdgeInsets.symmetric(horizontal: 12), child: Text('atau')), Expanded(child: Divider()), ]), ), ], TextField( controller: _phoneController, decoration: const InputDecoration( labelText: 'Nomor HP', hintText: '+628xxxxxxxxxx', border: OutlineInputBorder(), ), keyboardType: TextInputType.phone, ), const SizedBox(height: 12), ElevatedButton( onPressed: canSubmit ? () { final phone = _phoneController.text.trim(); if (phone.isEmpty) return; ref.read(authProvider.notifier).requestOtp(phone); } : null, child: isLoading ? const CircularProgressIndicator() : Text(isLockedOut ? 'Coba lagi dalam ${formatCountdown(_lockoutSeconds)}' : 'Kirim OTP'), ), if (_errorMessage != null) ...[ const SizedBox(height: 12), Text( _errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: Colors.red.shade700, fontSize: 13), ), ], ], ), ), ); } }