Initial Flutter app

This commit is contained in:
Forte11Cuba
2026-02-05 19:41:34 -06:00
commit d5bdc2987d
158 changed files with 13980 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
.metadata
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+17
View File
@@ -0,0 +1,17 @@
# elcaju
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
# elcaju
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+43
View File
@@ -0,0 +1,43 @@
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 = "org.cubabitcoin.elcaju"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
applicationId = "org.cubabitcoin.elcaju"
// 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 = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="elcaju"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.elcaju
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+21
View File
@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
+25
View File
@@ -0,0 +1,25 @@
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.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+616
View File
@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.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 = 12.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 = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -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)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Elcaju</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>elcaju</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+4
View File
@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_es.arb
output-localization-file: app_localizations.dart
output-class: L10n
+58
View File
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
/// Paleta de colores de El Caju
/// Concepto: "Organic Digital / Tropical Privacy"
class AppColors {
AppColors._();
// === GRADIENT BACKGROUND ===
// Degradado diagonal: púrpura profundo → naranja marañón
static const Color deepVoidPurple = Color(0xFF1A0B2E);
static const Color royalCashuPurple = Color(0xFF4A1259);
static const Color maranonOrange = Color(0xFFE35D33);
static const List<Color> backgroundGradient = [
deepVoidPurple,
royalCashuPurple,
maranonOrange,
];
static const List<double> backgroundGradientStops = [0.0, 0.6, 1.0];
// === ACCENT COLORS ===
static const Color primaryAction = Color(0xFFE35D33); // Naranja vibrante
static const Color secondaryAction = Color(0xFFFFB74D); // Amarillo pulpa/Bitcoin
static const Color success = Color(0xFF00E676); // Verde neón
static const Color error = Color(0xFFFF5252); // Rojo suave
static const Color warning = Color(0xFFFFB74D); // Amarillo
// === BUTTON GRADIENT ===
static const Color buttonGradientStart = Color(0xFFE35D33); // Naranja marañón
static const Color buttonGradientEnd = Color(0xFFFF9100); // Ámbar
static const List<Color> buttonGradient = [
buttonGradientStart,
buttonGradientEnd,
];
// === TEXT COLORS ===
static const Color textPrimary = Colors.white;
static const Color textSecondary = Color(0xFFFFE0B2); // Blanco hueso/crema
static Color textTertiary = Colors.white.withValues(alpha: 0.6);
static Color textPlaceholder = Colors.white.withValues(alpha: 0.4);
// === GLASS/CARD COLORS ===
static const Color glassBase = Color(0xFFFFF3E0); // Base naranja muy pálida
static const double glassOpacity = 0.05;
static const double glassBorderOpacity = 0.1;
// === TRANSACTION COLORS ===
static const Color transactionReceived = success;
static const Color transactionSent = maranonOrange;
static const Color transactionPending = secondaryAction;
static const Color transactionFailed = error;
// === MINT STATUS ===
static const Color mintConnected = success;
static const Color mintDisconnected = error;
}
+44
View File
@@ -0,0 +1,44 @@
/// Dimensiones y espaciados de El Caju
class AppDimensions {
AppDimensions._();
// === BORDER RADIUS ===
static const double radiusSmall = 8.0;
static const double radiusMedium = 12.0;
static const double radiusLarge = 16.0;
static const double radiusXLarge = 24.0;
// === PADDING/MARGIN ===
static const double paddingXSmall = 4.0;
static const double paddingSmall = 8.0;
static const double paddingMedium = 16.0;
static const double paddingLarge = 24.0;
static const double paddingXLarge = 32.0;
static const double paddingExtraLarge = 48.0;
// === BUTTON ===
static const double buttonHeight = 56.0;
static const double buttonHeightSmall = 44.0;
static const double buttonBorderRadius = 16.0;
// === ICON ===
static const double iconSmall = 20.0;
static const double iconMedium = 24.0;
static const double iconLarge = 32.0;
static const double iconXLarge = 48.0;
// === CARD ===
static const double cardBorderRadius = 16.0;
static const double cardBorderWidth = 1.0;
static const double cardShadowBlur = 10.0;
static const double cardShadowOffsetY = 4.0;
// === BALANCE DISPLAY ===
static const double balanceFontSize = 48.0;
static const double balanceUnitFontSize = 18.0;
// === MASCOT ===
static const double mascotSizeSmall = 80.0;
static const double mascotSizeMedium = 120.0;
static const double mascotSizeLarge = 160.0;
}
+137
View File
@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../constants/colors.dart';
/// Tema de El Caju
class AppTheme {
AppTheme._();
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
fontFamily: 'Inter',
// Color scheme
colorScheme: const ColorScheme.dark(
primary: AppColors.primaryAction,
secondary: AppColors.secondaryAction,
surface: AppColors.deepVoidPurple,
error: AppColors.error,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.white,
onError: Colors.white,
),
// Scaffold
scaffoldBackgroundColor: Colors.transparent,
// AppBar
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.light,
titleTextStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
iconTheme: IconThemeData(color: Colors.white),
),
// Text
textTheme: const TextTheme(
// Main titles - 48px Bold
displayLarge: TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
// Subtitles - 18px Medium
titleLarge: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
// Body - 16px Medium
bodyLarge: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
bodyMedium: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
// Button text - 16px SemiBold
labelLarge: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
// Input decoration
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.glassBase.withValues(alpha: AppColors.glassOpacity),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: Colors.white.withValues(alpha: AppColors.glassBorderOpacity),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: Colors.white.withValues(alpha: AppColors.glassBorderOpacity),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppColors.primaryAction.withValues(alpha: 0.5),
),
),
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.4),
fontFamily: 'Inter',
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
// Elevated button
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
// Icon
iconTheme: const IconThemeData(
color: Colors.white,
size: 24,
),
);
}
}
+74
View File
@@ -0,0 +1,74 @@
{
"@@locale": "en",
"appName": "ElCaju",
"appTagline": "Your private ecash wallet",
"welcomeTitle": "Welcome to ElCaju",
"welcomeSubtitle": "Your private ecash wallet.\nSimple, secure and non-custodial.",
"createWallet": "Create new wallet",
"restoreWallet": "Restore wallet",
"createWalletTitle": "Create wallet",
"creatingWallet": "Creating your wallet...",
"generatingSeed": "Generating your seed phrase securely",
"createWalletDescription": "A 12-word seed phrase will be generated.\nStore it in a safe place.",
"generateWallet": "Generate wallet",
"walletCreated": "Wallet created!",
"walletCreatedDescription": "Your wallet is ready. We recommend backing up your seed phrase now.",
"backupWarning": "Without backup, you will lose access to your funds if you lose the device.",
"backupNow": "Backup now",
"backupLater": "Do it later",
"backupTitle": "Backup",
"seedPhraseTitle": "Your seed phrase",
"seedPhraseDescription": "Save these 12 words in order. They are the only way to recover your wallet.",
"revealSeedPhrase": "Reveal seed phrase",
"tapToReveal": "Tap the button to reveal\nyour seed phrase",
"copyToClipboard": "Copy to clipboard",
"seedCopied": "Phrase copied to clipboard",
"neverShareSeed": "Never share your seed phrase with anyone.",
"confirmBackup": "I have saved my seed phrase in a safe place",
"continue_": "Continue",
"restoreTitle": "Restore wallet",
"enterSeedPhrase": "Enter your seed phrase",
"enterSeedDescription": "Type the 12 or 24 words separated by spaces.",
"seedPlaceholder": "word1 word2 word3 ...",
"wordCount": "{count} words",
"needWords": "(you need 12 or 24)",
"restoreError": "Restore error: {error}",
"homeTitle": "Home",
"receive": "Receive",
"send": "Send",
"deposit": "Deposit",
"withdraw": "Withdraw",
"lightning": "Lightning",
"history": "History",
"noTransactions": "No transactions yet",
"depositOrReceive": "Deposit or receive sats to start",
"balance": "Balance",
"sats": "sats",
"settings": "Settings",
"backupSeedPhrase": "Backup seed phrase",
"connectedMints": "Connected mints",
"pinAccess": "PIN access",
"language": "Language",
"spanish": "Español",
"english": "English",
"version": "Version",
"about": "About",
"deleteWallet": "Delete wallet",
"confirm": "Confirm",
"cancel": "Cancel",
"error": "Error",
"success": "Success",
"loading": "Loading...",
"retry": "Retry"
}
+88
View File
@@ -0,0 +1,88 @@
{
"@@locale": "es",
"appName": "ElCaju",
"appTagline": "Tu wallet de ecash privado",
"welcomeTitle": "Bienvenido a ElCaju",
"welcomeSubtitle": "Tu wallet de ecash privado.\nSimple, seguro y sin custodia.",
"createWallet": "Crear nueva wallet",
"restoreWallet": "Restaurar wallet",
"createWalletTitle": "Crear wallet",
"creatingWallet": "Creando tu wallet...",
"generatingSeed": "Generando tu frase semilla de forma segura",
"createWalletDescription": "Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.",
"generateWallet": "Generar wallet",
"walletCreated": "¡Wallet creada!",
"walletCreatedDescription": "Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.",
"backupWarning": "Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.",
"backupNow": "Hacer backup ahora",
"backupLater": "Hacerlo después",
"backupTitle": "Backup",
"seedPhraseTitle": "Tu frase semilla",
"seedPhraseDescription": "Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.",
"revealSeedPhrase": "Revelar frase semilla",
"tapToReveal": "Toca el botón para revelar\ntu frase semilla",
"copyToClipboard": "Copiar al portapapeles",
"seedCopied": "Frase copiada al portapapeles",
"neverShareSeed": "Nunca compartas tu frase semilla con nadie.",
"confirmBackup": "He guardado mi frase semilla en un lugar seguro",
"continue_": "Continuar",
"restoreTitle": "Restaurar wallet",
"enterSeedPhrase": "Ingresa tu frase semilla",
"enterSeedDescription": "Escribe las 12 o 24 palabras separadas por espacios.",
"seedPlaceholder": "palabra1 palabra2 palabra3 ...",
"wordCount": "{count} palabras",
"@wordCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"needWords": "(necesitas 12 o 24)",
"restoreError": "Error al restaurar: {error}",
"@restoreError": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"homeTitle": "Inicio",
"receive": "Recibir",
"send": "Enviar",
"deposit": "Depositar",
"withdraw": "Retirar",
"lightning": "Lightning",
"history": "Historial",
"noTransactions": "Sin transacciones aún",
"depositOrReceive": "Deposita o recibe sats para empezar",
"balance": "Balance",
"sats": "sats",
"settings": "Configuración",
"backupSeedPhrase": "Backup seed phrase",
"connectedMints": "Mints conectados",
"pinAccess": "PIN de acceso",
"language": "Idioma",
"spanish": "Español",
"english": "English",
"version": "Versión",
"about": "Acerca de",
"deleteWallet": "Borrar wallet",
"confirm": "Confirmar",
"cancel": "Cancelar",
"error": "Error",
"success": "Éxito",
"loading": "Cargando...",
"retry": "Reintentar"
}
+70
View File
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'core/theme/app_theme.dart';
import 'providers/wallet_provider.dart';
import 'providers/settings_provider.dart';
import 'screens/1_splash/splash_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializar CDK Flutter (Cashu SDK)
await CdkFlutter.init();
// Configurar orientación y estilo de barra de estado
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor: Color(0xFF1A0B2E),
systemNavigationBarIconBrightness: Brightness.light,
),
);
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => SettingsProvider()),
ChangeNotifierProvider(create: (_) => WalletProvider()),
],
child: const ElCajuApp(),
),
);
}
class ElCajuApp extends StatelessWidget {
const ElCajuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ElCaju',
debugShowCheckedModeBanner: false,
theme: AppTheme.darkTheme,
// Localizaciones
localizationsDelegates: const [
L10n.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('es'), // Español (por defecto)
Locale('en'), // English
],
locale: null, // null = detectar del sistema
home: const SplashScreen(),
);
}
}
+186
View File
@@ -0,0 +1,186 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Provider que gestiona preferencias y datos sensibles del usuario.
/// Usa FlutterSecureStorage para datos sensibles (mnemonic, PIN).
/// Usa SharedPreferences para preferencias generales.
class SettingsProvider extends ChangeNotifier {
final _secureStorage = const FlutterSecureStorage();
SharedPreferences? _prefs;
// Keys para almacenamiento
static const _mnemonicKey = 'wallet_mnemonic';
static const _pinKey = 'wallet_pin';
static const _hasWalletKey = 'has_wallet';
static const _defaultMintKey = 'default_mint';
static const _localeKey = 'locale';
static const _pinEnabledKey = 'pin_enabled';
// Estado interno
bool _isInitialized = false;
bool _hasWallet = false;
bool _pinEnabled = false;
String? _pin;
String _locale = 'es';
String _defaultMint = 'https://mint.cubabitcoin.org';
// Getters
bool get isInitialized => _isInitialized;
bool get hasWallet => _hasWallet;
bool get pinEnabled => _pinEnabled;
bool get hasPin => _pin != null && _pin!.isNotEmpty;
String get locale => _locale;
String get defaultMint => _defaultMint;
// ============================================================
// INICIALIZACION
// ============================================================
/// Inicializa el provider cargando preferencias guardadas.
/// Debe llamarse al inicio de la app.
Future<void> initialize() async {
if (_isInitialized) return;
_prefs = await SharedPreferences.getInstance();
// Cargar preferencias
_hasWallet = _prefs?.getBool(_hasWalletKey) ?? false;
_locale = _prefs?.getString(_localeKey) ?? 'es';
_defaultMint = _prefs?.getString(_defaultMintKey) ?? 'https://mint.cubabitcoin.org';
_pinEnabled = _prefs?.getBool(_pinEnabledKey) ?? false;
// Cargar PIN si existe
if (_pinEnabled) {
_pin = await _secureStorage.read(key: _pinKey);
}
_isInitialized = true;
notifyListeners();
}
// ============================================================
// MNEMONIC (Almacenamiento seguro)
// ============================================================
/// Guarda el mnemonic de forma segura (encriptado).
Future<void> saveMnemonic(String mnemonic) async {
await _secureStorage.write(key: _mnemonicKey, value: mnemonic);
await _prefs?.setBool(_hasWalletKey, true);
_hasWallet = true;
notifyListeners();
}
/// Lee el mnemonic guardado.
/// Retorna null si no existe.
Future<String?> getMnemonic() async {
return await _secureStorage.read(key: _mnemonicKey);
}
/// Verifica si hay un mnemonic guardado.
Future<bool> hasMnemonic() async {
final mnemonic = await _secureStorage.read(key: _mnemonicKey);
return mnemonic != null && mnemonic.isNotEmpty;
}
// ============================================================
// PIN DE ACCESO
// ============================================================
/// Configura un nuevo PIN.
Future<void> setPin(String pin) async {
await _secureStorage.write(key: _pinKey, value: pin);
await _prefs?.setBool(_pinEnabledKey, true);
_pin = pin;
_pinEnabled = true;
notifyListeners();
}
/// Verifica si el PIN ingresado es correcto.
bool verifyPin(String pin) {
return _pin == pin;
}
/// Elimina el PIN.
Future<void> removePin() async {
await _secureStorage.delete(key: _pinKey);
await _prefs?.setBool(_pinEnabledKey, false);
_pin = null;
_pinEnabled = false;
notifyListeners();
}
/// Cambia el PIN existente.
/// Retorna true si el PIN anterior era correcto y se cambió.
Future<bool> changePin(String oldPin, String newPin) async {
if (!verifyPin(oldPin)) {
return false;
}
await setPin(newPin);
return true;
}
// ============================================================
// PREFERENCIAS DE IDIOMA
// ============================================================
/// Cambia el idioma de la app.
Future<void> setLocale(String locale) async {
await _prefs?.setString(_localeKey, locale);
_locale = locale;
notifyListeners();
}
// ============================================================
// MINT POR DEFECTO
// ============================================================
/// Establece el mint por defecto.
Future<void> setDefaultMint(String mintUrl) async {
await _prefs?.setString(_defaultMintKey, mintUrl);
_defaultMint = mintUrl;
notifyListeners();
}
// ============================================================
// BORRAR WALLET
// ============================================================
/// Borra todos los datos del wallet (mnemonic, PIN, preferencias).
/// NOTA: La base de datos SQLite debe borrarse desde WalletProvider.
Future<void> deleteWallet() async {
// Borrar datos sensibles
await _secureStorage.delete(key: _mnemonicKey);
await _secureStorage.delete(key: _pinKey);
// Resetear preferencias
await _prefs?.setBool(_hasWalletKey, false);
await _prefs?.setBool(_pinEnabledKey, false);
// Mantener locale y defaultMint (preferencias de app, no de wallet)
// Resetear estado
_hasWallet = false;
_pinEnabled = false;
_pin = null;
notifyListeners();
}
/// Borra TODAS las preferencias (para reset completo de la app).
Future<void> clearAll() async {
// Borrar todo de secure storage
await _secureStorage.deleteAll();
// Borrar todas las preferencias
await _prefs?.clear();
// Resetear estado
_hasWallet = false;
_pinEnabled = false;
_pin = null;
_locale = 'es';
_defaultMint = 'https://mint.cubabitcoin.org';
notifyListeners();
}
}
+398
View File
@@ -0,0 +1,398 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:cdk_flutter/cdk_flutter.dart';
import 'package:path_provider/path_provider.dart';
/// Helper class para info de token parseado
class TokenInfo {
final BigInt amount;
final String mintUrl;
final String encoded;
TokenInfo({
required this.amount,
required this.mintUrl,
required this.encoded,
});
}
/// Provider que gestiona todas las operaciones del wallet Cashu.
/// Capa intermedia entre la UI y cdk_flutter (Rust).
class WalletProvider extends ChangeNotifier {
MultiMintWallet? _multiWallet;
Wallet? _activeWallet;
WalletDatabase? _db;
String? _activeMintUrl;
// Getters
bool get isInitialized => _multiWallet != null;
Wallet? get activeWallet => _activeWallet;
String? get activeMintUrl => _activeMintUrl;
MultiMintWallet? get multiWallet => _multiWallet;
// ============================================================
// INICIALIZACION
// ============================================================
/// Inicializa el wallet con un mnemonic.
/// MultiMintWallet carga automaticamente los mints guardados en la DB.
Future<void> initialize(String mnemonic) async {
// Obtener directorio de documentos (path absoluto requerido)
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
// Crear base de datos (se crea automaticamente si no existe)
_db = await WalletDatabase.newInstance(path: dbPath);
// Crear multi-mint wallet (carga mints de DB automaticamente)
_multiWallet = await MultiMintWallet.newInstance(
unit: 'sat',
mnemonic: mnemonic,
db: _db!,
);
// Verificar si es primera vez (sin mints)
final mints = await _multiWallet!.listMints();
if (mints.isEmpty) {
// Primera vez: agregar mint por defecto
await addMint('https://mint.cubabitcoin.org');
await setActiveMint('https://mint.cubabitcoin.org');
} else {
// Restauracion: usar primer mint como activo
await setActiveMint(mints.first.url);
}
notifyListeners();
}
/// Detecta si es primera vez (sin mints en la DB).
/// Usar despues de initialize().
Future<bool> isFirstTime() async {
if (_multiWallet == null) return true;
final mints = await _multiWallet!.listMints();
return mints.isEmpty;
}
/// Genera un nuevo mnemonic de 12 palabras BIP39.
/// Funcion global de cdk_flutter.
String generateNewMnemonic() {
return generateMnemonic();
}
// ============================================================
// MULTI-MINT OPERATIONS
// ============================================================
/// Agrega un mint. Idempotente (no error si ya existe).
Future<void> addMint(String mintUrl) async {
if (_multiWallet == null) return;
await _multiWallet!.addMint(mintUrl: mintUrl);
notifyListeners();
}
/// Lista los mints conectados.
/// Retorna List de Mint con propiedades: url, balance, info.
Future<List<Mint>> listMints() async {
if (_multiWallet == null) return [];
return await _multiWallet!.listMints();
}
/// Obtiene las URLs de los mints como lista de Strings.
Future<List<String>> getMintUrls() async {
final mints = await listMints();
return mints.map((m) => m.url).toList();
}
/// Remueve un mint. Lanza error si balance > 0.
Future<void> removeMint(String mintUrl) async {
if (_multiWallet == null) return;
await _multiWallet!.removeMint(mintUrl: mintUrl);
// Si era el mint activo, cambiar a otro
if (_activeMintUrl == mintUrl) {
final mints = await listMints();
if (mints.isNotEmpty) {
await setActiveMint(mints.first.url);
} else {
_activeWallet = null;
_activeMintUrl = null;
}
}
notifyListeners();
}
/// Cambia el mint activo.
/// Usa createOrGetWallet para garantizar que exista.
Future<void> setActiveMint(String mintUrl) async {
if (_multiWallet == null) return;
// createOrGetWallet nunca retorna null (crea si no existe)
_activeWallet = await _multiWallet!.createOrGetWallet(mintUrl: mintUrl);
_activeMintUrl = mintUrl;
notifyListeners();
}
// ============================================================
// BALANCE
// ============================================================
/// Stream de balance del wallet activo (reactivo).
Stream<BigInt>? streamBalance() {
return _activeWallet?.streamBalance();
}
/// Stream de balance total de todos los mints.
/// Nota: En MultiMintWallet el método es streamBalance() (no streamTotalBalance).
Stream<BigInt>? streamTotalBalance() {
return _multiWallet?.streamBalance();
}
/// Obtiene el balance total de todos los mints.
Future<BigInt> getTotalBalance() async {
if (_multiWallet == null) return BigInt.zero;
return await _multiWallet!.totalBalance();
}
/// Obtiene el balance de un mint especifico.
Future<BigInt> getBalanceForMint(String mintUrl) async {
if (_multiWallet == null) return BigInt.zero;
final wallet = await _multiWallet!.getWallet(mintUrl: mintUrl);
if (wallet == null) return BigInt.zero;
return await wallet.balance();
}
// ============================================================
// RECEIVE (Recibir tokens Cashu)
// ============================================================
/// Parsea un token sin reclamarlo. Retorna null si invalido.
TokenInfo? parseToken(String encodedToken) {
try {
final token = Token.parse(encoded: encodedToken);
return TokenInfo(
amount: token.amount,
mintUrl: token.mintUrl,
encoded: token.encoded,
);
} catch (e) {
// Token.parse lanza excepcion si invalido
return null;
}
}
/// Reclama un token Cashu. Retorna monto recibido.
Future<BigInt> receiveToken(String encodedToken) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
// Parsear token (lanza excepcion si invalido)
final token = Token.parse(encoded: encodedToken);
// Reclamar token
final amount = await _activeWallet!.receive(token: token);
notifyListeners();
return amount;
}
/// Reclama un token P2PK (bloqueado a una clave publica).
/// Requiere la clave privada correspondiente para firmar.
/// [signingKeys] lista de claves privadas en formato hex.
Future<BigInt> receiveP2pkToken(
String encodedToken,
List<String> signingKeys,
) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
final token = Token.parse(encoded: encodedToken);
// Reclamar con claves de firma para P2PK
final amount = await _activeWallet!.receive(
token: token,
opts: ReceiveOptions(signingKeys: signingKeys),
);
notifyListeners();
return amount;
}
// ============================================================
// SEND (Enviar tokens Cashu)
// ============================================================
/// Prepara un envio (reserva proofs, calcula fees).
/// Retorna PreparedSend para confirmar o cancelar.
Future<PreparedSend> prepareSend(BigInt amount) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
return await _activeWallet!.prepareSend(amount: amount);
}
/// Prepara un envio P2PK (bloqueado a una clave publica).
/// Solo el poseedor de la clave privada correspondiente puede reclamar.
/// [pubkey] clave publica del receptor en formato hex.
Future<PreparedSend> prepareSendP2pk(BigInt amount, String pubkey) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
return await _activeWallet!.prepareSend(
amount: amount,
opts: SendOptions(pubkey: pubkey),
);
}
/// Confirma un envio preparado y retorna el token encoded.
Future<String> confirmSend(PreparedSend prepared, String? memo) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
final token = await _activeWallet!.send(
send: prepared,
memo: memo,
includeMemo: memo != null && memo.isNotEmpty,
);
notifyListeners();
return token.encoded;
}
/// Cancela un envio preparado (libera proofs reservados).
Future<void> cancelSend(PreparedSend prepared) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
await _activeWallet!.cancelSend(send: prepared);
}
/// Metodo de conveniencia: prepara y confirma en un solo paso.
Future<String> sendTokens(BigInt amount, String? memo) async {
final prepared = await prepareSend(amount);
return await confirmSend(prepared, memo);
}
/// Envia tokens P2PK (bloqueados a una clave publica).
/// Solo el poseedor de la clave privada correspondiente puede reclamar.
Future<String> sendTokensP2pk(
BigInt amount,
String pubkey,
String? memo,
) async {
final prepared = await prepareSendP2pk(amount, pubkey);
return await confirmSend(prepared, memo);
}
// ============================================================
// MINT (Depositar via Lightning)
// ============================================================
/// Inicia un deposito via Lightning.
/// Retorna Stream con estados: unpaid -> paid -> issued.
/// quote.request contiene el invoice BOLT11 a pagar.
Stream<MintQuote> mintTokens(BigInt amount, String? description) {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
return _activeWallet!.mint(
amount: amount,
description: description,
);
}
// ============================================================
// MELT (Retirar a Lightning)
// ============================================================
/// Obtiene quote para pagar un invoice BOLT11.
/// MeltQuote contiene: amount, feeReserve, expiry.
Future<MeltQuote> getMeltQuote(String bolt11Invoice) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
return await _activeWallet!.meltQuote(request: bolt11Invoice);
}
/// Ejecuta el pago del invoice.
/// Retorna monto total pagado (incluyendo fee).
Future<BigInt> melt(MeltQuote quote) async {
if (_activeWallet == null) {
throw Exception('Wallet no inicializado');
}
final totalPaid = await _activeWallet!.melt(quote: quote);
notifyListeners();
return totalPaid;
}
// ============================================================
// HISTORIAL
// ============================================================
/// Obtiene transacciones del wallet activo.
/// Filtrable por direccion: incoming, outgoing.
Future<List<Transaction>> getTransactions({
TransactionDirection? direction,
}) async {
if (_activeWallet == null) return [];
return await _activeWallet!.listTransactions(direction: direction);
}
/// Obtiene transacciones de todos los mints.
Future<List<Transaction>> getAllTransactions({
TransactionDirection? direction,
}) async {
if (_multiWallet == null) return [];
return await _multiWallet!.listTransactions(direction: direction);
}
// ============================================================
// BORRAR WALLET
// ============================================================
/// Borra la base de datos SQLite del wallet.
/// Debe llamarse ANTES de borrar el mnemonic en SettingsProvider.
/// Retorna true si se borró correctamente.
Future<bool> deleteDatabase() async {
try {
// Limpiar referencias internas primero
_multiWallet = null;
_activeWallet = null;
_activeMintUrl = null;
_db = null;
// Obtener path de la base de datos
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
// Borrar archivo si existe
final dbFile = File(dbPath);
if (await dbFile.exists()) {
await dbFile.delete();
}
notifyListeners();
return true;
} catch (e) {
debugPrint('Error al borrar database: $e');
return false;
}
}
/// Resetea el estado del provider (sin borrar la DB).
/// Útil para logout sin borrar datos.
void reset() {
_multiWallet = null;
_activeWallet = null;
_activeMintUrl = null;
_db = null;
notifyListeners();
}
}
+194
View File
@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../providers/settings_provider.dart';
import '../../providers/wallet_provider.dart';
import '../2_onboarding/welcome_screen.dart';
import '../3_home/home_screen.dart';
/// Splash Screen de El Caju
/// Inicializa los providers y redirige según el estado del wallet.
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
String? _errorMessage;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_controller.forward();
// Inicializar la app
_initializeApp();
}
Future<void> _initializeApp() async {
try {
final settingsProvider = context.read<SettingsProvider>();
final walletProvider = context.read<WalletProvider>();
// Inicializar settings (cargar preferencias)
await settingsProvider.initialize();
// Esperar mínimo 2 segundos para mostrar splash
await Future.delayed(const Duration(milliseconds: 2000));
if (!mounted) return;
// Verificar si ya existe un wallet
if (settingsProvider.hasWallet) {
// Cargar mnemonic e inicializar wallet
final mnemonic = await settingsProvider.getMnemonic();
if (mnemonic != null && mnemonic.isNotEmpty) {
await walletProvider.initialize(mnemonic);
if (!mounted) return;
// Ir al Home
_navigateTo(const HomeScreen());
} else {
// Error: hasWallet=true pero sin mnemonic
// Esto no debería pasar, pero por si acaso
_navigateTo(const WelcomeScreen());
}
} else {
// No hay wallet, ir al onboarding
_navigateTo(const WelcomeScreen());
}
} catch (e) {
// Mostrar error pero permitir continuar al onboarding
debugPrint('Error al inicializar: $e');
setState(() {
_errorMessage = e.toString();
});
// Esperar y continuar al onboarding
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
_navigateTo(const WelcomeScreen());
}
}
}
void _navigateTo(Widget screen) {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Center(
child: FadeTransition(
opacity: _fadeAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Mascota / Logo
Image.asset(
'assets/img/elcajucubano.png',
width: 200,
height: 200,
fit: BoxFit.contain,
),
const SizedBox(height: AppDimensions.paddingLarge),
// Nombre de la app
Text(
l10n?.appName ?? 'ElCaju',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.2,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Subtítulo
Text(
l10n?.appTagline ?? 'Tu wallet de ecash privado',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.8),
),
),
const SizedBox(height: AppDimensions.paddingXLarge * 2),
// Loading indicator o mensaje de error
if (_errorMessage != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.error,
),
textAlign: TextAlign.center,
),
)
else
SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.secondaryAction.withValues(alpha: 0.8),
),
),
),
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,297 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/glass_card.dart';
/// Pantalla de backup de la frase semilla
class BackupSeedScreen extends StatefulWidget {
final String mnemonic;
final bool isFromSettings;
const BackupSeedScreen({
super.key,
required this.mnemonic,
this.isFromSettings = false,
});
@override
State<BackupSeedScreen> createState() => _BackupSeedScreenState();
}
class _BackupSeedScreenState extends State<BackupSeedScreen> {
bool _revealed = false;
bool _confirmed = false;
List<String> get _words => widget.mnemonic.split(' ');
void _copyToClipboard(L10n l10n) {
Clipboard.setData(ClipboardData(text: widget.mnemonic));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.seedCopied),
backgroundColor: AppColors.success.withValues(alpha: 0.9),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.backupTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.seedPhraseTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
l10n.seedPhraseDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.4,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Expanded(
child: GlassCard(
child: Column(
children: [
Expanded(
child: _revealed
? _buildWordsGrid()
: _buildHiddenView(l10n),
),
if (_revealed) ...[
const Divider(color: Colors.white24, height: 1),
const SizedBox(height: AppDimensions.paddingMedium),
GestureDetector(
onTap: () => _copyToClipboard(l10n),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.copy,
color: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 8),
Text(
l10n.copyToClipboard,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
),
],
],
),
),
),
const SizedBox(height: AppDimensions.paddingLarge),
if (_revealed)
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
children: [
const Icon(
Icons.warning_amber_rounded,
color: AppColors.warning,
size: 24,
),
const SizedBox(width: AppDimensions.paddingSmall),
Expanded(
child: Text(
l10n.neverShareSeed,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: AppColors.textSecondary,
),
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingLarge),
if (_revealed && !widget.isFromSettings)
GestureDetector(
onTap: () => setState(() => _confirmed = !_confirmed),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: _confirmed
? AppColors.success
: Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: _confirmed
? AppColors.success
: Colors.white38,
width: 2,
),
),
child: _confirmed
? const Icon(Icons.check, size: 16, color: Colors.white)
: null,
),
const SizedBox(width: AppDimensions.paddingSmall),
Expanded(
child: Text(
l10n.confirmBackup,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingMedium),
if (!_revealed)
PrimaryButton(
text: l10n.revealSeedPhrase,
icon: Icons.visibility,
onPressed: () => setState(() => _revealed = true),
)
else
PrimaryButton(
text: widget.isFromSettings ? 'Volver' : l10n.continue_,
onPressed: (widget.isFromSettings || _confirmed)
? () => Navigator.pop(context)
: null,
),
],
),
),
),
),
);
}
Widget _buildHiddenView(L10n l10n) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility_off,
size: 48,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
l10n.tapToReveal,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.7),
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildWordsGrid() {
return GridView.builder(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _words.length,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: AppColors.deepVoidPurple.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '${index + 1}. ',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.6),
),
),
TextSpan(
text: _words[index],
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
);
},
);
}
}
@@ -0,0 +1,302 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
import '../../widgets/common/glass_card.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/settings_provider.dart';
import 'backup_seed_screen.dart';
import '../3_home/home_screen.dart';
/// Pantalla de creación de wallet
class CreateWalletScreen extends StatefulWidget {
const CreateWalletScreen({super.key});
@override
State<CreateWalletScreen> createState() => _CreateWalletScreenState();
}
class _CreateWalletScreenState extends State<CreateWalletScreen> {
bool _isCreating = false;
bool _walletCreated = false;
String? _mnemonic;
String? _errorMessage;
Future<void> _createWallet() async {
setState(() {
_isCreating = true;
_errorMessage = null;
});
try {
final walletProvider = context.read<WalletProvider>();
final settingsProvider = context.read<SettingsProvider>();
// Generar mnemonic real (12 palabras BIP39)
_mnemonic = walletProvider.generateNewMnemonic();
// Guardar mnemonic de forma segura
await settingsProvider.saveMnemonic(_mnemonic!);
// Inicializar wallet con el mnemonic
await walletProvider.initialize(_mnemonic!);
setState(() {
_isCreating = false;
_walletCreated = true;
});
} catch (e) {
setState(() {
_isCreating = false;
_errorMessage = e.toString();
});
}
}
void _goToHome() {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
(route) => false,
);
}
void _goToBackup() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BackupSeedScreen(mnemonic: _mnemonic!),
),
).then((_) {
_goToHome();
});
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.createWalletTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: _walletCreated
? _buildBackupOptions(l10n)
: _buildCreateView(l10n),
),
),
),
);
}
Widget _buildCreateView(L10n l10n) {
return Column(
children: [
const Spacer(),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
_isCreating ? Icons.hourglass_top : Icons.account_balance_wallet,
size: 60,
color: AppColors.secondaryAction,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Text(
_isCreating ? l10n.creatingWallet : l10n.createWalletTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
_isCreating ? l10n.generatingSeed : l10n.createWalletDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
// Mensaje de error si existe
if (_errorMessage != null) ...[
const SizedBox(height: AppDimensions.paddingLarge),
Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
),
],
const Spacer(),
if (!_isCreating)
PrimaryButton(
text: l10n.generateWallet,
icon: Icons.auto_awesome,
onPressed: _createWallet,
),
if (_isCreating)
const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.secondaryAction),
),
),
const SizedBox(height: AppDimensions.paddingXLarge),
],
);
}
Widget _buildBackupOptions(L10n l10n) {
return Column(
children: [
const Spacer(),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_circle,
size: 60,
color: AppColors.success,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Text(
l10n.walletCreated,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
l10n.walletCreatedDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingLarge),
GlassCard(
child: Row(
children: [
const Icon(
Icons.warning_amber_rounded,
color: AppColors.warning,
size: 32,
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: Text(
l10n.backupWarning,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
height: 1.4,
),
),
),
],
),
),
const Spacer(),
PrimaryButton(
text: l10n.backupNow,
icon: Icons.shield,
onPressed: _goToBackup,
),
const SizedBox(height: AppDimensions.paddingMedium),
SecondaryButton(
text: l10n.backupLater,
onPressed: _goToHome,
),
const SizedBox(height: AppDimensions.paddingXLarge),
],
);
}
}
@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/glass_card.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/settings_provider.dart';
import '../3_home/home_screen.dart';
/// Pantalla de restauración de wallet
class RestoreWalletScreen extends StatefulWidget {
const RestoreWalletScreen({super.key});
@override
State<RestoreWalletScreen> createState() => _RestoreWalletScreenState();
}
class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
final TextEditingController _seedController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isRestoring = false;
String? _errorMessage;
int get _wordCount {
final text = _seedController.text.trim();
if (text.isEmpty) return 0;
return text.split(RegExp(r'\s+')).length;
}
bool get _isValidWordCount => _wordCount == 12 || _wordCount == 24;
@override
void initState() {
super.initState();
_seedController.addListener(() => setState(() {}));
}
@override
void dispose() {
_seedController.dispose();
_focusNode.dispose();
super.dispose();
}
Future<void> _restoreWallet() async {
if (!_isValidWordCount) return;
setState(() {
_isRestoring = true;
_errorMessage = null;
});
try {
final mnemonic = _seedController.text.trim().toLowerCase();
final walletProvider = context.read<WalletProvider>();
final settingsProvider = context.read<SettingsProvider>();
// Guardar mnemonic de forma segura
await settingsProvider.saveMnemonic(mnemonic);
// Inicializar wallet (esto valida el mnemonic internamente)
// Si el mnemonic es inválido, cdk_flutter lanzará una excepción
await walletProvider.initialize(mnemonic);
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomeScreen()),
(route) => false,
);
}
} catch (e) {
// Revertir el guardado del mnemonic si falló la inicialización
final settingsProvider = context.read<SettingsProvider>();
await settingsProvider.deleteWallet();
final l10n = L10n.of(context)!;
setState(() {
_errorMessage = l10n.restoreError(e.toString());
_isRestoring = false;
});
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.restoreTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.enterSeedPhrase,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
l10n.enterSeedDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.8),
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Expanded(
child: GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: TextField(
controller: _seedController,
focusNode: _focusNode,
maxLines: null,
expands: true,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
height: 1.6,
),
decoration: InputDecoration(
hintText: l10n.seedPlaceholder,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
),
autocorrect: false,
enableSuggestions: false,
textInputAction: TextInputAction.done,
),
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Row(
children: [
Icon(
_isValidWordCount
? Icons.check_circle
: Icons.info_outline,
size: 18,
color: _isValidWordCount
? AppColors.success
: AppColors.textSecondary.withValues(alpha: 0.6),
),
const SizedBox(width: 8),
Text(
l10n.wordCount(_wordCount),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: _isValidWordCount
? AppColors.success
: AppColors.textSecondary.withValues(alpha: 0.6),
),
),
if (_wordCount > 0 && !_isValidWordCount) ...[
const SizedBox(width: 8),
Text(
l10n.needWords,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
],
],
),
if (_errorMessage != null) ...[
const SizedBox(height: AppDimensions.paddingMedium),
Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
),
],
const SizedBox(height: AppDimensions.paddingLarge),
PrimaryButton(
text: l10n.restoreWallet,
icon: Icons.restore,
isLoading: _isRestoring,
onPressed: _isValidWordCount && !_isRestoring
? _restoreWallet
: null,
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
import 'create_wallet_screen.dart';
import 'restore_wallet_screen.dart';
/// Pantalla de bienvenida - Onboarding
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
children: [
const Spacer(flex: 1),
// Logo / Mascota
Image.asset(
'assets/img/elcajucubano.png',
width: 180,
height: 180,
fit: BoxFit.contain,
),
const SizedBox(height: AppDimensions.paddingLarge),
// Título
Text(
l10n.welcomeTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingSmall),
// Subtítulo
Text(
l10n.welcomeSubtitle,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
const Spacer(flex: 2),
// Botón crear wallet
PrimaryButton(
text: l10n.createWallet,
icon: Icons.add_circle_outline,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CreateWalletScreen(),
),
);
},
),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón restaurar wallet
SecondaryButton(
text: l10n.restoreWallet,
icon: Icons.restore,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RestoreWalletScreen(),
),
);
},
),
const SizedBox(height: AppDimensions.paddingXLarge),
],
),
),
),
),
);
}
}
+902
View File
@@ -0,0 +1,902 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/effects/cashu_confetti.dart';
import '../../providers/wallet_provider.dart';
import '../4_receive/receive_screen.dart';
import '../5_send/send_screen.dart';
import '../6_mint/mint_screen.dart';
import '../7_melt/melt_screen.dart';
import '../8_settings/settings_screen.dart';
import '../8_settings/mints_screen.dart';
/// Pantalla principal - Home
/// Muestra balance, acciones principales e historial
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// Estado local
bool _isBalanceVisible = true;
// Controller para el efecto confeti
final CashuConfettiController _confettiController = CashuConfettiController();
@override
void dispose() {
_confettiController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CashuConfetti(
controller: _confettiController,
child: GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
children: [
// Header (settings, nombre, mascota)
_buildHeader(),
// Mint Selector (debajo del header)
_buildMintSelector(),
// Contenido principal centrado
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Balance (reactivo con StreamBuilder)
_buildBalanceSection(),
const SizedBox(height: AppDimensions.paddingLarge),
// Acciones principales
_buildActions(),
],
),
),
),
// Botón de historial (bottom)
_buildHistoryButton(),
],
),
),
),
),
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Settings button (izquierda)
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall + 4),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
);
},
child: const Icon(
LucideIcons.settings,
color: Colors.white,
size: 32,
),
),
// Nombre de la wallet (centro)
const Text(
'ElCaju',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
// El Caju (derecha) - toca para confeti
GestureDetector(
onTap: () => _confettiController.fire(),
child: Image.asset(
'assets/img/elcajucubano.png',
width: 56,
height: 56,
fit: BoxFit.contain,
),
),
],
),
);
}
Widget _buildBalanceSection() {
final walletProvider = context.watch<WalletProvider>();
return Padding(
padding: const EdgeInsets.symmetric(
vertical: AppDimensions.paddingMedium,
horizontal: AppDimensions.paddingMedium,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Ojo encima del saldo (centrado)
Center(
child: GestureDetector(
onTap: () {
setState(() {
_isBalanceVisible = !_isBalanceVisible;
});
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_isBalanceVisible ? LucideIcons.eye : LucideIcons.eyeOff,
color: AppColors.textSecondary.withValues(alpha: 0.6),
size: 24,
),
),
),
),
const SizedBox(height: 8),
// Balance reactivo con StreamBuilder
StreamBuilder<BigInt>(
stream: walletProvider.streamTotalBalance(),
builder: (context, snapshot) {
final balance = snapshot.data ?? BigInt.zero;
final balanceInt = balance.toInt();
return Text(
_isBalanceVisible ? _formatBalance(balanceInt) : '••••••',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
},
),
const SizedBox(height: 4),
// Unidad (siempre sats por ahora)
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'sats',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.primaryAction,
),
),
),
],
),
);
}
Widget _buildMintSelector() {
final walletProvider = context.watch<WalletProvider>();
final activeMintUrl = walletProvider.activeMintUrl;
// Extraer solo el host del URL para mostrar
String displayMint = 'Sin mint';
if (activeMintUrl != null) {
try {
final uri = Uri.parse(activeMintUrl);
displayMint = uri.host;
} catch (_) {
displayMint = activeMintUrl;
}
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: AppDimensions.paddingSmall),
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MintsScreen()),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.success.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: walletProvider.isInitialized
? AppColors.success
: AppColors.warning,
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Text(
displayMint,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
const SizedBox(width: 6),
Icon(
LucideIcons.chevronDown,
color: Colors.white.withValues(alpha: 0.6),
size: 18,
),
],
),
),
),
);
}
Widget _buildActions() {
return Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
children: [
// Enviar (primero) - flecha diagonal arriba derecha
Expanded(
child: _ActionButton(label: 'Enviar ↗', onTap: _showSendOptions),
),
const SizedBox(width: AppDimensions.paddingMedium),
// Recibir (segundo) - flecha diagonal abajo derecha
Expanded(
child: _ActionButton(
label: '↘ Recibir',
onTap: _showReceiveOptions,
),
),
],
),
);
}
void _showReceiveOptions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _MethodSelectorModal(
title: 'Recibir',
options: [
_MethodOption(
icon: LucideIcons.bean,
label: 'Cashu',
description: 'Pegar token ecash',
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ReceiveScreen()),
);
},
),
_MethodOption(
icon: LucideIcons.zap,
label: 'Lightning',
description: 'Generar invoice para depositar',
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MintScreen()),
);
},
),
],
),
);
}
void _showSendOptions() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _MethodSelectorModal(
title: 'Enviar',
options: [
_MethodOption(
icon: LucideIcons.bean,
label: 'Cashu',
description: 'Crear token ecash',
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SendScreen()),
);
},
),
_MethodOption(
icon: LucideIcons.zap,
label: 'Lightning',
description: 'Pagar invoice Lightning',
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MeltScreen()),
);
},
),
],
),
);
}
Widget _buildHistoryButton() {
return Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: SizedBox(
width: double.infinity,
child: GlassCard(
onTap: _showHistoryModal,
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingLarge,
vertical: AppDimensions.paddingMedium + 4,
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.history, color: Colors.white, size: 28),
SizedBox(width: AppDimensions.paddingSmall),
Text(
'Historial',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
);
}
void _showHistoryModal() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => const _HistoryModal(),
);
}
String _formatBalance(int amount) {
if (amount >= 1000) {
final formatted = amount.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
return formatted;
}
return amount.toString();
}
}
/// Botón de acción para el home
class _ActionButton extends StatelessWidget {
final String label;
final VoidCallback onTap;
const _ActionButton({required this.label, required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: AppDimensions.paddingMedium,
horizontal: AppDimensions.paddingSmall,
),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColors.buttonGradient,
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(AppDimensions.cardBorderRadius),
boxShadow: [
BoxShadow(
color: AppColors.primaryAction.withValues(alpha: 0.4),
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: Center(
child: Text(
label,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
);
}
}
/// Modal del historial de transacciones
class _HistoryModal extends StatelessWidget {
const _HistoryModal();
@override
Widget build(BuildContext context) {
final walletProvider = context.watch<WalletProvider>();
return Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
children: [
// Handle
Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Título
const Padding(
padding: EdgeInsets.all(AppDimensions.paddingMedium),
child: Text(
'Historial',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
// Lista de transacciones
Expanded(
child: FutureBuilder<List<Transaction>>(
future: walletProvider.getAllTransactions(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.primaryAction,
),
);
}
final transactions = snapshot.data ?? [];
if (transactions.isEmpty) {
return _buildEmptyHistory();
}
return ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
),
itemCount: transactions.length,
itemBuilder: (context, index) {
final tx = transactions[index];
return _TransactionTile(transaction: tx);
},
);
},
),
),
],
),
);
}
Widget _buildEmptyHistory() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.history,
size: 48,
color: AppColors.textSecondary.withValues(alpha: 0.3),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
'Sin transacciones aún',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
'Recibe tokens Cashu para empezar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.4),
),
),
],
),
);
}
}
/// Tile para mostrar una transacción
class _TransactionTile extends StatelessWidget {
final Transaction transaction;
const _TransactionTile({required this.transaction});
@override
Widget build(BuildContext context) {
final isIncoming = transaction.direction == TransactionDirection.incoming;
final amount = transaction.amount.toInt();
final fee = transaction.fee.toInt();
// Convertir timestamp (BigInt unix) a DateTime
final timestamp = DateTime.fromMillisecondsSinceEpoch(
transaction.timestamp.toInt() * 1000,
);
// Formatear fecha
final dateStr = _formatDate(timestamp);
// Estado (pending o settled)
final isPending = transaction.status == TransactionStatus.pending;
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingSmall),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
children: [
// Icono de dirección
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: (isIncoming ? AppColors.success : AppColors.primaryAction)
.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
isIncoming ? LucideIcons.arrowDownLeft : LucideIcons.arrowUpRight,
color: isIncoming ? AppColors.success : AppColors.primaryAction,
size: 20,
),
),
const SizedBox(width: AppDimensions.paddingMedium),
// Info de la transacción
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
isIncoming ? 'Recibido' : 'Enviado',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (isPending) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.warning.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Pendiente',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.warning,
),
),
),
],
],
),
const SizedBox(height: 2),
Text(
dateStr,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.6),
),
),
if (transaction.memo != null && transaction.memo!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
transaction.memo!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// Monto
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${isIncoming ? '+' : '-'}$amount',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.bold,
color: isIncoming ? AppColors.success : AppColors.primaryAction,
),
),
if (fee > 0)
Text(
'fee: $fee',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 10,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
],
),
],
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final diff = now.difference(date);
if (diff.inMinutes < 1) {
return 'Ahora';
} else if (diff.inHours < 1) {
return 'Hace ${diff.inMinutes} min';
} else if (diff.inDays < 1) {
return 'Hace ${diff.inHours} h';
} else if (diff.inDays < 7) {
return 'Hace ${diff.inDays} días';
} else {
return '${date.day}/${date.month}/${date.year}';
}
}
}
/// Modelo para opciones del selector
class _MethodOption {
final IconData icon;
final String label;
final String description;
final VoidCallback onTap;
_MethodOption({
required this.icon,
required this.label,
required this.description,
required this.onTap,
});
}
/// Modal selector de método (Lightning / Cashu)
class _MethodSelectorModal extends StatelessWidget {
final String title;
final List<_MethodOption> options;
const _MethodSelectorModal({required this.title, required this.options});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Título
Text(
title,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Opciones
...options.map(
(option) => Padding(
padding: const EdgeInsets.only(
bottom: AppDimensions.paddingSmall,
),
child: _MethodOptionTile(option: option),
),
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
}
}
/// Tile para cada opción del selector
class _MethodOptionTile extends StatelessWidget {
final _MethodOption option;
const _MethodOptionTile({required this.option});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: option.onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
children: [
// Icono
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColors.buttonGradient,
),
borderRadius: BorderRadius.circular(12),
),
child: Icon(option.icon, color: Colors.white, size: 24),
),
const SizedBox(width: AppDimensions.paddingMedium),
// Texto
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.label,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 2),
Text(
option.description,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
// Flecha
Icon(
LucideIcons.chevronRight,
color: Colors.white.withValues(alpha: 0.5),
size: 24,
),
],
),
),
);
}
}
+448
View File
@@ -0,0 +1,448 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/effects/cashu_confetti.dart';
import '../../providers/wallet_provider.dart';
/// Pantalla para recibir tokens Cashu
class ReceiveScreen extends StatefulWidget {
const ReceiveScreen({super.key});
@override
State<ReceiveScreen> createState() => _ReceiveScreenState();
}
class _ReceiveScreenState extends State<ReceiveScreen> {
final TextEditingController _tokenController = TextEditingController();
final CashuConfettiController _confettiController = CashuConfettiController();
// Estado del token
bool _isValidToken = false;
bool _isProcessing = false;
bool _showSuccess = false;
int _receivedAmount = 0;
TokenInfo? _tokenInfo;
String? _errorMessage;
@override
void dispose() {
_tokenController.dispose();
_confettiController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CashuConfetti(
controller: _confettiController,
child: GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Recibir Cashu',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: _showSuccess ? _buildSuccessView() : _buildReceiveForm(),
),
),
),
),
);
}
Widget _buildReceiveForm() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Instrucciones
Text(
'Pega el token Cashu:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Campo de texto para el token
_buildTokenInput(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón pegar del portapapeles
_buildPasteButton(),
const SizedBox(height: AppDimensions.paddingLarge),
// Preview del token (si es válido)
if (_isValidToken && _tokenInfo != null) _buildTokenPreview(),
// Mensaje de error (si hay)
if (_errorMessage != null) _buildErrorMessage(),
const Spacer(),
// Botón reclamar
_buildClaimButton(),
],
);
}
Widget _buildSuccessView() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icono de éxito
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.checkCircle2,
color: AppColors.success,
size: 50,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Monto recibido
Text(
'+$_receivedAmount sats',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
'Tokens recibidos',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingLarge * 2),
// Botón volver
PrimaryButton(
text: 'Volver al inicio',
onPressed: () => Navigator.pop(context),
),
],
);
}
Widget _buildTokenInput() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _tokenController,
maxLines: 5,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
decoration: InputDecoration(
hintText: 'cashuA...',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(AppDimensions.paddingSmall),
),
onChanged: _onTokenChanged,
),
);
}
Widget _buildPasteButton() {
return GestureDetector(
onTap: _pasteFromClipboard,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall + 4,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.clipboard,
color: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 8),
Text(
'Pegar del portapapeles',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildTokenPreview() {
final amount = _tokenInfo!.amount.toInt();
// Extraer solo el host del mint URL
String mintDisplay = _tokenInfo!.mintUrl;
try {
final uri = Uri.parse(_tokenInfo!.mintUrl);
mintDisplay = uri.host;
} catch (_) {}
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Estado válido
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
color: AppColors.success,
size: 16,
),
),
const SizedBox(width: 8),
Text(
'Token válido',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.success,
),
),
],
),
const SizedBox(height: AppDimensions.paddingMedium),
// Monto
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Monto:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
Text(
'$amount sats',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 8),
// Mint
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Mint:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
Flexible(
child: Text(
mintDisplay,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
);
}
Widget _buildErrorMessage() {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
Widget _buildClaimButton() {
return PrimaryButton(
text: _isProcessing ? 'Reclamando...' : 'Reclamar tokens',
onPressed: _isValidToken && !_isProcessing ? _claimToken : null,
);
}
Future<void> _pasteFromClipboard() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData?.text != null) {
_tokenController.text = clipboardData!.text!.trim();
_onTokenChanged(clipboardData.text!.trim());
}
}
void _onTokenChanged(String value) {
final walletProvider = context.read<WalletProvider>();
setState(() {
_errorMessage = null;
if (value.isEmpty) {
_isValidToken = false;
_tokenInfo = null;
return;
}
// Parsear token real con cdk-flutter
final tokenInfo = walletProvider.parseToken(value.trim());
if (tokenInfo != null) {
_isValidToken = true;
_tokenInfo = tokenInfo;
} else {
_isValidToken = false;
_tokenInfo = null;
_errorMessage = 'Token inválido o malformado';
}
});
}
Future<void> _claimToken() async {
setState(() {
_isProcessing = true;
_errorMessage = null;
});
try {
final walletProvider = context.read<WalletProvider>();
// Reclamar token real
final amountReceived = await walletProvider.receiveToken(
_tokenController.text.trim(),
);
if (mounted) {
setState(() {
_receivedAmount = amountReceived.toInt();
_showSuccess = true;
_isProcessing = false;
});
// Disparar confetti
_confettiController.fire();
}
} catch (e) {
setState(() {
// Mensajes de error más amigables
final errorStr = e.toString().toLowerCase();
if (errorStr.contains('already spent') || errorStr.contains('token already')) {
_errorMessage = 'Este token ya fue reclamado';
} else if (errorStr.contains('unknown mint') || errorStr.contains('mint not found')) {
_errorMessage = 'Token de un mint desconocido';
} else {
_errorMessage = 'Error al reclamar: $e';
}
});
} finally {
if (mounted && !_showSuccess) {
setState(() {
_isProcessing = false;
});
}
}
}
}
+513
View File
@@ -0,0 +1,513 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
import 'share_token_screen.dart';
/// Pantalla para enviar tokens Cashu
class SendScreen extends StatefulWidget {
const SendScreen({super.key});
@override
State<SendScreen> createState() => _SendScreenState();
}
class _SendScreenState extends State<SendScreen> {
final TextEditingController _amountController = TextEditingController();
final TextEditingController _memoController = TextEditingController();
final String _unit = 'sats';
bool _isProcessing = false;
String? _errorMessage;
int _availableBalance = 0;
@override
void initState() {
super.initState();
_loadBalance();
}
Future<void> _loadBalance() async {
final walletProvider = context.read<WalletProvider>();
final balance = await walletProvider.getTotalBalance();
if (mounted) {
setState(() {
_availableBalance = balance.toInt();
});
}
}
@override
void dispose() {
_amountController.dispose();
_memoController.dispose();
super.dispose();
}
int get _amount => int.tryParse(_amountController.text) ?? 0;
bool get _isValidAmount => _amount > 0 && _amount <= _availableBalance;
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Enviar Cashu',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monto a enviar
_buildAmountSection(),
const SizedBox(height: AppDimensions.paddingLarge),
// Memo opcional
_buildMemoSection(),
const SizedBox(height: AppDimensions.paddingMedium),
// Mensaje de error (si hay)
if (_errorMessage != null) _buildErrorMessage(),
const Spacer(),
// Botón crear token
_buildCreateButton(),
],
),
),
),
),
);
}
Widget _buildAmountSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Monto a enviar:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Input de monto
GlassCard(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall,
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
decoration: InputDecoration(
hintText: '0',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
),
onChanged: (_) => setState(() {
_errorMessage = null;
}),
),
),
Text(
_unit,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Balance disponible
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Disponible:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
GestureDetector(
onTap: _setMaxAmount,
child: Row(
children: [
Text(
'$_availableBalance $_unit',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryAction,
),
),
const SizedBox(width: 4),
Text(
'(Max)',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.primaryAction.withValues(alpha: 0.7),
),
),
],
),
),
],
),
],
);
}
Widget _buildMemoSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Memo (opcional):',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _memoController,
maxLines: 2,
maxLength: 100,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: 'Ej: Para el cafe',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
counterStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
),
],
);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
Widget _buildCreateButton() {
return PrimaryButton(
text: _isProcessing ? 'Creando token...' : 'Crear token',
onPressed: _isValidAmount && !_isProcessing ? _showConfirmation : null,
);
}
void _setMaxAmount() {
_amountController.text = _availableBalance.toString();
setState(() {
_errorMessage = null;
});
}
void _showConfirmation() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _unit,
memo: _memoController.text.isNotEmpty ? _memoController.text : null,
onConfirm: () {
Navigator.pop(context);
_createToken();
},
onCancel: () => Navigator.pop(context),
),
);
}
Future<void> _createToken() async {
setState(() {
_isProcessing = true;
_errorMessage = null;
});
try {
final walletProvider = context.read<WalletProvider>();
// Crear token real con cdk-flutter
final memo = _memoController.text.isNotEmpty ? _memoController.text : null;
final token = await walletProvider.sendTokens(
BigInt.from(_amount),
memo,
);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ShareTokenScreen(
token: token,
amount: _amount,
unit: _unit,
memo: memo,
),
),
);
}
} catch (e) {
setState(() {
final errorStr = e.toString().toLowerCase();
if (errorStr.contains('insufficient') || errorStr.contains('not enough')) {
_errorMessage = 'Balance insuficiente';
} else {
_errorMessage = 'Error al crear token: $e';
}
});
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
}
/// Modal de confirmacion
class _ConfirmationModal extends StatelessWidget {
final int amount;
final String unit;
final String? memo;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const _ConfirmationModal({
required this.amount,
required this.unit,
this.memo,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.send,
color: AppColors.primaryAction,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Titulo
const Text(
'Confirmar envio',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Monto
Text(
'$amount $unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
// Memo
if (memo != null && memo!.isNotEmpty) ...[
const SizedBox(height: AppDimensions.paddingSmall),
Text(
'"$memo"',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: 'Confirmar',
onPressed: onConfirm,
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
}
}
+452
View File
@@ -0,0 +1,452 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
/// Pantalla para compartir el token Cashu generado
class ShareTokenScreen extends StatefulWidget {
final String token;
final int amount;
final String unit;
final String? memo;
const ShareTokenScreen({
super.key,
required this.token,
required this.amount,
required this.unit,
this.memo,
});
@override
State<ShareTokenScreen> createState() => _ShareTokenScreenState();
}
class _ShareTokenScreenState extends State<ShareTokenScreen> {
late PageController _pageController;
late List<String> _qrParts;
int _currentPage = 0;
Timer? _timer;
@override
void initState() {
super.initState();
_pageController = PageController();
_qrParts = _encodeTokenToQR();
_startQRAnimation();
}
@override
void dispose() {
_timer?.cancel();
_pageController.dispose();
super.dispose();
}
List<String> _encodeTokenToQR() {
// TODO: Implementar con cdk-flutter cuando esté disponible
// final qrParts = encodeQrToken(token: widget.token);
// Por ahora, simulamos fragmentos UR para el token
if (widget.token.length <= 200) {
return [widget.token];
}
// Simular fragmentación para tokens largos
final parts = <String>[];
const chunkSize = 200;
for (int i = 0; i < widget.token.length; i += chunkSize) {
final end = (i + chunkSize < widget.token.length)
? i + chunkSize
: widget.token.length;
parts.add(widget.token.substring(i, end));
}
return parts;
}
void _startQRAnimation() {
// Solo animar si hay múltiples fragmentos
if (_qrParts.length <= 1) return;
_timer = Timer.periodic(const Duration(seconds: 2), (timer) {
if (mounted) {
final nextPage = (_currentPage + 1) % _qrParts.length;
_pageController.animateToPage(
nextPage,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
}
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.x, color: Colors.white),
onPressed: () => _goToHome(context),
),
title: const Text(
'Token creado',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
const SizedBox(height: AppDimensions.paddingLarge),
// Icono de exito
_buildSuccessIcon(),
const SizedBox(height: AppDimensions.paddingLarge),
// Monto
_buildAmountDisplay(),
const SizedBox(height: AppDimensions.paddingLarge),
// QR del token (dinámico si hay múltiples fragmentos)
_buildQRDisplay(),
const SizedBox(height: AppDimensions.paddingMedium),
// Token truncado
_buildTokenTextDisplay(),
const SizedBox(height: AppDimensions.paddingSmall),
// Indicador de página (si hay múltiples fragmentos)
if (_qrParts.length > 1) _buildPageIndicator(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botones copiar y compartir
_buildActionButtons(context),
const SizedBox(height: AppDimensions.paddingLarge),
// Advertencia
_buildWarning(),
const Spacer(),
// Boton volver al inicio
PrimaryButton(
text: 'Volver al inicio',
onPressed: () => _goToHome(context),
),
],
),
),
),
),
);
}
Widget _buildSuccessIcon() {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(LucideIcons.check, color: AppColors.success, size: 40),
);
}
Widget _buildAmountDisplay() {
return Column(
children: [
Text(
'${widget.amount} ${widget.unit}',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (widget.memo != null && widget.memo!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
'"${widget.memo}"',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
],
);
}
Widget _buildQRDisplay() {
final size = 220.0;
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: _qrParts.length > 1
? PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentPage = index;
});
},
itemCount: _qrParts.length,
itemBuilder: (context, index) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: QrImageView(
data: _qrParts[index],
version: QrVersions.auto,
size: size - 40,
backgroundColor: Colors.white,
errorCorrectionLevel: QrErrorCorrectLevel.M,
),
),
);
},
)
: Padding(
padding: const EdgeInsets.all(20),
child: QrImageView(
data: _qrParts[0],
version: QrVersions.auto,
size: size - 40,
backgroundColor: Colors.white,
errorCorrectionLevel: QrErrorCorrectLevel.M,
),
),
),
);
}
Widget _buildTokenTextDisplay() {
final displayToken = widget.token.length > 50
? '${widget.token.substring(0, 25)}...${widget.token.substring(widget.token.length - 20)}'
: widget.token;
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 16),
const SizedBox(width: 6),
Text(
'Token Cashu (${_qrParts.length} ${_qrParts.length == 1 ? "parte" : "partes"})',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
const SizedBox(height: 4),
SelectableText(
displayToken,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 11,
color: Colors.white.withValues(alpha: 0.8),
),
),
],
),
);
}
Widget _buildPageIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
_qrParts.length,
(index) => AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: index == _currentPage ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: index == _currentPage
? AppColors.primaryAction
: AppColors.textSecondary.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
),
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Row(
children: [
// Copiar
Expanded(
child: GestureDetector(
onTap: () => _copyToken(context),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.copy, color: Colors.white, size: 20),
const SizedBox(width: 8),
const Text(
'Copiar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
// Compartir
Expanded(
child: GestureDetector(
onTap: () => _shareToken(context),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColors.buttonGradient,
),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.share2, color: Colors.white, size: 20),
const SizedBox(width: 8),
const Text(
'Compartir',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
),
],
);
}
Widget _buildWarning() {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.warning.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.warning.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertTriangle, color: AppColors.warning, size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderas los fondos.',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.warning,
),
),
),
],
),
);
}
void _copyToken(BuildContext context) {
Clipboard.setData(ClipboardData(text: widget.token));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Token copiado al portapapeles'),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
void _shareToken(BuildContext context) async {
final memo = widget.memo != null && widget.memo!.isNotEmpty
? '\n"${widget.memo}"'
: '';
await SharePlus.instance.share(
ShareParams(
text: '${widget.amount} ${widget.unit}$memo\n\n${widget.token}',
subject: 'Token Cashu - ${widget.amount} ${widget.unit}',
),
);
}
void _goToHome(BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
+510
View File
@@ -0,0 +1,510 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/effects/cashu_confetti.dart';
import '../../providers/wallet_provider.dart';
/// Estados del proceso de mint
enum MintStatus { loading, unpaid, paid, issued, error }
/// Pantalla para mostrar el invoice generado y esperar el pago
class InvoiceScreen extends StatefulWidget {
final int amount;
final String unit;
final String? description;
const InvoiceScreen({
super.key,
required this.amount,
required this.unit,
this.description,
});
@override
State<InvoiceScreen> createState() => _InvoiceScreenState();
}
class _InvoiceScreenState extends State<InvoiceScreen> {
MintStatus _status = MintStatus.loading;
String? _invoice;
String? _errorMessage;
StreamSubscription<MintQuote>? _mintSubscription;
final CashuConfettiController _confettiController = CashuConfettiController();
@override
void initState() {
super.initState();
_startMintProcess();
}
@override
void dispose() {
_mintSubscription?.cancel();
_confettiController.dispose();
super.dispose();
}
void _startMintProcess() {
final walletProvider = context.read<WalletProvider>();
try {
final mintStream = walletProvider.mintTokens(
BigInt.from(widget.amount),
widget.description,
);
_mintSubscription = mintStream.listen(
(quote) {
if (!mounted) return;
setState(() {
switch (quote.state) {
case MintQuoteState.unpaid:
_status = MintStatus.unpaid;
_invoice = quote.request;
break;
case MintQuoteState.paid:
_status = MintStatus.paid;
break;
case MintQuoteState.issued:
_status = MintStatus.issued;
_onMintCompleted();
break;
default:
// Cualquier otro estado se trata como error
_status = MintStatus.error;
_errorMessage = 'Estado desconocido';
}
});
},
onError: (error) {
if (!mounted) return;
setState(() {
_status = MintStatus.error;
_errorMessage = error.toString();
});
},
);
} catch (e) {
setState(() {
_status = MintStatus.error;
_errorMessage = e.toString();
});
}
}
void _onMintCompleted() {
// Disparar confetti inmediatamente
_confettiController.fire();
// Esperar a que termine el confetti antes de navegar
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
// Volver al home con mensaje de éxito
Navigator.popUntil(context, (route) => route.isFirst);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'+${widget.amount} ${widget.unit} depositados',
),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 3),
),
);
}
});
}
@override
Widget build(BuildContext context) {
return CashuConfetti(
controller: _confettiController,
child: GradientBackground(
child: PopScope(
canPop: _status == MintStatus.issued || _status == MintStatus.error,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: (_status == MintStatus.issued || _status == MintStatus.error)
? IconButton(
icon: const Icon(
LucideIcons.arrowLeft,
color: Colors.white,
),
onPressed: () => Navigator.pop(context),
)
: null,
title: const Text(
'Pagar invoice',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
actions: [
if (_status == MintStatus.unpaid)
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: _buildContent(),
),
),
),
),
),
);
}
Widget _buildContent() {
switch (_status) {
case MintStatus.loading:
return _buildLoading();
case MintStatus.unpaid:
case MintStatus.paid:
case MintStatus.issued:
return _buildInvoiceView();
case MintStatus.error:
return _buildError();
}
}
Widget _buildLoading() {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: AppColors.primaryAction,
),
SizedBox(height: AppDimensions.paddingMedium),
Text(
'Generando invoice...',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
],
),
);
}
Widget _buildInvoiceView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Título con monto
_buildHeader(),
const SizedBox(height: AppDimensions.paddingLarge),
// QR del invoice
if (_invoice != null) _buildQRSection(),
const SizedBox(height: AppDimensions.paddingLarge),
// Invoice truncado
if (_invoice != null) _buildInvoiceText(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón copiar
if (_invoice != null && _status == MintStatus.unpaid) _buildCopyButton(),
const Spacer(),
// Estado del invoice
_buildStatus(),
const SizedBox(height: AppDimensions.paddingMedium),
// Descripción (si existe)
if (widget.description != null && widget.description!.isNotEmpty)
_buildDescription(),
],
);
}
Widget _buildError() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
const Text(
'Error',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage ?? 'Error desconocido',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Volver',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
);
}
Widget _buildHeader() {
return Column(
children: [
Text(
'Depositar ${widget.amount} ${widget.unit}',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
);
}
Widget _buildQRSection() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Center(
child: QrImageView(
data: _invoice!,
version: QrVersions.auto,
size: 200,
backgroundColor: Colors.white,
errorCorrectionLevel: QrErrorCorrectLevel.M,
),
),
);
}
Widget _buildInvoiceText() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: SelectableText(
_invoice!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
textAlign: TextAlign.center,
maxLines: 2,
),
);
}
Widget _buildCopyButton() {
return GestureDetector(
onTap: _copyInvoice,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall + 4,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.copy, color: AppColors.textSecondary, size: 20),
const SizedBox(width: 8),
Text(
'Copiar invoice',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildStatus() {
IconData icon;
String text;
Color color;
switch (_status) {
case MintStatus.loading:
icon = LucideIcons.clock;
text = 'Generando...';
color = AppColors.textSecondary;
break;
case MintStatus.unpaid:
icon = LucideIcons.clock;
text = 'Esperando pago...';
color = AppColors.textSecondary;
break;
case MintStatus.paid:
icon = LucideIcons.checkCircle;
text = 'Pago recibido';
color = AppColors.success;
break;
case MintStatus.issued:
icon = LucideIcons.checkCircle2;
text = 'Tokens emitidos!';
color = AppColors.success;
break;
case MintStatus.error:
icon = LucideIcons.xCircle;
text = 'Error';
color = AppColors.error;
break;
}
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_status == MintStatus.unpaid || _status == MintStatus.loading)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
)
else
Icon(icon, color: color, size: 24),
const SizedBox(width: 12),
Text(
text,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: color,
),
),
],
),
);
}
Widget _buildDescription() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Descripcion:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
const SizedBox(height: 4),
Text(
widget.description!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
);
}
Future<void> _copyInvoice() async {
if (_invoice == null) return;
await Clipboard.setData(ClipboardData(text: _invoice!));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invoice copiado al portapapeles'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
}
}
+414
View File
@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
import 'invoice_screen.dart';
/// Pantalla para depositar sats via Lightning (Mint)
class MintScreen extends StatefulWidget {
const MintScreen({super.key});
@override
State<MintScreen> createState() => _MintScreenState();
}
class _MintScreenState extends State<MintScreen> {
final TextEditingController _amountController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final String _unit = 'sats';
bool _isProcessing = false;
String? _errorMessage;
@override
void dispose() {
_amountController.dispose();
_descriptionController.dispose();
super.dispose();
}
int get _amount => int.tryParse(_amountController.text) ?? 0;
bool get _isValidAmount => _amount > 0;
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Depositar',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monto a depositar
_buildAmountSection(),
const SizedBox(height: AppDimensions.paddingLarge),
// Descripción opcional
_buildDescriptionSection(),
const SizedBox(height: AppDimensions.paddingMedium),
// Mensaje de error (si hay)
if (_errorMessage != null) _buildErrorMessage(),
const Spacer(),
// Botón generar invoice
_buildGenerateButton(),
],
),
),
),
),
);
}
Widget _buildAmountSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Monto a depositar:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Input de monto
GlassCard(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall,
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _amountController,
keyboardType: TextInputType.number,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
decoration: InputDecoration(
hintText: '0',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
),
onChanged: (_) => setState(() {
_errorMessage = null;
}),
),
),
Text(
_unit,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
],
);
}
Widget _buildDescriptionSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Descripcion (opcional):',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _descriptionController,
maxLines: 2,
maxLength: 100,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: 'Ej: Deposito El Caju',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
counterStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
),
],
);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: AppColors.error, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
Widget _buildGenerateButton() {
return PrimaryButton(
text: _isProcessing ? 'Generando...' : 'Generar invoice',
onPressed: _isValidAmount && !_isProcessing ? _showConfirmation : null,
);
}
void _showConfirmation() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _unit,
description: _descriptionController.text.isNotEmpty
? _descriptionController.text
: null,
onConfirm: () {
Navigator.pop(context);
_navigateToInvoice();
},
onCancel: () => Navigator.pop(context),
),
);
}
void _navigateToInvoice() {
// Navegar a InvoiceScreen que manejará el stream
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceScreen(
amount: _amount,
unit: _unit,
description: _descriptionController.text.isNotEmpty
? _descriptionController.text
: null,
),
),
);
}
}
/// Modal de confirmación
class _ConfirmationModal extends StatelessWidget {
final int amount;
final String unit;
final String? description;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const _ConfirmationModal({
required this.amount,
required this.unit,
this.description,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.zap,
color: AppColors.primaryAction,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
const Text(
'Depositar Lightning',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Monto
Text(
'$amount $unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
// Descripción
if (description != null && description!.isNotEmpty) ...[
const SizedBox(height: AppDimensions.paddingSmall),
Text(
'"$description"',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: 'Generar invoice',
onPressed: onConfirm,
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
}
}
+713
View File
@@ -0,0 +1,713 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
/// Pantalla para retirar sats a Lightning (Melt)
class MeltScreen extends StatefulWidget {
const MeltScreen({super.key});
@override
State<MeltScreen> createState() => _MeltScreenState();
}
class _MeltScreenState extends State<MeltScreen> {
final TextEditingController _invoiceController = TextEditingController();
final String _unit = 'sats';
bool _isValidInvoice = false;
bool _isLoadingQuote = false;
bool _isProcessing = false;
MeltQuote? _quote;
int _invoiceAmount = 0;
int _feeReserve = 0;
int _total = 0;
int _availableBalance = 0;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadBalance();
}
Future<void> _loadBalance() async {
final walletProvider = context.read<WalletProvider>();
final balance = await walletProvider.getTotalBalance();
if (mounted) {
setState(() {
_availableBalance = balance.toInt();
});
}
}
@override
void dispose() {
_invoiceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Retirar',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Instrucciones
Text(
'Pega el invoice Lightning:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Campo de texto para el invoice
_buildInvoiceInput(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón pegar del portapapeles
_buildPasteButton(),
const SizedBox(height: AppDimensions.paddingLarge),
// Loading quote
if (_isLoadingQuote) _buildLoadingQuote(),
// Preview del invoice (si es válido)
if (_isValidInvoice && _quote != null && !_isLoadingQuote)
_buildInvoicePreview(),
// Mensaje de error (si hay)
if (_errorMessage != null) _buildErrorMessage(),
const Spacer(),
// Balance disponible
_buildBalanceInfo(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón pagar invoice
_buildPayButton(),
],
),
),
),
),
);
}
Widget _buildInvoiceInput() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _invoiceController,
maxLines: 3,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
decoration: InputDecoration(
hintText: 'lnbc1000n1pj...',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(AppDimensions.paddingSmall),
),
onChanged: _onInvoiceChanged,
),
);
}
Widget _buildPasteButton() {
return GestureDetector(
onTap: _pasteFromClipboard,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall + 4,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.clipboard,
color: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 8),
Text(
'Pegar del portapapeles',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildLoadingQuote() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryAction,
),
),
const SizedBox(width: 12),
Text(
'Obteniendo quote...',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
);
}
Widget _buildInvoicePreview() {
final hasEnoughBalance = _total <= _availableBalance;
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Estado válido
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.check,
color: AppColors.success,
size: 16,
),
),
const SizedBox(width: 8),
Text(
'Invoice válido',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.success,
),
),
],
),
const SizedBox(height: AppDimensions.paddingMedium),
// Monto
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Monto:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
Text(
'$_invoiceAmount $_unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: 8),
// Fee estimado
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Fee reservado:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
Text(
'~$_feeReserve $_unit',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
const SizedBox(height: 8),
// Separador
Container(height: 1, color: Colors.white.withValues(alpha: 0.1)),
const SizedBox(height: 8),
// Total
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Total:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'$_total $_unit',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: hasEnoughBalance
? AppColors.primaryAction
: AppColors.error,
),
),
],
),
if (!hasEnoughBalance) ...[
const SizedBox(height: 8),
Text(
'Balance insuficiente',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.error,
),
),
],
],
),
);
}
Widget _buildErrorMessage() {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: AppColors.error, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
Widget _buildBalanceInfo() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Disponible: ',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
Text(
'$_availableBalance $_unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryAction,
),
),
],
);
}
Widget _buildPayButton() {
final canPay = _isValidInvoice &&
_quote != null &&
!_isProcessing &&
!_isLoadingQuote &&
_total <= _availableBalance;
return PrimaryButton(
text: _isProcessing ? 'Pagando...' : 'Pagar invoice',
onPressed: canPay ? _showConfirmation : null,
);
}
Future<void> _pasteFromClipboard() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData?.text != null) {
_invoiceController.text = clipboardData!.text!.trim();
_onInvoiceChanged(clipboardData.text!.trim());
}
}
void _onInvoiceChanged(String value) {
setState(() {
_errorMessage = null;
_quote = null;
_isValidInvoice = false;
if (value.isEmpty) {
_invoiceAmount = 0;
_feeReserve = 0;
_total = 0;
return;
}
});
// Verificar formato básico
final trimmed = value.trim().toLowerCase();
if (!trimmed.startsWith('lnbc') &&
!trimmed.startsWith('lntb') &&
!trimmed.startsWith('lnbcrt')) {
setState(() {
_errorMessage = 'Invoice inválido';
});
return;
}
// Obtener quote real
_getQuote(value.trim());
}
Future<void> _getQuote(String invoice) async {
setState(() {
_isLoadingQuote = true;
_errorMessage = null;
});
try {
final walletProvider = context.read<WalletProvider>();
final quote = await walletProvider.getMeltQuote(invoice);
if (mounted) {
setState(() {
_quote = quote;
_invoiceAmount = quote.amount.toInt();
_feeReserve = quote.feeReserve.toInt();
_total = _invoiceAmount + _feeReserve;
_isValidInvoice = true;
_isLoadingQuote = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoadingQuote = false;
final errorStr = e.toString().toLowerCase();
if (errorStr.contains('expired')) {
_errorMessage = 'Invoice expirado';
} else if (errorStr.contains('invalid') || errorStr.contains('decode')) {
_errorMessage = 'Invoice inválido o malformado';
} else {
_errorMessage = 'Error al obtener quote: $e';
}
});
}
}
}
void _showConfirmation() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _ConfirmationModal(
amount: _invoiceAmount,
fee: _feeReserve,
total: _total,
unit: _unit,
onConfirm: () {
Navigator.pop(context);
_payInvoice();
},
onCancel: () => Navigator.pop(context),
),
);
}
Future<void> _payInvoice() async {
if (_quote == null) return;
setState(() {
_isProcessing = true;
_errorMessage = null;
});
try {
final walletProvider = context.read<WalletProvider>();
// Pagar invoice real
final totalPaid = await walletProvider.melt(_quote!);
if (mounted) {
// Mostrar éxito y volver
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('-${totalPaid.toInt()} $_unit enviados'),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 3),
),
);
Navigator.pop(context);
}
} catch (e) {
setState(() {
final errorStr = e.toString().toLowerCase();
if (errorStr.contains('insufficient') || errorStr.contains('not enough')) {
_errorMessage = 'Balance insuficiente';
} else if (errorStr.contains('expired')) {
_errorMessage = 'Invoice expirado';
} else if (errorStr.contains('already paid')) {
_errorMessage = 'Invoice ya fue pagado';
} else {
_errorMessage = 'Error al pagar: $e';
}
});
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
}
/// Modal de confirmación
class _ConfirmationModal extends StatelessWidget {
final int amount;
final int fee;
final int total;
final String unit;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const _ConfirmationModal({
required this.amount,
required this.fee,
required this.total,
required this.unit,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.zap,
color: AppColors.primaryAction,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
const Text(
'Confirmar pago',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Detalles
Text(
'$amount $unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'+ ~$fee $unit fee',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
'Total: $total $unit',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.primaryAction,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: 'Pagar',
onPressed: onConfirm,
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
}
}
+838
View File
@@ -0,0 +1,838 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../providers/wallet_provider.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
/// Pantalla para gestionar mints conectados
class MintsScreen extends StatefulWidget {
const MintsScreen({super.key});
@override
State<MintsScreen> createState() => _MintsScreenState();
}
class _MintsScreenState extends State<MintsScreen> {
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Mints conectados',
style: TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: Consumer<WalletProvider>(
builder: (context, walletProvider, child) {
return FutureBuilder<List<Mint>>(
future: walletProvider.listMints(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.primaryAction,
),
);
}
final mints = snapshot.data ?? [];
return Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Lista de mints
Expanded(
child: mints.isEmpty
? _buildEmptyState()
: ListView.builder(
itemCount: mints.length,
itemBuilder: (context, index) {
final mint = mints[index];
return Padding(
padding: const EdgeInsets.only(
bottom: AppDimensions.paddingMedium,
),
child: _buildMintCard(
mint,
walletProvider,
),
);
},
),
),
// Botón agregar mint
_buildAddMintButton(walletProvider),
],
),
);
},
);
},
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.server,
size: 64,
color: Colors.white.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'No hay mints conectados',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: 8),
Text(
'Agrega un mint para comenzar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.5),
),
),
],
),
);
}
Widget _buildMintCard(Mint mint, WalletProvider walletProvider) {
final isActive = walletProvider.activeMintUrl == mint.url;
return FutureBuilder<BigInt>(
future: walletProvider.getBalanceForMint(mint.url),
builder: (context, balanceSnapshot) {
final balance = balanceSnapshot.data ?? BigInt.zero;
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header: URL + estado
Row(
children: [
// Icono estado conexión (verde si es el activo)
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: isActive ? AppColors.success : AppColors.textSecondary,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
// URL del mint (solo el host)
Expanded(
child: Text(
_extractHost(mint.url),
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
// Badge activo
if (isActive)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Activo',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primaryAction,
),
),
),
],
),
const SizedBox(height: 4),
// URL completa (más pequeña)
Text(
mint.url,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.6),
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: AppDimensions.paddingSmall),
// Balance
Row(
children: [
Text(
'Balance:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
const SizedBox(width: 8),
if (balanceSnapshot.connectionState == ConnectionState.waiting)
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.textSecondary,
),
)
else
Text(
'${_formatBalance(balance)} sats',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
const SizedBox(height: AppDimensions.paddingMedium),
// Acciones
Row(
children: [
// Hacer activo (si no lo es)
if (!isActive)
Expanded(
child: GestureDetector(
onTap: () => _setActiveMint(mint.url, walletProvider),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.star,
color: AppColors.textSecondary,
size: 16,
),
const SizedBox(width: 6),
Text(
'Usar este mint',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
),
),
if (!isActive) const SizedBox(width: 8),
// Eliminar
Expanded(
child: GestureDetector(
onTap: () => _showDeleteDialog(mint, balance, walletProvider),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.trash2,
color: AppColors.error,
size: 16,
),
const SizedBox(width: 6),
Text(
'Eliminar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.error,
),
),
],
),
),
),
),
],
),
],
),
);
},
);
}
Widget _buildAddMintButton(WalletProvider walletProvider) {
return PrimaryButton(
text: 'Agregar mint',
icon: LucideIcons.plus,
onPressed: () => _showAddMintDialog(walletProvider),
);
}
String _extractHost(String url) {
try {
final uri = Uri.parse(url);
return uri.host;
} catch (e) {
return url;
}
}
String _formatBalance(BigInt balance) {
final value = balance.toInt();
if (value >= 1000) {
return '${(value / 1000).toStringAsFixed(value % 1000 == 0 ? 0 : 1)}k';
}
return value.toString();
}
Future<void> _setActiveMint(String mintUrl, WalletProvider walletProvider) async {
try {
await walletProvider.setActiveMint(mintUrl);
if (mounted) {
setState(() {}); // Rebuild para actualizar UI
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mint activo actualizado'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
backgroundColor: AppColors.error,
duration: const Duration(seconds: 3),
),
);
}
}
}
void _showDeleteDialog(Mint mint, BigInt balance, WalletProvider walletProvider) {
final hasBalance = balance > BigInt.zero;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => _DeleteMintModal(
mintUrl: mint.url,
hasBalance: hasBalance,
balance: balance.toInt(),
onConfirm: () async {
Navigator.pop(context);
await _deleteMint(mint.url, walletProvider);
},
onCancel: () => Navigator.pop(context),
),
);
}
Future<void> _deleteMint(String mintUrl, WalletProvider walletProvider) async {
try {
await walletProvider.removeMint(mintUrl);
if (mounted) {
setState(() {}); // Rebuild para actualizar UI
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mint eliminado'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: $e'),
backgroundColor: AppColors.error,
duration: const Duration(seconds: 3),
),
);
}
}
}
void _showAddMintDialog(WalletProvider walletProvider) {
showDialog(
context: context,
builder: (context) => _AddMintDialog(
walletProvider: walletProvider,
onSuccess: () {
// Forzar rebuild para mostrar el nuevo mint
setState(() {});
},
),
);
}
}
/// Modal para confirmar eliminación de mint
class _DeleteMintModal extends StatelessWidget {
final String mintUrl;
final bool hasBalance;
final int balance;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const _DeleteMintModal({
required this.mintUrl,
required this.hasBalance,
required this.balance,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono advertencia
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.alertTriangle,
color: AppColors.error,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
const Text(
'¿Eliminar mint?',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// URL del mint
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
mintUrl,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Advertencia según balance
Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: hasBalance
? AppColors.secondaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
hasBalance ? LucideIcons.alertCircle : LucideIcons.info,
color: hasBalance ? AppColors.secondaryAction : AppColors.textSecondary,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
hasBalance
? 'Este mint tiene $balance sats. Puedes recuperarlos después agregando el mint de nuevo.'
: 'Perderás acceso a este mint',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: hasBalance
? AppColors.secondaryAction
: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: GestureDetector(
onTap: onConfirm,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: AppColors.error,
borderRadius: BorderRadius.circular(16),
),
child: const Center(
child: Text(
'Eliminar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
}
}
/// Diálogo para agregar nuevo mint
class _AddMintDialog extends StatefulWidget {
final WalletProvider walletProvider;
final VoidCallback onSuccess;
const _AddMintDialog({
required this.walletProvider,
required this.onSuccess,
});
@override
State<_AddMintDialog> createState() => _AddMintDialogState();
}
class _AddMintDialogState extends State<_AddMintDialog> {
final TextEditingController _controller = TextEditingController();
bool _isValid = false;
bool _isAdding = false;
String? _errorMessage;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.deepVoidPurple,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: const Text(
'Agregar mint',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'URL del mint:',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
TextField(
controller: _controller,
keyboardType: TextInputType.url,
enabled: !_isAdding,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: 'https://mint.example.com',
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
errorText: _errorMessage,
errorStyle: const TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.error,
),
),
onChanged: _validateUrl,
),
if (_isAdding) ...[
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryAction,
),
),
const SizedBox(width: 12),
Text(
'Conectando al mint...',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
],
),
],
],
),
actions: [
TextButton(
onPressed: _isAdding ? null : () => Navigator.pop(context),
child: Text(
'Cancelar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: _isAdding
? AppColors.textSecondary.withValues(alpha: 0.5)
: AppColors.textSecondary,
),
),
),
ElevatedButton(
onPressed: (_isValid && !_isAdding) ? _addMint : null,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryAction,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.primaryAction.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Agregar',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
void _validateUrl(String value) {
setState(() {
_errorMessage = null;
});
if (value.isEmpty) {
setState(() {
_isValid = false;
});
return;
}
// Validación básica de URL
final isValidBasic =
value.startsWith('http://') || value.startsWith('https://');
if (!isValidBasic) {
setState(() {
_isValid = false;
_errorMessage = 'La URL debe comenzar con https://';
});
return;
}
setState(() {
_isValid = true;
});
}
Future<void> _addMint() async {
final url = _controller.text.trim();
if (url.isEmpty) return;
setState(() {
_isAdding = true;
_errorMessage = null;
});
try {
// Intentar agregar el mint (cdk_flutter validará la conexión)
await widget.walletProvider.addMint(url);
if (mounted) {
Navigator.pop(context);
widget.onSuccess();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mint agregado correctamente'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isAdding = false;
_errorMessage = 'No se pudo conectar al mint';
});
}
}
}
}
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
/// Tarjeta con efecto glassmorphism cálido
/// Cristal "ahumado" con tinte naranja sutil
class GlassCard extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry? padding;
final double? borderRadius;
final VoidCallback? onTap;
const GlassCard({
super.key,
required this.child,
this.padding,
this.borderRadius,
this.onTap,
});
@override
Widget build(BuildContext context) {
final card = Container(
padding: padding ?? const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.glassBase.withValues(alpha: AppColors.glassOpacity),
borderRadius: BorderRadius.circular(
borderRadius ?? AppDimensions.cardBorderRadius,
),
border: Border.all(
color: Colors.white.withValues(alpha: AppColors.glassBorderOpacity),
width: AppDimensions.cardBorderWidth,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: AppDimensions.cardShadowBlur,
offset: const Offset(0, AppDimensions.cardShadowOffsetY),
),
],
),
child: child,
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: card,
);
}
return card;
}
}
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import '../../core/constants/colors.dart';
/// Fondo con gradiente diagonal de El Caju
/// Púrpura profundo → Naranja marañón
class GradientBackground extends StatelessWidget {
final Widget child;
const GradientBackground({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: AppColors.backgroundGradient,
stops: AppColors.backgroundGradientStops,
),
),
child: child,
);
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
/// Botón primario con gradiente naranja (Sunset Gradient)
/// Efecto "gominola" o fruto maduro
class PrimaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final double? width;
final double? height;
const PrimaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
this.width,
this.height,
});
@override
Widget build(BuildContext context) {
return Container(
width: width ?? double.infinity,
height: height ?? AppDimensions.buttonHeight,
decoration: BoxDecoration(
gradient: onPressed != null
? const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: AppColors.buttonGradient,
)
: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
AppColors.buttonGradientStart.withValues(alpha: 0.5),
AppColors.buttonGradientEnd.withValues(alpha: 0.5),
],
),
borderRadius: BorderRadius.circular(AppDimensions.buttonBorderRadius),
boxShadow: onPressed != null
? [
BoxShadow(
color: AppColors.primaryAction.withValues(alpha: 0.4),
blurRadius: 12,
offset: const Offset(0, 6),
),
]
: null,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppDimensions.buttonBorderRadius),
child: Center(
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
],
Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
),
);
}
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
/// Botón secundario con efecto glass
/// Transparente con borde naranja sutil
class SecondaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
final double? width;
final double? height;
const SecondaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
this.width,
this.height,
});
@override
Widget build(BuildContext context) {
return Container(
width: width ?? double.infinity,
height: height ?? AppDimensions.buttonHeight,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(AppDimensions.buttonBorderRadius),
border: Border.all(
color: AppColors.primaryAction.withValues(alpha: 0.3),
width: 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isLoading ? null : onPressed,
borderRadius: BorderRadius.circular(AppDimensions.buttonBorderRadius),
child: Center(
child: isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.primaryAction.withValues(alpha: 0.8),
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
],
Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
),
);
}
}
+285
View File
@@ -0,0 +1,285 @@
import 'dart:math';
import 'package:flutter/material.dart';
/// Partícula de anacardo para el efecto confeti
class _CashuParticle {
double x;
double y;
double vx; // velocidad horizontal
double vy; // velocidad vertical
double size;
double rotation;
double rotationSpeed;
double opacity;
_CashuParticle({
required this.x,
required this.y,
required this.vx,
required this.vy,
required this.size,
required this.rotation,
required this.rotationSpeed,
}) : opacity = 1.0;
}
/// Controller para disparar el efecto de confeti
class CashuConfettiController extends ChangeNotifier {
bool _isActive = false;
bool get isActive => _isActive;
void fire() {
_isActive = true;
notifyListeners();
}
void reset() {
_isActive = false;
notifyListeners();
}
}
/// Widget que muestra explosión de anacardos tipo confeti
/// Usa IgnorePointer para no bloquear interacciones
class CashuConfetti extends StatefulWidget {
final CashuConfettiController controller;
final Widget child;
const CashuConfetti({
super.key,
required this.controller,
required this.child,
});
@override
State<CashuConfetti> createState() => _CashuConfettiState();
}
class _CashuConfettiState extends State<CashuConfetti>
with SingleTickerProviderStateMixin {
final List<_CashuParticle> _particles = [];
late AnimationController _animController;
final Random _random = Random();
// Configuración de timing (en segundos)
static const double _totalDuration = 4.0;
static const double _explosionEnd = 0.08; // 0.3s de 4s
static const double _fadeStart = 0.82; // fade en último 18%
// Física
static const double _gravity = 250.0; // caída suave
static const int _particleCount = 220; // lluvia densa
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: Duration(milliseconds: (_totalDuration * 1000).toInt()),
);
_animController.addListener(_updateParticles);
_animController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
_particles.clear();
});
widget.controller.reset();
}
});
widget.controller.addListener(_onControllerChange);
}
void _onControllerChange() {
if (widget.controller.isActive && !_animController.isAnimating) {
_spawnParticles();
_animController.forward(from: 0);
}
}
void _spawnParticles() {
final size = MediaQuery.of(context).size;
final centerX = size.width / 2;
final centerY = size.height / 2;
_particles.clear();
for (int i = 0; i < _particleCount; i++) {
// Ángulo aleatorio para explosión radial
final angle = _random.nextDouble() * 2 * pi;
// Velocidad inicial más moderada para lluvia más densa
final speed = 250 + _random.nextDouble() * 400;
_particles.add(_CashuParticle(
x: centerX,
y: centerY,
vx: cos(angle) * speed,
vy: sin(angle) * speed - 150, // Bias hacia arriba
size: 24 + _random.nextDouble() * 36, // 24-60px
rotation: _random.nextDouble() * 2 * pi,
rotationSpeed: (_random.nextDouble() - 0.5) * 6, // Rotación más suave
));
}
setState(() {});
}
void _updateParticles() {
if (_particles.isEmpty) return;
final progress = _animController.value;
final dt = 1 / 60; // ~60fps
// Calcular opacidad basada en fase
double targetOpacity = 1.0;
if (progress > _fadeStart) {
// Fade out suave
targetOpacity = 1.0 - ((progress - _fadeStart) / (1.0 - _fadeStart));
}
for (final p in _particles) {
// Aplicar velocidad (con ease-out en la explosión)
double speedMultiplier = 1.0;
if (progress < _explosionEnd) {
// Fase de explosión: rápida
speedMultiplier = 1.0;
} else {
// Fase de caída: lenta y suave
speedMultiplier = 0.5;
p.vx *= 0.997; // Fricción horizontal muy suave
}
p.x += p.vx * dt * speedMultiplier;
p.y += p.vy * dt * speedMultiplier;
// Aplicar gravedad después de la explosión (suave)
if (progress > _explosionEnd) {
p.vy += _gravity * dt * 0.6;
}
// Rotación continua
p.rotation += p.rotationSpeed * dt;
// Actualizar opacidad
p.opacity = targetOpacity.clamp(0.0, 1.0);
}
setState(() {});
}
@override
void dispose() {
widget.controller.removeListener(_onControllerChange);
_animController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
// Capa de confeti - IgnorePointer para no bloquear UI
if (_particles.isNotEmpty)
Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: _ConfettiPainter(
particles: _particles,
image: _cashuImage,
),
child: const SizedBox.expand(),
),
),
),
// Fallback con widgets si no hay imagen cargada
if (_particles.isNotEmpty && _cashuImage == null)
..._buildParticleWidgets(),
],
);
}
ImageInfo? _cashuImage;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadImage();
}
void _loadImage() async {
final imageProvider = const AssetImage('assets/img/cashu.png');
final stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((info, _) {
if (mounted) {
setState(() {
_cashuImage = info;
});
}
}));
}
List<Widget> _buildParticleWidgets() {
return _particles.map((p) {
return Positioned(
left: p.x - p.size / 2,
top: p.y - p.size / 2,
child: IgnorePointer(
child: Transform.rotate(
angle: p.rotation,
child: Opacity(
opacity: p.opacity,
child: Image.asset(
'assets/img/cashu.png',
width: p.size,
height: p.size,
fit: BoxFit.contain,
),
),
),
),
);
}).toList();
}
}
/// Painter para renderizar partículas con mejor rendimiento
class _ConfettiPainter extends CustomPainter {
final List<_CashuParticle> particles;
final ImageInfo? image;
_ConfettiPainter({required this.particles, this.image});
@override
void paint(Canvas canvas, Size size) {
if (image == null) return;
for (final p in particles) {
canvas.save();
canvas.translate(p.x, p.y);
canvas.rotate(p.rotation);
final paint = Paint()
..color = Colors.white.withValues(alpha: p.opacity)
..filterQuality = FilterQuality.medium;
final srcRect = Rect.fromLTWH(
0,
0,
image!.image.width.toDouble(),
image!.image.height.toDouble(),
);
final dstRect = Rect.fromCenter(
center: Offset.zero,
width: p.size,
height: p.size,
);
canvas.drawImageRect(image!.image, srcRect, dstRect, paint);
canvas.restore();
}
}
@override
bool shouldRepaint(covariant _ConfettiPainter oldDelegate) => true;
}
+1
View File
@@ -0,0 +1 @@
flutter/ephemeral
+126
View File
@@ -0,0 +1,126 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "elcaju")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.elcaju")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
+88
View File
@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
+26
View File
@@ -0,0 +1,26 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
cdk_flutter
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
+26
View File
@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+6
View File
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

Some files were not shown because too many files have changed in this diff Show More