diff --git a/.metadata b/.metadata
new file mode 100644
index 00000000..70a7d44a
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,45 @@
+# 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: "9f455d2486bcb28cad87b062475f42edc959f636"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: android
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: ios
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: linux
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: macos
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: web
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ - platform: windows
+ create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+ base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/README.md b/README.md
index e77cc37e..57a291a9 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,189 @@
-# Bienvenido al coding-interview-frontend
+# 💱 Crypto Calculator Challenge
-## Descripción
-Acá tienes todos los assets que necesitas para llevar a cabo una pequeña prueba técnica. El objetivo es que puedas demostrar tus habilidades de programación y de UI. El proyecto consiste de una pequeña calculadora que te muestra cuanto vas a recibir si quieres cambiar una determinada cantidad de una moneda a otra.
+
-## Características
-1. Hay dos tipos de monedas: "FIAT" y "CRYPTO".
-2. La tasa de cambio la podrás obtener de nuestro API público.
-3. La moneda del input
+
+
+
+
-## API
-- URL: https://74j6q7lg6a.execute-api.eu-west-1.amazonaws.com/stage/orderbook/public/recommendations
-- Query Params:
- - `type`: 0 -> Cambio de CRYPTO a FIAT, 1 -> Cambio de FIAT a CRYPTO
- - `cryptoCurrencyId`: La moneda crypto (el ID está en el nombre del asset)
- - `fiatCurrencyId`: La moneda fiat (el ID está en el nombre del asset)
- - `amount`: Cantidad a cambiar
- - `amountCurrencyId`: La moneda en la que está del input
+**Una calculadora de criptomonedas moderna, escalable y pixel-perfect construida con Flutter**
-Del response, simplemente obtener el `data.byPrice.fiatToCryptoExchangeRate` y multiplicarlo/dividirlo para mostrar toda la data necesaria.
+[Características](#-características) • [Arquitectura](#-arquitectura) • [Instalación](#-instalación) • [Testing](#-testing)
-### Que puedes hacer:
-- ✅ Preferiblemente, usa Flutter :)
-- ✅ Cuantas mejoras de UX como veas necesarias/quieras
-- ✅ No todo tiene que estar funcionando a la perfección, lo que más vamos a tomar en cuenta es el parecido con el diseño y la calidad del código.
-- ✅ Desarrolla la app con la arquitecura de una app que va a escalar, no hagas un código que no puedas mantener en el futuro.
+
+---
-### Que **no** puedes hacer:
-- ❌ Estresarte 🤗
+## 🎥 Video Demo
+
-## Pasos para comenzar
-1. Haz un fork usando este repositorio como template
-2. Clona el repositorio en tu máquina
-3. Desarrolla la mini-app
-4. Sube tus cambios a tu repositorio
-5. Avísanos que has terminado
-6. ???
-7. PROFIT
+### Android
-### Cualquier duda contactarme a https://www.linkedin.com/in/carlosfontest/
+https://github.com/user-attachments/assets/a6854466-b527-4db8-a559-f64a38a883f9
+
+### iOS
+
+https://github.com/user-attachments/assets/32faa560-6e04-4a00-a03f-ca6e2c438074
+
+
+
+---
+
+## ✨ Características
+
+### 🎨 UI/UX Excellence
+- **Pixel Perfect Implementation** - Diseño implementado con precisión absoluta
+- **Animaciones Suaves** - Chevron animados y transiciones fluidas
+- **Mejoras de UX** - Tasa de conversión mostrada (1 FIAT = X USDT) para mayor claridad
+- **Tema Moderno** - Paleta de colores vibrante con modo oscuro nativo
+- **Responsive Design** - Adaptable a diferentes tamaños de pantalla
+
+### 🏗️ Arquitectura Robusta
+- **Clean Architecture** - Separación clara en capas: Data, Domain, Presentation
+- **Escalabilidad** - Preparado para crecer con nuevas features
+- **SOLID Principles** - Código mantenible y de alta calidad
+- **Gestión de Estado** - Riverpod 3.0 con code generation
+- **Navegación Declarativa** - GoRouter para routing type-safe y deep linking
+
+### 🛡️ Manejo de Errores Profesional
+- **Validaciones Robustas** - Límites de monto, validaciones de entrada
+- **Mensajes Amigables** - Errores claros y comprensibles para el usuario
+- **Recuperación Elegante** - Manejo de errores de red y API
+- **Estados Consistentes** - Loading, Error y Success states bien definidos
+
+### ⚡ Optimizaciones Técnicas
+- **Use Cases** - Lógica de negocio encapsulada y testeable
+- **Providers Optimizados** - Mínimos rebuilds, máximo rendimiento
+- **Code Generation** - Riverpod Generator y Freezed para código type-safe
+- **Últimas Versiones** - Flutter 3.8+, Riverpod 3.0, Dio 5.9
+
+### 🧪 Testing & Calidad
+- **11 Unit Tests** - Cobertura completa de lógica crítica
+- **100% de Éxito** - Todos los tests pasan correctamente
+- **Validaciones Testeadas** - ValidateExchangeAmountUseCase
+- **Cálculos Testeados** - CalculateCryptoExchangeUseCase
+
+---
+
+## 🏛️ Arquitectura
+
+El proyecto sigue **Clean Architecture** con tres capas bien definidas:
+
+```
+lib/
+├── src/
+│ ├── core/ # Recursos compartidos
+│ │ ├── constants/ # Strings, validaciones, colores
+│ │ ├── theme/ # Temas y estilos
+│ │ ├── utils/ # Utilidades y helpers
+│ │ └── widgets/ # Widgets reutilizables
+│ │
+│ └── features/
+│ └── calculator/
+│ ├── data/ # Capa de Datos
+│ │ ├── datasources/ # API & Local data
+│ │ └── repositories/ # Implementaciones
+│ │
+│ ├── domain/ # Capa de Dominio
+│ │ ├── entities/ # Modelos de negocio
+│ │ ├── repositories/ # Contratos
+│ │ └── usecases/ # Lógica de negocio
+│ │
+│ └── presentation/ # Capa de Presentación
+│ ├── providers/ # Riverpod providers
+│ ├── screens/ # Pantallas
+│ ├── states/ # Estados
+│ └── widgets/ # UI components
+```
+
+### 📦 Paquetes Principales
+
+| Paquete | Versión | Propósito |
+|---------|---------|-----------|
+| `flutter_riverpod` | 3.0.3 | Gestión de estado reactiva |
+| `riverpod_annotation` | 3.0.3 | Code generation para providers |
+| `dio` | 5.9.0 | Cliente HTTP robusto |
+| `freezed` | 3.2.3 | Modelos inmutables |
+| `flutter_svg` | 2.2.1 | Renderizado de SVG |
+| `go_router` | 16.3.0 | Navegación declarativa y deep linking |
+
+---
+
+## 🚀 Instalación
+
+### Prerequisitos
+
+- Flutter SDK: `>= 3.8.0`
+- Dart SDK: `>= 3.8.0`
+
+### Pasos
+
+1. **Clonar el repositorio**
+```bash
+git clone https://github.com/cuambyte/crypto-calculator.git
+cd crypto-calculator
+```
+
+2. **Instalar dependencias**
+```bash
+flutter pub get
+```
+
+3. **Generar código**
+```bash
+dart run build_runner build -d
+```
+
+4. **Ejecutar la app**
+```bash
+flutter run
+```
+
+### Ejecutar Tests
+
+```bash
+flutter test
+```
+
+---
+
+## 🎯 Decisiones Técnicas
+
+### ¿Por qué Clean Architecture?
+- ✅ Facilita el testing
+- ✅ Código desacoplado y mantenible
+- ✅ Preparado para escalar
+- ✅ Independencia de frameworks
+
+### ¿Por qué Riverpod?
+- ✅ Type-safe y compile-time
+- ✅ Sin context necesario
+- ✅ Code generation
+- ✅ Performance optimizado
+
+### ¿Por qué Unit Testing?
+- ✅ Valida lógica crítica del negocio
+- ✅ Rápido de ejecutar
+- ✅ Detecta regresiones temprano
+- ✅ Documenta comportamiento esperado
+
+---
+
+## 🔮 Mejoras Futuras
+
+- [ ] Tests de integración E2E
+- [ ] Persistencia local con Hive/SQLite
+- [ ] Soporte multi-idioma (i18n)
+- [ ] Más criptomonedas y fiat
+- [ ] Historial de conversiones
+- [ ] Modo offline con cache
+
+---
+
+
+
+⭐ Si te gustó este proyecto, dale una estrella
+
+
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 00000000..0d290213
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 00000000..be3943c9
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 00000000..27547855
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.crypto.calculator.crypto_calculator"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.crypto.calculator.crypto_calculator"
+ // 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")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..399f6981
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fa976df0
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/crypto/calculator/crypto_calculator/MainActivity.kt b/android/app/src/main/kotlin/com/crypto/calculator/crypto_calculator/MainActivity.kt
new file mode 100644
index 00000000..3ee92428
--- /dev/null
+++ b/android/app/src/main/kotlin/com/crypto/calculator/crypto_calculator/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.crypto.calculator.crypto_calculator
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 00000000..f74085f3
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 00000000..304732f8
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..db77bb4b
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..17987b79
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..09d43914
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..d5f1c8d3
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..4d6372ee
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 00000000..06952be7
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..cb1ef880
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 00000000..399f6981
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 00000000..dbee657b
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 00000000..f018a618
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..ac3b4792
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 00000000..fb605bc8
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.9.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/assets/icons/swap.svg b/assets/icons/swap.svg
new file mode 100644
index 00000000..4dd16c2e
--- /dev/null
+++ b/assets/icons/swap.svg
@@ -0,0 +1,7 @@
+
+
+
+
+ Svg Vector Icons : http://www.onlinewebfonts.com/icon
+
+
\ No newline at end of file
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 00000000..7a7f9873
--- /dev/null
+++ b/ios/.gitignore
@@ -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
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 00000000..1dc6cf76
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 13.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 00000000..592ceee8
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 00000000..592ceee8
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..71464462
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,616 @@
+// !$*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 */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ 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;
+ 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 */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.crypto.calculator.cryptoCalculator;
+ 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.crypto.calculator.cryptoCalculator.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.crypto.calculator.cryptoCalculator.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.crypto.calculator.cryptoCalculator.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.crypto.calculator.cryptoCalculator;
+ 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.crypto.calculator.cryptoCalculator;
+ 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 */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 00000000..e3773d42
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..1d526a16
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 00000000..f9b0d7c5
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..62666446
--- /dev/null
+++ b/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..d36b1fab
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 00000000..dc9ada47
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 00000000..7353c41e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 00000000..797d452e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 00000000..6ed2d933
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 00000000..4cd7b009
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 00000000..fe730945
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 00000000..321773cd
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 00000000..797d452e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 00000000..502f463a
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 00000000..0ec30343
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 00000000..0ec30343
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 00000000..e9f5fea2
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 00000000..84ac32ae
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 00000000..8953cba0
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 00000000..0467bf12
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 00000000..0bedcf2f
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 00000000..9da19eac
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 00000000..89c2725b
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 00000000..f2e259c7
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 00000000..f3c28516
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 00000000..b8994b5d
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Crypto Calculator
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ crypto_calculator
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 00000000..308a2a56
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 00000000..86a7c3b1
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 00000000..864e5746
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,9 @@
+import 'package:flutter/material.dart';
+
+import 'src/crypo_calculator_app.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ runApp(const CryptoCalculatorApp());
+}
diff --git a/lib/src/core/constants/api_constants.dart b/lib/src/core/constants/api_constants.dart
new file mode 100644
index 00000000..a5594570
--- /dev/null
+++ b/lib/src/core/constants/api_constants.dart
@@ -0,0 +1,21 @@
+class ApiConstants {
+ static const String baseUrl =
+ 'https://74j6q7lg6a.execute-api.eu-west-1.amazonaws.com/stage';
+ static const String exchangeRateUrl =
+ '$baseUrl/orderbook/public/recommendations';
+
+ // Query parameters
+ static const String typeParam = 'type';
+ static const String cryptoCurrencyIdParam = 'cryptoCurrencyId';
+ static const String fiatCurrencyIdParam = 'fiatCurrencyId';
+ static const String amountParam = 'amount';
+ static const String amountCurrencyIdParam = 'amountCurrencyId';
+
+ // Exchange types
+ static const int cryptoToFiat = 0;
+ static const int fiatToCrypto = 1;
+
+ // Timeouts
+ static const Duration connectTimeout = Duration(seconds: 30);
+ static const Duration receiveTimeout = Duration(seconds: 30);
+}
diff --git a/lib/src/core/constants/string_constants.dart b/lib/src/core/constants/string_constants.dart
new file mode 100644
index 00000000..9497eb42
--- /dev/null
+++ b/lib/src/core/constants/string_constants.dart
@@ -0,0 +1,54 @@
+final class StringConstants {
+ // Errores generales
+ static const String errorMessage =
+ 'Ocurrió un error inesperado. Por favor, intenta nuevamente.';
+ static const String pageNotFound = 'Página no encontrada';
+ static const String goToHome = 'Ir a Inicio';
+ // Errores de validación
+ static const String invalidAmountError = 'Debes ingresar un monto válido';
+ static const String amountExceedsLimitError =
+ 'El monto excede el límite permitido';
+ static const String amountMustBePositiveError = 'El monto debe ser mayor a 0';
+
+ // Errores de red
+ static const String connectionTimeoutError =
+ 'La conexión está tardando demasiado. Verifica tu internet e intenta nuevamente.';
+ static const String noInternetConnectionError =
+ 'No hay conexión a internet. Verifica tu conexión e intenta nuevamente.';
+ static const String connectionProblemError =
+ 'Ocurrió un problema de conexión. Por favor, intenta más tarde.';
+
+ // Errores del servidor
+ static const String invalidServerResponseError =
+ 'Formato de respuesta inválido del servidor';
+ static const String conversionUnavailableError =
+ 'Esta conversión no está disponible en este momento. Por favor, intenta con otra moneda o cantidad.';
+ static const String serverProcessingError =
+ 'No se pudo procesar la información del servidor. Intenta con otra moneda.';
+ static const String serverRequestError =
+ 'El servidor no pudo procesar tu solicitud. Intenta más tarde.';
+ static const String serverErrorPrefix = 'Error del servidor: ';
+
+ // Labels de UI
+ static const String fiatLabel = 'FIAT';
+ static const String cryptoLabel = 'Cripto';
+ static const String haveLabel = 'TENGO';
+ static const String wantLabel = 'QUIERO';
+
+ // Mensajes informativos
+ static const String estimatedRateLabel = 'Tasa estimada';
+ static const String youWillReceiveLabel = 'Recibirás';
+ static const String estimatedTimeLabel = 'Tiempo estimado';
+ static const String estimatedTimeValue = '≈ 10 Min';
+ static const String exchangeInfoPlaceholder =
+ 'Ingresa el monto y selecciona las monedas para ver la información del intercambio';
+ static const String loadCurrenciesError = 'Error al cargar las monedas';
+
+ // Textos de botones
+ static const String exchangeButtonText = 'Cambiar';
+
+ // Placeholders
+ static const String amountPlaceholder = '0.00';
+ static const String loadingText = 'Cargando...';
+ static const String currencySymbol = '\$';
+}
diff --git a/lib/src/core/constants/validation_constants.dart b/lib/src/core/constants/validation_constants.dart
new file mode 100644
index 00000000..7494944a
--- /dev/null
+++ b/lib/src/core/constants/validation_constants.dart
@@ -0,0 +1,13 @@
+/// Constantes de validación para la aplicación
+///
+/// Centraliza todos los límites y reglas de validación
+/// para facilitar su mantenimiento y modificación.
+final class ValidationConstants {
+ // Validaciones de montos de intercambio
+ static const double maxExchangeAmount = 1000000.0;
+ static const double minExchangeAmount = 0.0;
+
+ // Prevenir instanciación
+ ValidationConstants._();
+}
+
diff --git a/lib/src/core/errors/failures.dart b/lib/src/core/errors/failures.dart
new file mode 100644
index 00000000..88cf5792
--- /dev/null
+++ b/lib/src/core/errors/failures.dart
@@ -0,0 +1,25 @@
+abstract final class Failure {
+ final String message;
+ final int? code;
+
+ const Failure({required this.message, this.code});
+
+ @override
+ String toString() => message;
+}
+
+final class ServerFailure extends Failure {
+ const ServerFailure({required super.message, super.code});
+}
+
+final class NetworkFailure extends Failure {
+ const NetworkFailure({required super.message, super.code});
+}
+
+final class ValidationFailure extends Failure {
+ const ValidationFailure({required super.message, super.code});
+}
+
+final class UnknownFailure extends Failure {
+ const UnknownFailure({required super.message, super.code});
+}
diff --git a/lib/src/core/network/dio_client.dart b/lib/src/core/network/dio_client.dart
new file mode 100644
index 00000000..51b03568
--- /dev/null
+++ b/lib/src/core/network/dio_client.dart
@@ -0,0 +1,36 @@
+import 'package:dio/dio.dart';
+
+import '../constants/api_constants.dart';
+
+class DioClient {
+ static Dio? _instance;
+
+ static Dio get instance {
+ _instance ??= _createDio();
+ return _instance!;
+ }
+
+ static Dio _createDio() {
+ final dio = Dio();
+
+ dio.options = BaseOptions(
+ baseUrl: ApiConstants.baseUrl,
+ connectTimeout: ApiConstants.connectTimeout,
+ receiveTimeout: ApiConstants.receiveTimeout,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+ );
+
+ dio.interceptors.add(
+ LogInterceptor(
+ requestBody: true,
+ responseBody: true,
+ error: true,
+ ),
+ );
+
+ return dio;
+ }
+}
diff --git a/lib/src/core/theme/app_theme.dart b/lib/src/core/theme/app_theme.dart
new file mode 100644
index 00000000..228c0b14
--- /dev/null
+++ b/lib/src/core/theme/app_theme.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+
+import 'colors/app_colors.dart';
+
+class AppTheme {
+ static ThemeData get lightTheme {
+ return ThemeData(
+ useMaterial3: true,
+ textSelectionTheme: TextSelectionThemeData(
+ cursorColor: AppColors.primaryYellow,
+ selectionColor: AppColors.primaryYellow.withValues(alpha: 0.2),
+ selectionHandleColor: AppColors.primaryYellow,
+ ),
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: AppColors.primaryYellow,
+ brightness: Brightness.light,
+ primary: AppColors.primaryYellow,
+ secondary: AppColors.secondaryYellow,
+ surface: AppColors.white,
+ onSurface: AppColors.black,
+ onPrimary: AppColors.white,
+ onSecondary: AppColors.white,
+ ),
+ scaffoldBackgroundColor: AppColors.lightBlue,
+ appBarTheme: const AppBarTheme(
+ backgroundColor: AppColors.primaryYellow,
+ foregroundColor: AppColors.white,
+ elevation: 0,
+ centerTitle: true,
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: AppColors.primaryYellow,
+ foregroundColor: AppColors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+ ),
+ ),
+ cardTheme: CardThemeData(
+ color: AppColors.white,
+ elevation: 4,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
+ margin: const EdgeInsets.all(16),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ contentPadding: const EdgeInsets.all(14.0),
+ filled: true,
+ fillColor: AppColors.white,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
+ enabledBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ borderSide: BorderSide(color: AppColors.secondaryYellow),
+ ),
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ borderSide: const BorderSide(
+ color: AppColors.secondaryYellow,
+ width: 1.5,
+ ),
+ ),
+ errorBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ borderSide: BorderSide(color: Colors.red.shade400, width: 2),
+ ),
+ focusedErrorBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ borderSide: BorderSide(color: Colors.red.shade400, width: 2),
+ ),
+ ),
+ textTheme: const TextTheme(
+ headlineLarge: TextStyle(
+ fontSize: 32,
+ fontWeight: FontWeight.bold,
+ color: AppColors.black,
+ ),
+ headlineMedium: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: AppColors.black,
+ ),
+ titleLarge: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: AppColors.black,
+ ),
+ titleMedium: TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: AppColors.black,
+ ),
+ bodyLarge: TextStyle(fontSize: 16, color: AppColors.black),
+ bodyMedium: TextStyle(fontSize: 14, color: AppColors.black),
+ bodySmall: TextStyle(fontSize: 12, color: AppColors.grey),
+ ),
+ );
+ }
+}
diff --git a/lib/src/core/theme/colors/app_colors.dart b/lib/src/core/theme/colors/app_colors.dart
new file mode 100644
index 00000000..9d89fbb6
--- /dev/null
+++ b/lib/src/core/theme/colors/app_colors.dart
@@ -0,0 +1,14 @@
+import 'package:flutter/material.dart';
+
+class AppColors {
+ // Colores principales del diseño
+ static const Color primaryYellow = Color.fromRGBO(255, 178, 0, 1);
+ static const Color secondaryYellow = Color.fromRGBO(255, 200, 1, 1);
+ static const Color lightBlue = Color.fromRGBO(224, 248, 250, 1);
+ static const Color white = Color(0xFFFFFFFF);
+ static const Color black = Color(0xFF212121);
+ static const Color grey = Color(0xFF757575);
+
+ // Color para handles de bottom sheets
+ static const Color bottomSheetHandle = Color.fromRGBO(190, 195, 203, 1);
+}
diff --git a/lib/src/core/ui/painters/background_painter.dart b/lib/src/core/ui/painters/background_painter.dart
new file mode 100644
index 00000000..1c9bf177
--- /dev/null
+++ b/lib/src/core/ui/painters/background_painter.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+
+import '../../theme/colors/app_colors.dart';
+
+class BackgroundPainter extends CustomPainter {
+ @override
+ void paint(Canvas canvas, Size size) {
+ final paint = Paint()
+ ..color = AppColors.primaryYellow
+ ..style = PaintingStyle.fill;
+
+ // Óvalo alto (más alto que ancho), corrido a la derecha y hacia arriba
+ final centerX = size.width * 1.3; // empuja a la derecha
+ final centerY = size.height * 0.40; // levanta hacia arriba
+ final ovalWidth = size.width * 1.5; // más angosto
+ final ovalHeight = size.height * 1.2; // ~alto de pantalla o un poco más
+
+ final rect = Rect.fromCenter(
+ center: Offset(centerX, centerY),
+ width: ovalWidth,
+ height: ovalHeight,
+ );
+
+ canvas.drawOval(rect, paint);
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
+}
diff --git a/lib/src/core/utils/formatters/rate_formatter.dart b/lib/src/core/utils/formatters/rate_formatter.dart
new file mode 100644
index 00000000..471a6e0f
--- /dev/null
+++ b/lib/src/core/utils/formatters/rate_formatter.dart
@@ -0,0 +1,8 @@
+final class RateFormatter {
+ static String formatRate({required double rate}) {
+ if (rate < 1) {
+ return rate.toStringAsFixed(4);
+ }
+ return rate.toStringAsFixed(2);
+ }
+}
diff --git a/lib/src/core/utils/formatters/thousand_separator_input_formatter.dart b/lib/src/core/utils/formatters/thousand_separator_input_formatter.dart
new file mode 100644
index 00000000..8c72bc5f
--- /dev/null
+++ b/lib/src/core/utils/formatters/thousand_separator_input_formatter.dart
@@ -0,0 +1,80 @@
+import 'package:flutter/services.dart';
+
+/// Formatter que agrega separadores de miles mientras el usuario escribe
+class ThousandSeparatorInputFormatter extends TextInputFormatter {
+ @override
+ TextEditingValue formatEditUpdate(
+ TextEditingValue oldValue,
+ TextEditingValue newValue,
+ ) {
+ // Si el texto está vacío, no hacer nada
+ if (newValue.text.isEmpty) {
+ return newValue;
+ }
+
+ // Remover todas las comas existentes
+ final String unformattedText = newValue.text.replaceAll(',', '');
+
+ // Validar que solo contenga números y punto decimal
+ if (!RegExp(r'^\d*\.?\d*$').hasMatch(unformattedText)) {
+ return oldValue;
+ }
+
+ // Separar parte entera y decimal
+ final parts = unformattedText.split('.');
+ String integerPart = parts[0];
+ final String? decimalPart = parts.length > 1 ? parts[1] : null;
+
+ // Agregar comas a la parte entera
+ String formattedInteger = '';
+ int counter = 0;
+ for (int i = integerPart.length - 1; i >= 0; i--) {
+ if (counter == 3) {
+ formattedInteger = ',$formattedInteger';
+ counter = 0;
+ }
+ formattedInteger = integerPart[i] + formattedInteger;
+ counter++;
+ }
+
+ // Construir el texto final
+ final String formattedText = decimalPart != null
+ ? '$formattedInteger.$decimalPart'
+ : formattedInteger;
+
+ // Calcular la nueva posición del cursor
+ int selectionOffset = newValue.selection.end;
+
+ // Contar cuántas comas hay antes del cursor en el texto antiguo
+ final int oldCommasBeforeCursor =
+ oldValue.text.substring(0, oldValue.selection.end).split(',').length -
+ 1;
+
+ // Contar cuántas comas hay antes del cursor en el texto nuevo
+ final String beforeCursor = unformattedText.substring(
+ 0,
+ newValue.selection.end - oldCommasBeforeCursor,
+ );
+ int newCommasBeforeCursor = 0;
+ int charCount = 0;
+
+ for (
+ int i = 0;
+ i < formattedText.length && charCount < beforeCursor.length;
+ i++
+ ) {
+ if (formattedText[i] == ',') {
+ newCommasBeforeCursor++;
+ } else {
+ charCount++;
+ }
+ }
+
+ selectionOffset = beforeCursor.length + newCommasBeforeCursor;
+
+ return TextEditingValue(
+ text: formattedText,
+ selection: TextSelection.collapsed(offset: selectionOffset),
+ );
+ }
+}
diff --git a/lib/src/core/widgets/bottom_sheet_handle.dart b/lib/src/core/widgets/bottom_sheet_handle.dart
new file mode 100644
index 00000000..90cbff64
--- /dev/null
+++ b/lib/src/core/widgets/bottom_sheet_handle.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+import '../theme/colors/app_colors.dart';
+
+/// Handle indicator para bottom sheets
+///
+/// Widget reutilizable que muestra el indicador visual en la parte
+/// superior de los bottom sheets para indicar que se pueden arrastrar.
+/// Usa el color global definido en AppColors.bottomSheetHandle.
+final class BottomSheetHandle extends StatelessWidget {
+ final double width;
+ final double height;
+ final double topMargin;
+
+ const BottomSheetHandle({
+ super.key,
+ this.width = 40,
+ this.height = 4,
+ this.topMargin = 12,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: EdgeInsets.only(top: topMargin),
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: AppColors.bottomSheetHandle,
+ borderRadius: BorderRadius.circular(height / 2),
+ ),
+ );
+ }
+}
+
diff --git a/lib/src/core/widgets/currency_image_widget.dart b/lib/src/core/widgets/currency_image_widget.dart
new file mode 100644
index 00000000..fde39d7c
--- /dev/null
+++ b/lib/src/core/widgets/currency_image_widget.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+
+/// Widget reutilizable para mostrar imágenes de monedas con fallback
+///
+/// Este widget encapsula la lógica de carga de imágenes de assets
+/// con un fallback elegante en caso de error.
+final class CurrencyImageWidget extends StatelessWidget {
+ final String assetPath;
+ final double size;
+ final double borderRadius;
+
+ const CurrencyImageWidget({
+ super.key,
+ required this.assetPath,
+ this.size = 40,
+ this.borderRadius = 8,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(borderRadius),
+ child: Image.asset(
+ assetPath,
+ width: size,
+ height: size,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) {
+ return Container(
+ width: size,
+ height: size,
+ decoration: BoxDecoration(
+ color: theme.colorScheme.outline.withValues(alpha: 0.1),
+ borderRadius: BorderRadius.circular(borderRadius),
+ ),
+ child: Icon(
+ Icons.currency_exchange,
+ size: size * 0.6,
+ color: theme.colorScheme.outline,
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
diff --git a/lib/src/core/widgets/error_state_widget.dart b/lib/src/core/widgets/error_state_widget.dart
new file mode 100644
index 00000000..19115174
--- /dev/null
+++ b/lib/src/core/widgets/error_state_widget.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+
+/// Widget genérico para mostrar estados de error
+///
+/// Este widget proporciona una UI consistente para mostrar
+/// mensajes de error con un ícono y texto descriptivo.
+final class ErrorStateWidget extends StatelessWidget {
+ final String title;
+ final String? message;
+ final IconData icon;
+ final VoidCallback? onRetry;
+ final String? retryButtonText;
+
+ const ErrorStateWidget({
+ super.key,
+ required this.title,
+ this.message,
+ this.icon = Icons.error_outline,
+ this.onRetry,
+ this.retryButtonText,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ icon,
+ size: 48,
+ color: theme.colorScheme.error,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ title,
+ style: theme.textTheme.titleMedium,
+ textAlign: TextAlign.center,
+ ),
+ if (message != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ message!,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ],
+ if (onRetry != null) ...[
+ const SizedBox(height: 24),
+ ElevatedButton(
+ onPressed: onRetry,
+ child: Text(retryButtonText ?? 'Reintentar'),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
diff --git a/lib/src/core/widgets/loading_button.dart b/lib/src/core/widgets/loading_button.dart
new file mode 100644
index 00000000..cf4c9613
--- /dev/null
+++ b/lib/src/core/widgets/loading_button.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+
+/// Botón reutilizable con estado de loading
+///
+/// Este widget encapsula un ElevatedButton que puede mostrar
+/// un indicador de carga cuando está procesando una acción.
+final class LoadingButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+ final bool isLoading;
+ final bool isEnabled;
+ final EdgeInsetsGeometry? padding;
+ final BorderRadius? borderRadius;
+
+ const LoadingButton({
+ super.key,
+ required this.text,
+ required this.onPressed,
+ this.isLoading = false,
+ this.isEnabled = true,
+ this.padding,
+ this.borderRadius,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: isEnabled && !isLoading ? onPressed : null,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: theme.colorScheme.primary,
+ foregroundColor: theme.colorScheme.onPrimary,
+ padding: padding ?? const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: borderRadius ?? BorderRadius.circular(12),
+ ),
+ ),
+ child: isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ valueColor: AlwaysStoppedAnimation(Colors.white),
+ ),
+ )
+ : Text(
+ text,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: theme.colorScheme.onPrimary,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
diff --git a/lib/src/crypo_calculator_app.dart b/lib/src/crypo_calculator_app.dart
new file mode 100644
index 00000000..aa30c1f7
--- /dev/null
+++ b/lib/src/crypo_calculator_app.dart
@@ -0,0 +1,21 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import 'core/theme/app_theme.dart';
+import 'routes/app_router.dart';
+
+class CryptoCalculatorApp extends StatelessWidget {
+ const CryptoCalculatorApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ProviderScope(
+ child: MaterialApp.router(
+ debugShowCheckedModeBanner: false,
+ title: 'Crypto Calculator',
+ theme: AppTheme.lightTheme,
+ routerConfig: AppRouter.router,
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/data/datasources/currency_local_datasource.dart b/lib/src/features/calculator/data/datasources/currency_local_datasource.dart
new file mode 100644
index 00000000..8f9ff691
--- /dev/null
+++ b/lib/src/features/calculator/data/datasources/currency_local_datasource.dart
@@ -0,0 +1,5 @@
+import '../../domain/entities/currency.dart';
+
+abstract class CurrencyLocalDataSource {
+ Future> getAvailableCurrencies();
+}
diff --git a/lib/src/features/calculator/data/datasources/currency_local_datasource_impl.dart b/lib/src/features/calculator/data/datasources/currency_local_datasource_impl.dart
new file mode 100644
index 00000000..32d7fdbd
--- /dev/null
+++ b/lib/src/features/calculator/data/datasources/currency_local_datasource_impl.dart
@@ -0,0 +1,52 @@
+// lib/src/features/calculator/data/datasources/currency_local_datasource_impl.dart
+import '../../../../core/constants/string_constants.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/currency_type.dart';
+import 'currency_local_datasource.dart';
+
+final class CurrencyLocalDataSourceImpl implements CurrencyLocalDataSource {
+ const CurrencyLocalDataSourceImpl();
+
+ @override
+ Future> getAvailableCurrencies() async {
+ await Future.delayed(const Duration(milliseconds: 2300));
+
+ return [
+ const Currency(
+ id: 'TATUM-TRON-USDT',
+ name: 'Tether (USDT)',
+ symbol: 'USDT',
+ assetPath: 'assets/cripto_currencies/TATUM-TRON-USDT.png',
+ type: CurrencyType.crypto,
+ ),
+ const Currency(
+ id: 'VES',
+ name: 'Bolívares (Bs)',
+ symbol: 'VES',
+ assetPath: 'assets/fiat_currencies/VES.png',
+ type: CurrencyType.fiat,
+ ),
+ const Currency(
+ id: 'BRL',
+ name: 'Real Brasileño (R${StringConstants.currencySymbol})',
+ symbol: 'BRL',
+ assetPath: 'assets/fiat_currencies/BRL.png',
+ type: CurrencyType.fiat,
+ ),
+ const Currency(
+ id: 'COP',
+ name: 'Pesos Colombianos (COL${StringConstants.currencySymbol})',
+ symbol: 'COP',
+ assetPath: 'assets/fiat_currencies/COP.png',
+ type: CurrencyType.fiat,
+ ),
+ const Currency(
+ id: 'PEN',
+ name: 'Soles Peruanos (S/)',
+ symbol: 'PEN',
+ assetPath: 'assets/fiat_currencies/PEN.png',
+ type: CurrencyType.fiat,
+ ),
+ ];
+ }
+}
diff --git a/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource.dart b/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource.dart
new file mode 100644
index 00000000..48e9b948
--- /dev/null
+++ b/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource.dart
@@ -0,0 +1,11 @@
+import '../models/exchange_rate_response_model.dart';
+
+abstract class ExchangeRateRemoteDataSource {
+ Future getExchangeRate({
+ required String cryptoCurrencyId,
+ required String fiatCurrencyId,
+ required String amount,
+ required String amountCurrencyId,
+ required int type,
+ });
+}
diff --git a/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource_impl.dart b/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource_impl.dart
new file mode 100644
index 00000000..86b15b8d
--- /dev/null
+++ b/lib/src/features/calculator/data/datasources/exchange_rate_remote_datasource_impl.dart
@@ -0,0 +1,103 @@
+import 'package:dio/dio.dart';
+
+import '../../../../core/constants/api_constants.dart';
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/errors/failures.dart';
+import '../models/exchange_rate_response_model.dart';
+import 'exchange_rate_remote_datasource.dart';
+
+final class ExchangeRateRemoteDataSourceImpl
+ implements ExchangeRateRemoteDataSource {
+ final Dio _dio;
+
+ const ExchangeRateRemoteDataSourceImpl({required Dio dio}) : _dio = dio;
+
+ @override
+ Future getExchangeRate({
+ required String cryptoCurrencyId,
+ required String fiatCurrencyId,
+ required String amount,
+ required String amountCurrencyId,
+ required int type,
+ }) async {
+ try {
+ ExchangeRateResponseModel exchangeRateResponseModel;
+
+ final response = await _dio.get(
+ ApiConstants.exchangeRateUrl,
+ queryParameters: {
+ ApiConstants.typeParam: type,
+ ApiConstants.cryptoCurrencyIdParam: cryptoCurrencyId,
+ ApiConstants.fiatCurrencyIdParam: fiatCurrencyId,
+ ApiConstants.amountParam: amount,
+ ApiConstants.amountCurrencyIdParam: amountCurrencyId,
+ },
+ );
+
+ if (response.statusCode == 200 && response.data != null) {
+ if (response.data is! Map) {
+ throw ServerFailure(
+ message: StringConstants.invalidServerResponseError,
+ code: response.statusCode,
+ );
+ }
+
+ final Map data = response.data;
+
+ if (data.isEmpty ||
+ data['data'] == null ||
+ (data['data'] is Map && (data['data'] as Map).isEmpty)) {
+ throw ServerFailure(
+ message: StringConstants.conversionUnavailableError,
+ code: response.statusCode,
+ );
+ }
+
+ try {
+ exchangeRateResponseModel = ExchangeRateResponseModel.fromJson(data);
+ } catch (_) {
+ throw ServerFailure(
+ message: StringConstants.serverProcessingError,
+ code: response.statusCode,
+ );
+ }
+ } else {
+ throw ServerFailure(
+ message: '${StringConstants.serverErrorPrefix}${response.statusCode}',
+ code: response.statusCode,
+ );
+ }
+
+ return exchangeRateResponseModel;
+ } on DioException catch (e) {
+ if (e.type == DioExceptionType.connectionTimeout ||
+ e.type == DioExceptionType.receiveTimeout ||
+ e.type == DioExceptionType.sendTimeout) {
+ throw NetworkFailure(
+ message: StringConstants.connectionTimeoutError,
+ code: e.response?.statusCode,
+ );
+ } else if (e.type == DioExceptionType.badResponse) {
+ throw ServerFailure(
+ message: StringConstants.serverRequestError,
+ code: e.response?.statusCode,
+ );
+ } else if (e.type == DioExceptionType.connectionError) {
+ throw NetworkFailure(
+ message: StringConstants.noInternetConnectionError,
+ code: e.response?.statusCode,
+ );
+ } else {
+ throw NetworkFailure(
+ message: StringConstants.connectionProblemError,
+ code: e.response?.statusCode,
+ );
+ }
+ } on Failure {
+ // Re-lanzar los Failures que ya creamos
+ rethrow;
+ } catch (e) {
+ throw UnknownFailure(message: StringConstants.errorMessage);
+ }
+ }
+}
diff --git a/lib/src/features/calculator/data/models/by_price.dart b/lib/src/features/calculator/data/models/by_price.dart
new file mode 100644
index 00000000..15d1e83c
--- /dev/null
+++ b/lib/src/features/calculator/data/models/by_price.dart
@@ -0,0 +1,19 @@
+final class ByPrice {
+ final double? fiatToCryptoExchangeRate;
+
+ const ByPrice({required this.fiatToCryptoExchangeRate});
+
+ static double? _toDouble(dynamic value) {
+ if (value == null) return null;
+ if (value is num) return value.toDouble();
+ if (value is String) return double.tryParse(value);
+ return null;
+ }
+
+ factory ByPrice.fromJson(Map json) {
+ final double? fiatToCryptoExchangeRate = _toDouble(
+ json['fiatToCryptoExchangeRate'],
+ );
+ return ByPrice(fiatToCryptoExchangeRate: fiatToCryptoExchangeRate);
+ }
+}
diff --git a/lib/src/features/calculator/data/models/data_wrapper.dart b/lib/src/features/calculator/data/models/data_wrapper.dart
new file mode 100644
index 00000000..a2807c81
--- /dev/null
+++ b/lib/src/features/calculator/data/models/data_wrapper.dart
@@ -0,0 +1,15 @@
+import 'by_price.dart';
+
+final class DataWrapper {
+ final ByPrice byPrice;
+
+ const DataWrapper({required this.byPrice});
+
+ factory DataWrapper.fromJson(Map json) {
+ final ByPrice byPrice = ByPrice.fromJson(
+ json['byPrice'] as Map,
+ );
+
+ return DataWrapper(byPrice: byPrice);
+ }
+}
diff --git a/lib/src/features/calculator/data/models/exchange_rate_response_model.dart b/lib/src/features/calculator/data/models/exchange_rate_response_model.dart
new file mode 100644
index 00000000..5c318ad6
--- /dev/null
+++ b/lib/src/features/calculator/data/models/exchange_rate_response_model.dart
@@ -0,0 +1,15 @@
+import 'data_wrapper.dart';
+
+final class ExchangeRateResponseModel {
+ final DataWrapper data;
+
+ const ExchangeRateResponseModel({required this.data});
+
+ factory ExchangeRateResponseModel.fromJson(Map json) {
+ final DataWrapper data = DataWrapper.fromJson(
+ json['data'] as Map,
+ );
+
+ return ExchangeRateResponseModel(data: data);
+ }
+}
diff --git a/lib/src/features/calculator/data/models/mappers/exchange_rate_response_mapper.dart b/lib/src/features/calculator/data/models/mappers/exchange_rate_response_mapper.dart
new file mode 100644
index 00000000..0e37ce0d
--- /dev/null
+++ b/lib/src/features/calculator/data/models/mappers/exchange_rate_response_mapper.dart
@@ -0,0 +1,8 @@
+import '../../../domain/entities/exchange_rate.dart';
+import '../exchange_rate_response_model.dart';
+
+extension ExchangeRateResponseMapper on ExchangeRateResponseModel {
+ ExchangeRate toEntity() => ExchangeRate(
+ fiatToCryptoExchangeRate: data.byPrice.fiatToCryptoExchangeRate ?? 0.0,
+ );
+}
diff --git a/lib/src/features/calculator/data/repositories/calculator_repository_impl.dart b/lib/src/features/calculator/data/repositories/calculator_repository_impl.dart
new file mode 100644
index 00000000..b051eb00
--- /dev/null
+++ b/lib/src/features/calculator/data/repositories/calculator_repository_impl.dart
@@ -0,0 +1,38 @@
+import 'package:crypto_calculator/src/features/calculator/data/models/mappers/exchange_rate_response_mapper.dart';
+
+import '../../domain/entities/exchange_rate.dart';
+import '../../domain/repositories/calculator_repository.dart';
+import '../datasources/exchange_rate_remote_datasource.dart';
+import '../models/exchange_rate_response_model.dart';
+
+final class CalculatorRepositoryImpl implements CalculatorRepository {
+ final ExchangeRateRemoteDataSource _exchangeRateRemoteDataSource;
+
+ const CalculatorRepositoryImpl({
+ required ExchangeRateRemoteDataSource exchangeRateRemoteDataSource,
+ }) : _exchangeRateRemoteDataSource = exchangeRateRemoteDataSource;
+
+ @override
+ Future getExchangeRate({
+ required String cryptoCurrencyId,
+ required String fiatCurrencyId,
+ required String amount,
+ required String amountCurrencyId,
+ required int type,
+ }) async {
+ final ExchangeRate exchangeRate;
+
+ final ExchangeRateResponseModel exchangeRateResult =
+ await _exchangeRateRemoteDataSource.getExchangeRate(
+ cryptoCurrencyId: cryptoCurrencyId,
+ fiatCurrencyId: fiatCurrencyId,
+ amount: amount,
+ amountCurrencyId: amountCurrencyId,
+ type: type,
+ );
+
+ exchangeRate = exchangeRateResult.toEntity();
+
+ return exchangeRate.fiatToCryptoExchangeRate;
+ }
+}
diff --git a/lib/src/features/calculator/data/repositories/currency_repository_impl.dart b/lib/src/features/calculator/data/repositories/currency_repository_impl.dart
new file mode 100644
index 00000000..8eb4b36b
--- /dev/null
+++ b/lib/src/features/calculator/data/repositories/currency_repository_impl.dart
@@ -0,0 +1,19 @@
+import '../../domain/entities/currency.dart';
+import '../../domain/repositories/currency_repository.dart';
+import '../datasources/currency_local_datasource.dart';
+
+class CurrencyRepositoryImpl implements CurrencyRepository {
+ final CurrencyLocalDataSource localDataSource;
+
+ const CurrencyRepositoryImpl({required this.localDataSource});
+
+ @override
+ Future> getAvailableCurrencies() async {
+ try {
+ return await localDataSource.getAvailableCurrencies();
+ } catch (e) {
+ // TODO: Manejar errores apropiadamente
+ rethrow;
+ }
+ }
+}
diff --git a/lib/src/features/calculator/domain/entities/calculate_exchange_params.dart b/lib/src/features/calculator/domain/entities/calculate_exchange_params.dart
new file mode 100644
index 00000000..413bf92a
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/calculate_exchange_params.dart
@@ -0,0 +1,13 @@
+import 'currency.dart';
+
+final class CalculateExchangeParams {
+ final Currency fromCurrency;
+ final Currency toCurrency;
+ final double amount;
+
+ const CalculateExchangeParams({
+ required this.fromCurrency,
+ required this.toCurrency,
+ required this.amount,
+ });
+}
diff --git a/lib/src/features/calculator/domain/entities/currency.dart b/lib/src/features/calculator/domain/entities/currency.dart
new file mode 100644
index 00000000..cdc892db
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/currency.dart
@@ -0,0 +1,17 @@
+import 'enum/currency_type.dart';
+
+final class Currency {
+ final String id;
+ final String name;
+ final String symbol;
+ final String assetPath;
+ final CurrencyType type;
+
+ const Currency({
+ required this.id,
+ required this.name,
+ required this.symbol,
+ required this.assetPath,
+ required this.type,
+ });
+}
diff --git a/lib/src/features/calculator/domain/entities/enum/bottom_sheet_type.dart b/lib/src/features/calculator/domain/entities/enum/bottom_sheet_type.dart
new file mode 100644
index 00000000..62c2c539
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/enum/bottom_sheet_type.dart
@@ -0,0 +1 @@
+enum BottomSheetType { none, from, to }
diff --git a/lib/src/features/calculator/domain/entities/enum/currency_type.dart b/lib/src/features/calculator/domain/entities/enum/currency_type.dart
new file mode 100644
index 00000000..3bf0b2d7
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/enum/currency_type.dart
@@ -0,0 +1 @@
+enum CurrencyType { fiat, crypto }
diff --git a/lib/src/features/calculator/domain/entities/exchange_calculation_result.dart b/lib/src/features/calculator/domain/entities/exchange_calculation_result.dart
new file mode 100644
index 00000000..d9aa6a56
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/exchange_calculation_result.dart
@@ -0,0 +1,9 @@
+final class ExchangeCalculationResult {
+ final double convertedAmount;
+ final double exchangeRate;
+
+ const ExchangeCalculationResult({
+ required this.convertedAmount,
+ required this.exchangeRate,
+ });
+}
diff --git a/lib/src/features/calculator/domain/entities/exchange_rate.dart b/lib/src/features/calculator/domain/entities/exchange_rate.dart
new file mode 100644
index 00000000..6dcc293a
--- /dev/null
+++ b/lib/src/features/calculator/domain/entities/exchange_rate.dart
@@ -0,0 +1,5 @@
+final class ExchangeRate {
+ final double fiatToCryptoExchangeRate;
+
+ const ExchangeRate({required this.fiatToCryptoExchangeRate});
+}
diff --git a/lib/src/features/calculator/domain/repositories/calculator_repository.dart b/lib/src/features/calculator/domain/repositories/calculator_repository.dart
new file mode 100644
index 00000000..4fd48031
--- /dev/null
+++ b/lib/src/features/calculator/domain/repositories/calculator_repository.dart
@@ -0,0 +1,9 @@
+abstract class CalculatorRepository {
+ Future getExchangeRate({
+ required String cryptoCurrencyId,
+ required String fiatCurrencyId,
+ required String amount,
+ required String amountCurrencyId,
+ required int type,
+ });
+}
diff --git a/lib/src/features/calculator/domain/repositories/currency_repository.dart b/lib/src/features/calculator/domain/repositories/currency_repository.dart
new file mode 100644
index 00000000..94867bb7
--- /dev/null
+++ b/lib/src/features/calculator/domain/repositories/currency_repository.dart
@@ -0,0 +1,5 @@
+import '../entities/currency.dart';
+
+abstract class CurrencyRepository {
+ Future> getAvailableCurrencies();
+}
diff --git a/lib/src/features/calculator/domain/usecases/calculate_crypto_exchange_usecase.dart b/lib/src/features/calculator/domain/usecases/calculate_crypto_exchange_usecase.dart
new file mode 100644
index 00000000..52ef9589
--- /dev/null
+++ b/lib/src/features/calculator/domain/usecases/calculate_crypto_exchange_usecase.dart
@@ -0,0 +1,54 @@
+import '../../../../core/constants/string_constants.dart';
+import '../../domain/entities/enum/currency_type.dart';
+import '../entities/calculate_exchange_params.dart';
+import '../entities/exchange_calculation_result.dart';
+import '../repositories/calculator_repository.dart';
+
+/// Use Case: Calcular el cambio entre criptomonedas y monedas fiat
+final class CalculateCryptoExchangeUseCase {
+ final CalculatorRepository _repository;
+
+ const CalculateCryptoExchangeUseCase({
+ required CalculatorRepository repository,
+ }) : _repository = repository;
+
+ Future call({
+ required CalculateExchangeParams params,
+ }) async {
+ if (params.amount <= 0) {
+ throw ArgumentError(StringConstants.amountMustBePositiveError);
+ }
+
+ final bool isCryptoToFiat =
+ params.fromCurrency.type == CurrencyType.crypto &&
+ params.toCurrency.type == CurrencyType.fiat;
+
+ final int exchangeType = isCryptoToFiat ? 0 : 1;
+
+ final String cryptoCurrencyId =
+ params.fromCurrency.type == CurrencyType.crypto
+ ? params.fromCurrency.id
+ : params.toCurrency.id;
+
+ final String fiatCurrencyId = params.fromCurrency.type == CurrencyType.fiat
+ ? params.fromCurrency.id
+ : params.toCurrency.id;
+
+ final double exchangeRate = await _repository.getExchangeRate(
+ cryptoCurrencyId: cryptoCurrencyId,
+ fiatCurrencyId: fiatCurrencyId,
+ amount: params.amount.toString(),
+ amountCurrencyId: params.fromCurrency.id,
+ type: exchangeType,
+ );
+
+ final convertedAmount = isCryptoToFiat
+ ? params.amount * exchangeRate
+ : params.amount / exchangeRate;
+
+ return ExchangeCalculationResult(
+ convertedAmount: convertedAmount,
+ exchangeRate: exchangeRate,
+ );
+ }
+}
diff --git a/lib/src/features/calculator/domain/usecases/get_available_currencies_usecase.dart b/lib/src/features/calculator/domain/usecases/get_available_currencies_usecase.dart
new file mode 100644
index 00000000..82a1df07
--- /dev/null
+++ b/lib/src/features/calculator/domain/usecases/get_available_currencies_usecase.dart
@@ -0,0 +1,12 @@
+import '../entities/currency.dart';
+import '../repositories/currency_repository.dart';
+
+final class GetAvailableCurrenciesUseCase {
+ final CurrencyRepository repository;
+
+ const GetAvailableCurrenciesUseCase({required this.repository});
+
+ Future> call() async {
+ return await repository.getAvailableCurrencies();
+ }
+}
diff --git a/lib/src/features/calculator/domain/usecases/validate_exchange_amount_usecase.dart b/lib/src/features/calculator/domain/usecases/validate_exchange_amount_usecase.dart
new file mode 100644
index 00000000..2d4975ba
--- /dev/null
+++ b/lib/src/features/calculator/domain/usecases/validate_exchange_amount_usecase.dart
@@ -0,0 +1,44 @@
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/constants/validation_constants.dart';
+
+/// Result de validación de monto
+sealed class AmountValidationResult {
+ const AmountValidationResult();
+}
+
+/// Validación exitosa
+final class AmountValidationSuccess extends AmountValidationResult {
+ const AmountValidationSuccess();
+}
+
+/// Validación fallida con mensaje de error
+final class AmountValidationFailure extends AmountValidationResult {
+ final String errorMessage;
+
+ const AmountValidationFailure(this.errorMessage);
+}
+
+/// Use Case para validar el monto de intercambio
+///
+/// Centraliza todas las reglas de validación de montos:
+/// - Debe ser mayor a 0
+/// - No debe exceder el límite máximo
+///
+/// Retorna un [AmountValidationResult] que puede ser Success o Failure.
+final class ValidateExchangeAmountUseCase {
+ const ValidateExchangeAmountUseCase();
+
+ AmountValidationResult call(double amount) {
+ if (amount <= ValidationConstants.minExchangeAmount) {
+ return const AmountValidationFailure(StringConstants.invalidAmountError);
+ }
+
+ if (amount > ValidationConstants.maxExchangeAmount) {
+ return const AmountValidationFailure(
+ StringConstants.amountExceedsLimitError,
+ );
+ }
+
+ return const AmountValidationSuccess();
+ }
+}
diff --git a/lib/src/features/calculator/presentation/providers/amount_input_field_controller.dart b/lib/src/features/calculator/presentation/providers/amount_input_field_controller.dart
new file mode 100644
index 00000000..f45678bb
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/amount_input_field_controller.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../states/amount_input_field_state.dart';
+
+part 'amount_input_field_controller.g.dart';
+
+@riverpod
+final class AmountInputFieldController extends _$AmountInputFieldController {
+ late final TextEditingController textController;
+
+ @override
+ AmountInputFieldState build() {
+ textController = TextEditingController();
+
+ ref.onDispose(() => textController.dispose());
+
+ return const AmountInputFieldState();
+ }
+
+ void onTextChanged({required String value}) {
+ final cleanValue = value.replaceAll(',', '');
+ final amount = double.tryParse(cleanValue) ?? 0.0;
+ state = state.copyWith(amount: amount, errorText: null);
+ }
+
+ void setError({required String? errorText}) =>
+ state = state.copyWith(errorText: errorText);
+
+ void selectAll() => textController.selection = TextSelection(
+ baseOffset: 0,
+ extentOffset: textController.text.length,
+ );
+}
diff --git a/lib/src/features/calculator/presentation/providers/amount_input_field_controller.g.dart b/lib/src/features/calculator/presentation/providers/amount_input_field_controller.g.dart
new file mode 100644
index 00000000..99e76761
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/amount_input_field_controller.g.dart
@@ -0,0 +1,67 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'amount_input_field_controller.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(AmountInputFieldController)
+const amountInputFieldControllerProvider =
+ AmountInputFieldControllerProvider._();
+
+final class AmountInputFieldControllerProvider
+ extends
+ $NotifierProvider {
+ const AmountInputFieldControllerProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'amountInputFieldControllerProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$amountInputFieldControllerHash();
+
+ @$internal
+ @override
+ AmountInputFieldController create() => AmountInputFieldController();
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(AmountInputFieldState value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$amountInputFieldControllerHash() =>
+ r'1cc24033a231044d02d3cff7a925a62c8e39185e';
+
+abstract class _$AmountInputFieldController
+ extends $Notifier {
+ AmountInputFieldState build();
+ @$mustCallSuper
+ @override
+ void runBuild() {
+ final created = build();
+ final ref = this.ref as $Ref;
+ final element =
+ ref.element
+ as $ClassProviderElement<
+ AnyNotifier,
+ AmountInputFieldState,
+ Object?,
+ Object?
+ >;
+ element.handleValue(ref, created);
+ }
+}
diff --git a/lib/src/features/calculator/presentation/providers/calculator_controller.dart b/lib/src/features/calculator/presentation/providers/calculator_controller.dart
new file mode 100644
index 00000000..2cee7349
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/calculator_controller.dart
@@ -0,0 +1,170 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/errors/failures.dart';
+import '../../domain/entities/calculate_exchange_params.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/exchange_calculation_result.dart';
+import '../../domain/usecases/validate_exchange_amount_usecase.dart';
+import '../states/calculator_state.dart';
+import 'providers.dart';
+
+part 'calculator_controller.g.dart';
+
+@riverpod
+final class CalculatorController extends _$CalculatorController {
+ final ValidateExchangeAmountUseCase _validateAmountUseCase =
+ const ValidateExchangeAmountUseCase();
+
+ @override
+ CalculatorState build() {
+ final AsyncValue> currenciesAsync = ref.watch(
+ availableCurrenciesProvider,
+ );
+
+ return currenciesAsync.when(
+ data: (currencies) {
+ final Currency usdt = currencies.firstWhere(
+ (c) => c.id == 'TATUM-TRON-USDT',
+ );
+ final Currency ves = currencies.firstWhere((c) => c.id == 'VES');
+
+ return CalculatorState(fromCurrency: usdt, toCurrency: ves);
+ },
+ loading: () => const CalculatorState(),
+ error: (error, stack) => const CalculatorState(),
+ );
+ }
+
+ void setFromCurrency({required Currency currency}) {
+ state = _updateCurrency(
+ currency: currency,
+ isFrom: true,
+ currentFrom: state.fromCurrency,
+ currentTo: state.toCurrency,
+ );
+ }
+
+ void setToCurrency({required Currency currency}) {
+ state = _updateCurrency(
+ currency: currency,
+ isFrom: false,
+ currentFrom: state.fromCurrency,
+ currentTo: state.toCurrency,
+ );
+ }
+
+ void setAmount({required double amount}) {
+ state = state.copyWith(
+ amount: amount,
+ errorMessage: null,
+ inputFieldError: null,
+ );
+ }
+
+ void swapCurrencies() {
+ if (!state.canSwap) return;
+
+ state = state.copyWith(
+ fromCurrency: state.toCurrency,
+ toCurrency: state.fromCurrency,
+ convertedAmount: null,
+ estimatedRate: null,
+ errorMessage: null,
+ inputFieldError: null,
+ );
+ }
+
+ Future calculateExchange() async {
+ if (!state.canCalculate) return;
+
+ final AmountValidationResult validationResult = _validateAmountUseCase(
+ state.amount,
+ );
+
+ if (validationResult is AmountValidationFailure) {
+ _handleValidationError(validationResult.errorMessage);
+ return;
+ }
+
+ state = state.copyWith(
+ isLoading: true,
+ errorMessage: null,
+ inputFieldError: null,
+ );
+
+ try {
+ final result = await _performCalculation();
+ _handleCalculationSuccess(result);
+ } on Failure catch (failure) {
+ _handleCalculationError(failure.message);
+ } catch (e) {
+ _handleCalculationError(StringConstants.errorMessage);
+ }
+ }
+
+ CalculatorState _updateCurrency({
+ required Currency currency,
+ required bool isFrom,
+ required Currency? currentFrom,
+ required Currency? currentTo,
+ }) {
+ final otherCurrency = isFrom ? currentTo : currentFrom;
+
+ if (otherCurrency != null && currency.type == otherCurrency.type) {
+ return state.copyWith(
+ fromCurrency: isFrom ? currency : otherCurrency,
+ toCurrency: isFrom ? otherCurrency : currency,
+ errorMessage: null,
+ convertedAmount: null,
+ estimatedRate: null,
+ inputFieldError: null,
+ );
+ }
+
+ return state.copyWith(
+ fromCurrency: isFrom ? currency : currentFrom,
+ toCurrency: isFrom ? currentTo : currency,
+ errorMessage: null,
+ convertedAmount: null,
+ estimatedRate: null,
+ inputFieldError: null,
+ );
+ }
+
+ /// Realiza el cálculo de intercambio
+ Future _performCalculation() async {
+ final calculateUseCase = ref.read(calculateCryptoExchangeUseCaseProvider);
+
+ final params = CalculateExchangeParams(
+ fromCurrency: state.fromCurrency!,
+ toCurrency: state.toCurrency!,
+ amount: state.amount,
+ );
+
+ return await calculateUseCase(params: params);
+ }
+
+ /// Maneja el resultado exitoso del cálculo
+ void _handleCalculationSuccess(ExchangeCalculationResult result) {
+ state = state.copyWith(
+ isLoading: false,
+ convertedAmount: result.convertedAmount,
+ estimatedRate: result.exchangeRate,
+ );
+ }
+
+ /// Maneja errores de validación
+ void _handleValidationError(String errorMessage) {
+ state = state.copyWith(inputFieldError: errorMessage);
+ }
+
+ /// Maneja errores durante el cálculo
+ void _handleCalculationError(String errorMessage) {
+ state = state.copyWith(
+ isLoading: false,
+ errorMessage: errorMessage,
+ inputFieldError: errorMessage,
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/providers/calculator_controller.g.dart b/lib/src/features/calculator/presentation/providers/calculator_controller.g.dart
new file mode 100644
index 00000000..20ba7a5b
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/calculator_controller.g.dart
@@ -0,0 +1,64 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'calculator_controller.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(CalculatorController)
+const calculatorControllerProvider = CalculatorControllerProvider._();
+
+final class CalculatorControllerProvider
+ extends $NotifierProvider {
+ const CalculatorControllerProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'calculatorControllerProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$calculatorControllerHash();
+
+ @$internal
+ @override
+ CalculatorController create() => CalculatorController();
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(CalculatorState value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$calculatorControllerHash() =>
+ r'350199e59a6ab62077009e1bd5c2ed8f39f5717b';
+
+abstract class _$CalculatorController extends $Notifier {
+ CalculatorState build();
+ @$mustCallSuper
+ @override
+ void runBuild() {
+ final created = build();
+ final ref = this.ref as $Ref;
+ final element =
+ ref.element
+ as $ClassProviderElement<
+ AnyNotifier,
+ CalculatorState,
+ Object?,
+ Object?
+ >;
+ element.handleValue(ref, created);
+ }
+}
diff --git a/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.dart b/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.dart
new file mode 100644
index 00000000..001632a4
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.dart
@@ -0,0 +1,18 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../domain/entities/enum/bottom_sheet_type.dart';
+
+part 'currency_bottom_sheet_controller.g.dart';
+
+@riverpod
+final class CurrencyBottomSheetController
+ extends _$CurrencyBottomSheetController {
+ @override
+ BottomSheetType build() => BottomSheetType.none;
+
+ void openFrom() => state = BottomSheetType.from;
+
+ void openTo() => state = BottomSheetType.to;
+
+ void close() => state = BottomSheetType.none;
+}
diff --git a/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.g.dart b/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.g.dart
new file mode 100644
index 00000000..a6d7ee68
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/currency_bottom_sheet_controller.g.dart
@@ -0,0 +1,66 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'currency_bottom_sheet_controller.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(CurrencyBottomSheetController)
+const currencyBottomSheetControllerProvider =
+ CurrencyBottomSheetControllerProvider._();
+
+final class CurrencyBottomSheetControllerProvider
+ extends $NotifierProvider {
+ const CurrencyBottomSheetControllerProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'currencyBottomSheetControllerProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$currencyBottomSheetControllerHash();
+
+ @$internal
+ @override
+ CurrencyBottomSheetController create() => CurrencyBottomSheetController();
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(BottomSheetType value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$currencyBottomSheetControllerHash() =>
+ r'd0c8b81d0315d0193e581c33d661d63452641928';
+
+abstract class _$CurrencyBottomSheetController
+ extends $Notifier {
+ BottomSheetType build();
+ @$mustCallSuper
+ @override
+ void runBuild() {
+ final created = build();
+ final ref = this.ref as $Ref;
+ final element =
+ ref.element
+ as $ClassProviderElement<
+ AnyNotifier,
+ BottomSheetType,
+ Object?,
+ Object?
+ >;
+ element.handleValue(ref, created);
+ }
+}
diff --git a/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.dart b/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.dart
new file mode 100644
index 00000000..7772a507
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.dart
@@ -0,0 +1,30 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/currency_type.dart';
+
+part 'exchange_rate_display_provider.g.dart';
+
+/// Provider que calcula la tasa de visualización según el tipo de conversión
+///
+/// Invierte la tasa cuando es una conversión de fiat a crypto para
+/// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+@riverpod
+double? exchangeRateDisplay(
+ Ref ref, {
+ required double? estimatedRate,
+ required Currency? fromCurrency,
+ required Currency? toCurrency,
+}) {
+ if (estimatedRate == null ||
+ fromCurrency == null ||
+ toCurrency == null) {
+ return null;
+ }
+
+ final bool isCryptoToFiat = fromCurrency.type == CurrencyType.crypto &&
+ toCurrency.type == CurrencyType.fiat;
+
+ return isCryptoToFiat ? estimatedRate : 1 / estimatedRate;
+}
+
diff --git a/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.g.dart b/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.g.dart
new file mode 100644
index 00000000..6544abb3
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/exchange_rate_display_provider.g.dart
@@ -0,0 +1,145 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'exchange_rate_display_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+/// Provider que calcula la tasa de visualización según el tipo de conversión
+///
+/// Invierte la tasa cuando es una conversión de fiat a crypto para
+/// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+
+@ProviderFor(exchangeRateDisplay)
+const exchangeRateDisplayProvider = ExchangeRateDisplayFamily._();
+
+/// Provider que calcula la tasa de visualización según el tipo de conversión
+///
+/// Invierte la tasa cuando es una conversión de fiat a crypto para
+/// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+
+final class ExchangeRateDisplayProvider
+ extends $FunctionalProvider
+ with $Provider {
+ /// Provider que calcula la tasa de visualización según el tipo de conversión
+ ///
+ /// Invierte la tasa cuando es una conversión de fiat a crypto para
+ /// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+ const ExchangeRateDisplayProvider._({
+ required ExchangeRateDisplayFamily super.from,
+ required ({
+ double? estimatedRate,
+ Currency? fromCurrency,
+ Currency? toCurrency,
+ })
+ super.argument,
+ }) : super(
+ retry: null,
+ name: r'exchangeRateDisplayProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$exchangeRateDisplayHash();
+
+ @override
+ String toString() {
+ return r'exchangeRateDisplayProvider'
+ ''
+ '$argument';
+ }
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ double? create(Ref ref) {
+ final argument =
+ this.argument
+ as ({
+ double? estimatedRate,
+ Currency? fromCurrency,
+ Currency? toCurrency,
+ });
+ return exchangeRateDisplay(
+ ref,
+ estimatedRate: argument.estimatedRate,
+ fromCurrency: argument.fromCurrency,
+ toCurrency: argument.toCurrency,
+ );
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(double? value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is ExchangeRateDisplayProvider && other.argument == argument;
+ }
+
+ @override
+ int get hashCode {
+ return argument.hashCode;
+ }
+}
+
+String _$exchangeRateDisplayHash() =>
+ r'587162ddb28600a3dfe663100f90a392bec08696';
+
+/// Provider que calcula la tasa de visualización según el tipo de conversión
+///
+/// Invierte la tasa cuando es una conversión de fiat a crypto para
+/// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+
+final class ExchangeRateDisplayFamily extends $Family
+ with
+ $FunctionalFamilyOverride<
+ double?,
+ ({
+ double? estimatedRate,
+ Currency? fromCurrency,
+ Currency? toCurrency,
+ })
+ > {
+ const ExchangeRateDisplayFamily._()
+ : super(
+ retry: null,
+ name: r'exchangeRateDisplayProvider',
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ isAutoDispose: true,
+ );
+
+ /// Provider que calcula la tasa de visualización según el tipo de conversión
+ ///
+ /// Invierte la tasa cuando es una conversión de fiat a crypto para
+ /// mostrar siempre la tasa desde la perspectiva de la moneda origen.
+
+ ExchangeRateDisplayProvider call({
+ required double? estimatedRate,
+ required Currency? fromCurrency,
+ required Currency? toCurrency,
+ }) => ExchangeRateDisplayProvider._(
+ argument: (
+ estimatedRate: estimatedRate,
+ fromCurrency: fromCurrency,
+ toCurrency: toCurrency,
+ ),
+ from: this,
+ );
+
+ @override
+ String toString() => r'exchangeRateDisplayProvider';
+}
diff --git a/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.dart b/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.dart
new file mode 100644
index 00000000..41df1386
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.dart
@@ -0,0 +1,22 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/currency_type.dart';
+import 'providers.dart';
+
+part 'filtered_currencies_provider.g.dart';
+
+/// Provider que filtra las monedas disponibles por tipo
+///
+/// Retorna solo las monedas del tipo especificado (crypto o fiat),
+/// facilitando la presentación en diferentes selectores.
+@riverpod
+Future> filteredCurrencies(
+ Ref ref, {
+ required CurrencyType currencyType,
+}) async {
+ final allCurrencies = await ref.watch(availableCurrenciesProvider.future);
+
+ return allCurrencies.where((c) => c.type == currencyType).toList();
+}
+
diff --git a/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.g.dart b/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.g.dart
new file mode 100644
index 00000000..9b660018
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/filtered_currencies_provider.g.dart
@@ -0,0 +1,109 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'filtered_currencies_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+/// Provider que filtra las monedas disponibles por tipo
+///
+/// Retorna solo las monedas del tipo especificado (crypto o fiat),
+/// facilitando la presentación en diferentes selectores.
+
+@ProviderFor(filteredCurrencies)
+const filteredCurrenciesProvider = FilteredCurrenciesFamily._();
+
+/// Provider que filtra las monedas disponibles por tipo
+///
+/// Retorna solo las monedas del tipo especificado (crypto o fiat),
+/// facilitando la presentación en diferentes selectores.
+
+final class FilteredCurrenciesProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue>,
+ List,
+ FutureOr>
+ >
+ with $FutureModifier>, $FutureProvider> {
+ /// Provider que filtra las monedas disponibles por tipo
+ ///
+ /// Retorna solo las monedas del tipo especificado (crypto o fiat),
+ /// facilitando la presentación en diferentes selectores.
+ const FilteredCurrenciesProvider._({
+ required FilteredCurrenciesFamily super.from,
+ required CurrencyType super.argument,
+ }) : super(
+ retry: null,
+ name: r'filteredCurrenciesProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$filteredCurrenciesHash();
+
+ @override
+ String toString() {
+ return r'filteredCurrenciesProvider'
+ ''
+ '($argument)';
+ }
+
+ @$internal
+ @override
+ $FutureProviderElement> $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr> create(Ref ref) {
+ final argument = this.argument as CurrencyType;
+ return filteredCurrencies(ref, currencyType: argument);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is FilteredCurrenciesProvider && other.argument == argument;
+ }
+
+ @override
+ int get hashCode {
+ return argument.hashCode;
+ }
+}
+
+String _$filteredCurrenciesHash() =>
+ r'0cf2f51de851d7e02c3ce6a051042ad06a73dcd4';
+
+/// Provider que filtra las monedas disponibles por tipo
+///
+/// Retorna solo las monedas del tipo especificado (crypto o fiat),
+/// facilitando la presentación en diferentes selectores.
+
+final class FilteredCurrenciesFamily extends $Family
+ with $FunctionalFamilyOverride>, CurrencyType> {
+ const FilteredCurrenciesFamily._()
+ : super(
+ retry: null,
+ name: r'filteredCurrenciesProvider',
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ isAutoDispose: true,
+ );
+
+ /// Provider que filtra las monedas disponibles por tipo
+ ///
+ /// Retorna solo las monedas del tipo especificado (crypto o fiat),
+ /// facilitando la presentación en diferentes selectores.
+
+ FilteredCurrenciesProvider call({required CurrencyType currencyType}) =>
+ FilteredCurrenciesProvider._(argument: currencyType, from: this);
+
+ @override
+ String toString() => r'filteredCurrenciesProvider';
+}
diff --git a/lib/src/features/calculator/presentation/providers/providers.dart b/lib/src/features/calculator/presentation/providers/providers.dart
new file mode 100644
index 00000000..3ad9eff1
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/providers.dart
@@ -0,0 +1,73 @@
+import 'package:dio/dio.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../../../core/network/dio_client.dart';
+import '../../data/datasources/currency_local_datasource.dart';
+import '../../data/datasources/currency_local_datasource_impl.dart';
+import '../../data/datasources/exchange_rate_remote_datasource_impl.dart';
+import '../../data/repositories/calculator_repository_impl.dart';
+import '../../data/repositories/currency_repository_impl.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/repositories/calculator_repository.dart';
+import '../../domain/repositories/currency_repository.dart';
+import '../../domain/usecases/calculate_crypto_exchange_usecase.dart';
+import '../../domain/usecases/get_available_currencies_usecase.dart';
+
+part 'providers.g.dart';
+
+@riverpod
+Dio dio(Ref ref) => DioClient.instance;
+
+@riverpod
+ExchangeRateRemoteDataSourceImpl exchangeRateRemoteDataSource(Ref ref) =>
+ ExchangeRateRemoteDataSourceImpl(dio: ref.read(dioProvider));
+
+@riverpod
+CalculatorRepository calculatorRepository(Ref ref) => CalculatorRepositoryImpl(
+ exchangeRateRemoteDataSource: ref.read(exchangeRateRemoteDataSourceProvider),
+);
+
+@riverpod
+CalculateCryptoExchangeUseCase calculateCryptoExchangeUseCase(Ref ref) =>
+ CalculateCryptoExchangeUseCase(
+ repository: ref.read(calculatorRepositoryProvider),
+ );
+
+@riverpod
+CurrencyLocalDataSource currencyLocalDataSource(Ref ref) =>
+ const CurrencyLocalDataSourceImpl();
+
+@riverpod
+CurrencyRepository currencyRepository(Ref ref) => CurrencyRepositoryImpl(
+ localDataSource: ref.read(currencyLocalDataSourceProvider),
+);
+
+@riverpod
+GetAvailableCurrenciesUseCase getAvailableCurrenciesUseCase(Ref ref) =>
+ GetAvailableCurrenciesUseCase(
+ repository: ref.read(currencyRepositoryProvider),
+ );
+
+@riverpod
+Future> availableCurrencies(Ref ref) async {
+ final GetAvailableCurrenciesUseCase useCase = ref.read(
+ getAvailableCurrenciesUseCaseProvider,
+ );
+ return await useCase();
+}
+
+@riverpod
+Currency defaultCryptoCurrency(Ref ref) {
+ final List currencies = ref
+ .watch(availableCurrenciesProvider)
+ .requireValue;
+ return currencies.firstWhere((c) => c.id == 'TATUM-TRON-USDT');
+}
+
+@riverpod
+Currency defaultFiatCurrency(Ref ref) {
+ final List currencies = ref
+ .watch(availableCurrenciesProvider)
+ .requireValue;
+ return currencies.firstWhere((c) => c.id == 'VES');
+}
diff --git a/lib/src/features/calculator/presentation/providers/providers.g.dart b/lib/src/features/calculator/presentation/providers/providers.g.dart
new file mode 100644
index 00000000..8fca82c9
--- /dev/null
+++ b/lib/src/features/calculator/presentation/providers/providers.g.dart
@@ -0,0 +1,471 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'providers.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(dio)
+const dioProvider = DioProvider._();
+
+final class DioProvider extends $FunctionalProvider
+ with $Provider {
+ const DioProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'dioProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$dioHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ Dio create(Ref ref) {
+ return dio(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(Dio value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$dioHash() => r'fb5e1293a1d2f2aca782a019e4e71351db0ad1ab';
+
+@ProviderFor(exchangeRateRemoteDataSource)
+const exchangeRateRemoteDataSourceProvider =
+ ExchangeRateRemoteDataSourceProvider._();
+
+final class ExchangeRateRemoteDataSourceProvider
+ extends
+ $FunctionalProvider<
+ ExchangeRateRemoteDataSourceImpl,
+ ExchangeRateRemoteDataSourceImpl,
+ ExchangeRateRemoteDataSourceImpl
+ >
+ with $Provider {
+ const ExchangeRateRemoteDataSourceProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'exchangeRateRemoteDataSourceProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$exchangeRateRemoteDataSourceHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ ExchangeRateRemoteDataSourceImpl create(Ref ref) {
+ return exchangeRateRemoteDataSource(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(ExchangeRateRemoteDataSourceImpl value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(
+ value,
+ ),
+ );
+ }
+}
+
+String _$exchangeRateRemoteDataSourceHash() =>
+ r'2114e183cc38b2c7cfc6d3630762182ee1cf602a';
+
+@ProviderFor(calculatorRepository)
+const calculatorRepositoryProvider = CalculatorRepositoryProvider._();
+
+final class CalculatorRepositoryProvider
+ extends
+ $FunctionalProvider<
+ CalculatorRepository,
+ CalculatorRepository,
+ CalculatorRepository
+ >
+ with $Provider {
+ const CalculatorRepositoryProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'calculatorRepositoryProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$calculatorRepositoryHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ CalculatorRepository create(Ref ref) {
+ return calculatorRepository(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(CalculatorRepository value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$calculatorRepositoryHash() =>
+ r'a932b0a7f40e74725b8923d93afab78ab7dc30c4';
+
+@ProviderFor(calculateCryptoExchangeUseCase)
+const calculateCryptoExchangeUseCaseProvider =
+ CalculateCryptoExchangeUseCaseProvider._();
+
+final class CalculateCryptoExchangeUseCaseProvider
+ extends
+ $FunctionalProvider<
+ CalculateCryptoExchangeUseCase,
+ CalculateCryptoExchangeUseCase,
+ CalculateCryptoExchangeUseCase
+ >
+ with $Provider {
+ const CalculateCryptoExchangeUseCaseProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'calculateCryptoExchangeUseCaseProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$calculateCryptoExchangeUseCaseHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ CalculateCryptoExchangeUseCase create(Ref ref) {
+ return calculateCryptoExchangeUseCase(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(CalculateCryptoExchangeUseCase value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(
+ value,
+ ),
+ );
+ }
+}
+
+String _$calculateCryptoExchangeUseCaseHash() =>
+ r'cdc2a18aa9672dbd95dcac9016fd358f283eac2f';
+
+@ProviderFor(currencyLocalDataSource)
+const currencyLocalDataSourceProvider = CurrencyLocalDataSourceProvider._();
+
+final class CurrencyLocalDataSourceProvider
+ extends
+ $FunctionalProvider<
+ CurrencyLocalDataSource,
+ CurrencyLocalDataSource,
+ CurrencyLocalDataSource
+ >
+ with $Provider {
+ const CurrencyLocalDataSourceProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'currencyLocalDataSourceProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$currencyLocalDataSourceHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ CurrencyLocalDataSource create(Ref ref) {
+ return currencyLocalDataSource(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(CurrencyLocalDataSource value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$currencyLocalDataSourceHash() =>
+ r'207f1c539760104831a3c0041fa0de576fc3fd45';
+
+@ProviderFor(currencyRepository)
+const currencyRepositoryProvider = CurrencyRepositoryProvider._();
+
+final class CurrencyRepositoryProvider
+ extends
+ $FunctionalProvider<
+ CurrencyRepository,
+ CurrencyRepository,
+ CurrencyRepository
+ >
+ with $Provider {
+ const CurrencyRepositoryProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'currencyRepositoryProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$currencyRepositoryHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ CurrencyRepository create(Ref ref) {
+ return currencyRepository(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(CurrencyRepository value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$currencyRepositoryHash() =>
+ r'b7519b9d2ae591a658bc1e3cc81aaed7cd5f7018';
+
+@ProviderFor(getAvailableCurrenciesUseCase)
+const getAvailableCurrenciesUseCaseProvider =
+ GetAvailableCurrenciesUseCaseProvider._();
+
+final class GetAvailableCurrenciesUseCaseProvider
+ extends
+ $FunctionalProvider<
+ GetAvailableCurrenciesUseCase,
+ GetAvailableCurrenciesUseCase,
+ GetAvailableCurrenciesUseCase
+ >
+ with $Provider {
+ const GetAvailableCurrenciesUseCaseProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'getAvailableCurrenciesUseCaseProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$getAvailableCurrenciesUseCaseHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $ProviderElement(pointer);
+
+ @override
+ GetAvailableCurrenciesUseCase create(Ref ref) {
+ return getAvailableCurrenciesUseCase(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(GetAvailableCurrenciesUseCase value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(
+ value,
+ ),
+ );
+ }
+}
+
+String _$getAvailableCurrenciesUseCaseHash() =>
+ r'3b46da41f0bd50241de23340966fa4fb2fec4d96';
+
+@ProviderFor(availableCurrencies)
+const availableCurrenciesProvider = AvailableCurrenciesProvider._();
+
+final class AvailableCurrenciesProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue>,
+ List,
+ FutureOr>
+ >
+ with $FutureModifier>, $FutureProvider> {
+ const AvailableCurrenciesProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'availableCurrenciesProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$availableCurrenciesHash();
+
+ @$internal
+ @override
+ $FutureProviderElement> $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr> create(Ref ref) {
+ return availableCurrencies(ref);
+ }
+}
+
+String _$availableCurrenciesHash() =>
+ r'6d817799ed5ee10a628d91afda08892fd59236f0';
+
+@ProviderFor(defaultCryptoCurrency)
+const defaultCryptoCurrencyProvider = DefaultCryptoCurrencyProvider._();
+
+final class DefaultCryptoCurrencyProvider
+ extends $FunctionalProvider
+ with $Provider {
+ const DefaultCryptoCurrencyProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'defaultCryptoCurrencyProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$defaultCryptoCurrencyHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ Currency create(Ref ref) {
+ return defaultCryptoCurrency(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(Currency value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$defaultCryptoCurrencyHash() =>
+ r'21e5361c97090785ce917a99620ec6d740e0e1f1';
+
+@ProviderFor(defaultFiatCurrency)
+const defaultFiatCurrencyProvider = DefaultFiatCurrencyProvider._();
+
+final class DefaultFiatCurrencyProvider
+ extends $FunctionalProvider
+ with $Provider {
+ const DefaultFiatCurrencyProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'defaultFiatCurrencyProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$defaultFiatCurrencyHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ Currency create(Ref ref) {
+ return defaultFiatCurrency(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(Currency value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$defaultFiatCurrencyHash() =>
+ r'6e94296a9a4d3150e03b9c92b68c4ba7001da280';
diff --git a/lib/src/features/calculator/presentation/screens/calculator_screen.dart b/lib/src/features/calculator/presentation/screens/calculator_screen.dart
new file mode 100644
index 00000000..7db416c0
--- /dev/null
+++ b/lib/src/features/calculator/presentation/screens/calculator_screen.dart
@@ -0,0 +1,123 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/ui/painters/background_painter.dart';
+import '../../../../core/widgets/loading_button.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/currency_type.dart';
+import '../providers/calculator_controller.dart';
+import '../providers/currency_bottom_sheet_controller.dart';
+import '../states/calculator_state.dart';
+import '../widgets/amount_input_field.dart';
+import '../widgets/currency_bottom_sheet.dart';
+import '../widgets/currency_exchange_bar.dart';
+import '../widgets/exchange_info_card.dart';
+
+final class CalculatorScreen extends ConsumerWidget {
+ const CalculatorScreen({super.key});
+
+ Future _showCurrencyBottomSheet({
+ required BuildContext context,
+ required WidgetRef ref,
+ required Currency? currency,
+ required void Function(Currency) onCurrencySelected,
+ required bool isFromSelector,
+ }) async {
+ if (isFromSelector) {
+ ref.read(currencyBottomSheetControllerProvider.notifier).openFrom();
+ } else {
+ ref.read(currencyBottomSheetControllerProvider.notifier).openTo();
+ }
+
+ final CurrencyType currencyType = currency?.type ?? CurrencyType.crypto;
+
+ await showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder: (context) => CurrencyBottomSheet(
+ selectedCurrency: currency,
+ onCurrencySelected: onCurrencySelected,
+ currencyType: currencyType,
+ ),
+ ).whenComplete(
+ () => ref.read(currencyBottomSheetControllerProvider.notifier).close(),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final CalculatorState state = ref.watch(calculatorControllerProvider);
+ final CalculatorController controller = ref.read(
+ calculatorControllerProvider.notifier,
+ );
+
+ return Scaffold(
+ body: SafeArea(
+ child: CustomPaint(
+ painter: BackgroundPainter(),
+ child: Center(
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20.0),
+ child: Card(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 38.0,
+ ),
+ child: Column(
+ spacing: 16.0,
+ children: [
+ CurrencyExchangeBar(
+ fromCurrency: state.fromCurrency,
+ toCurrency: state.toCurrency,
+ onFromTap: () => _showCurrencyBottomSheet(
+ context: context,
+ ref: ref,
+ currency: state.fromCurrency,
+ onCurrencySelected: (currency) =>
+ controller.setFromCurrency(currency: currency),
+ isFromSelector: true,
+ ),
+ onSwap: controller.swapCurrencies,
+ onToTap: () => _showCurrencyBottomSheet(
+ context: context,
+ ref: ref,
+ currency: state.toCurrency,
+ onCurrencySelected: (currency) =>
+ controller.setToCurrency(currency: currency),
+ isFromSelector: false,
+ ),
+ canSwap: state.canSwap,
+ ),
+ const AmountInputField(),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 10.0),
+ child: ExchangeInfoCard(
+ estimatedRate: state.estimatedRate,
+ convertedAmount: state.convertedAmount,
+ fromCurrency: state.fromCurrency,
+ toCurrency: state.toCurrency,
+ isLoading: state.isLoading,
+ ),
+ ),
+ LoadingButton(
+ text: StringConstants.exchangeButtonText,
+ onPressed: controller.calculateExchange,
+ isLoading: state.isLoading,
+ isEnabled: state.canCalculate,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/states/amount_input_field_state.dart b/lib/src/features/calculator/presentation/states/amount_input_field_state.dart
new file mode 100644
index 00000000..56368278
--- /dev/null
+++ b/lib/src/features/calculator/presentation/states/amount_input_field_state.dart
@@ -0,0 +1,15 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'amount_input_field_state.freezed.dart';
+
+@freezed
+abstract class AmountInputFieldState with _$AmountInputFieldState {
+ const factory AmountInputFieldState({
+ @Default(0.0) double amount,
+ String? errorText,
+ }) = _AmountInputFieldState;
+
+ const AmountInputFieldState._();
+
+ bool get hasValue => amount > 0;
+}
diff --git a/lib/src/features/calculator/presentation/states/amount_input_field_state.freezed.dart b/lib/src/features/calculator/presentation/states/amount_input_field_state.freezed.dart
new file mode 100644
index 00000000..689fd943
--- /dev/null
+++ b/lib/src/features/calculator/presentation/states/amount_input_field_state.freezed.dart
@@ -0,0 +1,274 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'amount_input_field_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity(T value) => value;
+/// @nodoc
+mixin _$AmountInputFieldState {
+
+ double get amount; String? get errorText;
+/// Create a copy of AmountInputFieldState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$AmountInputFieldStateCopyWith get copyWith => _$AmountInputFieldStateCopyWithImpl(this as AmountInputFieldState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is AmountInputFieldState&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.errorText, errorText) || other.errorText == errorText));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,amount,errorText);
+
+@override
+String toString() {
+ return 'AmountInputFieldState(amount: $amount, errorText: $errorText)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $AmountInputFieldStateCopyWith<$Res> {
+ factory $AmountInputFieldStateCopyWith(AmountInputFieldState value, $Res Function(AmountInputFieldState) _then) = _$AmountInputFieldStateCopyWithImpl;
+@useResult
+$Res call({
+ double amount, String? errorText
+});
+
+
+
+
+}
+/// @nodoc
+class _$AmountInputFieldStateCopyWithImpl<$Res>
+ implements $AmountInputFieldStateCopyWith<$Res> {
+ _$AmountInputFieldStateCopyWithImpl(this._self, this._then);
+
+ final AmountInputFieldState _self;
+ final $Res Function(AmountInputFieldState) _then;
+
+/// Create a copy of AmountInputFieldState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? amount = null,Object? errorText = freezed,}) {
+ return _then(_self.copyWith(
+amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
+as double,errorText: freezed == errorText ? _self.errorText : errorText // ignore: cast_nullable_to_non_nullable
+as String?,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [AmountInputFieldState].
+extension AmountInputFieldStatePatterns on AmountInputFieldState {
+/// A variant of `map` that fallback to returning `orElse`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeMap(TResult Function( _AmountInputFieldState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _AmountInputFieldState() when $default != null:
+return $default(_that);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// Callbacks receives the raw object, upcasted.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case final Subclass2 value:
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult map(TResult Function( _AmountInputFieldState value) $default,){
+final _that = this;
+switch (_that) {
+case _AmountInputFieldState():
+return $default(_that);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `map` that fallback to returning `null`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AmountInputFieldState value)? $default,){
+final _that = this;
+switch (_that) {
+case _AmountInputFieldState() when $default != null:
+return $default(_that);case _:
+ return null;
+
+}
+}
+/// A variant of `when` that fallback to an `orElse` callback.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeWhen(TResult Function( double amount, String? errorText)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _AmountInputFieldState() when $default != null:
+return $default(_that.amount,_that.errorText);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// As opposed to `map`, this offers destructuring.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case Subclass2(:final field2):
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult when(TResult Function( double amount, String? errorText) $default,) {final _that = this;
+switch (_that) {
+case _AmountInputFieldState():
+return $default(_that.amount,_that.errorText);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `when` that fallback to returning `null`
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? whenOrNull(TResult? Function( double amount, String? errorText)? $default,) {final _that = this;
+switch (_that) {
+case _AmountInputFieldState() when $default != null:
+return $default(_that.amount,_that.errorText);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _AmountInputFieldState extends AmountInputFieldState {
+ const _AmountInputFieldState({this.amount = 0.0, this.errorText}): super._();
+
+
+@override@JsonKey() final double amount;
+@override final String? errorText;
+
+/// Create a copy of AmountInputFieldState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$AmountInputFieldStateCopyWith<_AmountInputFieldState> get copyWith => __$AmountInputFieldStateCopyWithImpl<_AmountInputFieldState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _AmountInputFieldState&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.errorText, errorText) || other.errorText == errorText));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,amount,errorText);
+
+@override
+String toString() {
+ return 'AmountInputFieldState(amount: $amount, errorText: $errorText)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$AmountInputFieldStateCopyWith<$Res> implements $AmountInputFieldStateCopyWith<$Res> {
+ factory _$AmountInputFieldStateCopyWith(_AmountInputFieldState value, $Res Function(_AmountInputFieldState) _then) = __$AmountInputFieldStateCopyWithImpl;
+@override @useResult
+$Res call({
+ double amount, String? errorText
+});
+
+
+
+
+}
+/// @nodoc
+class __$AmountInputFieldStateCopyWithImpl<$Res>
+ implements _$AmountInputFieldStateCopyWith<$Res> {
+ __$AmountInputFieldStateCopyWithImpl(this._self, this._then);
+
+ final _AmountInputFieldState _self;
+ final $Res Function(_AmountInputFieldState) _then;
+
+/// Create a copy of AmountInputFieldState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? amount = null,Object? errorText = freezed,}) {
+ return _then(_AmountInputFieldState(
+amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
+as double,errorText: freezed == errorText ? _self.errorText : errorText // ignore: cast_nullable_to_non_nullable
+as String?,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/lib/src/features/calculator/presentation/states/calculator_state.dart b/lib/src/features/calculator/presentation/states/calculator_state.dart
new file mode 100644
index 00000000..7316ffb9
--- /dev/null
+++ b/lib/src/features/calculator/presentation/states/calculator_state.dart
@@ -0,0 +1,26 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import '../../domain/entities/currency.dart';
+
+part 'calculator_state.freezed.dart';
+
+@freezed
+abstract class CalculatorState with _$CalculatorState {
+ const factory CalculatorState({
+ Currency? fromCurrency,
+ Currency? toCurrency,
+ @Default(0.0) double amount,
+ double? convertedAmount,
+ double? estimatedRate,
+ @Default(false) bool isLoading,
+ String? errorMessage,
+ String? inputFieldError,
+ }) = _CalculatorState;
+
+ const CalculatorState._();
+
+ bool get canCalculate =>
+ fromCurrency != null && toCurrency != null && amount > 0;
+
+ bool get canSwap => fromCurrency != null && toCurrency != null;
+}
diff --git a/lib/src/features/calculator/presentation/states/calculator_state.freezed.dart b/lib/src/features/calculator/presentation/states/calculator_state.freezed.dart
new file mode 100644
index 00000000..1f37bef6
--- /dev/null
+++ b/lib/src/features/calculator/presentation/states/calculator_state.freezed.dart
@@ -0,0 +1,292 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'calculator_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity(T value) => value;
+/// @nodoc
+mixin _$CalculatorState {
+
+ Currency? get fromCurrency; Currency? get toCurrency; double get amount; double? get convertedAmount; double? get estimatedRate; bool get isLoading; String? get errorMessage; String? get inputFieldError;
+/// Create a copy of CalculatorState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$CalculatorStateCopyWith get copyWith => _$CalculatorStateCopyWithImpl(this as CalculatorState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is CalculatorState&&(identical(other.fromCurrency, fromCurrency) || other.fromCurrency == fromCurrency)&&(identical(other.toCurrency, toCurrency) || other.toCurrency == toCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.convertedAmount, convertedAmount) || other.convertedAmount == convertedAmount)&&(identical(other.estimatedRate, estimatedRate) || other.estimatedRate == estimatedRate)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.inputFieldError, inputFieldError) || other.inputFieldError == inputFieldError));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,fromCurrency,toCurrency,amount,convertedAmount,estimatedRate,isLoading,errorMessage,inputFieldError);
+
+@override
+String toString() {
+ return 'CalculatorState(fromCurrency: $fromCurrency, toCurrency: $toCurrency, amount: $amount, convertedAmount: $convertedAmount, estimatedRate: $estimatedRate, isLoading: $isLoading, errorMessage: $errorMessage, inputFieldError: $inputFieldError)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $CalculatorStateCopyWith<$Res> {
+ factory $CalculatorStateCopyWith(CalculatorState value, $Res Function(CalculatorState) _then) = _$CalculatorStateCopyWithImpl;
+@useResult
+$Res call({
+ Currency? fromCurrency, Currency? toCurrency, double amount, double? convertedAmount, double? estimatedRate, bool isLoading, String? errorMessage, String? inputFieldError
+});
+
+
+
+
+}
+/// @nodoc
+class _$CalculatorStateCopyWithImpl<$Res>
+ implements $CalculatorStateCopyWith<$Res> {
+ _$CalculatorStateCopyWithImpl(this._self, this._then);
+
+ final CalculatorState _self;
+ final $Res Function(CalculatorState) _then;
+
+/// Create a copy of CalculatorState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? fromCurrency = freezed,Object? toCurrency = freezed,Object? amount = null,Object? convertedAmount = freezed,Object? estimatedRate = freezed,Object? isLoading = null,Object? errorMessage = freezed,Object? inputFieldError = freezed,}) {
+ return _then(_self.copyWith(
+fromCurrency: freezed == fromCurrency ? _self.fromCurrency : fromCurrency // ignore: cast_nullable_to_non_nullable
+as Currency?,toCurrency: freezed == toCurrency ? _self.toCurrency : toCurrency // ignore: cast_nullable_to_non_nullable
+as Currency?,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
+as double,convertedAmount: freezed == convertedAmount ? _self.convertedAmount : convertedAmount // ignore: cast_nullable_to_non_nullable
+as double?,estimatedRate: freezed == estimatedRate ? _self.estimatedRate : estimatedRate // ignore: cast_nullable_to_non_nullable
+as double?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
+as String?,inputFieldError: freezed == inputFieldError ? _self.inputFieldError : inputFieldError // ignore: cast_nullable_to_non_nullable
+as String?,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [CalculatorState].
+extension CalculatorStatePatterns on CalculatorState {
+/// A variant of `map` that fallback to returning `orElse`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeMap(TResult Function( _CalculatorState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _CalculatorState() when $default != null:
+return $default(_that);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// Callbacks receives the raw object, upcasted.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case final Subclass2 value:
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult map(TResult Function( _CalculatorState value) $default,){
+final _that = this;
+switch (_that) {
+case _CalculatorState():
+return $default(_that);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `map` that fallback to returning `null`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? mapOrNull(TResult? Function( _CalculatorState value)? $default,){
+final _that = this;
+switch (_that) {
+case _CalculatorState() when $default != null:
+return $default(_that);case _:
+ return null;
+
+}
+}
+/// A variant of `when` that fallback to an `orElse` callback.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeWhen(TResult Function( Currency? fromCurrency, Currency? toCurrency, double amount, double? convertedAmount, double? estimatedRate, bool isLoading, String? errorMessage, String? inputFieldError)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _CalculatorState() when $default != null:
+return $default(_that.fromCurrency,_that.toCurrency,_that.amount,_that.convertedAmount,_that.estimatedRate,_that.isLoading,_that.errorMessage,_that.inputFieldError);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// As opposed to `map`, this offers destructuring.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case Subclass2(:final field2):
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult when(TResult Function( Currency? fromCurrency, Currency? toCurrency, double amount, double? convertedAmount, double? estimatedRate, bool isLoading, String? errorMessage, String? inputFieldError) $default,) {final _that = this;
+switch (_that) {
+case _CalculatorState():
+return $default(_that.fromCurrency,_that.toCurrency,_that.amount,_that.convertedAmount,_that.estimatedRate,_that.isLoading,_that.errorMessage,_that.inputFieldError);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `when` that fallback to returning `null`
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? whenOrNull(TResult? Function( Currency? fromCurrency, Currency? toCurrency, double amount, double? convertedAmount, double? estimatedRate, bool isLoading, String? errorMessage, String? inputFieldError)? $default,) {final _that = this;
+switch (_that) {
+case _CalculatorState() when $default != null:
+return $default(_that.fromCurrency,_that.toCurrency,_that.amount,_that.convertedAmount,_that.estimatedRate,_that.isLoading,_that.errorMessage,_that.inputFieldError);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _CalculatorState extends CalculatorState {
+ const _CalculatorState({this.fromCurrency, this.toCurrency, this.amount = 0.0, this.convertedAmount, this.estimatedRate, this.isLoading = false, this.errorMessage, this.inputFieldError}): super._();
+
+
+@override final Currency? fromCurrency;
+@override final Currency? toCurrency;
+@override@JsonKey() final double amount;
+@override final double? convertedAmount;
+@override final double? estimatedRate;
+@override@JsonKey() final bool isLoading;
+@override final String? errorMessage;
+@override final String? inputFieldError;
+
+/// Create a copy of CalculatorState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$CalculatorStateCopyWith<_CalculatorState> get copyWith => __$CalculatorStateCopyWithImpl<_CalculatorState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _CalculatorState&&(identical(other.fromCurrency, fromCurrency) || other.fromCurrency == fromCurrency)&&(identical(other.toCurrency, toCurrency) || other.toCurrency == toCurrency)&&(identical(other.amount, amount) || other.amount == amount)&&(identical(other.convertedAmount, convertedAmount) || other.convertedAmount == convertedAmount)&&(identical(other.estimatedRate, estimatedRate) || other.estimatedRate == estimatedRate)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.inputFieldError, inputFieldError) || other.inputFieldError == inputFieldError));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,fromCurrency,toCurrency,amount,convertedAmount,estimatedRate,isLoading,errorMessage,inputFieldError);
+
+@override
+String toString() {
+ return 'CalculatorState(fromCurrency: $fromCurrency, toCurrency: $toCurrency, amount: $amount, convertedAmount: $convertedAmount, estimatedRate: $estimatedRate, isLoading: $isLoading, errorMessage: $errorMessage, inputFieldError: $inputFieldError)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$CalculatorStateCopyWith<$Res> implements $CalculatorStateCopyWith<$Res> {
+ factory _$CalculatorStateCopyWith(_CalculatorState value, $Res Function(_CalculatorState) _then) = __$CalculatorStateCopyWithImpl;
+@override @useResult
+$Res call({
+ Currency? fromCurrency, Currency? toCurrency, double amount, double? convertedAmount, double? estimatedRate, bool isLoading, String? errorMessage, String? inputFieldError
+});
+
+
+
+
+}
+/// @nodoc
+class __$CalculatorStateCopyWithImpl<$Res>
+ implements _$CalculatorStateCopyWith<$Res> {
+ __$CalculatorStateCopyWithImpl(this._self, this._then);
+
+ final _CalculatorState _self;
+ final $Res Function(_CalculatorState) _then;
+
+/// Create a copy of CalculatorState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? fromCurrency = freezed,Object? toCurrency = freezed,Object? amount = null,Object? convertedAmount = freezed,Object? estimatedRate = freezed,Object? isLoading = null,Object? errorMessage = freezed,Object? inputFieldError = freezed,}) {
+ return _then(_CalculatorState(
+fromCurrency: freezed == fromCurrency ? _self.fromCurrency : fromCurrency // ignore: cast_nullable_to_non_nullable
+as Currency?,toCurrency: freezed == toCurrency ? _self.toCurrency : toCurrency // ignore: cast_nullable_to_non_nullable
+as Currency?,amount: null == amount ? _self.amount : amount // ignore: cast_nullable_to_non_nullable
+as double,convertedAmount: freezed == convertedAmount ? _self.convertedAmount : convertedAmount // ignore: cast_nullable_to_non_nullable
+as double?,estimatedRate: freezed == estimatedRate ? _self.estimatedRate : estimatedRate // ignore: cast_nullable_to_non_nullable
+as double?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
+as String?,inputFieldError: freezed == inputFieldError ? _self.inputFieldError : inputFieldError // ignore: cast_nullable_to_non_nullable
+as String?,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/lib/src/features/calculator/presentation/widgets/amount_input_field.dart b/lib/src/features/calculator/presentation/widgets/amount_input_field.dart
new file mode 100644
index 00000000..5726f9d3
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/amount_input_field.dart
@@ -0,0 +1,88 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/theme/colors/app_colors.dart';
+import '../../../../core/utils/formatters/thousand_separator_input_formatter.dart';
+import '../providers/amount_input_field_controller.dart';
+import '../providers/calculator_controller.dart';
+
+final class AmountInputField extends ConsumerStatefulWidget {
+ const AmountInputField({super.key});
+
+ @override
+ ConsumerState createState() => _AmountInputFieldState();
+}
+
+class _AmountInputFieldState extends ConsumerState {
+ late final FocusNode _focusNode;
+
+ @override
+ void initState() {
+ super.initState();
+ _focusNode = FocusNode();
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ _focusNode.requestFocus();
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _focusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final AmountInputFieldController controller = ref.watch(
+ amountInputFieldControllerProvider.notifier,
+ );
+
+ // Escuchar el error desde CalculatorState en lugar de AmountInputFieldState
+ final String? errorText = ref.watch(
+ calculatorControllerProvider.select((value) => value.inputFieldError),
+ );
+
+ final String currencySymbol = ref.watch(
+ calculatorControllerProvider.select(
+ (value) => value.fromCurrency?.symbol ?? '',
+ ),
+ );
+
+ ref.listen(
+ amountInputFieldControllerProvider.select((value) => value.amount),
+ (previous, next) {
+ ref.read(calculatorControllerProvider.notifier).setAmount(amount: next);
+ },
+ );
+
+ return TextFormField(
+ controller: controller.textController,
+ focusNode: _focusNode,
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
+ inputFormatters: [ThousandSeparatorInputFormatter()],
+ decoration: InputDecoration(
+ hintText: StringConstants.amountPlaceholder,
+ prefixText: currencySymbol.isNotEmpty ? '$currencySymbol ' : null,
+ prefixStyle: TextStyle(
+ color: AppColors.primaryYellow,
+ fontSize: 16,
+ height: 1.0,
+ ),
+ errorText: errorText,
+ errorMaxLines: 3,
+ ),
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ fontSize: 16,
+ height: 1.0,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ onChanged: (value) => controller.onTextChanged(value: value),
+ onTap: controller.selectAll,
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/currency_bottom_sheet.dart b/lib/src/features/calculator/presentation/widgets/currency_bottom_sheet.dart
new file mode 100644
index 00000000..0db86bbf
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/currency_bottom_sheet.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/widgets/bottom_sheet_handle.dart';
+import '../../../../core/widgets/error_state_widget.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/currency_type.dart';
+import '../providers/filtered_currencies_provider.dart';
+import 'currency_bottomsheet_tile.dart';
+
+final class CurrencyBottomSheet extends ConsumerWidget {
+ final Currency? selectedCurrency;
+ final void Function(Currency) onCurrencySelected;
+ final CurrencyType currencyType;
+
+ const CurrencyBottomSheet({
+ super.key,
+ required this.selectedCurrency,
+ required this.onCurrencySelected,
+ required this.currencyType,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final ThemeData theme = Theme.of(context);
+ // Usar provider de currencies filtradas
+ final AsyncValue> filteredCurrenciesAsync = ref.watch(
+ filteredCurrenciesProvider(currencyType: currencyType),
+ );
+ final String title = currencyType == CurrencyType.fiat
+ ? StringConstants.fiatLabel
+ : StringConstants.cryptoLabel;
+
+ return Container(
+ height: MediaQuery.of(context).size.height * 0.7,
+ decoration: BoxDecoration(
+ color: theme.colorScheme.surface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Column(
+ children: [
+ const BottomSheetHandle(),
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Text(
+ title,
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ Expanded(
+ child: filteredCurrenciesAsync.when(
+ data: (List filteredCurrencies) {
+ return RadioGroup(
+ groupValue: selectedCurrency?.id,
+ onChanged: (String? currencyId) {
+ if (currencyId != null) {
+ final currency = filteredCurrencies.firstWhere(
+ (c) => c.id == currencyId,
+ );
+ onCurrencySelected(currency);
+ Navigator.of(context).pop();
+ }
+ },
+ child: ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ itemCount: filteredCurrencies.length,
+ itemBuilder: (context, index) {
+ final currency = filteredCurrencies[index];
+ return CurrencyBottomSheetTile(
+ currency: currency,
+ isSelected: selectedCurrency?.id == currency.id,
+ onTap: () {
+ onCurrencySelected(currency);
+ Navigator.of(context).pop();
+ },
+ );
+ },
+ ),
+ );
+ },
+ loading: () => const Center(child: CircularProgressIndicator()),
+ error: (error, _) => ErrorStateWidget(
+ title: StringConstants.loadCurrenciesError,
+ message: error.toString(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/currency_bottomsheet_tile.dart b/lib/src/features/calculator/presentation/widgets/currency_bottomsheet_tile.dart
new file mode 100644
index 00000000..8d809442
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/currency_bottomsheet_tile.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+
+import '../../../../core/widgets/currency_image_widget.dart';
+import '../../domain/entities/currency.dart';
+
+final class CurrencyBottomSheetTile extends StatelessWidget {
+ final Currency currency;
+ final bool isSelected;
+ final VoidCallback onTap;
+
+ const CurrencyBottomSheetTile({
+ super.key,
+ required this.currency,
+ required this.isSelected,
+ required this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(12),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ margin: const EdgeInsets.only(bottom: 8),
+ decoration: BoxDecoration(
+ color: isSelected
+ ? theme.colorScheme.primary.withValues(alpha: 0.1)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(12),
+ border: isSelected
+ ? Border.all(color: theme.colorScheme.primary, width: 2)
+ : null,
+ ),
+ child: Row(
+ children: [
+ CurrencyImageWidget(assetPath: currency.assetPath, size: 40),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ currency.symbol,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ Text(
+ currency.name,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ ],
+ ),
+ ),
+ Radio(
+ value: currency.id,
+ activeColor: theme.colorScheme.primary,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/currency_exchange_bar.dart b/lib/src/features/calculator/presentation/widgets/currency_exchange_bar.dart
new file mode 100644
index 00000000..8dca2545
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/currency_exchange_bar.dart
@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/theme/colors/app_colors.dart';
+import '../../domain/entities/currency.dart';
+import 'currency_label.dart';
+import 'currency_selector_compact.dart';
+
+final class CurrencyExchangeBar extends StatelessWidget {
+ final Currency? fromCurrency;
+ final Currency? toCurrency;
+ final VoidCallback onFromTap;
+ final VoidCallback onSwap;
+ final VoidCallback onToTap;
+ final bool canSwap;
+
+ const CurrencyExchangeBar({
+ super.key,
+ required this.fromCurrency,
+ required this.toCurrency,
+ required this.onFromTap,
+ required this.onSwap,
+ required this.onToTap,
+ this.canSwap = true,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.center,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: AppColors.primaryYellow, width: 2.5),
+ borderRadius: BorderRadius.horizontal(
+ left: Radius.circular(100),
+ right: Radius.circular(100),
+ ),
+ ),
+ height: 46,
+ ),
+ Positioned(
+ top: -6,
+ left: 30,
+ child: CurrencyLabel(
+ text: StringConstants.haveLabel,
+ color: theme.colorScheme.onSurface,
+ backgroundColor: theme.colorScheme.surface,
+ ),
+ ),
+ Positioned(
+ top: -6,
+ right: 30,
+ child: CurrencyLabel(
+ text: StringConstants.wantLabel,
+ color: theme.colorScheme.onSurface,
+ backgroundColor: theme.colorScheme.surface,
+ ),
+ ),
+ InkWell(
+ onTap: onSwap,
+ child: CircleAvatar(
+ backgroundColor: AppColors.primaryYellow,
+ radius: 27.0,
+ child: SvgPicture.asset(
+ 'assets/icons/swap.svg',
+ height: 30,
+ colorFilter: const ColorFilter.mode(
+ Colors.white,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0),
+ child: CurrencySelectorCompact(
+ selectedCurrency: fromCurrency,
+ onTap: onFromTap,
+ isFromSelector: true,
+ ),
+ ),
+ CurrencySelectorCompact(
+ selectedCurrency: toCurrency,
+ onTap: onToTap,
+ isFromSelector: false,
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/currency_label.dart b/lib/src/features/calculator/presentation/widgets/currency_label.dart
new file mode 100644
index 00000000..322809ff
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/currency_label.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+final class CurrencyLabel extends StatelessWidget {
+ final String text;
+ final Color color;
+ final Color backgroundColor;
+
+ const CurrencyLabel({
+ super.key,
+ required this.text,
+ required this.color,
+ required this.backgroundColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ text,
+ style: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w600,
+ color: color,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/currency_selector_compact.dart b/lib/src/features/calculator/presentation/widgets/currency_selector_compact.dart
new file mode 100644
index 00000000..236e7eba
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/currency_selector_compact.dart
@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/widgets/currency_image_widget.dart';
+import '../../domain/entities/currency.dart';
+import '../../domain/entities/enum/bottom_sheet_type.dart';
+import '../providers/currency_bottom_sheet_controller.dart';
+
+final class CurrencySelectorCompact extends ConsumerWidget {
+ final Currency? selectedCurrency;
+ final VoidCallback onTap;
+ final bool isFromSelector;
+
+ const CurrencySelectorCompact({
+ super.key,
+ required this.selectedCurrency,
+ required this.onTap,
+ required this.isFromSelector,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final ThemeData theme = Theme.of(context);
+ final BottomSheetType currentBottomSheet = ref.watch(
+ currencyBottomSheetControllerProvider,
+ );
+
+ final bool isThisOpen = isFromSelector
+ ? currentBottomSheet == BottomSheetType.from
+ : currentBottomSheet == BottomSheetType.to;
+
+ return InkWell(
+ onTap: onTap,
+ child: Row(
+ children: [
+ if (selectedCurrency != null)
+ CurrencyImageWidget(
+ assetPath: selectedCurrency!.assetPath,
+ size: 24,
+ ),
+ const SizedBox(width: 10),
+ Text(
+ selectedCurrency != null
+ ? selectedCurrency!.symbol
+ : StringConstants.loadingText,
+ style: theme.textTheme.titleSmall?.copyWith(
+ fontWeight: selectedCurrency != null
+ ? FontWeight.w600
+ : FontWeight.w400,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ if (selectedCurrency != null)
+ AnimatedRotation(
+ turns: isThisOpen ? 0.75 : 0.25,
+ duration: const Duration(milliseconds: 200),
+ child: Icon(
+ Icons.chevron_right,
+ size: 30,
+ color: theme.colorScheme.outline,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/exchange_info_card.dart b/lib/src/features/calculator/presentation/widgets/exchange_info_card.dart
new file mode 100644
index 00000000..1f573fda
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/exchange_info_card.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+
+import '../../../../core/constants/string_constants.dart';
+import '../../../../core/utils/formatters/rate_formatter.dart';
+import '../../domain/entities/currency.dart';
+import '../providers/exchange_rate_display_provider.dart';
+import 'exchange_info_row.dart';
+
+final class ExchangeInfoCard extends ConsumerWidget {
+ final double? estimatedRate;
+ final double? convertedAmount;
+ final Currency? fromCurrency;
+ final Currency? toCurrency;
+ final bool isLoading;
+
+ const ExchangeInfoCard({
+ super.key,
+ this.estimatedRate,
+ this.convertedAmount,
+ this.fromCurrency,
+ this.toCurrency,
+ this.isLoading = false,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final ThemeData theme = Theme.of(context);
+
+ // Usar provider para calcular la tasa de visualización
+ final double? displayRate = ref.watch(
+ exchangeRateDisplayProvider(
+ estimatedRate: estimatedRate,
+ fromCurrency: fromCurrency,
+ toCurrency: toCurrency,
+ ),
+ );
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (isLoading) ...[
+ const Center(child: CircularProgressIndicator()),
+ ] else if (displayRate != null && convertedAmount != null) ...[
+ ExchangeInfoRow(
+ label: StringConstants.estimatedRateLabel,
+ value:
+ '1 ${fromCurrency?.symbol ?? ''} ≈ ${RateFormatter.formatRate(rate: displayRate)} ${toCurrency?.symbol ?? ''}',
+ ),
+ const SizedBox(height: 12),
+ ExchangeInfoRow(
+ label: StringConstants.youWillReceiveLabel,
+ value:
+ '≈ ${convertedAmount!.toStringAsFixed(2)} ${toCurrency?.symbol ?? ''}',
+ ),
+ const SizedBox(height: 12),
+ ExchangeInfoRow(
+ label: StringConstants.estimatedTimeLabel,
+ value: StringConstants.estimatedTimeValue,
+ ),
+ ] else ...[
+ Text(
+ StringConstants.exchangeInfoPlaceholder,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ ],
+ ],
+ );
+ }
+}
diff --git a/lib/src/features/calculator/presentation/widgets/exchange_info_row.dart b/lib/src/features/calculator/presentation/widgets/exchange_info_row.dart
new file mode 100644
index 00000000..00bf580d
--- /dev/null
+++ b/lib/src/features/calculator/presentation/widgets/exchange_info_row.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+
+final class ExchangeInfoRow extends StatelessWidget {
+ final String label;
+ final String value;
+
+ const ExchangeInfoRow({super.key, required this.label, required this.value});
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ label,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
+ ),
+ ),
+ Text(
+ value,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ fontWeight: FontWeight.w500,
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/src/routes/app_router.dart b/lib/src/routes/app_router.dart
new file mode 100644
index 00000000..fcb7bec7
--- /dev/null
+++ b/lib/src/routes/app_router.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+import '../core/constants/string_constants.dart';
+import '../features/calculator/presentation/screens/calculator_screen.dart';
+import 'app_routes.dart';
+
+/// Configuración de rutas de la aplicación usando GoRouter
+final class AppRouter {
+ /// Router principal de la aplicación
+ static final GoRouter router = GoRouter(
+ debugLogDiagnostics: true,
+ initialLocation: AppRoutes.calculator.path,
+ routes: [
+ GoRoute(
+ path: AppRoutes.calculator.path,
+ name: AppRoutes.calculator.name,
+ builder: (BuildContext context, GoRouterState state) {
+ return const CalculatorScreen();
+ },
+ ),
+ ],
+ errorBuilder: (BuildContext context, GoRouterState state) {
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.error_outline, size: 64, color: Colors.red),
+ const SizedBox(height: 16),
+ Text(
+ StringConstants.pageNotFound,
+ style: Theme.of(context).textTheme.headlineSmall,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ state.uri.toString(),
+ style: Theme.of(
+ context,
+ ).textTheme.bodyMedium?.copyWith(color: Colors.grey),
+ ),
+ const SizedBox(height: 24),
+ ElevatedButton.icon(
+ onPressed: () => context.go(AppRoutes.calculator.path),
+ icon: const Icon(Icons.home),
+ label: const Text(StringConstants.goToHome),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+}
diff --git a/lib/src/routes/app_routes.dart b/lib/src/routes/app_routes.dart
new file mode 100644
index 00000000..3f621303
--- /dev/null
+++ b/lib/src/routes/app_routes.dart
@@ -0,0 +1,17 @@
+/// Enumeración de todas las rutas de la aplicación
+///
+/// Cada ruta tiene un [path] y un [name] únicos.
+/// El path es la URL de la ruta y el name es el identificador interno.
+enum AppRoutes {
+ /// Ruta principal - Calculadora
+ calculator('/', 'calculator');
+
+ const AppRoutes(this.path, this.name);
+
+ /// Path de la ruta (usado en la URL)
+ final String path;
+
+ /// Nombre de la ruta (usado para navegación programática)
+ final String name;
+}
+
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 00000000..df25db42
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,41 @@
+name: crypto_calculator
+description: "Crypto Calculator Challenge - Flutter app with Riverpod"
+publish_to: 'none'
+
+version: 1.0.0+1
+
+environment:
+ sdk: '>=3.8.0 <4.0.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ flutter_riverpod: ^3.0.3
+ riverpod_annotation: ^3.0.3
+
+ dio: ^5.9.0
+ go_router: ^16.3.0
+
+ cupertino_icons: ^1.0.8
+ freezed: ^3.2.3
+ freezed_annotation: ^3.1.0
+ flutter_svg: ^2.2.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ build_runner: ^2.4.0
+ riverpod_generator: ^3.0.3
+
+ # Linting
+ flutter_lints: ^6.0.0
+
+flutter:
+ uses-material-design: true
+
+ assets:
+ - assets/cripto_currencies/
+ - assets/fiat_currencies/
+ - assets/icons/
diff --git a/test/features/calculator/domain/usecases/calculate_crypto_exchange_usecase_test.dart b/test/features/calculator/domain/usecases/calculate_crypto_exchange_usecase_test.dart
new file mode 100644
index 00000000..ddf132bb
--- /dev/null
+++ b/test/features/calculator/domain/usecases/calculate_crypto_exchange_usecase_test.dart
@@ -0,0 +1,146 @@
+import 'package:crypto_calculator/src/features/calculator/domain/entities/calculate_exchange_params.dart';
+import 'package:crypto_calculator/src/features/calculator/domain/entities/currency.dart';
+import 'package:crypto_calculator/src/features/calculator/domain/entities/enum/currency_type.dart';
+import 'package:crypto_calculator/src/features/calculator/domain/repositories/calculator_repository.dart';
+import 'package:crypto_calculator/src/features/calculator/domain/usecases/calculate_crypto_exchange_usecase.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+/// Mock simple del repository para los tests
+class MockCalculatorRepository implements CalculatorRepository {
+ final double exchangeRateToReturn;
+
+ MockCalculatorRepository({required this.exchangeRateToReturn});
+
+ @override
+ Future getExchangeRate({
+ required String cryptoCurrencyId,
+ required String fiatCurrencyId,
+ required String amount,
+ required String amountCurrencyId,
+ required int type,
+ }) async {
+ return exchangeRateToReturn;
+ }
+}
+
+void main() {
+ late CalculateCryptoExchangeUseCase useCase;
+ late MockCalculatorRepository mockRepository;
+
+ const Currency cryptoCurrency = Currency(
+ id: 'TATUM-TRON-USDT',
+ name: 'Tether',
+ symbol: 'USDT',
+ assetPath: 'assets/cripto_currencies/TATUM-TRON-USDT.png',
+ type: CurrencyType.crypto,
+ );
+
+ const Currency fiatCurrency = Currency(
+ id: 'VES',
+ name: 'Bolívar',
+ symbol: 'Bs.',
+ assetPath: 'assets/fiat_currencies/VES.png',
+ type: CurrencyType.fiat,
+ );
+
+ setUp(() {
+ mockRepository = MockCalculatorRepository(exchangeRateToReturn: 50.0);
+ useCase = CalculateCryptoExchangeUseCase(repository: mockRepository);
+ });
+
+ group('CalculateCryptoExchangeUseCase', () {
+ test('debe calcular correctamente crypto a fiat', () async {
+ // Arrange
+ const double amount = 10.0;
+ const double expectedExchangeRate = 50.0;
+ const double expectedConvertedAmount = 500.0; // 10 * 50
+
+ final params = CalculateExchangeParams(
+ fromCurrency: cryptoCurrency,
+ toCurrency: fiatCurrency,
+ amount: amount,
+ );
+
+ // Act
+ final result = await useCase(params: params);
+
+ // Assert
+ expect(result.exchangeRate, expectedExchangeRate);
+ expect(result.convertedAmount, expectedConvertedAmount);
+ });
+
+ test('debe calcular correctamente fiat a crypto', () async {
+ // Arrange
+ const double amount = 500.0;
+ const double expectedExchangeRate = 50.0;
+ const double expectedConvertedAmount = 10.0; // 500 / 50
+
+ final params = CalculateExchangeParams(
+ fromCurrency: fiatCurrency,
+ toCurrency: cryptoCurrency,
+ amount: amount,
+ );
+
+ // Act
+ final result = await useCase(params: params);
+
+ // Assert
+ expect(result.exchangeRate, expectedExchangeRate);
+ expect(result.convertedAmount, expectedConvertedAmount);
+ });
+
+ test('debe lanzar ArgumentError si el monto es 0', () async {
+ // Arrange
+ final params = CalculateExchangeParams(
+ fromCurrency: cryptoCurrency,
+ toCurrency: fiatCurrency,
+ amount: 0.0,
+ );
+
+ // Act & Assert
+ expect(
+ () async => await useCase(params: params),
+ throwsA(isA()),
+ );
+ });
+
+ test('debe lanzar ArgumentError si el monto es negativo', () async {
+ // Arrange
+ final params = CalculateExchangeParams(
+ fromCurrency: cryptoCurrency,
+ toCurrency: fiatCurrency,
+ amount: -10.0,
+ );
+
+ // Act & Assert
+ expect(
+ () async => await useCase(params: params),
+ throwsA(isA()),
+ );
+ });
+
+ test('debe manejar correctamente tasas de cambio decimales', () async {
+ // Arrange
+ mockRepository = MockCalculatorRepository(exchangeRateToReturn: 0.025);
+ useCase = CalculateCryptoExchangeUseCase(repository: mockRepository);
+
+ const double amount = 100.0;
+ const double expectedExchangeRate = 0.025;
+ const double expectedConvertedAmount = 2.5; // 100 * 0.025
+
+ final params = CalculateExchangeParams(
+ fromCurrency: cryptoCurrency,
+ toCurrency: fiatCurrency,
+ amount: amount,
+ );
+
+ // Act
+ final result = await useCase(params: params);
+
+ // Assert
+ expect(result.exchangeRate, expectedExchangeRate);
+ expect(result.convertedAmount, expectedConvertedAmount);
+ });
+ });
+}
+
diff --git a/test/features/calculator/domain/usecases/validate_exchange_amount_usecase_test.dart b/test/features/calculator/domain/usecases/validate_exchange_amount_usecase_test.dart
new file mode 100644
index 00000000..c28e091d
--- /dev/null
+++ b/test/features/calculator/domain/usecases/validate_exchange_amount_usecase_test.dart
@@ -0,0 +1,89 @@
+import 'package:crypto_calculator/src/core/constants/validation_constants.dart';
+import 'package:crypto_calculator/src/features/calculator/domain/usecases/validate_exchange_amount_usecase.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ late ValidateExchangeAmountUseCase useCase;
+
+ setUp(() {
+ useCase = const ValidateExchangeAmountUseCase();
+ });
+
+ group('ValidateExchangeAmountUseCase', () {
+ test('debe retornar Success cuando el monto es válido', () {
+ // Arrange
+ const double validAmount = 100.0;
+
+ // Act
+ final result = useCase(validAmount);
+
+ // Assert
+ expect(result, isA());
+ });
+
+ test('debe retornar Failure cuando el monto es 0', () {
+ // Arrange
+ const double invalidAmount = 0.0;
+
+ // Act
+ final result = useCase(invalidAmount);
+
+ // Assert
+ expect(result, isA());
+ if (result is AmountValidationFailure) {
+ expect(result.errorMessage, isNotEmpty);
+ }
+ });
+
+ test('debe retornar Failure cuando el monto es negativo', () {
+ // Arrange
+ const double invalidAmount = -10.0;
+
+ // Act
+ final result = useCase(invalidAmount);
+
+ // Assert
+ expect(result, isA());
+ if (result is AmountValidationFailure) {
+ expect(result.errorMessage, isNotEmpty);
+ }
+ });
+
+ test('debe retornar Failure cuando el monto excede el límite máximo', () {
+ // Arrange
+ final double invalidAmount = ValidationConstants.maxExchangeAmount + 1;
+
+ // Act
+ final result = useCase(invalidAmount);
+
+ // Assert
+ expect(result, isA());
+ if (result is AmountValidationFailure) {
+ expect(result.errorMessage, isNotEmpty);
+ }
+ });
+
+ test('debe retornar Success cuando el monto está en el límite máximo', () {
+ // Arrange
+ final double validAmount = ValidationConstants.maxExchangeAmount;
+
+ // Act
+ final result = useCase(validAmount);
+
+ // Assert
+ expect(result, isA());
+ });
+
+ test('debe retornar Success con un monto pequeño pero positivo', () {
+ // Arrange
+ const double validAmount = 0.01;
+
+ // Act
+ final result = useCase(validAmount);
+
+ // Assert
+ expect(result, isA());
+ });
+ });
+}
+