feat: integrate enakgame and voucher
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run
This commit is contained in:
@@ -22,7 +22,7 @@ import 'widgets/wallet_confirm_dialog.dart';
|
||||
import 'widgets/wallet_history_section.dart';
|
||||
import 'widgets/wallet_result_view.dart';
|
||||
|
||||
/// Tukar EnakCoin → EnakPoint (§8.1).
|
||||
/// Tukar EnakCoin → EnakPoint (§7.1).
|
||||
@RoutePage()
|
||||
class ExchangePage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const ExchangePage({super.key});
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import 'widgets/wallet_history_section.dart';
|
||||
import 'widgets/wallet_confirm_dialog.dart';
|
||||
import 'widgets/wallet_result_view.dart';
|
||||
|
||||
/// Transfer EnakPoint / EnakCoin ke customer lain (§8.2).
|
||||
/// Transfer EnakPoint / EnakCoin ke customer lain (§7.2).
|
||||
@RoutePage()
|
||||
class TransferPage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const TransferPage({super.key});
|
||||
|
||||
@@ -83,6 +83,7 @@ class _WalletHistoryPageState extends State<WalletHistoryPage>
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _FilterSheet(
|
||||
currency: state.currency,
|
||||
initial: _HistoryFilter(
|
||||
types: state.types,
|
||||
from: state.from,
|
||||
@@ -264,8 +265,10 @@ class _HistoryFilter {
|
||||
}
|
||||
|
||||
class _FilterSheet extends StatefulWidget {
|
||||
const _FilterSheet({required this.initial});
|
||||
const _FilterSheet({required this.currency, required this.initial});
|
||||
|
||||
/// Tab yang sedang aktif; hanya tipe milik mata uang ini yang ditawarkan.
|
||||
final WalletCurrency currency;
|
||||
final _HistoryFilter initial;
|
||||
|
||||
@override
|
||||
@@ -273,6 +276,8 @@ class _FilterSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FilterSheetState extends State<_FilterSheet> {
|
||||
late final List<WalletTransactionType> _filterable =
|
||||
WalletTransactionType.filterableFor(widget.currency);
|
||||
late final Set<WalletTransactionType> _types = {...widget.initial.types};
|
||||
late DateTime? _from = widget.initial.from;
|
||||
late DateTime? _to = widget.initial.to;
|
||||
@@ -316,7 +321,7 @@ class _FilterSheetState extends State<_FilterSheet> {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: WalletTransactionType.filterable.map((type) {
|
||||
children: _filterable.map((type) {
|
||||
final selected = _types.contains(type);
|
||||
return FilterChip(
|
||||
label: Text(type.label, style: AppStyle.sm),
|
||||
@@ -361,9 +366,7 @@ class _FilterSheetState extends State<_FilterSheet> {
|
||||
title: 'Terapkan',
|
||||
onPressed: () => Navigator.of(context).pop(
|
||||
_HistoryFilter(
|
||||
types: WalletTransactionType.filterable
|
||||
.where(_types.contains)
|
||||
.toList(),
|
||||
types: _filterable.where(_types.contains).toList(),
|
||||
from: _from,
|
||||
to: _to,
|
||||
),
|
||||
|
||||
@@ -211,11 +211,11 @@ class _WalletPageState extends State<WalletPage> {
|
||||
}
|
||||
|
||||
Widget _buildActions(WalletSummary summary) {
|
||||
final actions = [
|
||||
(Icons.qr_code_2, 'Bayar di kasir', const PaymentCodeRoute()),
|
||||
final actions = <(IconData, String, PageRouteInfo)>[
|
||||
(Icons.swap_horiz, 'Tukar EnakCoin', const ExchangeRoute()),
|
||||
(Icons.send_outlined, 'Transfer', const TransferRoute()),
|
||||
(Icons.casino_outlined, 'Main game', const FerrisWheelRoute()),
|
||||
(Icons.casino_outlined, 'Main game', const EnakGameRoute()),
|
||||
(Icons.confirmation_number_outlined, 'Voucher', const RewardRoute()),
|
||||
];
|
||||
|
||||
return Container(
|
||||
|
||||
@@ -22,6 +22,9 @@ class WalletTransactionTile extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final tx = transaction;
|
||||
final color = signedAmountColor(tx.amount);
|
||||
// Tipe baru yang belum dikenal app: description saja, tanpa label.
|
||||
final isKnownType = tx.type != WalletTransactionType.unknown;
|
||||
final title = isKnownType ? tx.type.label : tx.description;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
@@ -47,10 +50,10 @@ class WalletTransactionTile extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
tx.type.label,
|
||||
title,
|
||||
style: AppStyle.md.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
if (tx.description.isNotEmpty) ...[
|
||||
if (isKnownType && tx.description.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
tx.description,
|
||||
|
||||
Reference in New Issue
Block a user