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
@@ -5,12 +5,20 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'dart:math' as math;
import 'package:dartz/dartz.dart' hide State;
import '../../../../application/auth/auth_bloc.dart';
import '../../../../application/game/ferris_wheel_loader/ferris_wheel_loader_bloc.dart';
import '../../../../application/game/game_spin/game_spin_bloc.dart';
import '../../../../application/wallet/wallet_loader/wallet_loader_bloc.dart';
import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart';
import '../../../../common/painter/wheel_painter.dart';
import '../../../../injection.dart';
import '../../../../domain/game/game.dart';
import '../../../../domain/wallet/wallet.dart';
import '../../../components/toast/flushbar.dart';
import '../../wallet/wallet_ui.dart';
// Simple models for UI state
class PrizeHistory {
@@ -39,10 +47,15 @@ class FerrisWheelPage extends StatefulWidget implements AutoRouteWrapper {
State<FerrisWheelPage> createState() => _FerrisWheelPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<FerrisWheelLoaderBloc>()
..add(const FerrisWheelLoaderEvent.fetched()),
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<FerrisWheelLoaderBloc>()
..add(const FerrisWheelLoaderEvent.fetched()),
),
BlocProvider(create: (context) => getIt<GameSpinBloc>()),
],
child: this,
);
}
@@ -69,7 +82,6 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
bool _isMusicEnabled = true;
// Game State
int tokens = 3;
bool isSpinning = false;
String resultText = 'Belum pernah spin';
double currentRotation = 0.0;
@@ -84,6 +96,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
super.initState();
_initializeAudio();
_initializeAnimations();
context.read<WalletLoaderBloc>().add(const WalletLoaderEvent.fetched());
}
@override
@@ -167,39 +180,11 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
_idleRotationController.repeat();
}
int _selectPrizeWithWeight() {
if (gamePrizes.isEmpty) return 0;
int get _coinBalance =>
context.read<WalletLoaderBloc>().state.summary.coinBalance;
// Jika GamePrize memiliki properti weight, gunakan ini:
// List<int> weights = gamePrizes.map((prize) => prize.weight ?? 1).toList();
// Untuk sementara, gunakan bobot default yang berbeda
// Hadiah langka (index awal) memiliki bobot lebih kecil
List<int> weights = [];
for (int i = 0; i < gamePrizes.length; i++) {
// Bobot menurun: hadiah pertama langka, hadiah terakhir mudah didapat
int weight = gamePrizes.length - i;
weights.add(weight);
}
// Hitung total bobot
int totalWeight = weights.reduce((a, b) => a + b);
// Generate random number
int randomNum = math.Random().nextInt(totalWeight);
// Tentukan hadiah berdasarkan bobot
int currentWeight = 0;
for (int i = 0; i < gamePrizes.length; i++) {
currentWeight += weights[i];
if (randomNum < currentWeight) {
return i;
}
}
// Fallback (seharusnya tidak pernah terjadi)
return gamePrizes.length - 1;
}
int get _coinCost =>
context.read<FerrisWheelLoaderBloc>().state.ferrisWheel.coinCost;
Color _getPrizeColor(GamePrize prize, int index) {
final colors = [
@@ -214,22 +199,70 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
return colors[index % colors.length];
}
/// Hadiah ditentukan server; roda hanya dianimasikan ke hadiah itu.
void _spinWheel() {
if (isSpinning || tokens <= 0 || gamePrizes.isEmpty) return;
if (isSpinning || gamePrizes.isEmpty) return;
final game = context.read<FerrisWheelLoaderBloc>().state.ferrisWheel;
if (_coinBalance < _coinCost) {
AppFlushbar.showError(
context,
'EnakCoin kamu kurang. Butuh $_coinCost EnakCoin untuk main.',
);
return;
}
_playWheelSpin();
_playTokenSound();
setState(() {
isSpinning = true;
tokens--;
resultText = 'Sedang berputar...';
});
context.read<GameSpinBloc>().add(GameSpinEvent.spun(game.id));
}
void _onSpinResult(Either<GameFailure, SpinResult> either) {
either.fold(
(f) {
final message = f.map(
serverError: (value) => value.failure.toStringFormatted(context),
unexpectedError: (_) => 'Terjadi kesalahan, coba lagi',
dynamicErrorMessage: (value) => value.erroMessage,
);
setState(() {
isSpinning = false;
resultText = 'Belum berhasil, silakan coba lagi';
});
AppFlushbar.showError(context, message);
},
(result) {
context.read<WalletLoaderBloc>().add(
WalletLoaderEvent.coinBalanceUpdated(result.coinsRemaining),
);
var index = gamePrizes.indexWhere((p) => p.id == result.prizeId);
if (index < 0) {
index = gamePrizes.indexWhere((p) => p.name == result.prizeName);
}
_animateToPrize(
index < 0 ? 0 : index,
index < 0
? GamePrize.empty().copyWith(
id: result.prizeId,
name: result.prizeName,
)
: gamePrizes[index],
);
},
);
}
void _animateToPrize(int selectedPrizeIndex, GamePrize wonPrize) {
_playWheelSpin();
_idleRotationController.stop();
_idleRotationController.reset();
int selectedPrizeIndex = _selectPrizeWithWeight();
double sectionAngle = (2 * math.pi) / gamePrizes.length;
double currentPos = currentRotation;
@@ -254,7 +287,7 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
_rotationController.reset();
_rotationController.animateTo(1.0).then((_) {
final wonPrize = gamePrizes[selectedPrizeIndex];
if (!mounted) return;
_playWinSound();
setState(() {
@@ -373,6 +406,15 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
@override
Widget build(BuildContext context) {
return BlocListener<GameSpinBloc, GameSpinState>(
listenWhen: (p, c) => p.failureOrResultOption != c.failureOrResultOption,
listener: (context, state) =>
state.failureOrResultOption.fold(() {}, _onSpinResult),
child: _buildPage(),
);
}
Widget _buildPage() {
return BlocBuilder<FerrisWheelLoaderBloc, FerrisWheelLoaderState>(
builder: (context, state) {
if (state.isFetching) {
@@ -599,21 +641,15 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
),
Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: AppColor.warning,
shape: BoxShape.circle,
),
child: Icon(Icons.circle, size: 12, color: AppColor.textWhite),
),
const WalletCurrencyIcon(currency: WalletCurrency.coin, size: 20),
const SizedBox(width: 8),
Text(
'Token: $tokens',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
BlocBuilder<WalletLoaderBloc, WalletLoaderState>(
builder: (context, wallet) => Text(
'${wallet.summary.coinBalance.thousandFormat} EnakCoin',
style: AppStyle.lg.copyWith(
fontWeight: FontWeight.bold,
color: AppColor.textPrimary,
),
),
),
],
@@ -689,6 +725,16 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
}
Widget _buildSpinButton() {
return BlocSelector<WalletLoaderBloc, WalletLoaderState, int>(
selector: (state) => state.summary.coinBalance,
builder: (context, coinBalance) => Opacity(
opacity: coinBalance >= _coinCost ? 1 : 0.5,
child: _buildSpinButtonBody(),
),
);
}
Widget _buildSpinButtonBody() {
return AnimatedBuilder(
animation: _pulseAnimation ?? const AlwaysStoppedAnimation(1.0),
builder: (context, child) => Transform.scale(
@@ -737,21 +783,31 @@ class _FerrisWheelPageState extends State<FerrisWheelPage>
}
Widget _buildBottomBanner() {
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [AppColor.warning, AppColor.warning]),
borderRadius: BorderRadius.circular(15),
),
child: Text(
'Spin 30x lagi buat mainin spesial spin',
textAlign: TextAlign.center,
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
return BlocSelector<WalletLoaderBloc, WalletLoaderState, int>(
selector: (state) => state.summary.coinBalance,
builder: (context, coinBalance) {
final cost = _coinCost;
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColor.warning, AppColor.warning],
),
borderRadius: BorderRadius.circular(15),
),
child: Text(
coinBalance >= cost
? 'Sekali main: $cost EnakCoin'
: 'Sekali main: $cost EnakCoin. EnakCoin kamu belum cukup.',
textAlign: TextAlign.center,
style: AppStyle.lg.copyWith(
color: AppColor.textWhite,
fontWeight: FontWeight.bold,
),
),
);
},
);
}