Phase 2 refinements: Firebase config, dev environment fixes, phase 3 requirement draft

- Integrated Firebase SDK in both Flutter apps (google-services, firebase_options)
- Fixed auth flow, API client, and pairing/status blocs for dev environment
- Added full Flutter project scaffolds (android, ios, web, etc.)
- Added phase 3 chat engine requirement document
- Added bugreport zip pattern to gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 19:16:34 +08:00
parent d668112edd
commit 844d7234e6
229 changed files with 10439 additions and 102 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/auth/auth_bloc.dart';
@@ -11,20 +12,63 @@ class OtpScreen extends StatefulWidget {
}
class _OtpScreenState extends State<OtpScreen> {
final _otpController = TextEditingController();
final List<TextEditingController> _controllers =
List.generate(6, (_) => TextEditingController());
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
@override
void dispose() {
_otpController.dispose();
for (final c in _controllers) {
c.dispose();
}
for (final f in _focusNodes) {
f.dispose();
}
super.dispose();
}
String get _otp => _controllers.map((c) => c.text).join();
void _onChanged(int index, String value) {
if (value.length == 1 && index < 5) {
_focusNodes[index + 1].requestFocus();
}
if (_otp.length == 6) {
_submit();
}
}
void _onKeyDown(int index, KeyEvent event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.backspace &&
_controllers[index].text.isEmpty &&
index > 0) {
_controllers[index - 1].clear();
_focusNodes[index - 1].requestFocus();
}
}
void _submit() {
final otp = _otp;
if (otp.length != 6) return;
final state = context.read<AuthBloc>().state;
final verificationId = state is AuthOtpSent ? state.verificationId : '';
context.read<AuthBloc>().add(OtpVerified(verificationId, otp));
}
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
// Clear fields on error
for (final c in _controllers) {
c.clear();
}
_focusNodes[0].requestFocus();
}
},
child: Scaffold(
@@ -34,26 +78,47 @@ class _OtpScreenState extends State<OtpScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Kode OTP telah dikirim ke ${widget.phone}'),
const SizedBox(height: 24),
TextField(
controller: _otpController,
decoration: const InputDecoration(
labelText: 'Kode OTP',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
maxLength: 6,
Text(
'Kode OTP telah dikirim ke ${widget.phone}',
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(6, (index) {
return SizedBox(
width: 48,
child: KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (event) => _onKeyDown(index, event),
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
decoration: const InputDecoration(
counterText: '',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 14),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
onChanged: (value) => _onChanged(index, value),
),
),
);
}),
),
const SizedBox(height: 32),
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) => ElevatedButton(
onPressed: state is AuthLoading ? null : () {
final otp = _otpController.text.trim();
if (otp.length != 6) return;
final verificationId = state is AuthOtpSent ? state.verificationId : '';
context.read<AuthBloc>().add(OtpVerified(verificationId, otp));
},
onPressed: state is AuthLoading ? null : _submit,
child: state is AuthLoading
? const CircularProgressIndicator()
: const Text('Verifikasi'),