EnakPoint & EnakCoin wallet, PIN, outlet, order, community, reward
- 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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5954510b93
commit
f5e3075205
@@ -0,0 +1,131 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user