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:
@@ -0,0 +1,480 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user