Initial commit: QR Scanner Flutter app

App móvil para validar pases QR contra base-admin-web con login Cognito,
escáner en tiempo real, feedback auditivo/háptico y README completo.
This commit is contained in:
Alejandro Lara
2026-06-09 17:03:06 -06:00
commit 50fe9a9d36
83 changed files with 4613 additions and 0 deletions
+45
View File
@@ -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
+33
View File
@@ -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'
+480
View File
@@ -0,0 +1,480 @@
# QR Scanner
Aplicación móvil Flutter para validar **pases de acceso** mediante códigos QR. Se conecta al backend **base-admin-web** para autenticar operadores con rol escáner y registrar cada lectura en el servidor.
---
## Tabla de contenidos
- [Características](#características)
- [Arquitectura](#arquitectura)
- [Flujos principales](#flujos-principales)
- [Estructura del proyecto](#estructura-del-proyecto)
- [Dependencias](#dependencias)
- [Requisitos](#requisitos)
- [Configuración e instalación](#configuración-e-instalación)
- [API del backend](#api-del-backend)
- [Permisos](#permisos)
- [Pruebas](#pruebas)
- [Compilación](#compilación)
---
## Características
- Login con usuario y contraseña contra el API de **base-admin-web** (Cognito / JWT).
- Sesión persistente opcional con **Recordarme** (`shared_preferences`).
- Escáner QR en tiempo real con ventana de enfoque y overlay visual.
- Validación del pase en el servidor con `accessToken` (Bearer).
- Retroalimentación auditiva y háptica al detectar, validar o rechazar un código.
- UI oscura con animaciones en login, home y resultados de escaneo.
---
## Arquitectura
```mermaid
flowchart TB
subgraph App["App Flutter (qrscanner)"]
UI["Screens\n(login · home · scanner)"]
Services["Services\n(auth · pass_code · feedback)"]
Models["Models\n(login_response · auth_session · pass_code)"]
Config["ApiConfig"]
end
subgraph Device["Dispositivo"]
Camera["Cámara"]
Prefs["SharedPreferences"]
Audio["Audio / Vibración"]
end
subgraph Backend["base-admin-web :8080"]
AuthAPI["POST /api/auth/login"]
ScanAPI["POST /api/pass-codes/scan"]
Cognito["AWS Cognito / JWT"]
end
UI --> Services
Services --> Models
Services --> Config
Services --> Prefs
UI --> Camera
Services --> Audio
Config --> AuthAPI
Config --> ScanAPI
AuthAPI --> Cognito
ScanAPI --> Cognito
```
### Capas de la app
```mermaid
graph LR
A[Pantallas] --> B[Servicios]
B --> C[Modelos]
B --> D[HTTP / API]
A --> E[Tema / UI]
B --> F[Almacenamiento local]
A --> G[mobile_scanner]
```
| Capa | Responsabilidad |
|------|-----------------|
| **Screens** | UI, navegación y estados visuales |
| **Services** | Lógica de negocio, HTTP y feedback |
| **Models** | Parseo JSON y sesión de autenticación |
| **Config** | URL base del API configurable en compilación |
---
## Flujos principales
### Login
```mermaid
sequenceDiagram
actor Usuario
participant Login as LoginScreen
participant Auth as AuthService
participant API as base-admin-web
participant Home as HomeScreen
Usuario->>Login: Usuario + contraseña
Login->>Auth: login()
Auth->>API: POST /api/auth/login
API-->>Auth: accessToken, refreshToken, idToken, roles
Auth->>Auth: Valida ROLE_SCANNER
alt Credenciales válidas
Auth-->>Login: LoginResponse success
Login->>Home: Navegar con AuthSession
else Error
API-->>Login: success false / sin token
Login-->>Usuario: Mensaje de error
end
```
### Escaneo y validación
```mermaid
sequenceDiagram
actor Operador
participant Home as HomeScreen
participant Scan as ScannerScreen
participant MS as mobile_scanner
participant Pass as PassCodeService
participant API as base-admin-web
participant FB as FeedbackService
Operador->>Home: Abrir escáner
Home->>Scan: AuthSession
loop Escaneo continuo
MS->>Scan: onDetect (QR)
Scan->>FB: Sonido + vibración
Scan->>Pass: scan(code)
Pass->>API: POST /api/pass-codes/scan\nAuthorization: Bearer accessToken
API-->>Pass: success + passCode
Pass-->>Scan: ScanResponse
Scan->>FB: success / warning
Scan-->>Operador: Tarjeta verde (nombre + código)\no amarilla (error)
end
```
### Estados del escáner
```mermaid
stateDiagram-v2
[*] --> scanning: Abrir cámara
scanning --> processing: QR detectado
processing --> success: API válida
processing --> warning: API rechaza / error red
success --> scanning: Escanear otro
warning --> scanning: Escanear otro
scanning --> [*]: Volver atrás
```
---
## Estructura del proyecto
```
qrscanner/
├── lib/
│ ├── main.dart # Punto de entrada
│ ├── config/
│ │ └── api_config.dart # URL base y endpoints
│ ├── models/
│ │ ├── auth_session.dart # Sesión y header Authorization
│ │ ├── login_response.dart # Respuesta de login
│ │ ├── pass_code.dart # Datos del pase
│ │ └── scan_response.dart # Respuesta de escaneo
│ ├── screens/
│ │ ├── login_screen.dart # Pantalla de inicio de sesión
│ │ ├── home_screen.dart # Pantalla principal
│ │ └── scanner_screen.dart # Cámara y validación QR
│ ├── services/
│ │ ├── auth_service.dart # Login y sesión
│ │ ├── pass_code_service.dart# Validación de pases
│ │ └── feedback_service.dart # Audio y vibración
│ └── theme/
│ └── app_theme.dart # Colores y tema oscuro
├── assets/
│ └── sounds/
│ ├── scan.wav
│ ├── success.wav
│ └── warning.wav
├── android/ # Configuración Android
├── ios/ # Configuración iOS
├── test/ # Pruebas unitarias y widget
└── pubspec.yaml # Dependencias
```
---
## Dependencias
### Directas (producción)
| Paquete | Versión en `pubspec.yaml` | Versión instalada | Uso |
|---------|---------------------------|-------------------|-----|
| [flutter](https://flutter.dev) | SDK | — | Framework UI |
| [cupertino_icons](https://pub.dev/packages/cupertino_icons) | `^1.0.8` | 1.0.9 | Iconos iOS |
| [http](https://pub.dev/packages/http) | `^1.2.2` | 1.6.0 | Peticiones REST al backend |
| [mobile_scanner](https://pub.dev/packages/mobile_scanner) | `^7.0.1` | 7.2.0 | Lectura de códigos QR con cámara |
| [shared_preferences](https://pub.dev/packages/shared_preferences) | `^2.3.3` | 2.5.5 | Persistir sesión (Recordarme) |
| [audioplayers](https://pub.dev/packages/audioplayers) | `^6.5.0` | 6.7.1 | Sonidos de feedback |
| [vibration](https://pub.dev/packages/vibration) | `^3.1.3` | 3.1.8 | Vibración al escanear |
### Desarrollo
| Paquete | Versión | Uso |
|---------|---------|-----|
| [flutter_test](https://api.flutter.dev/flutter/flutter_test/flutter_test-library.html) | SDK | Pruebas widget |
| [flutter_lints](https://pub.dev/packages/flutter_lints) | `^6.0.0` (6.0.0) | Reglas de análisis estático |
### Dependencias transitivas relevantes
Incluidas automáticamente por los paquetes anteriores:
- `path_provider` — rutas de almacenamiento (audioplayers, shared_preferences)
- `device_info_plus` — información del dispositivo (vibration)
- `camera` / plugins nativos — soporte de cámara (mobile_scanner)
Para verificar versiones actualizadas:
```bash
flutter pub outdated
```
---
## Requisitos
- **Flutter** 3.12+ (Dart SDK `^3.12.1`)
- **Backend** [base-admin-web](https://github.com) corriendo y accesible en red
- Dispositivo físico o emulador con **cámara** (recomendado dispositivo real para QR)
- Usuario con rol **`ROLE_SCANNER`** en Cognito / base-admin-web
---
## Configuración e instalación
### 1. Clonar e instalar dependencias
```bash
cd flutter-app/qrscanner
flutter pub get
```
### 2. URL del API
Por defecto la app apunta a:
```
http://10.99.51.6:8080
```
Para usar otra IP o puerto:
```bash
flutter run --dart-define=API_BASE_URL=http://192.168.1.100:8080
```
Compilación release con URL personalizada:
```bash
flutter build apk --dart-define=API_BASE_URL=http://10.99.51.6:8080
```
### 3. Ejecutar en dispositivo
```bash
# Listar dispositivos
flutter devices
# Ejecutar
flutter run
# Ejecutar con URL custom
flutter run --dart-define=API_BASE_URL=http://10.99.51.6:8080
```
### 4. Credenciales de prueba
Según el entorno de desarrollo de **base-admin-web**:
| Campo | Valor |
|-------|-------|
| Usuario | `scanner` |
| Contraseña | `Scanner123*` |
> El usuario debe pertenecer al grupo **scanners** / rol `ROLE_SCANNER`.
---
## API del backend
Base URL configurable: `ApiConfig.baseUrl`
### Login
```http
POST /api/auth/login
Content-Type: application/json
```
**Request:**
```json
{
"username": "scanner",
"password": "Scanner123*"
}
```
**Response (200):**
```json
{
"success": true,
"accessToken": "eyJ...",
"refreshToken": "dXMt...",
"idToken": "eyJ...",
"tokenType": "Bearer",
"expiresIn": 3600,
"username": "scanner",
"roles": ["ROLE_SCANNER"]
}
```
La app usa **`accessToken`** en todas las peticiones autenticadas.
### Escanear pase
```http
POST /api/pass-codes/scan
Content-Type: application/json
Authorization: Bearer <accessToken>
```
**Request:**
```json
{
"code": "ABC123XYZ"
}
```
**Response exitosa (200):**
```json
{
"success": true,
"message": "Codigo pase escaneado correctamente.",
"passCode": {
"id": 1,
"name": "Pase visitante",
"code": "ABC123XYZ",
"status": "ACTIVE",
"scanned": true,
"createdAt": "2026-06-09T10:00:00",
"scannedAt": "2026-06-09T15:30:00"
}
}
```
**Response error (400):**
```json
{
"success": false,
"message": "Descripción del error"
}
```
### Formato del QR
La app acepta:
- Texto plano con el código del pase
- URL con el código en query: `?code=ABC123`
- URL con el código como último segmento del path
---
## Permisos
### Android (`AndroidManifest.xml`)
| Permiso | Motivo |
|---------|--------|
| `INTERNET` | Comunicación con el API |
| `CAMERA` | Escaneo QR |
| `VIBRATE` | Feedback háptico |
`android:usesCleartextTraffic="true"` habilitado para HTTP en red local (desarrollo).
### iOS
Requiere descripción de uso de cámara en `Info.plist` al desplegar en producción.
---
## Pruebas
```bash
# Todas las pruebas
flutter test
# Análisis estático
flutter analyze
```
Pruebas incluidas:
- `test/widget_test.dart` — renderizado de la pantalla de login
- `test/login_response_test.dart` — parseo de respuesta Cognito y uso de `accessToken`
---
## Compilación
### Android APK
```bash
flutter build apk --release \
--dart-define=API_BASE_URL=http://10.99.51.6:8080
```
Salida: `build/app/outputs/flutter-apk/app-release.apk`
### Android App Bundle (Play Store)
```bash
flutter build appbundle --release \
--dart-define=API_BASE_URL=https://tu-servidor.com
```
### iOS
```bash
flutter build ios --release \
--dart-define=API_BASE_URL=https://tu-servidor.com
```
---
## Diagrama de integración con base-admin-web
```mermaid
flowchart LR
subgraph Mobile["📱 QR Scanner"]
A[Login]
B[Escáner]
end
subgraph Server["🖥️ base-admin-web"]
C["/api/auth/login"]
D["/api/pass-codes/scan"]
E[(Base de datos)]
end
subgraph Auth["🔐 Cognito"]
F[JWT / Tokens]
end
A -->|username + password| C
C --> F
F -->|accessToken| A
B -->|Bearer accessToken + code| D
D --> E
D -->|passCode| B
```
---
## Notas técnicas
- **Token activo:** solo se envía `accessToken` en el header `Authorization`.
- **Sesión:** si "Recordarme" está activo, se guardan `accessToken`, `refreshToken`, `idToken`, `username` y `roles` en `SharedPreferences`.
- **Escáner:** modo `DetectionSpeed.normal`, solo formato `QR`, con `autoZoom` en Android.
- **Resultado válido:** la tarjeta verde muestra únicamente **nombre** y **código** del pase.
+28
View File
@@ -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
+14
View File
@@ -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
+45
View File
@@ -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 = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+50
View File
@@ -0,0 +1,50 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:label="qrscanner"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.qrscanner
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+6
View File
@@ -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
+5
View File
@@ -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
+26
View File
@@ -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")
Binary file not shown.
Binary file not shown.
Binary file not shown.
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+644
View File
@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -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)
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Qrscanner</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>qrscanner</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>La app necesita la cámara para escanear códigos QR de pase.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -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.
}
}
+16
View File
@@ -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';
}
+21
View File
@@ -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(),
);
}
}
+64
View File
@@ -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<String, dynamic> 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<dynamic>?)
?.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<String> 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<String, dynamic> toJson() => {
'username': username,
'accessToken': accessToken,
'refreshToken': refreshToken,
'idToken': idToken,
'tokenType': tokenType,
'expiresIn': expiresIn,
'roles': roles,
};
}
+64
View File
@@ -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<String, dynamic> 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<dynamic>?)
?.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<String> roles;
final String? message;
LoginResponse copyWith({
bool? success,
String? accessToken,
String? refreshToken,
String? idToken,
String? tokenType,
int? expiresIn,
String? username,
List<String>? 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,
);
}
}
+31
View File
@@ -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<String, dynamic> 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;
}
+23
View File
@@ -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<String, dynamic> json) {
return ScanResponse(
success: json['success'] == true,
message: json['message'] as String?,
passCode: json['passCode'] is Map<String, dynamic>
? PassCode.fromJson(json['passCode'] as Map<String, dynamic>)
: null,
);
}
final bool success;
final String? message;
final PassCode? passCode;
}
+311
View File
@@ -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<void> _logout(BuildContext context) async {
await AuthService().clearSession();
if (!context.mounted) return;
Navigator.of(context).pushReplacement(
PageRouteBuilder<void>(
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<void>(
pageBuilder: (context, animation, secondaryAnimation) =>
ScannerScreen(session: session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
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<double> _glow;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1800),
)..repeat(reverse: true);
_glow = Tween<double>(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,
),
],
),
),
),
),
),
);
}
}
+440
View File
@@ -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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = AuthService();
late final AnimationController _animationController;
late final Animation<double> _fadeAnimation;
late final Animation<Offset> _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<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_animationController.forward();
_checkStoredSession();
}
Future<void> _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<void> _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<void>(
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,
),
);
}
}
+590
View File
@@ -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<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen>
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<double> _scanLineAnimation;
late final Animation<double> _scaleAnimation;
late final Animation<Offset> _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<Offset>(
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<void> _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<void> _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<void> _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;
}
}
+104
View File
@@ -0,0 +1,104 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/login_response.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
static const _sessionKey = 'auth_session';
AuthSession? _currentSession;
AuthSession? get currentSession => _currentSession;
Future<LoginResponse> login({
required String username,
required String password,
bool rememberMe = false,
}) async {
final response = await http.post(
Uri.parse(ApiConfig.loginUrl),
headers: const {'Content-Type': 'application/json'},
body: jsonEncode({
'username': username,
'password': password,
}),
);
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final loginResponse = LoginResponse.fromJson(body);
final accessToken = loginResponse.accessToken?.trim();
if (response.statusCode == 200 &&
loginResponse.success &&
accessToken != null &&
accessToken.isNotEmpty) {
final session = AuthSession.fromLoginResponse(
loginResponse.copyWith(
username: loginResponse.username ?? username,
accessToken: accessToken,
),
);
if (!session.isScanner) {
_currentSession = null;
await _clearStoredSession();
return const LoginResponse(
success: false,
message: 'El usuario no tiene permisos para escanear.',
);
}
_currentSession = session;
if (rememberMe) {
await _saveSession(session);
} else {
await _clearStoredSession();
}
return loginResponse.copyWith(accessToken: accessToken);
}
return LoginResponse(
success: false,
message: loginResponse.message ??
'No se pudo iniciar sesión (${response.statusCode})',
);
}
Future<void> _saveSession(AuthSession session) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_sessionKey, jsonEncode(session.toJson()));
}
Future<void> _clearStoredSession() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_sessionKey);
}
Future<void> clearSession() async {
_currentSession = null;
await _clearStoredSession();
}
Future<AuthSession?> loadStoredSession() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_sessionKey);
if (raw == null || raw.isEmpty) return null;
final session = AuthSession.fromJson(
jsonDecode(raw) as Map<String, dynamic>,
);
_currentSession = session;
return session;
}
Future<bool> hasStoredSession() async {
final session = await loadStoredSession();
return session != null && session.accessToken.isNotEmpty;
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
class FeedbackService {
FeedbackService() : _player = AudioPlayer() {
_player.setReleaseMode(ReleaseMode.stop);
}
final AudioPlayer _player;
Future<void> onCodeDetected() async {
await Future.wait([
_play('sounds/scan.wav'),
_vibratePattern(const [0, 40]),
]);
}
Future<void> onSuccess() async {
await Future.wait([
_play('sounds/success.wav'),
_vibratePattern(const [0, 90, 70, 140]),
]);
await HapticFeedback.heavyImpact();
}
Future<void> onWarning() async {
await Future.wait([
_play('sounds/warning.wav'),
_vibratePattern(const [0, 120, 80, 120, 80, 120]),
]);
await HapticFeedback.mediumImpact();
}
Future<void> _play(String asset) async {
try {
await _player.stop();
await _player.play(AssetSource(asset));
} catch (_) {
// Ignore audio errors on devices without speaker routing.
}
}
Future<void> _vibratePattern(List<int> pattern) async {
try {
final hasVibrator = await Vibration.hasVibrator();
if (hasVibrator != true) return;
final hasAmplitude = await Vibration.hasAmplitudeControl();
if (hasAmplitude == true) {
final intensities = pattern
.asMap()
.entries
.map((entry) => entry.key.isOdd ? 180 : 0)
.toList();
await Vibration.vibrate(
pattern: pattern,
intensities: intensities,
);
return;
}
await Vibration.vibrate(pattern: pattern);
} catch (_) {
await HapticFeedback.selectionClick();
}
}
Future<void> dispose() async {
await _player.dispose();
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/scan_response.dart';
class PassCodeService {
Future<ScanResponse> scan({
required AuthSession session,
required String code,
}) async {
final response = await http.post(
Uri.parse(ApiConfig.scanUrl),
headers: {
'Content-Type': 'application/json',
'Authorization': session.authorizationHeader,
},
body: jsonEncode({'code': code}),
);
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
return ScanResponse.fromJson(body);
}
}
+59
View File
@@ -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),
),
),
),
);
}
}
+634
View File
@@ -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"
+92
View File
@@ -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
+28
View File
@@ -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);
});
}
+13
View File
@@ -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);
});
}