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:
efrilm
2026-10-01 01:43:22 +07:00
co-authored by Claude Opus 5.5
parent 5954510b93
commit f5e3075205
253 changed files with 47212 additions and 17437 deletions
+15 -4
View File
@@ -17,6 +17,7 @@ import 'interceptors/bad_request_interceptor.dart';
import 'interceptors/connection_timeout_interceptor.dart';
import 'interceptors/internal_server_interceptor.dart';
import 'interceptors/not_found_interceptor.dart';
import 'interceptors/sensitive_aware_log_interceptor.dart';
import 'interceptors/unauthorized_interceptor.dart';
@lazySingleton
@@ -37,10 +38,12 @@ class ApiClient {
if (kDebugMode) {
_dio.interceptors.add(
AwesomeDioInterceptor(
logResponseHeaders: false,
logRequestTimeout: false,
logRequestHeaders: true,
SensitiveAwareLogInterceptor(
AwesomeDioInterceptor(
logResponseHeaders: false,
logRequestTimeout: false,
logRequestHeaders: true,
),
),
);
}
@@ -54,6 +57,7 @@ class ApiClient {
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
Map<String, dynamic>? extra,
}) async {
try {
return await _dio.post(
@@ -64,6 +68,7 @@ class ApiClient {
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
extra: extra,
),
queryParameters: params,
);
@@ -103,6 +108,7 @@ class ApiClient {
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
Map<String, dynamic>? extra,
}) async {
try {
return await _dio.get(
@@ -112,6 +118,7 @@ class ApiClient {
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
extra: extra,
),
queryParameters: params,
);
@@ -153,6 +160,7 @@ class ApiClient {
bool Function(int?)? validateStatus,
String? contentType,
void Function(int count, int total)? onSendProgress,
Map<String, dynamic>? extra,
}) async {
try {
return await _dio.put(
@@ -163,6 +171,7 @@ class ApiClient {
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
extra: extra,
),
queryParameters: params,
onSendProgress: onSendProgress,
@@ -204,6 +213,7 @@ class ApiClient {
bool followRedirects = true,
bool Function(int?)? validateStatus,
String? contentType,
Map<String, dynamic>? extra,
}) async {
try {
return await _dio.delete(
@@ -214,6 +224,7 @@ class ApiClient {
followRedirects: followRedirects,
validateStatus: validateStatus,
contentType: contentType,
extra: extra,
),
queryParameters: params,
);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
import 'package:dio/dio.dart';
/// Key untuk `Options.extra` yang menandai request berisi data rahasia
/// (PIN, OTP). Request bertanda ini tidak dicetak ke log sama sekali.
const String kSensitiveRequest = 'sensitive';
class SensitiveAwareLogInterceptor extends Interceptor {
final Interceptor _logger;
SensitiveAwareLogInterceptor(this._logger);
bool _isSensitive(RequestOptions options) =>
options.extra[kSensitiveRequest] == true;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (_isSensitive(options)) return handler.next(options);
_logger.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (_isSensitive(response.requestOptions)) return handler.next(response);
_logger.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (_isSensitive(err.requestOptions)) return handler.next(err);
_logger.onError(err, handler);
}
}
+7 -1
View File
@@ -1,9 +1,15 @@
import 'dart:ui';
import '../../sample/sample_data.dart';
class AppConstant {
static const String appName = "Enaklo";
static const String coinName = "EnakCoin";
static const String poinName = "EnakPoin";
static const String poinName = "EnakPoint";
static const String gameUrl = "https://game.enaklo.com/";
/// `theme-color` game.enaklo.com.
static const Color gameBackgroundColor = Color(0xFF09090B);
}
MerchantModel merchant = merchants.first;
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:injectable/injectable.dart';
@module
abstract class SecureStorageDi {
@lazySingleton
FlutterSecureStorage get secureStorage => const FlutterSecureStorage();
}
@@ -25,4 +25,7 @@ extension IntegerExt on int {
symbol: 'Rp ',
decimalDigits: 0,
).format(this);
/// Format ribuan Indonesia tanpa simbol: 12.500
String get thousandFormat => NumberFormat.decimalPattern('id').format(this);
}
+9
View File
@@ -30,4 +30,13 @@ extension DateTimeIndonesia on DateTime {
String get toHourMinute {
return DateFormat('HH:mm', 'id_ID').format(this);
}
/// Waktu dinding WIB (UTC+7), apa pun zona waktu perangkat.
/// Hanya untuk ditampilkan; jangan dipakai untuk membandingkan waktu.
DateTime get toWib => toUtc().add(const Duration(hours: 7));
/// Format: 13 Agu 2025 20:00
String get toShortDatetime {
return DateFormat('d MMM yyyy HH:mm', 'id_ID').format(this);
}
}
+14
View File
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -18,6 +20,18 @@ String getNormalizePhone(String phoneNumber) {
return '62$normalizedPhone';
}
/// UUID v4 acak dari [Random.secure], dipakai sebagai `Idempotency-Key`
/// dan `device_id`.
String generateUuidV4() {
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-'
'${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}';
}
Map<String, dynamic> getAuthorizationHeader() {
return {
'Authorization':
+25 -2
View File
@@ -8,7 +8,30 @@ class ApiPath {
// Marketing
static String ferrisWheel = '/api/v1/customer/ferris-wheel';
static String spin = '/api/v1/customer/spin';
// Customer
static String customerPoint = '/api/v1/customer/points';
// Wallet
static String wallet = '/api/v1/customer/wallet';
static String walletTransactions = '/api/v1/customer/wallet/transactions';
static String walletExpiring = '/api/v1/customer/wallet/expiring';
static String walletPaymentCode = '/api/v1/customer/wallet/payment-code';
static String walletExchangePreview =
'/api/v1/customer/wallet/exchange/preview';
static String walletExchange = '/api/v1/customer/wallet/exchange';
static String walletTransferRecipient =
'/api/v1/customer/wallet/transfer/recipient';
static String walletTransfer = '/api/v1/customer/wallet/transfer';
// Outlet & order
static String outlets = '/api/v1/customer/outlets';
static String orders = '/api/v1/customer/orders';
// PIN
static String pin = '/api/v1/customer/pin';
static String pinStatus = '/api/v1/customer/pin/status';
static String pinOtp = '/api/v1/customer/pin/otp';
static String pinReset = '/api/v1/customer/pin/reset';
// Device (push notification)
static String devices = '/api/v1/customer/devices';
}