- Bottom navbar: Home, Pesanan, Bayar (tombol tengah ke kode bayar), Community, Profil; menu EnakCommunity di beranda pindah ke tab - Image placeholder: logo Enaklo diperbesar, ilustrasi gelas dihapus - Fix assertion Navigator saat PIN gagal karena error non-PIN: flushbar kini tampil lewat onFailure setelah sheet PIN tertutup (bayar, tukar, transfer) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
6.9 KiB
Dart
227 lines
6.9 KiB
Dart
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<PaymentCodePage> createState() => _PaymentCodePageState();
|
|
|
|
@override
|
|
Widget wrappedRoute(BuildContext context) =>
|
|
BlocProvider(create: (_) => getIt<PaymentCodeFormBloc>(), child: this);
|
|
}
|
|
|
|
class _PaymentCodePageState extends State<PaymentCodePage> {
|
|
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<PaymentCodeFormBloc>().state.paymentCode;
|
|
if (code == null) return;
|
|
|
|
if (!DateTime.now().isBefore(code.expiresAt)) {
|
|
context.read<PaymentCodeFormBloc>().add(
|
|
const PaymentCodeFormEvent.expired(),
|
|
);
|
|
} else {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Future<void> _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<PaymentCodeFormBloc>();
|
|
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<PaymentCodeFormBloc, PaymentCodeFormState>(
|
|
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<WalletLoaderBloc, WalletLoaderState>(
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|