50fe9a9d36
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.
481 lines
12 KiB
Markdown
481 lines
12 KiB
Markdown
# QR Scanner
|
|
|
|
Aplicación móvil Flutter para validar **pases de acceso** mediante códigos QR. Se conecta al backend **base-admin-web** para autenticar operadores con rol escáner y registrar cada lectura en el servidor.
|
|
|
|
---
|
|
|
|
## Tabla de contenidos
|
|
|
|
- [Características](#características)
|
|
- [Arquitectura](#arquitectura)
|
|
- [Flujos principales](#flujos-principales)
|
|
- [Estructura del proyecto](#estructura-del-proyecto)
|
|
- [Dependencias](#dependencias)
|
|
- [Requisitos](#requisitos)
|
|
- [Configuración e instalación](#configuración-e-instalación)
|
|
- [API del backend](#api-del-backend)
|
|
- [Permisos](#permisos)
|
|
- [Pruebas](#pruebas)
|
|
- [Compilación](#compilación)
|
|
|
|
---
|
|
|
|
## Características
|
|
|
|
- Login con usuario y contraseña contra el API de **base-admin-web** (Cognito / JWT).
|
|
- Sesión persistente opcional con **Recordarme** (`shared_preferences`).
|
|
- Escáner QR en tiempo real con ventana de enfoque y overlay visual.
|
|
- Validación del pase en el servidor con `accessToken` (Bearer).
|
|
- Retroalimentación auditiva y háptica al detectar, validar o rechazar un código.
|
|
- UI oscura con animaciones en login, home y resultados de escaneo.
|
|
|
|
---
|
|
|
|
## Arquitectura
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph App["App Flutter (qrscanner)"]
|
|
UI["Screens\n(login · home · scanner)"]
|
|
Services["Services\n(auth · pass_code · feedback)"]
|
|
Models["Models\n(login_response · auth_session · pass_code)"]
|
|
Config["ApiConfig"]
|
|
end
|
|
|
|
subgraph Device["Dispositivo"]
|
|
Camera["Cámara"]
|
|
Prefs["SharedPreferences"]
|
|
Audio["Audio / Vibración"]
|
|
end
|
|
|
|
subgraph Backend["base-admin-web :8080"]
|
|
AuthAPI["POST /api/auth/login"]
|
|
ScanAPI["POST /api/pass-codes/scan"]
|
|
Cognito["AWS Cognito / JWT"]
|
|
end
|
|
|
|
UI --> Services
|
|
Services --> Models
|
|
Services --> Config
|
|
Services --> Prefs
|
|
UI --> Camera
|
|
Services --> Audio
|
|
Config --> AuthAPI
|
|
Config --> ScanAPI
|
|
AuthAPI --> Cognito
|
|
ScanAPI --> Cognito
|
|
```
|
|
|
|
### Capas de la app
|
|
|
|
```mermaid
|
|
graph LR
|
|
A[Pantallas] --> B[Servicios]
|
|
B --> C[Modelos]
|
|
B --> D[HTTP / API]
|
|
A --> E[Tema / UI]
|
|
B --> F[Almacenamiento local]
|
|
A --> G[mobile_scanner]
|
|
```
|
|
|
|
| Capa | Responsabilidad |
|
|
|------|-----------------|
|
|
| **Screens** | UI, navegación y estados visuales |
|
|
| **Services** | Lógica de negocio, HTTP y feedback |
|
|
| **Models** | Parseo JSON y sesión de autenticación |
|
|
| **Config** | URL base del API configurable en compilación |
|
|
|
|
---
|
|
|
|
## Flujos principales
|
|
|
|
### Login
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor Usuario
|
|
participant Login as LoginScreen
|
|
participant Auth as AuthService
|
|
participant API as base-admin-web
|
|
participant Home as HomeScreen
|
|
|
|
Usuario->>Login: Usuario + contraseña
|
|
Login->>Auth: login()
|
|
Auth->>API: POST /api/auth/login
|
|
API-->>Auth: accessToken, refreshToken, idToken, roles
|
|
Auth->>Auth: Valida ROLE_SCANNER
|
|
alt Credenciales válidas
|
|
Auth-->>Login: LoginResponse success
|
|
Login->>Home: Navegar con AuthSession
|
|
else Error
|
|
API-->>Login: success false / sin token
|
|
Login-->>Usuario: Mensaje de error
|
|
end
|
|
```
|
|
|
|
### Escaneo y validación
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor Operador
|
|
participant Home as HomeScreen
|
|
participant Scan as ScannerScreen
|
|
participant MS as mobile_scanner
|
|
participant Pass as PassCodeService
|
|
participant API as base-admin-web
|
|
participant FB as FeedbackService
|
|
|
|
Operador->>Home: Abrir escáner
|
|
Home->>Scan: AuthSession
|
|
loop Escaneo continuo
|
|
MS->>Scan: onDetect (QR)
|
|
Scan->>FB: Sonido + vibración
|
|
Scan->>Pass: scan(code)
|
|
Pass->>API: POST /api/pass-codes/scan\nAuthorization: Bearer accessToken
|
|
API-->>Pass: success + passCode
|
|
Pass-->>Scan: ScanResponse
|
|
Scan->>FB: success / warning
|
|
Scan-->>Operador: Tarjeta verde (nombre + código)\no amarilla (error)
|
|
end
|
|
```
|
|
|
|
### Estados del escáner
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> scanning: Abrir cámara
|
|
scanning --> processing: QR detectado
|
|
processing --> success: API válida
|
|
processing --> warning: API rechaza / error red
|
|
success --> scanning: Escanear otro
|
|
warning --> scanning: Escanear otro
|
|
scanning --> [*]: Volver atrás
|
|
```
|
|
|
|
---
|
|
|
|
## Estructura del proyecto
|
|
|
|
```
|
|
qrscanner/
|
|
├── lib/
|
|
│ ├── main.dart # Punto de entrada
|
|
│ ├── config/
|
|
│ │ └── api_config.dart # URL base y endpoints
|
|
│ ├── models/
|
|
│ │ ├── auth_session.dart # Sesión y header Authorization
|
|
│ │ ├── login_response.dart # Respuesta de login
|
|
│ │ ├── pass_code.dart # Datos del pase
|
|
│ │ └── scan_response.dart # Respuesta de escaneo
|
|
│ ├── screens/
|
|
│ │ ├── login_screen.dart # Pantalla de inicio de sesión
|
|
│ │ ├── home_screen.dart # Pantalla principal
|
|
│ │ └── scanner_screen.dart # Cámara y validación QR
|
|
│ ├── services/
|
|
│ │ ├── auth_service.dart # Login y sesión
|
|
│ │ ├── pass_code_service.dart# Validación de pases
|
|
│ │ └── feedback_service.dart # Audio y vibración
|
|
│ └── theme/
|
|
│ └── app_theme.dart # Colores y tema oscuro
|
|
├── assets/
|
|
│ └── sounds/
|
|
│ ├── scan.wav
|
|
│ ├── success.wav
|
|
│ └── warning.wav
|
|
├── android/ # Configuración Android
|
|
├── ios/ # Configuración iOS
|
|
├── test/ # Pruebas unitarias y widget
|
|
└── pubspec.yaml # Dependencias
|
|
```
|
|
|
|
---
|
|
|
|
## Dependencias
|
|
|
|
### Directas (producción)
|
|
|
|
| Paquete | Versión en `pubspec.yaml` | Versión instalada | Uso |
|
|
|---------|---------------------------|-------------------|-----|
|
|
| [flutter](https://flutter.dev) | SDK | — | Framework UI |
|
|
| [cupertino_icons](https://pub.dev/packages/cupertino_icons) | `^1.0.8` | 1.0.9 | Iconos iOS |
|
|
| [http](https://pub.dev/packages/http) | `^1.2.2` | 1.6.0 | Peticiones REST al backend |
|
|
| [mobile_scanner](https://pub.dev/packages/mobile_scanner) | `^7.0.1` | 7.2.0 | Lectura de códigos QR con cámara |
|
|
| [shared_preferences](https://pub.dev/packages/shared_preferences) | `^2.3.3` | 2.5.5 | Persistir sesión (Recordarme) |
|
|
| [audioplayers](https://pub.dev/packages/audioplayers) | `^6.5.0` | 6.7.1 | Sonidos de feedback |
|
|
| [vibration](https://pub.dev/packages/vibration) | `^3.1.3` | 3.1.8 | Vibración al escanear |
|
|
|
|
### Desarrollo
|
|
|
|
| Paquete | Versión | Uso |
|
|
|---------|---------|-----|
|
|
| [flutter_test](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html) | SDK | Pruebas widget |
|
|
| [flutter_lints](https://pub.dev/packages/flutter_lints) | `^6.0.0` (6.0.0) | Reglas de análisis estático |
|
|
|
|
### Dependencias transitivas relevantes
|
|
|
|
Incluidas automáticamente por los paquetes anteriores:
|
|
|
|
- `path_provider` — rutas de almacenamiento (audioplayers, shared_preferences)
|
|
- `device_info_plus` — información del dispositivo (vibration)
|
|
- `camera` / plugins nativos — soporte de cámara (mobile_scanner)
|
|
|
|
Para verificar versiones actualizadas:
|
|
|
|
```bash
|
|
flutter pub outdated
|
|
```
|
|
|
|
---
|
|
|
|
## Requisitos
|
|
|
|
- **Flutter** 3.12+ (Dart SDK `^3.12.1`)
|
|
- **Backend** [base-admin-web](https://github.com) corriendo y accesible en red
|
|
- Dispositivo físico o emulador con **cámara** (recomendado dispositivo real para QR)
|
|
- Usuario con rol **`ROLE_SCANNER`** en Cognito / base-admin-web
|
|
|
|
---
|
|
|
|
## Configuración e instalación
|
|
|
|
### 1. Clonar e instalar dependencias
|
|
|
|
```bash
|
|
cd flutter-app/qrscanner
|
|
flutter pub get
|
|
```
|
|
|
|
### 2. URL del API
|
|
|
|
Por defecto la app apunta a:
|
|
|
|
```
|
|
http://10.99.51.6:8080
|
|
```
|
|
|
|
Para usar otra IP o puerto:
|
|
|
|
```bash
|
|
flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080
|
|
```
|
|
|
|
Compilación release con URL personalizada:
|
|
|
|
```bash
|
|
flutter build apk --dart-define=API_BASE_URL=http://10.99.51.6:8080
|
|
```
|
|
|
|
### 3. Ejecutar en dispositivo
|
|
|
|
```bash
|
|
# Listar dispositivos
|
|
flutter devices
|
|
|
|
# Ejecutar
|
|
flutter run
|
|
|
|
# Ejecutar con URL custom
|
|
flutter run --dart-define=API_BASE_URL=http://10.99.51.6:8080
|
|
```
|
|
|
|
### 4. Credenciales de prueba
|
|
|
|
Según el entorno de desarrollo de **base-admin-web**:
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| Usuario | `scanner` |
|
|
| Contraseña | `Scanner123*` |
|
|
|
|
> El usuario debe pertenecer al grupo **scanners** / rol `ROLE_SCANNER`.
|
|
|
|
---
|
|
|
|
## API del backend
|
|
|
|
Base URL configurable: `ApiConfig.baseUrl`
|
|
|
|
### Login
|
|
|
|
```http
|
|
POST /api/auth/login
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request:**
|
|
|
|
```json
|
|
{
|
|
"username": "scanner",
|
|
"password": "Scanner123*"
|
|
}
|
|
```
|
|
|
|
**Response (200):**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"accessToken": "eyJ...",
|
|
"refreshToken": "dXMt...",
|
|
"idToken": "eyJ...",
|
|
"tokenType": "Bearer",
|
|
"expiresIn": 3600,
|
|
"username": "scanner",
|
|
"roles": ["ROLE_SCANNER"]
|
|
}
|
|
```
|
|
|
|
La app usa **`accessToken`** en todas las peticiones autenticadas.
|
|
|
|
### Escanear pase
|
|
|
|
```http
|
|
POST /api/pass-codes/scan
|
|
Content-Type: application/json
|
|
Authorization: Bearer <accessToken>
|
|
```
|
|
|
|
**Request:**
|
|
|
|
```json
|
|
{
|
|
"code": "ABC123XYZ"
|
|
}
|
|
```
|
|
|
|
**Response exitosa (200):**
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Codigo pase escaneado correctamente.",
|
|
"passCode": {
|
|
"id": 1,
|
|
"name": "Pase visitante",
|
|
"code": "ABC123XYZ",
|
|
"status": "ACTIVE",
|
|
"scanned": true,
|
|
"createdAt": "2026-06-09T10:00:00",
|
|
"scannedAt": "2026-06-09T15:30:00"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response error (400):**
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"message": "Descripción del error"
|
|
}
|
|
```
|
|
|
|
### Formato del QR
|
|
|
|
La app acepta:
|
|
|
|
- Texto plano con el código del pase
|
|
- URL con el código en query: `?code=ABC123`
|
|
- URL con el código como último segmento del path
|
|
|
|
---
|
|
|
|
## Permisos
|
|
|
|
### Android (`AndroidManifest.xml`)
|
|
|
|
| Permiso | Motivo |
|
|
|---------|--------|
|
|
| `INTERNET` | Comunicación con el API |
|
|
| `CAMERA` | Escaneo QR |
|
|
| `VIBRATE` | Feedback háptico |
|
|
|
|
`android:usesCleartextTraffic="true"` habilitado para HTTP en red local (desarrollo).
|
|
|
|
### iOS
|
|
|
|
Requiere descripción de uso de cámara en `Info.plist` al desplegar en producción.
|
|
|
|
---
|
|
|
|
## Pruebas
|
|
|
|
```bash
|
|
# Todas las pruebas
|
|
flutter test
|
|
|
|
# Análisis estático
|
|
flutter analyze
|
|
```
|
|
|
|
Pruebas incluidas:
|
|
|
|
- `test/widget_test.dart` — renderizado de la pantalla de login
|
|
- `test/login_response_test.dart` — parseo de respuesta Cognito y uso de `accessToken`
|
|
|
|
---
|
|
|
|
## Compilación
|
|
|
|
### Android APK
|
|
|
|
```bash
|
|
flutter build apk --release \
|
|
--dart-define=API_BASE_URL=http://10.99.51.6:8080
|
|
```
|
|
|
|
Salida: `build/app/outputs/flutter-apk/app-release.apk`
|
|
|
|
### Android App Bundle (Play Store)
|
|
|
|
```bash
|
|
flutter build appbundle --release \
|
|
--dart-define=API_BASE_URL=https://tu-servidor.com
|
|
```
|
|
|
|
### iOS
|
|
|
|
```bash
|
|
flutter build ios --release \
|
|
--dart-define=API_BASE_URL=https://tu-servidor.com
|
|
```
|
|
|
|
---
|
|
|
|
## Diagrama de integración con base-admin-web
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph Mobile["📱 QR Scanner"]
|
|
A[Login]
|
|
B[Escáner]
|
|
end
|
|
|
|
subgraph Server["🖥️ base-admin-web"]
|
|
C["/api/auth/login"]
|
|
D["/api/pass-codes/scan"]
|
|
E[(Base de datos)]
|
|
end
|
|
|
|
subgraph Auth["🔐 Cognito"]
|
|
F[JWT / Tokens]
|
|
end
|
|
|
|
A -->|username + password| C
|
|
C --> F
|
|
F -->|accessToken| A
|
|
B -->|Bearer accessToken + code| D
|
|
D --> E
|
|
D -->|passCode| B
|
|
```
|
|
|
|
---
|
|
|
|
## Notas técnicas
|
|
|
|
- **Token activo:** solo se envía `accessToken` en el header `Authorization`.
|
|
- **Sesión:** si "Recordarme" está activo, se guardan `accessToken`, `refreshToken`, `idToken`, `username` y `roles` en `SharedPreferences`.
|
|
- **Escáner:** modo `DetectionSpeed.normal`, solo formato `QR`, con `autoZoom` en Android.
|
|
- **Resultado válido:** la tarjeta verde muestra únicamente **nombre** y **código** del pase.
|