From 50fe9a9d36eddfb876821d6f1ddc4a545e6bd915 Mon Sep 17 00:00:00 2001 From: Alejandro Lara Date: Tue, 9 Jun 2026 17:03:06 -0600 Subject: [PATCH] Initial commit: QR Scanner Flutter app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 45 ++ .metadata | 33 + README.md | 480 +++++++++++++ analysis_options.yaml | 28 + android/.gitignore | 14 + android/app/build.gradle.kts | 45 ++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 50 ++ .../com/example/qrscanner/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + android/app/src/main/res/values/styles.xml | 18 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle.kts | 24 + android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + android/settings.gradle.kts | 26 + assets/sounds/scan.wav | Bin 0 -> 5336 bytes assets/sounds/success.wav | Bin 0 -> 26504 bytes assets/sounds/warning.wav | Bin 0 -> 35324 bytes ios/.gitignore | 34 + ios/Flutter/AppFrameworkInfo.plist | 24 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Runner.xcodeproj/project.pbxproj | 644 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 ++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 + ios/Runner/Base.lproj/Main.storyboard | 26 + ios/Runner/Info.plist | 72 ++ ios/Runner/Runner-Bridging-Header.h | 1 + ios/Runner/SceneDelegate.swift | 6 + ios/RunnerTests/RunnerTests.swift | 12 + lib/config/api_config.dart | 16 + lib/main.dart | 21 + lib/models/auth_session.dart | 64 ++ lib/models/login_response.dart | 64 ++ lib/models/pass_code.dart | 31 + lib/models/scan_response.dart | 23 + lib/screens/home_screen.dart | 311 +++++++++ lib/screens/login_screen.dart | 440 ++++++++++++ lib/screens/scanner_screen.dart | 590 ++++++++++++++++ lib/services/auth_service.dart | 104 +++ lib/services/feedback_service.dart | 72 ++ lib/services/pass_code_service.dart | 27 + lib/theme/app_theme.dart | 59 ++ pubspec.lock | 634 +++++++++++++++++ pubspec.yaml | 92 +++ test/login_response_test.dart | 28 + test/widget_test.dart | 13 + 83 files changed, 4613 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle.kts create mode 100644 assets/sounds/scan.wav create mode 100644 assets/sounds/success.wav create mode 100644 assets/sounds/warning.wav create mode 100644 ios/.gitignore create mode 100644 ios/Flutter/AppFrameworkInfo.plist create mode 100644 ios/Flutter/Debug.xcconfig create mode 100644 ios/Flutter/Release.xcconfig create mode 100644 ios/Runner.xcodeproj/project.pbxproj create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 ios/Runner/AppDelegate.swift create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 ios/Runner/Base.lproj/Main.storyboard create mode 100644 ios/Runner/Info.plist create mode 100644 ios/Runner/Runner-Bridging-Header.h create mode 100644 ios/Runner/SceneDelegate.swift create mode 100644 ios/RunnerTests/RunnerTests.swift create mode 100644 lib/config/api_config.dart create mode 100644 lib/main.dart create mode 100644 lib/models/auth_session.dart create mode 100644 lib/models/login_response.dart create mode 100644 lib/models/pass_code.dart create mode 100644 lib/models/scan_response.dart create mode 100644 lib/screens/home_screen.dart create mode 100644 lib/screens/login_screen.dart create mode 100644 lib/screens/scanner_screen.dart create mode 100644 lib/services/auth_service.dart create mode 100644 lib/services/feedback_service.dart create mode 100644 lib/services/pass_code_service.dart create mode 100644 lib/theme/app_theme.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/login_response_test.dart create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..5d301a1 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "924134a44c189315be2148659913dda1671cbe99" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: android + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + - platform: ios + create_revision: 924134a44c189315be2148659913dda1671cbe99 + base_revision: 924134a44c189315be2148659913dda1671cbe99 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..335436c --- /dev/null +++ b/README.md @@ -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 +``` + +**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. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..de7f06f --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.qrscanner" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.qrscanner" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2fbd078 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt b/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt new file mode 100644 index 0000000..ca0660c --- /dev/null +++ b/android/app/src/main/kotlin/com/example/qrscanner/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.qrscanner + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/assets/sounds/scan.wav b/assets/sounds/scan.wav new file mode 100644 index 0000000000000000000000000000000000000000..298f9899a645cd35ea05635ccab498eeaa54c240 GIT binary patch literal 5336 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 literal 0 HcmV?d00001 diff --git a/assets/sounds/warning.wav b/assets/sounds/warning.wav new file mode 100644 index 0000000000000000000000000000000000000000..c4c35506325d70be251bc2c1338a01aa8781d1bc GIT binary patch literal 35324 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 + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d04de4b --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.qrscanner; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..1b77c6e --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,72 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Qrscanner + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + qrscanner + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSCameraUsageDescription + La app necesita la cámara para escanear códigos QR de pase. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/config/api_config.dart b/lib/config/api_config.dart new file mode 100644 index 0000000..ddb0b71 --- /dev/null +++ b/lib/config/api_config.dart @@ -0,0 +1,16 @@ +/// URL base del API de base-admin-web. +/// +/// Configúrala al ejecutar o compilar si necesitas otra IP: +/// `flutter run --dart-define=API_BASE_URL=http://otra-ip:8080` +class ApiConfig { + ApiConfig._(); + + static const String baseUrl = String.fromEnvironment( + 'API_BASE_URL', + defaultValue: 'http://10.99.51.6:8080', + ); + + static String get loginUrl => '$baseUrl/api/auth/login'; + + static String get scanUrl => '$baseUrl/api/pass-codes/scan'; +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..3261d47 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:qrscanner/screens/login_screen.dart'; +import 'package:qrscanner/theme/app_theme.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'QR Scanner', + debugShowCheckedModeBanner: false, + theme: AppTheme.darkTheme, + home: const LoginScreen(), + ); + } +} \ No newline at end of file diff --git a/lib/models/auth_session.dart b/lib/models/auth_session.dart new file mode 100644 index 0000000..0d755ac --- /dev/null +++ b/lib/models/auth_session.dart @@ -0,0 +1,64 @@ +import 'package:qrscanner/models/login_response.dart'; + +class AuthSession { + const AuthSession({ + required this.username, + required this.accessToken, + required this.tokenType, + required this.expiresIn, + required this.roles, + this.refreshToken, + this.idToken, + }); + + factory AuthSession.fromLoginResponse(LoginResponse response) { + return AuthSession( + username: response.username ?? '', + accessToken: response.accessToken ?? '', + refreshToken: response.refreshToken, + idToken: response.idToken, + tokenType: response.tokenType ?? 'Bearer', + expiresIn: response.expiresIn ?? 0, + roles: response.roles, + ); + } + + factory AuthSession.fromJson(Map json) { + return AuthSession( + username: json['username'] as String? ?? '', + accessToken: json['accessToken'] as String? ?? '', + refreshToken: json['refreshToken'] as String?, + idToken: json['idToken'] as String?, + tokenType: json['tokenType'] as String? ?? 'Bearer', + expiresIn: (json['expiresIn'] as num?)?.toInt() ?? 0, + roles: (json['roles'] as List?) + ?.map((role) => role.toString()) + .toList() ?? + const [], + ); + } + + final String username; + final String accessToken; + final String? refreshToken; + final String? idToken; + final String tokenType; + final int expiresIn; + final List roles; + + /// Token usado en las peticiones API (`Authorization: Bearer ...`). + String get authorizationHeader => '$tokenType $accessToken'; + + bool get isScanner => + roles.contains('ROLE_SCANNER') || roles.contains('scanners'); + + Map toJson() => { + 'username': username, + 'accessToken': accessToken, + 'refreshToken': refreshToken, + 'idToken': idToken, + 'tokenType': tokenType, + 'expiresIn': expiresIn, + 'roles': roles, + }; +} \ No newline at end of file diff --git a/lib/models/login_response.dart b/lib/models/login_response.dart new file mode 100644 index 0000000..f8d680e --- /dev/null +++ b/lib/models/login_response.dart @@ -0,0 +1,64 @@ +class LoginResponse { + const LoginResponse({ + required this.success, + this.accessToken, + this.refreshToken, + this.idToken, + this.tokenType, + this.expiresIn, + this.username, + this.roles = const [], + this.message, + }); + + factory LoginResponse.fromJson(Map json) { + return LoginResponse( + success: json['success'] == true, + accessToken: json['accessToken'] as String?, + refreshToken: json['refreshToken'] as String?, + idToken: json['idToken'] as String?, + tokenType: json['tokenType'] as String?, + expiresIn: (json['expiresIn'] as num?)?.toInt(), + username: json['username'] as String?, + roles: (json['roles'] as List?) + ?.map((role) => role.toString()) + .toList() ?? + const [], + message: json['message'] as String?, + ); + } + + final bool success; + final String? accessToken; + final String? refreshToken; + final String? idToken; + final String? tokenType; + final int? expiresIn; + final String? username; + final List roles; + final String? message; + + LoginResponse copyWith({ + bool? success, + String? accessToken, + String? refreshToken, + String? idToken, + String? tokenType, + int? expiresIn, + String? username, + List? roles, + String? message, + }) { + return LoginResponse( + success: success ?? this.success, + accessToken: accessToken ?? this.accessToken, + refreshToken: refreshToken ?? this.refreshToken, + idToken: idToken ?? this.idToken, + tokenType: tokenType ?? this.tokenType, + expiresIn: expiresIn ?? this.expiresIn, + username: username ?? this.username, + roles: roles ?? this.roles, + message: message ?? this.message, + ); + } +} \ No newline at end of file diff --git a/lib/models/pass_code.dart b/lib/models/pass_code.dart new file mode 100644 index 0000000..3a1f87b --- /dev/null +++ b/lib/models/pass_code.dart @@ -0,0 +1,31 @@ +class PassCode { + const PassCode({ + required this.id, + required this.name, + required this.code, + required this.status, + required this.scanned, + required this.createdAt, + this.scannedAt, + }); + + factory PassCode.fromJson(Map json) { + return PassCode( + id: (json['id'] as num).toInt(), + name: json['name'] as String? ?? '', + code: json['code'] as String? ?? '', + status: json['status'] as String? ?? '', + scanned: json['scanned'] == true, + createdAt: json['createdAt'] as String? ?? '', + scannedAt: json['scannedAt'] as String?, + ); + } + + final int id; + final String name; + final String code; + final String status; + final bool scanned; + final String createdAt; + final String? scannedAt; +} \ No newline at end of file diff --git a/lib/models/scan_response.dart b/lib/models/scan_response.dart new file mode 100644 index 0000000..392e34b --- /dev/null +++ b/lib/models/scan_response.dart @@ -0,0 +1,23 @@ +import 'package:qrscanner/models/pass_code.dart'; + +class ScanResponse { + const ScanResponse({ + required this.success, + this.message, + this.passCode, + }); + + factory ScanResponse.fromJson(Map json) { + return ScanResponse( + success: json['success'] == true, + message: json['message'] as String?, + passCode: json['passCode'] is Map + ? PassCode.fromJson(json['passCode'] as Map) + : null, + ); + } + + final bool success; + final String? message; + final PassCode? passCode; +} \ No newline at end of file diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart new file mode 100644 index 0000000..6555771 --- /dev/null +++ b/lib/screens/home_screen.dart @@ -0,0 +1,311 @@ +import 'package:flutter/material.dart'; +import 'package:qrscanner/models/auth_session.dart'; +import 'package:qrscanner/screens/login_screen.dart'; +import 'package:qrscanner/screens/scanner_screen.dart'; +import 'package:qrscanner/services/auth_service.dart'; +import 'package:qrscanner/theme/app_theme.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({ + super.key, + required this.session, + }); + + final AuthSession session; + + Future _logout(BuildContext context) async { + await AuthService().clearSession(); + + if (!context.mounted) return; + + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + const LoginScreen(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + transitionDuration: const Duration(milliseconds: 400), + ), + ); + } + + void _openScanner(BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + ScannerScreen(session: session), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.06), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + )), + child: child, + ), + ); + }, + transitionDuration: const Duration(milliseconds: 380), + ), + ); + } + + @override + Widget build(BuildContext context) { + final expiresHours = (session.expiresIn / 3600).round(); + + return Scaffold( + body: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.backgroundDark, + Color(0xFF0D1B2A), + Color(0xFF12182B), + ], + ), + ), + ), + Positioned( + top: -60, + right: -40, + child: Container( + width: 220, + height: 220, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + AppTheme.primary.withValues(alpha: 0.12), + Colors.transparent, + ], + ), + ), + ), + ), + SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + children: [ + const Spacer(), + IconButton( + icon: const Icon(Icons.logout_rounded), + tooltip: 'Cerrar sesión', + onPressed: () => _logout(context), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 110, + height: 110, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppTheme.primary, AppTheme.primaryDark], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primary.withValues(alpha: 0.35), + blurRadius: 28, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.qr_code_scanner_rounded, + size: 52, + color: Colors.white, + ), + ), + const SizedBox(height: 28), + Text( + 'Hola, ${session.username}', + style: + Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Text( + 'Listo para validar pases', + style: + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.white.withValues(alpha: 0.55), + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withValues(alpha: 0.08), + ), + ), + child: Text( + 'Sesión activa · $expiresHours h', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.45), + fontSize: 13, + ), + ), + ), + const SizedBox(height: 48), + _ScanButton(onPressed: () => _openScanner(context)), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ScanButton extends StatefulWidget { + const _ScanButton({required this.onPressed}); + + final VoidCallback onPressed; + + @override + State<_ScanButton> createState() => _ScanButtonState(); +} + +class _ScanButtonState extends State<_ScanButton> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _glow; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1800), + )..repeat(reverse: true); + _glow = Tween(begin: 0.18, end: 0.38).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _glow, + builder: (context, child) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.primary.withValues(alpha: _glow.value), + blurRadius: 28, + offset: const Offset(0, 10), + ), + ], + ), + child: child, + ); + }, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onPressed, + borderRadius: BorderRadius.circular(20), + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppTheme.primary, AppTheme.primaryDark], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.qr_code_scanner_rounded, + color: Colors.white, + size: 26, + ), + ), + const SizedBox(width: 16), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Abrir escáner', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'Validar código de pase', + style: TextStyle( + color: Color(0xCCFFFFFF), + fontSize: 13, + ), + ), + ], + ), + const Spacer(), + const Icon( + Icons.arrow_forward_rounded, + color: Colors.white, + ), + ], + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart new file mode 100644 index 0000000..57353b1 --- /dev/null +++ b/lib/screens/login_screen.dart @@ -0,0 +1,440 @@ +import 'package:flutter/material.dart'; +import 'package:qrscanner/config/api_config.dart'; +import 'package:qrscanner/models/auth_session.dart'; +import 'package:qrscanner/screens/home_screen.dart'; +import 'package:qrscanner/services/auth_service.dart'; +import 'package:qrscanner/theme/app_theme.dart'; + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State + with SingleTickerProviderStateMixin { + final _formKey = GlobalKey(); + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + final _authService = AuthService(); + + late final AnimationController _animationController; + late final Animation _fadeAnimation; + late final Animation _slideAnimation; + + bool _obscurePassword = true; + bool _isLoading = false; + bool _rememberMe = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + ); + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + ); + _slideAnimation = Tween( + begin: const Offset(0, 0.08), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )); + _animationController.forward(); + _checkStoredSession(); + } + + Future _checkStoredSession() async { + final session = await _authService.loadStoredSession(); + if (!mounted || session == null) return; + + _goToHome(session); + } + + @override + void dispose() { + _animationController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _handleLogin() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final response = await _authService.login( + username: _usernameController.text.trim(), + password: _passwordController.text, + rememberMe: _rememberMe, + ); + + if (!mounted) return; + + if (response.success && response.accessToken != null) { + _goToHome( + AuthSession.fromLoginResponse( + response.copyWith( + username: response.username ?? _usernameController.text.trim(), + ), + ), + ); + return; + } + + setState(() { + _errorMessage = response.message ?? 'Credenciales incorrectas'; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _errorMessage = + 'No se pudo conectar al servidor. Revisa API_BASE_URL (${ApiConfig.baseUrl})'; + }); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + void _goToHome(AuthSession session) { + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + HomeScreen(session: session), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + transitionDuration: const Duration(milliseconds: 400), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + _buildBackground(), + SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 28), + child: Column( + children: [ + _buildHeader(), + const SizedBox(height: 40), + _buildLoginCard(), + const SizedBox(height: 24), + _buildFooter(), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildBackground() { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.backgroundDark, + Color(0xFF0D1B2A), + Color(0xFF1A1A3E), + ], + ), + ), + child: Stack( + children: [ + Positioned( + top: -80, + right: -60, + child: Container( + width: 260, + height: 260, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + AppTheme.primary.withValues(alpha: 0.15), + Colors.transparent, + ], + ), + ), + ), + ), + Positioned( + bottom: -100, + left: -80, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + AppTheme.accent.withValues(alpha: 0.12), + Colors.transparent, + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Column( + children: [ + Container( + width: 88, + height: 88, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(22), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppTheme.primary, AppTheme.primaryDark], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primary.withValues(alpha: 0.35), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: const Icon( + Icons.qr_code_scanner_rounded, + size: 44, + color: Colors.white, + ), + ), + const SizedBox(height: 20), + Text( + 'QR Scanner', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: -0.5, + ), + ), + const SizedBox(height: 8), + Text( + 'Inicia sesión para continuar', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white.withValues(alpha: 0.55), + ), + ), + ], + ); + } + + Widget _buildLoginCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(28), + decoration: BoxDecoration( + color: AppTheme.surfaceDark.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 32, + offset: const Offset(0, 16), + ), + ], + ), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (_errorMessage != null) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Text( + _errorMessage!, + style: const TextStyle(color: Color(0xFFFF8A80), fontSize: 13), + ), + ), + const SizedBox(height: 16), + ], + TextFormField( + controller: _usernameController, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, + autocorrect: false, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Usuario', + prefixIcon: Icon( + Icons.person_outline_rounded, + color: Colors.white.withValues(alpha: 0.5), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Ingresa tu usuario'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + textInputAction: TextInputAction.done, + onFieldSubmitted: (_) => _handleLogin(), + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Contraseña', + prefixIcon: Icon( + Icons.lock_outline_rounded, + color: Colors.white.withValues(alpha: 0.5), + ), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: Colors.white.withValues(alpha: 0.5), + ), + onPressed: () { + setState(() => _obscurePassword = !_obscurePassword); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ingresa tu contraseña'; + } + return null; + }, + ), + const SizedBox(height: 12), + Row( + children: [ + SizedBox( + height: 36, + child: Checkbox( + value: _rememberMe, + activeColor: AppTheme.primary, + side: BorderSide( + color: Colors.white.withValues(alpha: 0.3), + ), + onChanged: (value) { + setState(() => _rememberMe = value ?? false); + }, + ), + ), + Text( + 'Recordarme', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 14, + ), + ), + ], + ), + const SizedBox(height: 8), + _buildLoginButton(), + const SizedBox(height: 16), + Text( + 'API: ${ApiConfig.baseUrl}', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.35), + fontSize: 11, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLoginButton() { + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: const LinearGradient( + colors: [AppTheme.primary, AppTheme.primaryDark], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.primary.withValues(alpha: 0.3), + blurRadius: 16, + offset: const Offset(0, 6), + ), + ], + ), + child: ElevatedButton( + onPressed: _isLoading ? null : _handleLogin, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + disabledBackgroundColor: Colors.transparent, + ), + child: _isLoading + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: Colors.white, + ), + ) + : const Text( + 'Iniciar sesión', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ); + } + + Widget _buildFooter() { + return Text( + 'POST /api/auth/login', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.35), + fontSize: 12, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart new file mode 100644 index 0000000..e7aa8db --- /dev/null +++ b/lib/screens/scanner_screen.dart @@ -0,0 +1,590 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:qrscanner/models/auth_session.dart'; +import 'package:qrscanner/models/pass_code.dart'; +import 'package:qrscanner/models/scan_response.dart'; +import 'package:qrscanner/services/feedback_service.dart'; +import 'package:qrscanner/services/pass_code_service.dart'; +import 'package:qrscanner/theme/app_theme.dart'; + +enum _ScanPhase { scanning, processing, success, warning } + +const double _scanWindowSize = 260; +const double _scanWindowRadius = 28; +const Alignment _scanWindowAlignment = Alignment(0, -0.12); + +class ScannerScreen extends StatefulWidget { + const ScannerScreen({ + super.key, + required this.session, + }); + + final AuthSession session; + + @override + State createState() => _ScannerScreenState(); +} + +class _ScannerScreenState extends State + with TickerProviderStateMixin { + final _scannerController = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + detectionTimeoutMs: 500, + facing: CameraFacing.back, + formats: const [BarcodeFormat.qrCode], + autoZoom: true, + ); + final _passCodeService = PassCodeService(); + final _feedbackService = FeedbackService(); + + _ScanPhase _phase = _ScanPhase.scanning; + bool _isHandlingDetection = false; + String? _lastScannedCode; + String? _message; + PassCode? _passCode; + + late final AnimationController _scanLineController; + late final AnimationController _resultController; + late final Animation _scanLineAnimation; + late final Animation _scaleAnimation; + late final Animation _arrowSlide; + + @override + void initState() { + super.initState(); + _scanLineController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2400), + )..repeat(); + _scanLineAnimation = CurvedAnimation( + parent: _scanLineController, + curve: Curves.easeInOut, + ); + + _resultController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 700), + ); + _scaleAnimation = CurvedAnimation( + parent: _resultController, + curve: Curves.elasticOut, + ); + _arrowSlide = Tween( + begin: const Offset(0, -0.35), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _resultController, + curve: Curves.easeOutBack, + )); + } + + Rect _scanWindowRect(Size size) { + final center = _scanWindowAlignment.alongSize(size); + return Rect.fromCenter( + center: center, + width: _scanWindowSize, + height: _scanWindowSize, + ); + } + + @override + void dispose() { + _scanLineController.dispose(); + _resultController.dispose(); + _scannerController.dispose(); + _feedbackService.dispose(); + super.dispose(); + } + + Future _onDetect(BarcodeCapture capture) async { + if (_phase != _ScanPhase.scanning || _isHandlingDetection) return; + + final rawValue = capture.barcodes.firstOrNull?.rawValue?.trim(); + if (rawValue == null || rawValue.isEmpty) return; + + final code = _extractCode(rawValue); + if (code.isEmpty || code == _lastScannedCode) return; + + _isHandlingDetection = true; + setState(() { + _phase = _ScanPhase.processing; + _lastScannedCode = code; + _message = null; + _passCode = null; + }); + + _scanLineController.stop(); + await _scannerController.stop(); + await _feedbackService.onCodeDetected(); + + try { + final response = await _passCodeService.scan( + session: widget.session, + code: code, + ); + + if (!mounted) return; + await _showResult(response); + } catch (_) { + if (!mounted) return; + await _showResult( + const ScanResponse( + success: false, + message: 'No se pudo validar el código. Revisa la conexión.', + ), + ); + } finally { + _isHandlingDetection = false; + } + } + + String _extractCode(String rawValue) { + final uri = Uri.tryParse(rawValue); + if (uri != null) { + final queryCode = uri.queryParameters['code']; + if (queryCode != null && queryCode.isNotEmpty) return queryCode; + + final segments = uri.pathSegments.where((s) => s.isNotEmpty).toList(); + if (segments.isNotEmpty) return segments.last; + } + return rawValue; + } + + Future _showResult(ScanResponse response) async { + _resultController.reset(); + + setState(() { + _message = response.message; + _passCode = response.passCode; + _phase = response.success ? _ScanPhase.success : _ScanPhase.warning; + }); + + if (response.success) { + await _feedbackService.onSuccess(); + } else { + await _feedbackService.onWarning(); + } + + await _resultController.forward(); + } + + Future _scanAgain() async { + _resultController.reset(); + _isHandlingDetection = false; + setState(() { + _phase = _ScanPhase.scanning; + _lastScannedCode = null; + _message = null; + _passCode = null; + }); + _scanLineController.repeat(); + await _scannerController.start(); + } + + @override + Widget build(BuildContext context) { + final isSuccess = _phase == _ScanPhase.success; + final isWarning = _phase == _ScanPhase.warning; + final overlayColor = isSuccess + ? AppTheme.success.withValues(alpha: 0.82) + : isWarning + ? AppTheme.warning.withValues(alpha: 0.82) + : Colors.transparent; + + return Scaffold( + backgroundColor: Colors.black, + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_rounded), + onPressed: () => Navigator.of(context).pop(), + ), + title: const Text('Escanear pase'), + centerTitle: true, + ), + body: Stack( + fit: StackFit.expand, + children: [ + if (_phase == _ScanPhase.scanning || _phase == _ScanPhase.processing) + LayoutBuilder( + builder: (context, constraints) { + final scanRect = _scanWindowRect(constraints.biggest); + return MobileScanner( + controller: _scannerController, + onDetect: _onDetect, + scanWindow: scanRect, + overlayBuilder: (context, constraints) { + if (_phase != _ScanPhase.scanning) { + return const SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: _scanLineAnimation, + builder: (context, child) { + return CustomPaint( + size: constraints.biggest, + painter: _ScannerOverlayPainter( + scanRect: scanRect, + scanLineProgress: _scanLineAnimation.value, + ), + ); + }, + ); + }, + ); + }, + ), + if (_phase != _ScanPhase.scanning && _phase != _ScanPhase.processing) + Container(color: AppTheme.backgroundDark), + AnimatedContainer( + duration: const Duration(milliseconds: 450), + curve: Curves.easeOutCubic, + color: overlayColor, + ), + if (_phase == _ScanPhase.processing) + SafeArea( + child: Center(child: _buildProcessingCard()), + ), + if (_phase == _ScanPhase.success || _phase == _ScanPhase.warning) + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16), + child: _buildResultCard(), + ), + ), + ), + if (_phase == _ScanPhase.scanning) + SafeArea( + child: Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 28), + child: _buildHint(), + ), + ), + ), + ], + ), + ); + } + + Widget _buildProcessingCard() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + decoration: BoxDecoration( + color: AppTheme.surfaceDark.withValues(alpha: 0.92), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withValues(alpha: 0.08)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 42, + height: 42, + child: CircularProgressIndicator( + strokeWidth: 3, + color: AppTheme.primary, + ), + ), + const SizedBox(height: 18), + Text( + 'Validando código...', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (_lastScannedCode != null) ...[ + const SizedBox(height: 8), + Text( + _lastScannedCode!, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.45), + letterSpacing: 1.2, + ), + ), + ], + ], + ), + ); + } + + Widget _buildResultCard() { + final isSuccess = _phase == _ScanPhase.success; + final accent = isSuccess ? AppTheme.success : AppTheme.warning; + final accentDark = isSuccess ? AppTheme.successDark : AppTheme.warningDark; + + return FadeTransition( + opacity: _scaleAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.fromLTRB(24, 32, 24, 24), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + accent.withValues(alpha: 0.95), + accentDark.withValues(alpha: 0.95), + ], + ), + borderRadius: BorderRadius.circular(28), + boxShadow: [ + BoxShadow( + color: accent.withValues(alpha: 0.45), + blurRadius: 32, + offset: const Offset(0, 12), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SlideTransition( + position: _arrowSlide, + child: Container( + width: 88, + height: 88, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.18), + shape: BoxShape.circle, + ), + child: Icon( + isSuccess + ? Icons.arrow_downward_rounded + : Icons.warning_amber_rounded, + size: isSuccess ? 52 : 46, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 20), + Text( + isSuccess ? '¡Pase válido!' : 'No se pudo validar', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + if (!isSuccess && (_message?.isNotEmpty ?? false)) ...[ + const SizedBox(height: 10), + Text( + _message!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + height: 1.4, + color: Colors.white.withValues(alpha: 0.92), + ), + ), + ], + if (isSuccess && _passCode != null) ...[ + const SizedBox(height: 20), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + _detailRow('Nombre', _passCode!.name), + const SizedBox(height: 8), + _detailRow('Código', _passCode!.code), + ], + ), + ), + ], + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: _scanAgain, + icon: const Icon(Icons.qr_code_scanner_rounded), + label: const Text('Escanear otro'), + style: FilledButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: accentDark, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _detailRow(String label, String value) { + return Row( + children: [ + Text( + '$label: ', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 14, + ), + ), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ); + } + + Widget _buildHint() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + 'Apunta la cámara al código QR del pase', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.65), + fontSize: 14, + ), + ), + ); + } +} + +class _ScannerOverlayPainter extends CustomPainter { + _ScannerOverlayPainter({ + required this.scanRect, + required this.scanLineProgress, + }); + + final Rect scanRect; + final double scanLineProgress; + + static const double _cornerLength = 36; + static const double _cornerStroke = 4; + + @override + void paint(Canvas canvas, Size size) { + final cutout = RRect.fromRectAndRadius( + scanRect, + const Radius.circular(_scanWindowRadius), + ); + + final background = Path()..addRect(Offset.zero & size); + final hole = Path()..addRRect(cutout); + final overlay = Path.combine(PathOperation.difference, background, hole); + + canvas.drawPath( + overlay, + Paint()..color = Colors.black.withValues(alpha: 0.55), + ); + + final borderPaint = Paint() + ..color = AppTheme.primary.withValues(alpha: 0.35) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + canvas.drawRRect(cutout, borderPaint); + + final cornerPaint = Paint() + ..color = AppTheme.primary + ..style = PaintingStyle.stroke + ..strokeWidth = _cornerStroke + ..strokeCap = StrokeCap.round; + + _drawCorners(canvas, scanRect, cornerPaint); + + final lineY = scanRect.top + + _scanWindowRadius + + (scanRect.height - _scanWindowRadius * 2) * scanLineProgress; + final linePaint = Paint() + ..shader = LinearGradient( + colors: [ + AppTheme.primary.withValues(alpha: 0), + AppTheme.primary.withValues(alpha: 0.85), + AppTheme.primary.withValues(alpha: 0), + ], + stops: const [0, 0.5, 1], + ).createShader( + Rect.fromLTWH(scanRect.left + 16, lineY - 1, scanRect.width - 32, 2), + ) + ..strokeWidth = 2 + ..strokeCap = StrokeCap.round; + + canvas.drawLine( + Offset(scanRect.left + 16, lineY), + Offset(scanRect.right - 16, lineY), + linePaint, + ); + } + + void _drawCorners(Canvas canvas, Rect rect, Paint paint) { + const r = _scanWindowRadius; + const len = _cornerLength; + + canvas.drawLine( + Offset(rect.left + r, rect.top), + Offset(rect.left + r + len, rect.top), + paint, + ); + canvas.drawLine( + Offset(rect.left, rect.top + r), + Offset(rect.left, rect.top + r + len), + paint, + ); + + canvas.drawLine( + Offset(rect.right - r - len, rect.top), + Offset(rect.right - r, rect.top), + paint, + ); + canvas.drawLine( + Offset(rect.right, rect.top + r), + Offset(rect.right, rect.top + r + len), + paint, + ); + + canvas.drawLine( + Offset(rect.left + r, rect.bottom), + Offset(rect.left + r + len, rect.bottom), + paint, + ); + canvas.drawLine( + Offset(rect.left, rect.bottom - r - len), + Offset(rect.left, rect.bottom - r), + paint, + ); + + canvas.drawLine( + Offset(rect.right - r - len, rect.bottom), + Offset(rect.right - r, rect.bottom), + paint, + ); + canvas.drawLine( + Offset(rect.right, rect.bottom - r - len), + Offset(rect.right, rect.bottom - r), + paint, + ); + } + + @override + bool shouldRepaint(covariant _ScannerOverlayPainter oldDelegate) { + return oldDelegate.scanRect != scanRect || + oldDelegate.scanLineProgress != scanLineProgress; + } +} \ No newline at end of file diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..f7a1591 --- /dev/null +++ b/lib/services/auth_service.dart @@ -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 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 body = + jsonDecode(response.body) as Map; + 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 _saveSession(AuthSession session) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_sessionKey, jsonEncode(session.toJson())); + } + + Future _clearStoredSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_sessionKey); + } + + Future clearSession() async { + _currentSession = null; + await _clearStoredSession(); + } + + Future 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, + ); + _currentSession = session; + return session; + } + + Future hasStoredSession() async { + final session = await loadStoredSession(); + return session != null && session.accessToken.isNotEmpty; + } +} \ No newline at end of file diff --git a/lib/services/feedback_service.dart b/lib/services/feedback_service.dart new file mode 100644 index 0000000..e53aa55 --- /dev/null +++ b/lib/services/feedback_service.dart @@ -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 onCodeDetected() async { + await Future.wait([ + _play('sounds/scan.wav'), + _vibratePattern(const [0, 40]), + ]); + } + + Future onSuccess() async { + await Future.wait([ + _play('sounds/success.wav'), + _vibratePattern(const [0, 90, 70, 140]), + ]); + await HapticFeedback.heavyImpact(); + } + + Future onWarning() async { + await Future.wait([ + _play('sounds/warning.wav'), + _vibratePattern(const [0, 120, 80, 120, 80, 120]), + ]); + await HapticFeedback.mediumImpact(); + } + + Future _play(String asset) async { + try { + await _player.stop(); + await _player.play(AssetSource(asset)); + } catch (_) { + // Ignore audio errors on devices without speaker routing. + } + } + + Future _vibratePattern(List 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 dispose() async { + await _player.dispose(); + } +} \ No newline at end of file diff --git a/lib/services/pass_code_service.dart b/lib/services/pass_code_service.dart new file mode 100644 index 0000000..686b83b --- /dev/null +++ b/lib/services/pass_code_service.dart @@ -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 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 body = + jsonDecode(response.body) as Map; + + return ScanResponse.fromJson(body); + } +} \ No newline at end of file diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..410974c --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + static const Color primary = Color(0xFF00D4AA); + static const Color primaryDark = Color(0xFF00A88A); + static const Color backgroundDark = Color(0xFF0A0E1A); + static const Color surfaceDark = Color(0xFF141B2D); + static const Color accent = Color(0xFF6C63FF); + static const Color success = Color(0xFF22C55E); + static const Color successDark = Color(0xFF16A34A); + static const Color warning = Color(0xFFFBBF24); + static const Color warningDark = Color(0xFFF59E0B); + + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: ColorScheme.dark( + primary: primary, + secondary: accent, + surface: surfaceDark, + onPrimary: Colors.white, + onSurface: Colors.white, + ), + scaffoldBackgroundColor: backgroundDark, + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white.withValues(alpha: 0.06), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.08)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: primary, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: const BorderSide(color: Color(0xFFFF6B6B)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.4)), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..b231bc5 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,634 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d + url: "https://pub.dev" + source: hosted + version: "6.7.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a" + url: "https://pub.dev" + source: hosted + version: "4.3.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" + url: "https://pub.dev" + source: hosted + version: "13.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce + url: "https://pub.dev" + source: hosted + version: "7.2.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0 + url: "https://pub.dev" + source: hosted + version: "2.4.25" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vibration: + dependency: "direct main" + description: + name: vibration + sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87" + url: "https://pub.dev" + source: hosted + version: "3.1.8" + vibration_platform_interface: + dependency: transitive + description: + name: vibration_platform_interface + sha256: "258c273268f8aa40c88d29741137c536874a738779b92ddb8aa32ed093721ec5" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.12.1 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..e0bbd60 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,92 @@ +name: qrscanner +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.12.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + audioplayers: ^6.5.0 + http: ^1.2.2 + mobile_scanner: ^7.0.1 + shared_preferences: ^2.3.3 + vibration: ^3.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - assets/sounds/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/login_response_test.dart b/test/login_response_test.dart new file mode 100644 index 0000000..83bce61 --- /dev/null +++ b/test/login_response_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:qrscanner/models/auth_session.dart'; +import 'package:qrscanner/models/login_response.dart'; + +void main() { + test('LoginResponse parsea la respuesta Cognito y usa accessToken', () { + const json = { + 'success': true, + 'accessToken': 'access-token-value', + 'refreshToken': 'refresh-token-value', + 'idToken': 'id-token-value', + 'tokenType': 'Bearer', + 'expiresIn': 3600, + 'username': 'scanner', + 'roles': ['ROLE_SCANNER'], + }; + + final response = LoginResponse.fromJson(json); + final session = AuthSession.fromLoginResponse(response); + + expect(response.success, isTrue); + expect(response.accessToken, 'access-token-value'); + expect(response.refreshToken, 'refresh-token-value'); + expect(response.idToken, 'id-token-value'); + expect(session.authorizationHeader, 'Bearer access-token-value'); + expect(session.isScanner, isTrue); + }); +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..f125623 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:qrscanner/main.dart'; + +void main() { + testWidgets('Login screen renders correctly', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + + expect(find.text('QR Scanner'), findsOneWidget); + expect(find.text('Iniciar sesión'), findsOneWidget); + expect(find.text('Usuario'), findsOneWidget); + }); +} \ No newline at end of file