Files
enaklo-flutter/lib/presentation/pages/pin/pin_change_page.dart
T
efrilmandClaude Opus 5.5 f5e3075205 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>
2026-10-01 01:43:22 +07:00

204 lines
6.2 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../../../application/auth/auth_bloc.dart';
import '../../../application/pin/pin_change_form/pin_change_form_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/wallet/wallet.dart';
import '../../../injection.dart';
import '../../components/toast/flushbar.dart';
import '../../router/app_router.gr.dart';
import '../wallet/wallet_ui.dart';
import 'pin_rules.dart';
import 'widgets/pin_code_field.dart';
enum _Step { oldPin, newPin, confirm }
/// Ganti PIN (ยง6.3).
@RoutePage()
class PinChangePage extends StatefulWidget implements AutoRouteWrapper {
const PinChangePage({super.key});
@override
State<PinChangePage> createState() => _PinChangePageState();
@override
Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<PinChangeFormBloc>(), child: this);
}
class _PinChangePageState extends State<PinChangePage> {
final TextEditingController _controller = TextEditingController();
_Step _step = _Step.oldPin;
String _oldPin = '';
String _newPin = '';
String? _errorText;
bool _isLocked = false;
@override
void dispose() {
_controller.dispose();
_oldPin = '';
_newPin = '';
super.dispose();
}
void _goTo(_Step step, {String? error}) {
setState(() {
_step = step;
_errorText = error;
});
_controller.clear();
}
void _onCompleted(String value) {
switch (_step) {
case _Step.oldPin:
_oldPin = value;
_goTo(_Step.newPin);
case _Step.newPin:
final error = value == _oldPin
? 'PIN baru harus berbeda dari PIN lama.'
: validateNewPin(
value,
birthDate: context.read<AuthBloc>().state.user.birthDate,
);
if (error != null) {
_goTo(_Step.newPin, error: error);
return;
}
_newPin = value;
_goTo(_Step.confirm);
case _Step.confirm:
if (value != _newPin) {
_newPin = '';
_goTo(_Step.newPin, error: 'PIN tidak sama. Silakan ulangi.');
return;
}
context.read<PinChangeFormBloc>().add(
PinChangeFormEvent.submitted(
oldPin: _oldPin,
pin: _newPin,
confirmPin: value,
),
);
_controller.clear();
}
}
void _onFailure(WalletFailure failure) {
_oldPin = '';
_newPin = '';
failure.maybeMap(
pinInvalid: (_) =>
_goTo(_Step.oldPin, error: walletFailureMessage(failure)),
pinLocked: (_) {
_goTo(_Step.oldPin, error: walletFailureMessage(failure));
setState(() => _isLocked = true);
},
pinNotSet: (_) =>
context.router.replace(PinSetupRoute(purpose: PinOtpPurpose.setup)),
rejected: (_) => _goTo(
_Step.newPin,
error: 'PIN baru tidak memenuhi syarat. Coba PIN lain.',
),
orElse: () {
_goTo(_Step.oldPin);
AppFlushbar.showError(context, walletFailureMessage(failure));
},
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<PinChangeFormBloc, PinChangeFormState>(
listenWhen: (p, c) =>
p.failureOrSuccessOption != c.failureOrSuccessOption,
listener: (context, state) {
state.failureOrSuccessOption.fold(
() {},
(either) => either.fold(_onFailure, (_) {
_oldPin = '';
_newPin = '';
AppFlushbar.showSuccess(context, 'PIN berhasil diganti');
context.router.maybePop();
}),
);
},
builder: (context, state) {
final title = switch (_step) {
_Step.oldPin => 'Masukkan PIN lama',
_Step.newPin => 'Masukkan PIN baru',
_Step.confirm => 'Konfirmasi PIN baru',
};
return Scaffold(
appBar: AppBar(title: const Text('Ganti PIN')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppStyle.xl.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'PIN 6 digit, bukan angka berulang, berurutan, atau tanggal '
'lahir.',
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
),
const SizedBox(height: 28),
PinCodeField(
key: ValueKey(_step),
controller: _controller,
enabled: !state.isSubmitting && !_isLocked,
hasError: _errorText != null,
onCompleted: _onCompleted,
),
const SizedBox(height: 16),
if (state.isSubmitting)
const Center(
child: SpinKitFadingCircle(
color: AppColor.primary,
size: 28,
),
)
else if (_errorText != null)
Text(
_errorText!,
style: AppStyle.md.copyWith(
color: AppColor.error,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
if (_step == _Step.oldPin)
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => context.router.replace(
PinSetupRoute(purpose: PinOtpPurpose.reset),
),
child: Text(
'Lupa PIN',
style: AppStyle.md.copyWith(
color: AppColor.primary,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
);
},
);
}
}