- Wallet EnakPoint & EnakCoin: beranda, riwayat, saldo kedaluwarsa, kode bayar, tukar EnakCoin, transfer - PIN (buat, ganti, lupa), outlet, riwayat order, game spin - Halaman community, webview, coming soon, dan ikon menu beranda baru - Redesign Reward Saya: kartu tiket, filter chip, detail kode + QR - Hapus customer point loader, menu, dan checkout lama - Base URL ke enaklo-pos-api.altru.id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
4.8 KiB
Dart
132 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:intl/intl.dart';
|
||
|
||
import '../../../common/api/api_failure.dart';
|
||
import '../../../common/extension/extension.dart';
|
||
import '../../../common/theme/theme.dart';
|
||
import '../../../domain/wallet/wallet.dart';
|
||
import '../../components/assets/assets.gen.dart';
|
||
|
||
/// Pesan ramah untuk customer. [rejectedMessage] dipakai untuk penolakan
|
||
/// aturan bisnis (`304`), karena `cause` dari server tidak boleh tampil mentah.
|
||
String walletFailureMessage(WalletFailure failure, {String? rejectedMessage}) {
|
||
return failure.map(
|
||
serverError: (value) => value.failure.maybeMap(
|
||
connectionError: (_) => 'Tidak ada koneksi internet. Coba lagi.',
|
||
connectionTimeout: (_) => 'Koneksi lambat. Coba lagi.',
|
||
orElse: () => 'Terjadi kesalahan, coba lagi',
|
||
),
|
||
unexpectedError: (_) => 'Terjadi kesalahan, coba lagi',
|
||
unauthorized: (_) => 'Sesi kamu sudah berakhir. Silakan masuk lagi.',
|
||
invalidRequest: (_) => 'Permintaan tidak dapat diproses. Coba lagi.',
|
||
rejected: (_) => rejectedMessage ?? 'Permintaan tidak dapat diproses.',
|
||
notFound: (_) => 'Data tidak ditemukan',
|
||
tooManyRequests: (value) =>
|
||
'Terlalu sering meminta kode. Coba lagi dalam '
|
||
'${value.retryAfterSeconds} detik.',
|
||
internalError: (_) => 'Terjadi kesalahan, coba lagi',
|
||
pinNotSet: (_) => 'Kamu belum membuat PIN.',
|
||
pinInvalid: (value) => value.remainingAttempts == null
|
||
? 'PIN salah.'
|
||
: 'PIN salah, sisa ${value.remainingAttempts} percobaan.',
|
||
pinLocked: (value) => value.lockedUntil == null
|
||
? 'PIN terkunci. Silakan gunakan Lupa PIN.'
|
||
: 'PIN terkunci sampai ${formatWibTime(value.lockedUntil!)}.',
|
||
transferBlocked: (value) => value.transferBlockedUntil == null
|
||
? 'Transfer sementara ditahan karena PIN baru direset.'
|
||
: 'Transfer bisa dilakukan lagi pada '
|
||
'${formatWibDateTime(value.transferBlockedUntil!)}.',
|
||
);
|
||
}
|
||
|
||
/// Jam WIB, dengan tanggal bila bukan hari ini: "14:30 WIB" /
|
||
/// "1 Okt 2026 14:30 WIB".
|
||
String formatWibTime(DateTime time) {
|
||
final wib = time.toWib;
|
||
final today = DateTime.now().toWib;
|
||
final sameDay =
|
||
wib.year == today.year &&
|
||
wib.month == today.month &&
|
||
wib.day == today.day;
|
||
return sameDay ? '${wib.toHourMinute} WIB' : formatWibDateTime(time);
|
||
}
|
||
|
||
String formatWibDateTime(DateTime time) => '${time.toWib.toShortDatetime} WIB';
|
||
|
||
/// Tanggal kalender WIB dari timestamp, mis. `expires_at`.
|
||
String formatWibDate(DateTime time) => time.toWib.toDate;
|
||
|
||
/// Nilai rupiah EnakPoint selalu "setara potongan", tidak pernah saldo atau
|
||
/// uang (§1 aturan 2).
|
||
String pointDiscountLabel(int discountValue) =>
|
||
'setara potongan ${discountValue.currencyFormatRp}';
|
||
|
||
/// Jumlah bertanda: "+875" / "−120" (tanda minus U+2212).
|
||
String formatSignedAmount(int amount) => amount >= 0
|
||
? '+${amount.thousandFormat}'
|
||
: '−${amount.abs().thousandFormat}';
|
||
|
||
Color signedAmountColor(int amount) =>
|
||
amount >= 0 ? AppColor.success : AppColor.error;
|
||
|
||
String formatCountdown(Duration duration) {
|
||
final seconds = duration.inSeconds < 0 ? 0 : duration.inSeconds;
|
||
final m = (seconds ~/ 60).toString().padLeft(2, '0');
|
||
final s = (seconds % 60).toString().padLeft(2, '0');
|
||
return '$m:$s';
|
||
}
|
||
|
||
/// Hanya digit, untuk nomor HP dan jumlah.
|
||
String digitsOnly(String value) => value.replaceAll(RegExp(r'[^0-9]'), '');
|
||
|
||
/// Tanggal pendek untuk chip filter: "1 Sep 2026".
|
||
String formatShortDate(DateTime date) =>
|
||
DateFormat('d MMM yyyy', 'id_ID').format(date);
|
||
|
||
/// Menampilkan jumlah dengan pemisah ribuan saat diketik: "10000" → "10.000".
|
||
/// Baca nilainya dengan [digitsOnly].
|
||
class ThousandsInputFormatter extends TextInputFormatter {
|
||
static const int _maxDigits = 12;
|
||
|
||
@override
|
||
TextEditingValue formatEditUpdate(
|
||
TextEditingValue oldValue,
|
||
TextEditingValue newValue,
|
||
) {
|
||
final digits = digitsOnly(newValue.text);
|
||
if (digits.isEmpty) return const TextEditingValue();
|
||
if (digits.length > _maxDigits) return oldValue;
|
||
|
||
final text = int.parse(digits).thousandFormat;
|
||
return TextEditingValue(
|
||
text: text,
|
||
selection: TextSelection.collapsed(offset: text.length),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Ikon 3D EnakPoint / EnakCoin.
|
||
class WalletCurrencyIcon extends StatelessWidget {
|
||
const WalletCurrencyIcon({super.key, required this.currency, this.size = 24});
|
||
|
||
final WalletCurrency currency;
|
||
final double size;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final asset = currency == WalletCurrency.point
|
||
? Assets.icons.enakpointIcon
|
||
: Assets.icons.enakcoinIcon;
|
||
// Aset aslinya 512–1024px; decode seukuran tampilan saja.
|
||
final cacheSize = (size * MediaQuery.devicePixelRatioOf(context)).round();
|
||
|
||
return asset.image(
|
||
width: size,
|
||
height: size,
|
||
fit: BoxFit.contain,
|
||
cacheWidth: cacheSize,
|
||
);
|
||
}
|
||
}
|