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:
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user