feat: integrate enakgame and voucher
Build & Deploy iOS to TestFlight / build-and-deploy (push) Waiting to run

This commit is contained in:
efrilm
2026-10-10 02:33:16 +07:00
parent e5515d5871
commit 38850b281f
104 changed files with 7122 additions and 8063 deletions
@@ -1,40 +1,55 @@
import 'dart:developer';
import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../injection.dart';
import '../../../presentation/router/app_router.dart';
import '../../../presentation/router/app_router.gr.dart';
import '../../constant/local_storage_key.dart';
import '../errors/unauthorized_error.dart';
/// Token login ditolak server: HTTP `401`, atau `304` dengan `entity`
/// `auth_handler` (§2.2). Belum ada refresh token, jadi customer harus login
/// ulang.
///
/// Bukan `403`: `PIN_NOT_SET` dan `TRANSFER_BLOCKED` juga memakai `403`.
bool isTokenRejected(Response response) {
if (response.statusCode == 401) return true;
final body = response.data;
if (body is! Map) return false;
final errors = body['errors'];
if (errors is! List || errors.isEmpty || errors.first is! Map) return false;
final first = errors.first as Map;
return first['code']?.toString() == '304' &&
first['entity'] == 'auth_handler';
}
/// Menghapus sesi dan membuka halaman login bila token yang dipakai request
/// ditolak. `validateStatus` menerima semua status, jadi pengecekan ada di
/// [onResponse], bukan `onError`.
class UnauthorizedInterceptor extends Interceptor {
static final GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 ||
err.response?.statusCode == 403 ||
err.response?.statusCode == 419) {
await _handleTokenExpired();
return super.onError(UnauthorizedError(err, null), handler);
void onResponse(Response response, ResponseInterceptorHandler handler) async {
try {
if (isTokenRejected(response)) await _handleTokenRejected(response);
} finally {
handler.next(response);
}
super.onError(err, handler);
}
Future<void> _handleTokenExpired() async {
// Clear stored token
final prefs = await SharedPreferences.getInstance();
Future<void> _handleTokenRejected(Response response) async {
final prefs = getIt<SharedPreferences>();
final token = prefs.getString(LocalStorageKey.token);
// Hanya token yang sedang dipakai: abaikan request tanpa login dan jawaban
// basi dari token lama setelah customer login lagi.
final sentAuthorization = response.requestOptions.headers['Authorization'];
if (token == null || sentAuthorization != 'Bearer $token') return;
log('token rejected', name: 'UnauthorizedInterceptor');
await prefs.remove(LocalStorageKey.token);
await prefs.remove(LocalStorageKey.user);
await prefs.clear(); // Optional: clear all user data
log('handleTokenExpired');
// Navigate to login page
final context = navigatorKey.currentContext;
if (context != null) {
// Option 1: Navigate and remove all previous routes
context.router.replaceAll([LoginRoute()]);
}
getIt<AppRouter>().replaceAll([LoginRoute()]);
}
}
+2 -1
View File
@@ -8,7 +8,8 @@ class AppConstant {
static const String poinName = "EnakPoint";
static const String gameUrl = "https://game.enaklo.com/";
/// `theme-color` game.enaklo.com.
/// Latar webview EnakGame selama halaman game dimuat (`theme-color`
/// game.enaklo.com).
static const Color gameBackgroundColor = Color(0xFF09090B);
}
-148
View File
@@ -1,148 +0,0 @@
// wheel_painter.dart - Fixed implementation with consistent positioning
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../domain/game/game.dart';
class WheelPainter extends CustomPainter {
final List<GamePrize> gamePrizes;
final Color Function(GamePrize prize, int index) getPrizeColor;
WheelPainter({required this.gamePrizes, required this.getPrizeColor});
@override
void paint(Canvas canvas, Size size) {
if (gamePrizes.isEmpty) return;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final sectionAngle = 2 * math.pi / gamePrizes.length;
// Draw outer white border
final outerBorderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius, outerBorderPaint);
// Draw blue border ring
final blueBorderPaint = Paint()
..color = const Color(0xFF3B82F6)
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius - 8, blueBorderPaint);
// Draw inner white circle
final innerWhitePaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
canvas.drawCircle(center, radius - 20, innerWhitePaint);
// Draw sections - KONSISTEN dengan logic spin
for (int i = 0; i < gamePrizes.length; i++) {
final prize = gamePrizes[i];
// Section 0 di top (-π/2), section 1 di kanan atas, dst (clockwise)
final startAngle = (-math.pi / 2) + (i * sectionAngle);
// Section background
final sectionPaint = Paint()
..color = getPrizeColor(prize, i)
..style = PaintingStyle.fill;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius - 22),
startAngle,
sectionAngle,
true,
sectionPaint,
);
// Draw prize text
final textAngle = startAngle + sectionAngle / 2;
final textPosition = Offset(
center.dx + (radius - 80) * math.cos(textAngle),
center.dy + (radius - 80) * math.sin(textAngle),
);
canvas.save();
canvas.translate(textPosition.dx, textPosition.dy);
canvas.rotate(textAngle + math.pi / 2);
final textPainter = TextPainter(
text: TextSpan(
text: prize.name,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
textPainter.layout(maxWidth: 100);
textPainter.paint(
canvas,
Offset(-textPainter.width / 2, -textPainter.height / 2),
);
canvas.restore();
// DEBUG: Draw section number
final numberPosition = Offset(
center.dx + (radius - 50) * math.cos(textAngle),
center.dy + (radius - 50) * math.sin(textAngle),
);
canvas.drawCircle(numberPosition, 15, Paint()..color = Colors.white);
final numberPainter = TextPainter(
text: TextSpan(
text: i.toString(),
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
numberPainter.layout();
numberPainter.paint(
canvas,
Offset(
numberPosition.dx - numberPainter.width / 2,
numberPosition.dy - numberPainter.height / 2,
),
);
}
// Draw white dots
final dotPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
for (int i = 0; i < 24; i++) {
final dotAngle = (2 * math.pi / 24) * i;
final dotPosition = Offset(
center.dx + (radius - 14) * math.cos(dotAngle),
center.dy + (radius - 14) * math.sin(dotAngle),
);
canvas.drawCircle(dotPosition, 3, dotPaint);
}
// Draw section dividers
final dividerPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (int i = 0; i < gamePrizes.length; i++) {
final angle = (-math.pi / 2) + (i * sectionAngle);
final lineStart = Offset(
center.dx + (radius - 110) * math.cos(angle),
center.dy + (radius - 110) * math.sin(angle),
);
final lineEnd = Offset(
center.dx + (radius - 22) * math.cos(angle),
center.dy + (radius - 22) * math.sin(angle),
);
canvas.drawLine(lineStart, lineEnd, dividerPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
+6 -5
View File
@@ -6,15 +6,10 @@ class ApiPath {
static String login = '/api/v1/customer-auth/login';
static String resend = '/api/v1/customer-auth/resend-otp';
// Marketing
static String ferrisWheel = '/api/v1/customer/ferris-wheel';
static String spin = '/api/v1/customer/spin';
// 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';
@@ -32,6 +27,12 @@ class ApiPath {
static String pinOtp = '/api/v1/customer/pin/otp';
static String pinReset = '/api/v1/customer/pin/reset';
// Voucher
static String vouchers = '/api/v1/customer/vouchers';
static String voucherRedemptions = '/api/v1/customer/vouchers/redemptions';
static String voucherRedeem(String voucherId) =>
'/api/v1/customer/vouchers/$voucherId/redeem';
// Device (push notification)
static String devices = '/api/v1/customer/devices';
}