import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../../../application/wallet/payment_code_form/payment_code_form_bloc.dart'; import '../../../application/wallet/wallet_loader/wallet_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/wallet/wallet.dart'; import '../../../injection.dart'; import '../../components/button/button.dart'; import '../../components/toast/flushbar.dart'; import '../pin/pin_guard.dart'; import '../pin/widgets/pin_sheet.dart'; import 'wallet_ui.dart'; /// Bayar di kasir dengan kode bayar (ยง7.1). @RoutePage() class PaymentCodePage extends StatefulWidget implements AutoRouteWrapper { const PaymentCodePage({super.key}); @override State createState() => _PaymentCodePageState(); @override Widget wrappedRoute(BuildContext context) => BlocProvider(create: (_) => getIt(), child: this); } class _PaymentCodePageState extends State { Timer? _ticker; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _generate()); _ticker = Timer.periodic(const Duration(seconds: 1), (_) => _tick()); } @override void dispose() { _ticker?.cancel(); super.dispose(); } void _tick() { if (!mounted) return; final code = context.read().state.paymentCode; if (code == null) return; if (!DateTime.now().isBefore(code.expiresAt)) { context.read().add( const PaymentCodeFormEvent.expired(), ); } else { setState(() {}); } } Future _generate() async { if (!await ensurePinReady(context) || !mounted) return; await showPinSheet( context, title: 'Bayar di kasir', subtitle: 'Masukkan PIN untuk menampilkan kode bayar', onSubmit: (pin) async { final bloc = context.read(); bloc.add(PaymentCodeFormEvent.submitted(pin)); final state = await bloc.stream.firstWhere((s) => !s.isSubmitting); return state.failureOption.toNullable(); }, onFailure: (f) => AppFlushbar.showError(context, walletFailureMessage(f)), ); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { final code = state.paymentCode; return Scaffold( backgroundColor: AppColor.background, appBar: AppBar(title: const Text('Bayar di Kasir')), body: ListView( padding: const EdgeInsets.all(16), children: [ _buildBalance(), const SizedBox(height: 16), code == null ? _buildEmpty(state.isSubmitting) : _buildCode(code), const SizedBox(height: 16), Text( 'Tunjukkan kode ini ke kasir. Kasir yang memilih jumlah ' 'EnakPoint yang dipakai. Kode hanya bisa dipakai sekali, dan ' 'membuat kode baru membatalkan kode sebelumnya.', style: AppStyle.sm.copyWith(color: AppColor.textSecondary), textAlign: TextAlign.center, ), ], ), ); }, ); } Widget _buildBalance() { return BlocBuilder( builder: (context, state) { final summary = state.summary; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Row( children: [ const WalletCurrencyIcon( currency: WalletCurrency.point, size: 28, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${summary.pointBalance.thousandFormat} EnakPoint', style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700), ), Text( pointDiscountLabel(summary.pointDiscountValue), style: AppStyle.sm.copyWith( color: AppColor.textSecondary, ), ), ], ), ), ], ), ); }, ); } Widget _buildEmpty(bool isSubmitting) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Column( children: [ const Icon(Icons.qr_code_2, size: 72, color: AppColor.textLight), const SizedBox(height: 12), Text( 'Kode bayar belum dibuat atau sudah kedaluwarsa', textAlign: TextAlign.center, style: AppStyle.md.copyWith(color: AppColor.textSecondary), ), const SizedBox(height: 16), AppElevatedButton( title: 'Buat kode baru', isLoading: isSubmitting, onPressed: isSubmitting ? null : _generate, ), ], ), ); } Widget _buildCode(PaymentCode code) { final remaining = code.expiresAt.difference(DateTime.now()); final spacedCode = code.code.split('').join(' '); return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Column( children: [ Text( spacedCode, style: AppStyle.h1.copyWith( fontWeight: FontWeight.w700, letterSpacing: 2, ), ), const SizedBox(height: 16), QrImageView( data: code.qrPayload, size: 220, backgroundColor: AppColor.white, ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.timer_outlined, size: 18, color: AppColor.warning, ), const SizedBox(width: 6), Text( 'Berlaku ${formatCountdown(remaining)}', style: AppStyle.md.copyWith( color: AppColor.warning, fontWeight: FontWeight.w700, ), ), ], ), ], ), ); } }