import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import '../../../../common/theme/theme.dart'; import '../../../../domain/wallet/wallet.dart'; import '../../../router/app_router.gr.dart'; import '../../wallet/wallet_ui.dart'; import 'pin_code_field.dart'; /// Mengirim PIN ke server. Kembalikan `null` bila sukses, atau failure-nya. typedef PinSubmitCallback = Future Function(String pin); enum _PinSheetResult { success, failed, forgotPin, pinNotSet } /// Meminta PIN lalu menjalankan [onSubmit]. Error PIN (ยง6.5) ditangani di dalam /// sheet; failure lain menutup sheet dan ditampilkan oleh halaman pemanggil. /// Mengembalikan `true` bila [onSubmit] sukses. Future showPinSheet( BuildContext context, { required String title, String? subtitle, required PinSubmitCallback onSubmit, }) async { final result = await showModalBottomSheet<_PinSheetResult>( context: context, isScrollControlled: true, isDismissible: false, backgroundColor: AppColor.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _PinSheet(title: title, subtitle: subtitle, onSubmit: onSubmit), ); if (!context.mounted) return false; switch (result) { case _PinSheetResult.success: return true; case _PinSheetResult.forgotPin: context.router.push(PinSetupRoute(purpose: PinOtpPurpose.reset)); return false; case _PinSheetResult.pinNotSet: context.router.push(PinSetupRoute(purpose: PinOtpPurpose.setup)); return false; case _PinSheetResult.failed: case null: return false; } } class _PinSheet extends StatefulWidget { const _PinSheet({required this.title, this.subtitle, required this.onSubmit}); final String title; final String? subtitle; final PinSubmitCallback onSubmit; @override State<_PinSheet> createState() => _PinSheetState(); } class _PinSheetState extends State<_PinSheet> { final TextEditingController _controller = TextEditingController(); bool _isSubmitting = false; String? _errorText; /// PIN terkunci atau transfer ditahan: input tidak berguna lagi. bool _isBlocked = false; @override void dispose() { _controller.dispose(); super.dispose(); } Future _submit(String pin) async { if (_isSubmitting || _isBlocked) return; setState(() { _isSubmitting = true; _errorText = null; }); final failure = await widget.onSubmit(pin); if (!mounted) return; // PIN tidak disimpan lebih lama dari yang diperlukan. _controller.clear(); if (failure == null) { Navigator.of(context).pop(_PinSheetResult.success); return; } final handled = failure.maybeMap( pinNotSet: (_) { Navigator.of(context).pop(_PinSheetResult.pinNotSet); return true; }, pinInvalid: (_) { setState(() => _errorText = walletFailureMessage(failure)); return true; }, pinLocked: (_) { setState(() { _errorText = walletFailureMessage(failure); _isBlocked = true; }); return true; }, transferBlocked: (_) { setState(() { _errorText = walletFailureMessage(failure); _isBlocked = true; }); return true; }, orElse: () => false, ); if (!handled) { Navigator.of(context).pop(_PinSheetResult.failed); return; } setState(() => _isSubmitting = false); } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 12, bottom: MediaQuery.of(context).viewInsets.bottom + 24, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( widget.title, style: AppStyle.xl.copyWith(fontWeight: FontWeight.w700), ), ), IconButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: AppColor.textSecondary), ), ], ), Text( widget.subtitle ?? 'Masukkan PIN 6 digit kamu', style: AppStyle.md.copyWith(color: AppColor.textSecondary), ), const SizedBox(height: 24), PinCodeField( controller: _controller, enabled: !_isSubmitting && !_isBlocked, hasError: _errorText != null, onCompleted: _submit, ), const SizedBox(height: 16), if (_isSubmitting) const Center( child: SpinKitFadingCircle(color: AppColor.primary, size: 28), ) else if (_errorText != null) Text( _errorText!, style: AppStyle.md.copyWith( color: AppColor.error, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(_PinSheetResult.forgotPin), child: Text( 'Lupa PIN', style: AppStyle.md.copyWith( color: AppColor.primary, fontWeight: FontWeight.w700, ), ), ), ), ], ), ); } }