Initial commit: QR Scanner Flutter app

App móvil para validar pases QR contra base-admin-web con login Cognito,
escáner en tiempo real, feedback auditivo/háptico y README completo.
This commit is contained in:
Alejandro Lara
2026-06-09 17:03:06 -06:00
commit 50fe9a9d36
83 changed files with 4613 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/login_response.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
static const _sessionKey = 'auth_session';
AuthSession? _currentSession;
AuthSession? get currentSession => _currentSession;
Future<LoginResponse> login({
required String username,
required String password,
bool rememberMe = false,
}) async {
final response = await http.post(
Uri.parse(ApiConfig.loginUrl),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({
'username': username,
'password': password,
}),
);
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final loginResponse = LoginResponse.fromJson(body);
final accessToken = loginResponse.accessToken?.trim();
if (response.statusCode == 200 &&
loginResponse.success &&
accessToken != null &&
accessToken.isNotEmpty) {
final session = AuthSession.fromLoginResponse(
loginResponse.copyWith(
username: loginResponse.username ?? username,
accessToken: accessToken,
),
);
if (!session.isScanner) {
_currentSession = null;
await _clearStoredSession();
return const LoginResponse(
success: false,
message: 'El usuario no tiene permisos para escanear.',
);
}
_currentSession = session;
if (rememberMe) {
await _saveSession(session);
} else {
await _clearStoredSession();
}
return loginResponse.copyWith(accessToken: accessToken);
}
return LoginResponse(
success: false,
message: loginResponse.message ??
'No se pudo iniciar sesión (${response.statusCode})',
);
}
Future<void> _saveSession(AuthSession session) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_sessionKey, jsonEncode(session.toJson()));
}
Future<void> _clearStoredSession() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_sessionKey);
}
Future<void> clearSession() async {
_currentSession = null;
await _clearStoredSession();
}
Future<AuthSession?> loadStoredSession() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_sessionKey);
if (raw == null || raw.isEmpty) return null;
final session = AuthSession.fromJson(
jsonDecode(raw) as Map<String, dynamic>,
);
_currentSession = session;
return session;
}
Future<bool> hasStoredSession() async {
final session = await loadStoredSession();
return session != null && session.accessToken.isNotEmpty;
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
class FeedbackService {
FeedbackService() : _player = AudioPlayer() {
_player.setReleaseMode(ReleaseMode.stop);
}
final AudioPlayer _player;
Future<void> onCodeDetected() async {
await Future.wait([
_play('sounds/scan.wav'),
_vibratePattern(const [0, 40]),
]);
}
Future<void> onSuccess() async {
await Future.wait([
_play('sounds/success.wav'),
_vibratePattern(const [0, 90, 70, 140]),
]);
await HapticFeedback.heavyImpact();
}
Future<void> onWarning() async {
await Future.wait([
_play('sounds/warning.wav'),
_vibratePattern(const [0, 120, 80, 120, 80, 120]),
]);
await HapticFeedback.mediumImpact();
}
Future<void> _play(String asset) async {
try {
await _player.stop();
await _player.play(AssetSource(asset));
} catch (_) {
// Ignore audio errors on devices without speaker routing.
}
}
Future<void> _vibratePattern(List<int> pattern) async {
try {
final hasVibrator = await Vibration.hasVibrator();
if (hasVibrator != true) return;
final hasAmplitude = await Vibration.hasAmplitudeControl();
if (hasAmplitude == true) {
final intensities = pattern
.asMap()
.entries
.map((entry) => entry.key.isOdd ? 180 : 0)
.toList();
await Vibration.vibrate(
pattern: pattern,
intensities: intensities,
);
return;
}
await Vibration.vibrate(pattern: pattern);
} catch (_) {
await HapticFeedback.selectionClick();
}
}
Future<void> dispose() async {
await _player.dispose();
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/scan_response.dart';
class PassCodeService {
Future<ScanResponse> scan({
required AuthSession session,
required String code,
}) async {
final response = await http.post(
Uri.parse(ApiConfig.scanUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': session.authorizationHeader,
},
body: jsonEncode({'code': code}),
);
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
return ScanResponse.fromJson(body);
}
}