diff --git a/app/android/app/build_local b/app/android/app/build_local index fa51847d..1ad129a2 100644 --- a/app/android/app/build_local +++ b/app/android/app/build_local @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + // compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 ndkVersion flutter.ndkVersion sourceSets { @@ -34,8 +35,11 @@ android { } lintOptions { - disable 'InvalidPackage' - } + disable 'InvalidPackage' + disable "Instantiatable" + checkReleaseBuilds false + abortOnError false + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +51,7 @@ android { } defaultConfig { - applicationId "org.jimber.threebotlogin.local" + applicationId "org.jimber.threebotlogin" minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -66,7 +70,6 @@ android { } } - buildTypes { release { signingConfig signingConfigs.release @@ -75,6 +78,15 @@ android { signingConfig signingConfigs.debug } } + buildscript { + ext { + stripeVersion = "20.19.1" // This is the version BEFORE the kotlin bump + } + } + + dataBinding { + enabled = true + } } flutter { @@ -89,6 +101,7 @@ dependencies { implementation 'com.hbb20:ccp:2.4.0' implementation 'com.google.code.gson:gson:2.8.6' implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + } diff --git a/app/android/app/build_production b/app/android/app/build_production index fbcde236..d7f2d032 100644 --- a/app/android/app/build_production +++ b/app/android/app/build_production @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + // compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 ndkVersion flutter.ndkVersion sourceSets { @@ -34,8 +35,11 @@ android { } lintOptions { - disable 'InvalidPackage' - } + disable 'InvalidPackage' + disable "Instantiatable" + checkReleaseBuilds false + abortOnError false + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -74,6 +78,15 @@ android { signingConfig signingConfigs.debug } } + buildscript { + ext { + stripeVersion = "20.19.1" // This is the version BEFORE the kotlin bump + } + } + + dataBinding { + enabled = true + } } flutter { @@ -88,6 +101,7 @@ dependencies { implementation 'com.hbb20:ccp:2.4.0' implementation 'com.google.code.gson:gson:2.8.6' implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + } diff --git a/app/android/app/build_staging b/app/android/app/build_staging index 786d8861..1ad129a2 100644 --- a/app/android/app/build_staging +++ b/app/android/app/build_staging @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + // compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 ndkVersion flutter.ndkVersion sourceSets { @@ -34,8 +35,11 @@ android { } lintOptions { - disable 'InvalidPackage' - } + disable 'InvalidPackage' + disable "Instantiatable" + checkReleaseBuilds false + abortOnError false + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +51,7 @@ android { } defaultConfig { - applicationId "org.jimber.threebotlogin.staging" + applicationId "org.jimber.threebotlogin" minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -74,6 +78,15 @@ android { signingConfig signingConfigs.debug } } + buildscript { + ext { + stripeVersion = "20.19.1" // This is the version BEFORE the kotlin bump + } + } + + dataBinding { + enabled = true + } } flutter { @@ -88,6 +101,7 @@ dependencies { implementation 'com.hbb20:ccp:2.4.0' implementation 'com.google.code.gson:gson:2.8.6' implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + } diff --git a/app/android/app/build_testing b/app/android/app/build_testing index ff4c55c6..d7f2d032 100644 --- a/app/android/app/build_testing +++ b/app/android/app/build_testing @@ -26,7 +26,8 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + // compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 ndkVersion flutter.ndkVersion sourceSets { @@ -34,8 +35,11 @@ android { } lintOptions { - disable 'InvalidPackage' - } + disable 'InvalidPackage' + disable "Instantiatable" + checkReleaseBuilds false + abortOnError false + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -47,7 +51,7 @@ android { } defaultConfig { - applicationId "org.jimber.threebotlogin.testing" + applicationId "org.jimber.threebotlogin" minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -57,10 +61,10 @@ android { signingConfigs { release { - // keyAlias localProperties.getProperty('keyAlias') - // keyPassword localProperties.getProperty('keyPassword') - // storeFile file(localProperties.getProperty('storeFile')) - // storePassword localProperties.getProperty('storePassword') + keyAlias localProperties.getProperty('keyAlias') + keyPassword localProperties.getProperty('keyPassword') + storeFile file(localProperties.getProperty('storeFile')) + storePassword localProperties.getProperty('storePassword') } debug { @@ -74,6 +78,15 @@ android { signingConfig signingConfigs.debug } } + buildscript { + ext { + stripeVersion = "20.19.1" // This is the version BEFORE the kotlin bump + } + } + + dataBinding { + enabled = true + } } flutter { @@ -88,6 +101,7 @@ dependencies { implementation 'com.hbb20:ccp:2.4.0' implementation 'com.google.code.gson:gson:2.8.6' implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + } diff --git a/app/android/app/src/main/AndroidManifest_local b/app/android/app/src/main/AndroidManifest_local index ee4786bc..ce9dba7a 100644 --- a/app/android/app/src/main/AndroidManifest_local +++ b/app/android/app/src/main/AndroidManifest_local @@ -1,7 +1,7 @@ + android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:enableOnBackInvokedCallback="false"> diff --git a/app/android/app/src/main/AndroidManifest_production b/app/android/app/src/main/AndroidManifest_production index 936dedac..01a6a985 100644 --- a/app/android/app/src/main/AndroidManifest_production +++ b/app/android/app/src/main/AndroidManifest_production @@ -1,7 +1,7 @@ + android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:enableOnBackInvokedCallback="false"> diff --git a/app/android/app/src/main/AndroidManifest_staging b/app/android/app/src/main/AndroidManifest_staging index cea04629..47c590d8 100644 --- a/app/android/app/src/main/AndroidManifest_staging +++ b/app/android/app/src/main/AndroidManifest_staging @@ -1,7 +1,7 @@ + android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:enableOnBackInvokedCallback="false"> diff --git a/app/android/app/src/main/AndroidManifest_testing b/app/android/app/src/main/AndroidManifest_testing index 5e3d3efa..5baaf0b4 100644 --- a/app/android/app/src/main/AndroidManifest_testing +++ b/app/android/app/src/main/AndroidManifest_testing @@ -1,7 +1,7 @@ + android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" android:enableOnBackInvokedCallback="false"> diff --git a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity.kt b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity.kt index d621fae4..35f37c04 100644 --- a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity.kt +++ b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity.kt @@ -8,4 +8,4 @@ class MainActivity: FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) } -} \ No newline at end of file +} diff --git a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_local b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_local index ece5499c..78f68439 100644 --- a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_local +++ b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_local @@ -8,4 +8,4 @@ class MainActivity: FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) } -} \ No newline at end of file +} diff --git a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_production b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_production index d621fae4..35f37c04 100644 --- a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_production +++ b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_production @@ -8,4 +8,4 @@ class MainActivity: FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) } -} \ No newline at end of file +} diff --git a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_staging b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_staging index c64d56e8..435c13df 100644 --- a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_staging +++ b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_staging @@ -8,4 +8,4 @@ class MainActivity: FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) } -} \ No newline at end of file +} diff --git a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_testing b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_testing index 61b8a58c..369e8b9d 100644 --- a/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_testing +++ b/app/android/app/src/main/kotlin/org/jimber/threebotlogin/MainActivity_testing @@ -8,4 +8,4 @@ class MainActivity: FlutterFragmentActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) } -} \ No newline at end of file +} diff --git a/app/android/build.gradle b/app/android/build.gradle index 76a1c8f2..1c97bb52 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -3,12 +3,15 @@ buildscript { repositories { google() jcenter() - maven { url 'https://maven.google.com' } mavenCentral() + maven { url 'https://jitpack.io' } // add this line + maven { url 'https://maven.google.com' } + } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + // classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath 'com.google.gms:google-services:4.3.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } @@ -18,8 +21,9 @@ allprojects { repositories { google() jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } maven { url 'https://maven.google.com' } - maven { url 'https://jitpack.io' } } } diff --git a/app/android/gradle.properties b/app/android/gradle.properties index 95db81cd..8eb01a2a 100644 --- a/app/android/gradle.properties +++ b/app/android/gradle.properties @@ -3,6 +3,7 @@ org.gradle.daemon=true org.gradle.configureondemand=true # org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=2048m -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 android.useAndroidX=true +android.jetifier.ignorelist=bcprov-jdk15on android.enableJetifier=true org.gradle.jvmargs=-Xmx1536M \ --add-exports=java.base/sun.nio.ch=ALL-UNNAMED \ diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index 562c5e44..cfe88f69 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/app/assets/fingerprint.svg b/app/assets/fingerprint.svg new file mode 100644 index 00000000..8773bff6 --- /dev/null +++ b/app/assets/fingerprint.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/journey.svg b/app/assets/journey.svg new file mode 100644 index 00000000..7099a7ff --- /dev/null +++ b/app/assets/journey.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/news.svg b/app/assets/news.svg new file mode 100644 index 00000000..66843c0b --- /dev/null +++ b/app/assets/news.svg @@ -0,0 +1,5034 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/tft.png b/app/assets/tft.png new file mode 100644 index 00000000..43d51823 Binary files /dev/null and b/app/assets/tft.png differ diff --git a/app/assets/tft.svg b/app/assets/tft.svg new file mode 100644 index 00000000..543c11fe --- /dev/null +++ b/app/assets/tft.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/tft_icon.png b/app/assets/tft_icon.png new file mode 100644 index 00000000..91a4480e Binary files /dev/null and b/app/assets/tft_icon.png differ diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 6053a9f4..4727f21d 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -9,70 +9,66 @@ PODS: - flutter_inappwebview/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - - flutter_native_splash (0.0.1): - - Flutter - flutter_pkid (0.0.1): - Flutter - local_auth (0.0.1): - Flutter - - lottie-ios (4.1.3) - MTBBarcodeScanner (5.0.11) - open_filex (0.0.2): - Flutter - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter - - permission_handler_apple (9.0.4): + - FlutterMacOS + - permission_handler_apple (9.1.1): - Flutter - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - shared_preferences_ios (0.0.1): + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - ShuftiPro-Onsite (1.2.9) + - shuftipro_onsite_sdk (1.0.2): - Flutter - - ShuftiPro (3.4.7): - - lottie-ios - - Socket.IO-Client-Swift - - shuftipro_sdk (2.0.5): + - ShuftiPro-Onsite (~> 1.2.0) + - smart_auth (0.0.1): - Flutter - - ShuftiPro (~> 3.4.1) - - Socket.IO-Client-Swift (16.1.0): - - Starscream (~> 4.0.6) - - sodium_libs (2.1.0): + - sodium_libs (2.2.0): - Flutter - - Starscream (4.0.6) - uni_links (0.0.1): - Flutter - url_launcher_ios (0.0.1): - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_pkid (from `.symlinks/plugins/flutter_pkid/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - - shuftipro_sdk (from `.symlinks/plugins/shuftipro_sdk/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - shuftipro_onsite_sdk (from `.symlinks/plugins/shuftipro_onsite_sdk/ios`) + - smart_auth (from `.symlinks/plugins/smart_auth/ios`) - sodium_libs (from `.symlinks/plugins/sodium_libs/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) SPEC REPOS: trunk: - - lottie-ios - MTBBarcodeScanner - OrderedSet - - ShuftiPro - - Socket.IO-Client-Swift - - Starscream + - ShuftiPro-Onsite EXTERNAL SOURCES: device_info_plus: @@ -81,8 +77,6 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview: :path: ".symlinks/plugins/flutter_inappwebview/ios" - flutter_native_splash: - :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_pkid: :path: ".symlinks/plugins/flutter_pkid/ios" local_auth: @@ -91,46 +85,48 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" qr_code_scanner: :path: ".symlinks/plugins/qr_code_scanner/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" - shuftipro_sdk: - :path: ".symlinks/plugins/shuftipro_sdk/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + shuftipro_onsite_sdk: + :path: ".symlinks/plugins/shuftipro_onsite_sdk/ios" + smart_auth: + :path: ".symlinks/plugins/smart_auth/ios" sodium_libs: :path: ".symlinks/plugins/sodium_libs/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" SPEC CHECKSUMS: - device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf flutter_pkid: 7808bd0baee7580afc1cebebf2a6f3b290e190c5 local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c - lottie-ios: d0954d3150061f662ed0adf96ef98d7421864c47 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad - ShuftiPro: 3178fb1123ccb6c407beceabc12a81923243f0b0 - shuftipro_sdk: c9f5c5c719930894be64f8736fed11e1a0aa616a - Socket.IO-Client-Swift: ee4b8f80a3db18dd7f32b266ddf273310609443c - sodium_libs: ede573662dadcbe9823a374c3c970557ad7a4725 - Starscream: fb2c4510bebf908c62bd383bcf05e673720e91fd + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + ShuftiPro-Onsite: 45989f1626cc7bb7d150954e6d4a3f0cc8a86db9 + shuftipro_onsite_sdk: 1e6ca562c98e8dfb7b8e56940d4d00cb5416d7af + smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 + sodium_libs: 0486eb2c3172ce494406367d4b379042444b769d uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1 PODFILE CHECKSUM: 2262097366c615de59b03ca3bf748d7aaad51773 diff --git a/app/lib/app_config.dart b/app/lib/app_config.dart index bfee8a1c..eae04122 100644 --- a/app/lib/app_config.dart +++ b/app/lib/app_config.dart @@ -217,5 +217,8 @@ void setFallbackConfigs() { Globals().phoneVerification = false; Globals().chainUrl = ''; Globals().gridproxyUrl = ''; + Globals().activationUrl = ''; + Globals().relayUrl = ''; + Globals().termsAndConditionsUrl = ''; Globals().spendingLimit = 0; } diff --git a/app/lib/apps/dao/dao.dart b/app/lib/apps/dao/dao.dart new file mode 100644 index 00000000..704dd6f6 --- /dev/null +++ b/app/lib/apps/dao/dao.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/app.dart'; +import 'package:threebotlogin/apps/farmers/farmers_user_data.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_home_event.dart'; +import 'package:threebotlogin/screens/dao_screen.dart'; + +class Dao implements App { + static final Dao _singleton = Dao._internal(); + static const Widget _daoWidget = DaoPage(); + + factory Dao() { + return _singleton; + } + + Dao._internal(); + + @override + Future widget() async { + return _daoWidget; + } + + @override + void clearData() { + clearAllData(); + } + + @override + bool emailVerificationRequired() { + return false; + } + + @override + bool pinRequired() { + return true; + } + + @override + void back() { + Events().emit(GoHomeEvent()); + } +} diff --git a/app/lib/apps/farmers/farmers.dart b/app/lib/apps/farmers/farmers.dart index b8085795..8e46fa05 100644 --- a/app/lib/apps/farmers/farmers.dart +++ b/app/lib/apps/farmers/farmers.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/app.dart'; -import 'package:threebotlogin/apps/farmers/farmers_events.dart'; import 'package:threebotlogin/apps/farmers/farmers_user_data.dart'; -import 'package:threebotlogin/apps/farmers/farmers_widget.dart'; import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_home_event.dart'; +import 'package:threebotlogin/screens/farm_screen.dart'; class Farmers implements App { static final Farmers _singleton = Farmers._internal(); - static const FarmersWidget _farmersWidget = FarmersWidget(); + static const Widget _farmersWidget = FarmScreen(); factory Farmers() { return _singleton; @@ -37,6 +37,6 @@ class Farmers implements App { @override void back() { - Events().emit(FarmersBackEvent()); + Events().emit(GoHomeEvent()); } } diff --git a/app/lib/apps/wallet/wallet.dart b/app/lib/apps/wallet/wallet.dart index 8ce9c923..1fbe1321 100644 --- a/app/lib/apps/wallet/wallet.dart +++ b/app/lib/apps/wallet/wallet.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/app.dart'; -import 'package:threebotlogin/apps/wallet/wallet_events.dart'; import 'package:threebotlogin/apps/wallet/wallet_user_data.dart'; -import 'package:threebotlogin/apps/wallet/wallet_widget.dart'; import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_home_event.dart'; +import 'package:threebotlogin/screens/wallets/wallet_screen.dart'; class Wallet implements App { static final Wallet _singleton = Wallet._internal(); - static const WalletWidget _walletWidget = WalletWidget(); + static const Widget _walletWidget = WalletScreen(); factory Wallet() { return _singleton; @@ -37,6 +37,6 @@ class Wallet implements App { @override void back() { - Events().emit(WalletBackEvent()); + Events().emit(GoHomeEvent()); } } diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index 5bdfdbf6..54f1ed1e 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -64,6 +64,12 @@ class Flags { (await Flags().getFlagValueByFeatureName('chain-url'))!; Globals().gridproxyUrl = (await Flags().getFlagValueByFeatureName('gridproxy-url'))!; + Globals().activationUrl = + (await Flags().getFlagValueByFeatureName('activation-url'))!; + Globals().relayUrl = + (await Flags().getFlagValueByFeatureName('relay-url'))!; + Globals().termsAndConditionsUrl = + (await Flags().getFlagValueByFeatureName('terms-conditions-url'))!; Globals().spendingLimit = int.parse( (await Flags().getFlagValueByFeatureName('spending-limit')).toString()); } diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index 9a614add..0921a5bc 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -56,6 +56,9 @@ class Globals { bool phoneVerification = false; String chainUrl = ''; String gridproxyUrl = ''; + String activationUrl = ''; + String relayUrl = ''; + String termsAndConditionsUrl = ''; bool isCacheClearedWallet = false; bool isCacheClearedFarmer = false; diff --git a/app/lib/jrouter.dart b/app/lib/jrouter.dart index 84d0b5d4..f6be41c3 100644 --- a/app/lib/jrouter.dart +++ b/app/lib/jrouter.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/app.dart'; import 'package:threebotlogin/apps/chatbot/chatbot.dart'; +import 'package:threebotlogin/apps/dao/dao.dart'; import 'package:threebotlogin/apps/wallet/wallet.dart'; import 'package:threebotlogin/screens/identity_verification_screen.dart'; import 'package:threebotlogin/screens/preference_screen.dart'; @@ -53,6 +54,14 @@ class JRouter { view: await Farmers().widget(), ), app: Farmers()), + AppInfo( + route: Route( + path: '/dao', + name: 'Dao', + icon: Icons.how_to_vote_outlined, + view: await Dao().widget(), + ), + app: Dao()), AppInfo( route: Route( path: '/chatbot', diff --git a/app/lib/main.dart b/app/lib/main.dart index 02bf0663..2e7e3994 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/screens/splash_screen.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:threebotlogin/widgets/wizard/terms_agreement.dart'; extension ColorSchemeExtension on ColorScheme { Color get warning => brightness == Brightness.light @@ -24,7 +26,7 @@ extension ColorSchemeExtension on ColorScheme { Color get backgroundDarker => brightness == Brightness.light ? const Color.fromARGB(255, 240, 240, 240) - : const Color.fromARGB(255, 15, 15, 15); + : const Color.fromARGB(255, 10, 10, 10); } Future main() async { @@ -37,7 +39,9 @@ Future main() async { await setGlobalValues(); bool registered = doubleName != null; - runApp(MyApp(initDone: initDone, registered: registered)); + runApp(ChangeNotifierProvider( + create: ((context) => TermsAgreement()), + child: MyApp(initDone: initDone, registered: registered))); } Future setGlobalValues() async { diff --git a/app/lib/models/contact.dart b/app/lib/models/contact.dart new file mode 100644 index 00000000..44c52904 --- /dev/null +++ b/app/lib/models/contact.dart @@ -0,0 +1,25 @@ +import 'package:threebotlogin/models/wallet.dart'; + +enum ContactOperation { Add, Edit } + +class PkidContact { + PkidContact({ + required this.name, + required this.address, + required this.type, + }); + String name; + String address; + final ChainType type; + + factory PkidContact.fromJson(Map json) { + return PkidContact( + name: json['name'], + address: json['address'], + type: + json['type'] == 'stellar' ? ChainType.Stellar : ChainType.TFChain); + } + toMap() { + return {'name': name, 'address': address, 'type': type.name.toLowerCase()}; + } +} diff --git a/app/lib/models/farm.dart b/app/lib/models/farm.dart index 34caba84..1bcecd92 100644 --- a/app/lib/models/farm.dart +++ b/app/lib/models/farm.dart @@ -5,7 +5,7 @@ class Node { required this.nodeId, required this.status, }); - final String nodeId; + final int nodeId; final NodeStatus status; } @@ -24,7 +24,7 @@ class Farm { final String walletAddress; final String tfchainWalletSecret; final String walletName; - final String twinId; - final String farmId; + final int twinId; + final int farmId; final List nodes; } diff --git a/app/lib/models/wallet.dart b/app/lib/models/wallet.dart index 44b885b6..290109b6 100644 --- a/app/lib/models/wallet.dart +++ b/app/lib/models/wallet.dart @@ -1,3 +1,7 @@ +enum WalletType { NATIVE, IMPORTED } + +enum ChainType { Stellar, TFChain } + class Wallet { Wallet({ required this.name, @@ -7,13 +11,64 @@ class Wallet { required this.tfchainSecret, required this.tfchainAddress, required this.tfchainBalance, + required this.type, }); - - final String name; - final String stellarAddress; + String name; final String stellarSecret; - final String stellarBalance; + final String stellarAddress; final String tfchainSecret; final String tfchainAddress; - final String tfchainBalance; + String stellarBalance; + String tfchainBalance; + final WalletType type; +} + +class PkidWallet { + PkidWallet({ + required this.name, + required this.index, + required this.seed, + required this.type, + }); + String name; + final int index; + final String seed; + WalletType type; + + factory PkidWallet.fromJson(Map json) { + return PkidWallet( + index: json["index"], + name: json['name'], + seed: json['seed'], + type: + json['type'] == 'NATIVE' ? WalletType.NATIVE : WalletType.IMPORTED); + } + toMap() { + return {'name': name, 'index': index, 'seed': seed, 'type': type.name}; + } +} + +enum TransactionType { Create, Payment, Receive } + +class Transaction { + Transaction({ + required this.hash, + required this.from, + required this.to, + required this.asset, + required this.amount, + // required this.memo, //TODO: check how to get it (transaction link) + required this.type, + required this.status, + required this.date, + }); + final String hash; + final String from; + final String to; + final String asset; + final String amount; + // final String memo; + final TransactionType type; + final bool status; + final String date; } diff --git a/app/lib/screens/dao_screen.dart b/app/lib/screens/dao_screen.dart new file mode 100644 index 00000000..31babc9a --- /dev/null +++ b/app/lib/screens/dao_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:tfchain_client/models/dao.dart'; +import 'package:threebotlogin/widgets/layout_drawer.dart'; +import 'package:threebotlogin/widgets/dao/proposals.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; + +class DaoPage extends StatefulWidget { + const DaoPage({super.key}); + + @override + State createState() => _DaoPageState(); +} + +class _DaoPageState extends State { + final List activeList = []; + final List inactiveList = []; + bool loading = true; + + void loadProposals() async { + setState(() { + loading = true; + }); + try { + final proposals = await getProposals(); + activeList.addAll(proposals['activeProposals']!); + inactiveList.addAll(proposals['inactiveProposals']!); + } catch (e) { + print('Failed to load proposals due to $e'); + if (context.mounted) { + final loadingProposalFailure = SnackBar( + content: Text( + 'Failed to load proposals', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingProposalFailure); + } + } finally { + setState(() { + loading = false; + }); + } + } + + @override + void initState() { + loadProposals(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Widget content; + if (loading) { + content = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Proposals...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + )); + } else { + content = DefaultTabController( + length: 2, + child: Column( + children: [ + PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + labelColor: Theme.of(context).colorScheme.primary, + indicatorColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Theme.of(context).colorScheme.onBackground, + dividerColor: Theme.of(context).scaffoldBackgroundColor, + labelStyle: Theme.of(context).textTheme.titleLarge, + unselectedLabelStyle: Theme.of(context).textTheme.titleMedium, + tabs: const [ + Tab(text: 'Active'), + Tab(text: 'Executable'), + ], + ), + ), + ), + Expanded( + child: TabBarView( + children: [ + ProposalsWidget(proposals: activeList, active: true), + ProposalsWidget( + proposals: inactiveList, + ), + ], + ), + ), + ], + ), + ); + } + return LayoutDrawer(titleText: 'Dao', content: content); + } +} diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index b76fa974..b9f71f5d 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/models/farm.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/gridproxy_service.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/add_farm.dart'; import 'package:threebotlogin/widgets/farm_item.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; @@ -11,72 +16,134 @@ class FarmScreen extends StatefulWidget { } class _FarmScreenState extends State { - final List farms = [ - Farm( - name: 'Hamada', - walletAddress: 'GCNHLX2ZTX2HDXCIQATZRSIHK2ECKEMKZCMSMIWBOTS2DZYUJBMHNXJA', - tfchainWalletSecret: - 'miss secret news run cliff lens exist clerk lucky cube fall soldier', - walletName: 'Farming wallet', - twinId: '26', - farmId: '56', - nodes: [ - Node( - nodeId: '88', - status: NodeStatus.Up, - ), - ], - ), - Farm( - name: 'My Farm', - walletAddress: 'GCNHLX2ZTX2HDXCIQATZRSIHK2ECKEMKZCMSMIWBOTS2DZYUJBMHNXJA', - tfchainWalletSecret: - 'miss secret news run cliff lens exist clerk lucky cube fall soldier', - walletName: 'Farming wallet', - twinId: '26', - farmId: '154', - nodes: [ - Node( - nodeId: '193', - status: NodeStatus.Down, - ), - Node( - nodeId: '493', - status: NodeStatus.Standby, - ), - Node( - nodeId: '584', - status: NodeStatus.Up, - ), - ], - ), - Farm( - name: 'My Animals', - walletAddress: 'GCNHLX2ZTX2HDXCIQATZRSIHK2ECKEMKZCMSMIWBOTS2DZYUJBMHNXJA', - tfchainWalletSecret: - 'miss secret news run cliff lens exist clerk lucky cube fall soldier', - walletName: 'Farming wallet', - twinId: '26', - farmId: '389', - nodes: [ - Node( - nodeId: '1034', - status: NodeStatus.Standby, - ), - Node( - nodeId: '1203', - status: NodeStatus.Up, - ), - ], - ), - ]; + List farms = []; + List wallets = []; + + bool loading = true; + + @override + void initState() { + super.initState(); + listFarms(); + } + + listFarms() async { + setState(() { + loading = true; + }); + try { + wallets = await listWallets(); + final Map twinIdWallets = {}; + for (final w in wallets) { + final twinId = await getTwinId(w.tfchainSecret); + if (twinId != 0) { + twinIdWallets[twinId] = w; + } + } + final farmsList = await getFarmsByTwinIds(twinIdWallets.keys.toList()); + for (final f in farmsList) { + final seed = twinIdWallets[f.twinId]!.tfchainSecret; + final walletName = twinIdWallets[f.twinId]!.name; + final nodes = await getNodesByFarmId(f.farmID); + farms.add(Farm( + name: f.name, + walletAddress: f.stellarAddress, + tfchainWalletSecret: seed, + walletName: walletName, + twinId: f.twinId, + farmId: f.farmID, + nodes: nodes.map((n) { + return Node( + nodeId: n.nodeId, + status: NodeStatus.values.firstWhere((e) => + e.toString().toLowerCase() == 'nodestatus.${n.status}'), + ); + }).toList())); + } + } catch (e) { + print('Failed to get farms due to $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to load farms', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } finally { + setState(() { + loading = false; + }); + } + } @override Widget build(BuildContext context) { + Widget mainWidget; + if (loading) { + mainWidget = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Farms...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + )); + } else if (farms.isEmpty) { + mainWidget = Center( + child: Text( + 'No farms yet.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onBackground), + ), + ); + } else { + mainWidget = ListView( + children: [for (final farm in farms) FarmItemWidget(farm: farm)]); + } return LayoutDrawer( - titleText: 'Farms', - content: ListView( - children: [for (final farm in farms) FarmItemWidget(farm: farm)]), + titleText: 'Farming', + content: mainWidget, + appBarActions: loading + ? [] + : [ + IconButton( + onPressed: _openAddFarmOverlay, + icon: const Icon( + Icons.add, + )) + ], ); } + + _openAddFarmOverlay() { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => NewFarm( + onAddFarm: _addFarm, + wallets: wallets, + )); + } + + _addFarm(Farm farm) { + farms.add(farm); + setState(() {}); + } } diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index d5bb9dce..a1117e0b 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:http/http.dart'; -import 'package:shuftipro_sdk/shuftipro_sdk.dart'; +import 'package:shuftipro_onsite_sdk/shuftipro_onsite_sdk.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; @@ -64,6 +64,7 @@ class _IdentityVerificationScreenState }; Map authObject = { + 'auth_type': 'access_token', 'access_token': '', }; @@ -332,7 +333,8 @@ class _IdentityVerificationScreenState context: context, barrierDismissible: false, builder: (BuildContext customContext) => CustomDialog( - image: Icons.info, + type: DialogType.Warning, + image: Icons.warning, title: 'Are you sure', description: 'Are you sure you want to exit the verification process', actions: [ @@ -406,6 +408,7 @@ class _IdentityVerificationScreenState context: context, barrierDismissible: false, builder: (BuildContext dialogContext) => CustomDialog( + type: DialogType.Error, image: Icons.close, title: 'Request canceled', description: 'Verification process has been canceled.', @@ -425,6 +428,7 @@ class _IdentityVerificationScreenState context: context, barrierDismissible: false, builder: (BuildContext dialogContext) => CustomDialog( + type: DialogType.Error, image: Icons.close, title: 'Request canceled', description: @@ -985,6 +989,7 @@ class _IdentityVerificationScreenState return showDialog( context: context, builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, image: Icons.warning, title: 'Maximum requests Reached', description: @@ -1008,7 +1013,8 @@ class _IdentityVerificationScreenState return showDialog( context: context, builder: (BuildContext context) => CustomDialog( - image: Icons.warning, + type: DialogType.Error, + image: Icons.error, title: "Couldn't setup verification process", description: 'Something went wrong. Please contact support if this issue persists.', @@ -1052,7 +1058,8 @@ class _IdentityVerificationScreenState return showDialog( context: context, builder: (BuildContext context) => CustomDialog( - image: Icons.warning, + type: DialogType.Error, + image: Icons.error, title: 'Failed to setup process', description: 'Something went wrong. \n If this issue persist, please contact support', diff --git a/app/lib/screens/init_screen.dart b/app/lib/screens/init_screen.dart index f94b5281..92a07e0e 100644 --- a/app/lib/screens/init_screen.dart +++ b/app/lib/screens/init_screen.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:threebotlogin/app_config.dart'; -import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:threebotlogin/screens/wizard/swipe_page.dart'; class InitScreen extends StatefulWidget { const InitScreen({super.key}); @@ -11,56 +9,9 @@ class InitScreen extends StatefulWidget { } class _InitState extends State { - late InAppWebViewController webView; - late InAppWebView iaWebView; - - finish(List params) async { - print('**** LOAD DONE '); - saveInitDone(); - Navigator.pop(context, true); - } - - addHandler() { - webView.addJavaScriptHandler(handlerName: 'FINISH', callback: finish); - } - - _InitState() { - iaWebView = InAppWebView( - initialUrlRequest: URLRequest( - url: Uri.parse( - '${AppConfig().wizardUrl()}?cache_buster=${DateTime.now().millisecondsSinceEpoch}')), - initialOptions: InAppWebViewGroupOptions( - android: AndroidInAppWebViewOptions( - supportMultipleWindows: true, useHybridComposition: true), - ), - onWebViewCreated: (InAppWebViewController controller) { - webView = controller; - addHandler(); - }, - onCreateWindow: - (InAppWebViewController controller, CreateWindowAction req) { - return Future.value(true); - }, - onLoadStart: (InAppWebViewController controller, Uri? url) {}, - onLoadStop: (InAppWebViewController controller, Uri? url) async {}, - onProgressChanged: (InAppWebViewController controller, int progress) {}, - ); - } - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } @override Widget build(BuildContext context) { - return MaterialApp( - home: SafeArea(child: iaWebView), - ); + return const Scaffold(body: SafeArea(child: SwipePage())); } } diff --git a/app/lib/screens/mobile_registration_screen.dart b/app/lib/screens/mobile_registration_screen.dart index 42bc42b6..ff14c528 100644 --- a/app/lib/screens/mobile_registration_screen.dart +++ b/app/lib/screens/mobile_registration_screen.dart @@ -183,6 +183,7 @@ class _MobileRegistrationScreenState extends State { showDialog( context: context, builder: (BuildContext context) => CustomDialog( + type: DialogType.Error, image: Icons.error, title: 'Error', description: diff --git a/app/lib/screens/preference_screen.dart b/app/lib/screens/preference_screen.dart index 8de8dc56..e09beb9c 100644 --- a/app/lib/screens/preference_screen.dart +++ b/app/lib/screens/preference_screen.dart @@ -193,7 +193,8 @@ class _PreferenceScreenState extends State { showDialog( context: context, builder: (BuildContext context) => CustomDialog( - image: Icons.error, + type: DialogType.Warning, + image: Icons.warning, title: 'Disable Fingerprint', description: 'Are you sure you want to deactivate fingerprint as authentication method?', @@ -225,7 +226,8 @@ class _PreferenceScreenState extends State { showDialog( context: context, builder: (BuildContext context) => CustomDialog( - image: Icons.error, + type: DialogType.Warning, + image: Icons.warning, title: 'Are you sure?', description: 'If you confirm, your account will be removed from this device. You can always recover your account with your username and phrase.', @@ -237,7 +239,13 @@ class _PreferenceScreenState extends State { }, ), TextButton( - child: const Text('Yes'), + child: Text( + 'Yes', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.error), + ), onPressed: () async { // try { // String deviceID = await _listener.getToken(); @@ -257,6 +265,7 @@ class _PreferenceScreenState extends State { showDialog( context: preferenceContext!, builder: (BuildContext context) => CustomDialog( + type: DialogType.Error, title: 'Error', description: 'Something went wrong when trying to remove your account.', @@ -349,7 +358,7 @@ class _PreferenceScreenState extends State { context: context, builder: (BuildContext context) => CustomDialog( hiddenAction: copySeedPhrase, - image: Icons.create, + image: Icons.info, title: 'Please write this down on a piece of paper', description: phrase.toString(), actions: [ diff --git a/app/lib/screens/qr_code_screen.dart b/app/lib/screens/qr_code_screen.dart new file mode 100644 index 00000000..13a1a82d --- /dev/null +++ b/app/lib/screens/qr_code_screen.dart @@ -0,0 +1,59 @@ +/* + * QR.Flutter + * Copyright (c) 2019 the QR.Flutter authors. + * See LICENSE for distribution and usage details. + */ + +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:screen_brightness/screen_brightness.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +/// This is the screen that you'll see when the app starts +class GenerateQRCodeScreen extends StatefulWidget { + const GenerateQRCodeScreen({ + super.key, + required this.message, + }); + + final String message; + + @override + State createState() => _GenerateQRCodeScreenState(); +} + +class _GenerateQRCodeScreenState extends State { + @override + void initState() { + ScreenBrightness().setScreenBrightness(1); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return CustomDialog( + title: 'Scan QR code', + image: Icons.qr_code, + widgetDescription: Center( + child: Container( + color: Colors.white, + width: 280, + child: QrImageView( + data: widget.message, + version: QrVersions.auto, + ), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + ScreenBrightness().resetScreenBrightness(); + Navigator.pop(context); + setState(() {}); + }, + ), + ], + ); + } +} diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index 3197d835..d76162e1 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -27,84 +27,104 @@ class _RegisteredScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.3, - width: MediaQuery.of(context).size.width, - child: Stack( - alignment: Alignment.center, - children: [ - Image.asset( - 'assets/map.png', - fit: BoxFit.cover, - ), - const Hero( - tag: 'logo', - child: HomeLogoWidget(), - ), - ], - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 50, horizontal: 10), - height: MediaQuery.of(context).size.height * 0.5, - width: MediaQuery.of(context).size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: MediaQuery.of(context).size.width / 1.2, - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - children: const [ - TextSpan( - text: - 'ThreeFold Connect App is 2FA authenticator. '), - TextSpan( - text: - 'By using ThreeFold Connect you can ensure that a user is who the say they are.'), - ]), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.3, + width: MediaQuery.of(context).size.width, + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset( + 'assets/map.png', + fit: BoxFit.cover, + ), + const Hero( + tag: 'logo', + child: HomeLogoWidget(), ), - ), - const Spacer(), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - HomeCardWidget( - name: 'News', icon: Icons.article, pageNumber: 1), - HomeCardWidget( - name: 'Wallet', - icon: Icons.account_balance_wallet, - pageNumber: 2), - HomeCardWidget( - name: 'Farming', - icon: Icons.storage, - pageNumber: 3), - ], - ), - const Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - HomeCardWidget( - name: 'Support', icon: Icons.build, pageNumber: 4), - HomeCardWidget( - name: 'Identity', icon: Icons.person, pageNumber: 5), - HomeCardWidget( - name: 'Settings', icon: Icons.settings, pageNumber: 6), - ], - ), - ], + ], + ), ), - ) - ], + Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 10), + height: MediaQuery.of(context).size.height * 0.6, + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 1.2, + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + children: const [ + TextSpan( + text: + 'ThreeFold Connect App is 2FA authenticator. '), + TextSpan( + text: + 'By using ThreeFold Connect you can ensure that a user is who the say they are.'), + ]), + ), + ), + const Spacer(), + const Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeCardWidget( + name: 'Wallet', + icon: Icons.account_balance_wallet, + pageNumber: 2, + fullWidth: true), + ], + ), + const Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeCardWidget( + name: 'Farming', icon: Icons.storage, pageNumber: 3), + HomeCardWidget( + name: 'Dao', + icon: Icons.how_to_vote_outlined, + pageNumber: 4), + ], + ), + const Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeCardWidget( + name: 'News', icon: Icons.article, pageNumber: 1), + HomeCardWidget( + name: 'Support', icon: Icons.build, pageNumber: 5), + ], + ), + const Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeCardWidget( + name: 'Identity', icon: Icons.person, pageNumber: 6), + HomeCardWidget( + name: 'Settings', icon: Icons.settings, pageNumber: 7), + ], + ), + ], + ), + ) + ], + ), )); } diff --git a/app/lib/screens/scan_screen.dart b/app/lib/screens/scan_screen.dart index 670aeaec..9d46f99f 100644 --- a/app/lib/screens/scan_screen.dart +++ b/app/lib/screens/scan_screen.dart @@ -52,7 +52,7 @@ class _ScanScreenState extends State { color: Colors.transparent, child: Container( decoration: BoxDecoration( - color: Theme.of(context).primaryColor, + color: Theme.of(context).colorScheme.primaryContainer, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0))), @@ -72,13 +72,13 @@ class _ScanScreenState extends State { }, child: const Icon(Icons.arrow_back_ios), ), - const Text( + Text( 'Scan QR', textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 21.0), + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), ), const SizedBox( width: 60.0, @@ -88,12 +88,12 @@ class _ScanScreenState extends State { ), ), Container( - color: Theme.of(context).primaryColor, + color: Theme.of(context).colorScheme.primaryContainer, child: Container( color: Colors.transparent, child: Container( decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, + color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0))), @@ -107,7 +107,10 @@ class _ScanScreenState extends State { child: Center( child: Text( helperText, - style: const TextStyle(fontSize: 16.0), + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), ), ), ), diff --git a/app/lib/screens/sign_screen.dart b/app/lib/screens/sign_screen.dart index d02adc4b..c88898cd 100644 --- a/app/lib/screens/sign_screen.dart +++ b/app/lib/screens/sign_screen.dart @@ -410,6 +410,7 @@ class _SignScreenState extends State with BlockAndRunMixin { context: context, barrierDismissible: false, builder: (BuildContext customContext) => CustomDialog( + type: DialogType.Warning, image: Icons.warning, title: 'Are you sure', description: diff --git a/app/lib/screens/wallet_screen.dart b/app/lib/screens/wallet_screen.dart deleted file mode 100644 index 96b6b9a3..00000000 --- a/app/lib/screens/wallet_screen.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:threebotlogin/models/wallet.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; -import 'package:threebotlogin/widgets/wallet_card.dart'; - -class WalletScreen extends StatefulWidget { - const WalletScreen({super.key}); - - @override - State createState() => _WalletScreenState(); -} - -class _WalletScreenState extends State { - final List wallets = [ - Wallet( - name: 'Daily', - stellarSecret: 'stsecret', - stellarAddress: 'staddress', - stellarBalance: '10.54', - tfchainSecret: 'tfsecret', - tfchainAddress: 'tfaddress', - tfchainBalance: '43.26') - ]; - - @override - Widget build(BuildContext context) { - return LayoutDrawer( - titleText: 'Wallet', - content: ListView( - children: [ - for (final wallet in wallets) WalletCardWidget(wallet: wallet) - ], - ), - ); - } -} diff --git a/app/lib/screens/wallets/contacts.dart b/app/lib/screens/wallets/contacts.dart new file mode 100644 index 00000000..89b4e13d --- /dev/null +++ b/app/lib/screens/wallets/contacts.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/contact.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/contact_service.dart'; +import 'package:threebotlogin/widgets/wallets/add_edit_contact.dart'; +import 'package:threebotlogin/widgets/wallets/contacts_widget.dart'; + +class ContractsScreen extends StatefulWidget { + const ContractsScreen( + {super.key, + required this.chainType, + required this.currentWalletAddress, + required this.wallets, + required this.onSelectToAddress}); + + final ChainType chainType; + final String currentWalletAddress; + final List wallets; + final void Function(String address) onSelectToAddress; + + @override + State createState() => _ContractsScreenState(); +} + +class _ContractsScreenState extends State { + List myWalletContacts = []; + List myPkidContacts = []; + + _loadMyWalletContacts() { + for (final w in widget.wallets) { + if (widget.chainType == ChainType.Stellar) { + myWalletContacts.add(PkidContact( + name: w.name, address: w.stellarAddress, type: ChainType.Stellar)); + } + if (widget.chainType == ChainType.TFChain) { + myWalletContacts.add(PkidContact( + name: w.name, address: w.tfchainAddress, type: ChainType.TFChain)); + } + } + } + + _loadFavouriteContacts() async { + myPkidContacts = await getPkidContacts(); + myPkidContacts = + myPkidContacts.where((c) => c.type == widget.chainType).toList(); + setState(() {}); + } + + _onAddContact(PkidContact contact) async { + myPkidContacts.add(contact); + setState(() {}); + } + + _openAddContactOverlay() { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => AddEditContact( + onAddContact: _onAddContact, + chainType: widget.chainType, + contacts: [...myPkidContacts, ...myWalletContacts], + )); + } + + _openEditContactOverlay(String name, String address) { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => AddEditContact( + chainType: widget.chainType, + contacts: [...myPkidContacts, ...myWalletContacts], + operation: ContactOperation.Edit, + name: name, + address: address, + onEditContact: _onEditContact, + )); + } + + _onEditContact(String oldName, String newName, String newAddress) { + for (final c in myPkidContacts) { + if (c.name == oldName) { + c.name = newName; + c.address = newAddress; + } + } + setState(() {}); + } + + _onDeleteContact(String name) { + myPkidContacts = myPkidContacts.where((c) => c.name != name).toList(); + setState(() {}); + } + + @override + void initState() { + _loadMyWalletContacts(); + _loadFavouriteContacts(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Contacts'), + actions: [ + IconButton( + onPressed: _openAddContactOverlay, icon: const Icon(Icons.add)) + ], + ), + body: DefaultTabController( + length: 2, + child: Column( + children: [ + PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + labelColor: Theme.of(context).colorScheme.primary, + indicatorColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Theme.of(context).colorScheme.onBackground, + dividerColor: Theme.of(context).scaffoldBackgroundColor, + labelStyle: Theme.of(context).textTheme.titleLarge, + unselectedLabelStyle: + Theme.of(context).textTheme.titleMedium, + tabs: const [ + Tab(text: 'My Wallets'), + Tab(text: 'Favorites'), + ], + ), + ), + ), + Expanded( + child: TabBarView( + children: [ + ContactsWidget( + contacts: myWalletContacts + .where( + (c) => c.address != widget.currentWalletAddress) + .toList(), + onSelectToAddress: widget.onSelectToAddress), + ContactsWidget( + contacts: myPkidContacts, + onSelectToAddress: widget.onSelectToAddress, + onDeleteContact: _onDeleteContact, + onEditContact: _openEditContactOverlay, + canEditAndDelete: true, + ), + ], + ), + ), + ], + ), + )); + } +} diff --git a/app/lib/screens/wallets/receive.dart b/app/lib/screens/wallets/receive.dart new file mode 100644 index 00000000..384f919f --- /dev/null +++ b/app/lib/screens/wallets/receive.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/qr_code_screen.dart'; +import 'package:validators/validators.dart'; + +class WalletReceiveScreen extends StatefulWidget { + const WalletReceiveScreen({super.key, required this.wallet}); + final Wallet wallet; + + @override + State createState() => _WalletReceiveScreenState(); +} + +class _WalletReceiveScreenState extends State { + final toController = TextEditingController(); + final amountController = TextEditingController(); + final memoController = TextEditingController(); + ChainType chainType = ChainType.Stellar; + String? amountError; + + @override + void initState() { + toController.text = widget.wallet.stellarAddress; + super.initState(); + } + + @override + void dispose() { + toController.dispose(); + amountController.dispose(); + memoController.dispose(); + super.dispose(); + } + + bool _validate() { + final amount = amountController.text.trim(); + amountError = null; + + if (amount.isEmpty) { + amountError = "Amount can't be empty"; + setState(() {}); + return false; + } + + if (!isFloat(amount)) { + amountError = 'Amount should have numeric values only'; + setState(() {}); + return false; + } + + return true; + } + + _showQRCode() { + final quaryParams = {'amount': amountController.text.trim()}; + if (chainType == ChainType.Stellar) { + quaryParams['message'] = memoController.text.trim(); + } + final uri = Uri( + scheme: 'TFT', + path: toController.text.trim(), + queryParameters: quaryParams); + final codeMessage = uri.toString(); + showDialog( + context: context, + builder: (context) => GenerateQRCodeScreen(message: codeMessage), + ); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Scaffold( + appBar: AppBar(title: const Text('Receive')), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column(children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => setState(() { + toController.text = widget.wallet.stellarAddress; + chainType = ChainType.Stellar; + }), + style: ElevatedButton.styleFrom( + fixedSize: Size.fromWidth(width / 3), + backgroundColor: chainType == ChainType.Stellar + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + side: BorderSide( + color: chainType == ChainType.Stellar + ? Theme.of(context) + .colorScheme + .primaryContainer + : Theme.of(context) + .colorScheme + .secondaryContainer))), + child: Text( + 'Stellar', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: chainType == ChainType.Stellar + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground), + ), + ), + ElevatedButton( + onPressed: () => setState(() { + toController.text = widget.wallet.tfchainAddress; + chainType = ChainType.TFChain; + }), + style: ElevatedButton.styleFrom( + fixedSize: Size.fromWidth(width / 3), + backgroundColor: chainType == ChainType.TFChain + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: BorderSide( + color: chainType == ChainType.TFChain + ? Theme.of(context) + .colorScheme + .primaryContainer + : Theme.of(context) + .colorScheme + .secondaryContainer))), + child: Text( + 'TFChain', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: chainType == ChainType.TFChain + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground), + ), + ), + ], + ), + ), + const SizedBox(height: 40), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: toController, + decoration: InputDecoration( + labelText: 'To (name: ${widget.wallet.name})', + )), + ), + const SizedBox(height: 10), + ListTile( + title: TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + keyboardType: TextInputType.number, + controller: amountController, + decoration: InputDecoration( + suffixText: 'TFT', + labelText: 'Amount', + hintText: '100', + errorText: amountError)), + ), + const SizedBox(height: 10), + if (chainType == ChainType.Stellar) + ListTile( + title: TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: memoController, + decoration: const InputDecoration( + labelText: 'Memo', + )), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: ElevatedButton( + onPressed: () { + final valid = _validate(); + print(valid); + if (valid) _showQRCode(); + }, + style: ElevatedButton.styleFrom(), + child: SizedBox( + width: double.infinity, + child: Text( + 'Generate QR code', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + ), + ]), + ), + ), + ); + } +} diff --git a/app/lib/screens/wallets/send.dart b/app/lib/screens/wallets/send.dart new file mode 100644 index 00000000..7ad01e7e --- /dev/null +++ b/app/lib/screens/wallets/send.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/scan_screen.dart'; +import 'package:threebotlogin/screens/wallets/contacts.dart'; +import 'package:threebotlogin/services/stellar_service.dart'; +import 'package:threebotlogin/widgets/wallets/send_confirmation.dart'; +import 'package:validators/validators.dart'; + +class WalletSendScreen extends StatefulWidget { + const WalletSendScreen( + {super.key, required this.wallet, required this.allWallets}); + final Wallet wallet; + final List allWallets; + + @override + State createState() => _WalletSendScreenState(); +} + +class _WalletSendScreenState extends State { + final fromController = TextEditingController(); + final toController = TextEditingController(); + final amountController = TextEditingController(); + final memoController = TextEditingController(); + ChainType chainType = ChainType.Stellar; + String? toAddressError; + String? amountError; + + @override + void initState() { + fromController.text = widget.wallet.stellarAddress; + super.initState(); + } + + @override + void dispose() { + fromController.dispose(); + toController.dispose(); + amountController.dispose(); + memoController.dispose(); + super.dispose(); + } + + bool _validate() { + final toAddress = toController.text.trim(); + final amount = amountController.text.trim(); + amountError = null; + toAddressError = null; + if (toAddress.isEmpty) { + toAddressError = "Address can't be empty"; + setState(() {}); + return false; + } + if (amount.isEmpty) { + amountError = "Amount can't be empty"; + setState(() {}); + return false; + } + if (!isFloat(amount)) { + amountError = 'Amount should have numeric values only'; + setState(() {}); + return false; + } + if (chainType == ChainType.TFChain) { + if (toAddress.length != 48) { + toAddressError = 'Address length should be 48 characters'; + setState(() {}); + return false; + } + + if (double.parse(amount) < 0.01) { + amountError = "Amount can't be less than 0.01"; + setState(() {}); + return false; + } + if (double.parse(widget.wallet.tfchainBalance) - + double.parse(amount) - + 0.01 < + 0) { + amountError = "Amount shouldn't be more than the wallet balance"; + setState(() {}); + return false; + } + } + if (chainType == ChainType.Stellar) { + if (!isValidStellarAddress(toAddress)) { + toAddressError = 'Invaild Stellar address'; + setState(() {}); + return false; + } + + if (double.parse(amount) < 0.1) { + amountError = "Amount can't be less than 0.1"; + setState(() {}); + return false; + } + if (double.parse(widget.wallet.stellarBalance) - + double.parse(amount) - + 0.1 < + 0) { + amountError = "Amount shouldn't be more than the wallet balance"; + setState(() {}); + return false; + } + } + setState(() {}); + return true; + } + + void _selectToAddress(String address) { + toController.text = address; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + return Scaffold( + appBar: AppBar(title: const Text('Send')), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column(children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => setState(() { + fromController.text = widget.wallet.stellarAddress; + chainType = ChainType.Stellar; + }), + style: ElevatedButton.styleFrom( + fixedSize: Size.fromWidth(width / 3), + backgroundColor: chainType == ChainType.Stellar + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + side: BorderSide( + color: chainType == ChainType.Stellar + ? Theme.of(context) + .colorScheme + .primaryContainer + : Theme.of(context) + .colorScheme + .secondaryContainer))), + child: Text( + 'Stellar', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: chainType == ChainType.Stellar + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground), + ), + ), + ElevatedButton( + onPressed: () => setState(() { + fromController.text = widget.wallet.tfchainAddress; + chainType = ChainType.TFChain; + }), + style: ElevatedButton.styleFrom( + fixedSize: Size.fromWidth(width / 3), + backgroundColor: chainType == ChainType.TFChain + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + side: BorderSide( + color: chainType == ChainType.TFChain + ? Theme.of(context) + .colorScheme + .primaryContainer + : Theme.of(context) + .colorScheme + .secondaryContainer))), + child: Text( + 'TFChain', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: chainType == ChainType.TFChain + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground), + ), + ), + ], + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: TextButton( + onPressed: () { + scanQrCode(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2), + ), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + child: SizedBox( + width: double.infinity, + child: Text( + 'Scan QR code', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + ), + const SizedBox(height: 20), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: fromController, + decoration: InputDecoration( + labelText: 'From (name: ${widget.wallet.name})', + )), + ), + const SizedBox(height: 10), + ListTile( + title: TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: toController, + decoration: InputDecoration( + labelText: 'To', + errorText: toAddressError, + suffixIcon: IconButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ContractsScreen( + chainType: chainType, + currentWalletAddress: fromController.text, + wallets: widget.allWallets, + onSelectToAddress: _selectToAddress), + )); + }, + icon: const Icon(Icons.person)))), + ), + const SizedBox(height: 10), + ListTile( + title: TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + keyboardType: TextInputType.number, + controller: amountController, + decoration: InputDecoration( + labelText: + 'Amount (Balance: ${chainType == ChainType.Stellar ? widget.wallet.stellarBalance : widget.wallet.tfchainBalance})', + hintText: '100', + suffixText: 'TFT', + errorText: amountError)), + subtitle: Text( + 'Max Fee: ${chainType == ChainType.Stellar ? 0.1 : 0.01} TFT'), + ), + const SizedBox(height: 10), + if (chainType == ChainType.Stellar) + ListTile( + title: TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: memoController, + decoration: const InputDecoration( + labelText: 'Memo', + )), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: ElevatedButton( + onPressed: () async { + if (_validate()) { + await _send_confirmation(); + } + }, + style: ElevatedButton.styleFrom(), + child: SizedBox( + width: double.infinity, + child: Text( + 'Transfer', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + ), + ]), + ), + ), + ); + } + + scanQrCode() async { + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + // QRCode scanner is black if we don't sleep here. + bool slept = + await Future.delayed(const Duration(milliseconds: 400), () => true); + late Barcode result; + if (slept) { + if (context.mounted) { + result = await Navigator.push(context, + MaterialPageRoute(builder: (context) => const ScanScreen())); + } + } + if (result.code != null) { + final code = Uri.parse(result.code!); + toController.text = code.path; + if (code.queryParameters.containsKey('amount')) { + amountController.text = code.queryParameters['amount']!; + } + if (chainType == ChainType.Stellar && + code.queryParameters.containsKey('message')) { + memoController.text = code.queryParameters['message']!; + } + setState(() {}); + } + + return result.code; + } + + _send_confirmation() async { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => SendConfirmationWidget( + chainType: chainType, + secret: chainType == ChainType.Stellar + ? widget.wallet.stellarSecret + : widget.wallet.tfchainSecret, + from: fromController.text, + to: toController.text, + amount: amountController.text, + memo: memoController.text, + )); + } +} diff --git a/app/lib/screens/wallets/transaction_details.dart b/app/lib/screens/wallets/transaction_details.dart new file mode 100644 index 00000000..83fab732 --- /dev/null +++ b/app/lib/screens/wallets/transaction_details.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/widgets/wallets/transaction_details.dart'; + +class TransactionDetailsScreen extends StatelessWidget { + final Transaction transaction; + + const TransactionDetailsScreen({ + super.key, + required this.transaction, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Transaction Details')), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: TransactionDetails( + transaction: transaction, + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/wallets/transactions.dart b/app/lib/screens/wallets/transactions.dart new file mode 100644 index 00000000..df3c297f --- /dev/null +++ b/app/lib/screens/wallets/transactions.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/stellar_service.dart'; +import 'package:threebotlogin/widgets/wallets/transaction.dart'; +import 'package:threebotlogin/widgets/wallets/vertical_divider.dart'; +import 'package:stellar_flutter_sdk/src/responses/operations/payment_operation_response.dart'; +// import 'package:stellar_flutter_sdk/src/responses/operations/path_payment_strict_receive_operation_response.dart'; + +class WalletTransactionsWidget extends StatefulWidget { + const WalletTransactionsWidget({super.key, required this.wallet}); + final Wallet wallet; + + @override + State createState() => + _WalletTransactionsWidgetState(); +} + +class _WalletTransactionsWidgetState extends State { + List transactions = []; + bool loading = true; + + _listTransactions() async { + setState(() { + loading = true; + }); + try { + final txs = await listTransactions(widget.wallet.stellarSecret); + final transactionsList = txs.map((tx) { + if (tx is PaymentOperationResponse) { + return Transaction( + hash: tx.transactionHash!, + from: tx.from!.accountId, + to: tx.to!.accountId, + asset: tx.assetCode.toString(), + amount: tx.amount!, + type: tx.to!.accountId == widget.wallet.stellarAddress + ? TransactionType.Receive + : TransactionType.Payment, + status: tx.transactionSuccessful!, + date: DateTime.parse(tx.createdAt!).toLocal().toString()); + // } else if (tx is PathPaymentStrictReceiveOperationResponse) { + // return Transaction( + // hash: tx.transactionHash!, + // from: tx.from!, + // to: tx.to!, + // asset: tx.assetCode.toString(), + // type: TransactionType.Receive, + // status: tx.transactionSuccessful!, + // amount: tx.amount!, + // date: tx.createdAt!); + } + // TODO: handle creation transaction + }).toList(); + transactions = transactionsList.where((tx) => tx != null).toList(); + } catch (e) { + print('Failed to load transactions due to $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to load transaction', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } finally { + setState(() { + loading = false; + }); + } + } + + @override + void initState() { + _listTransactions(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Widget mainWidget; + if (loading) { + mainWidget = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Transactions...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + )); + } else if (transactions.isEmpty) { + mainWidget = Center( + child: Text( + 'No transactions yet.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onBackground), + ), + ); + } else { + mainWidget = ListView( + children: [ + for (final tx in transactions) + Column( + children: [ + TransactionWidget(transaction: tx!), + tx == transactions.last + ? const SizedBox() + : const CustomVerticalDivider() + ], + ) + ], + ); + } + return mainWidget; + } +} diff --git a/app/lib/screens/wallets/wallet_assets.dart b/app/lib/screens/wallets/wallet_assets.dart new file mode 100644 index 00000000..2dd2bdc9 --- /dev/null +++ b/app/lib/screens/wallets/wallet_assets.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:stellar_client/models/vesting_account.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/wallets/receive.dart'; +import 'package:threebotlogin/screens/wallets/send.dart'; +import 'package:threebotlogin/services/stellar_service.dart' as Stellar; +import 'package:threebotlogin/services/tfchain_service.dart' as TFChain; +import 'package:threebotlogin/widgets/wallets/arrow_inward.dart'; +import 'package:threebotlogin/widgets/wallets/balance_tile.dart'; + +class WalletAssetsWidget extends StatefulWidget { + const WalletAssetsWidget( + {super.key, required this.wallet, required this.allWallets}); + final Wallet wallet; + final List allWallets; + + @override + State createState() => _WalletAssetsWidgetState(); +} + +class _WalletAssetsWidgetState extends State { + List? vestedWallets = []; + bool tfchainBalaceLoading = true; + bool stellarBalaceLoading = true; + + _listVestedAccounts() async { + vestedWallets = + await Stellar.listVestedAccounts(widget.wallet.stellarSecret); + setState(() {}); + } + + _loadTFChainBalance() async { + setState(() { + tfchainBalaceLoading = true; + }); + final chainUrl = Globals().chainUrl; + final balance = + await TFChain.getBalance(chainUrl, widget.wallet.tfchainAddress); + widget.wallet.tfchainBalance = + balance.toString() == '0.0' ? '0' : balance.toString(); + setState(() { + tfchainBalaceLoading = false; + }); + } + + _loadStellarBalance() async { + setState(() { + stellarBalaceLoading = true; + }); + widget.wallet.stellarBalance = + (await Stellar.getBalance(widget.wallet.stellarSecret)).toString(); + setState(() { + stellarBalaceLoading = false; + }); + } + + @override + void initState() { + _listVestedAccounts(); + _loadTFChainBalance(); + _loadStellarBalance(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + List vestWidgets = []; + if (vestedWallets != null && vestedWallets!.isNotEmpty) { + vestWidgets = [ + const Divider(), + const SizedBox(height: 10), + Text( + 'Vest', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 20, + ), + WalletBalanceTileWidget( + name: 'Stellar', + balance: vestedWallets![0].tft.toString(), + loading: false, + ), + ]; + } + + return Padding( + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 30), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => WalletSendScreen( + wallet: widget.wallet, + allWallets: widget.allWallets, + ), + )); + }, + child: CircleAvatar( + radius: 30, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + child: Icon( + Icons.arrow_outward_outlined, + color: + Theme.of(context).colorScheme.onPrimaryContainer, + size: 30, + ), + ), + ), + const SizedBox(height: 10), + Text( + 'Send', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + Column( + children: [ + InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => WalletReceiveScreen( + wallet: widget.wallet, + ), + )); + }, + child: CircleAvatar( + radius: 30, + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + child: ArrowInward( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + size: 30, + )), + ), + const SizedBox(height: 10), + Text( + 'Receive', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ], + ), + ), + const Divider(), + const SizedBox(height: 10), + Text( + 'Assets', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold), + ), + const SizedBox( + height: 20, + ), + if (double.parse(widget.wallet.stellarBalance) >= 0) + WalletBalanceTileWidget( + name: 'Stellar', + balance: widget.wallet.stellarBalance, + loading: stellarBalaceLoading, + ), + const SizedBox(height: 10), + if (double.parse(widget.wallet.tfchainBalance) >= 0) + WalletBalanceTileWidget( + name: 'TFChain', + balance: widget.wallet.tfchainBalance, + loading: tfchainBalaceLoading, + ), + const SizedBox( + height: 20, + ), + ...vestWidgets + ], + ), + ); + } +} diff --git a/app/lib/screens/wallet_details.dart b/app/lib/screens/wallets/wallet_details.dart similarity index 51% rename from app/lib/screens/wallet_details.dart rename to app/lib/screens/wallets/wallet_details.dart index 9804ee67..b3387517 100644 --- a/app/lib/screens/wallet_details.dart +++ b/app/lib/screens/wallets/wallet_details.dart @@ -1,12 +1,20 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/models/wallet.dart'; -import 'package:threebotlogin/widgets/transactions.dart'; -import 'package:threebotlogin/widgets/wallet_balance.dart'; -import 'package:threebotlogin/widgets/wallet_details.dart'; +import 'package:threebotlogin/screens/wallets/transactions.dart'; +import 'package:threebotlogin/screens/wallets/wallet_assets.dart'; +import 'package:threebotlogin/screens/wallets/wallet_info.dart'; class WalletDetailsScreen extends StatefulWidget { - const WalletDetailsScreen({super.key, required this.wallet}); + const WalletDetailsScreen( + {super.key, + required this.wallet, + required this.allWallets, + required this.onDeleteWallet, + required this.onEditWallet}); final Wallet wallet; + final List allWallets; + final void Function(String name) onDeleteWallet; + final void Function(String oldName, String newName) onEditWallet; @override State createState() => _WalletDetailsScreenState(); @@ -21,15 +29,30 @@ class _WalletDetailsScreenState extends State { }); } + void _onEditWallet(String oldName, String newName) { + widget.wallet.name = newName; + widget.onEditWallet(oldName, newName); + setState(() {}); + } + @override Widget build(BuildContext context) { Widget content; if (currentScreenIndex == 1) { - content = const WalletTransactionsWidget(); + content = WalletTransactionsWidget( + wallet: widget.wallet, + ); } else if (currentScreenIndex == 2) { - content = WalletDetailsWidget(wallet: widget.wallet); + content = WalletDetailsWidget( + wallet: widget.wallet, + onDeleteWallet: widget.onDeleteWallet, + onEditWallet: _onEditWallet, + ); } else { - content = const WalletBalanceWidget(); + content = WalletAssetsWidget( + allWallets: widget.allWallets, + wallet: widget.wallet, + ); } return Scaffold( appBar: AppBar(title: Text(widget.wallet.name)), diff --git a/app/lib/screens/wallets/wallet_info.dart b/app/lib/screens/wallets/wallet_info.dart new file mode 100644 index 00000000..c393c8c6 --- /dev/null +++ b/app/lib/screens/wallets/wallet_info.dart @@ -0,0 +1,330 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class WalletDetailsWidget extends StatefulWidget { + const WalletDetailsWidget( + {super.key, + required this.wallet, + required this.onDeleteWallet, + required this.onEditWallet}); + final Wallet wallet; + final void Function(String name) onDeleteWallet; + final void Function(String oldName, String newName) onEditWallet; + + @override + State createState() => _WalletDetailsWidgetState(); +} + +class _WalletDetailsWidgetState extends State { + final stellarSecretController = TextEditingController(); + final stellarAddressController = TextEditingController(); + final tfchainSecretController = TextEditingController(); + final tfchainAddressController = TextEditingController(); + final walletNameController = TextEditingController(); + final nameFocus = FocusNode(); + String walletName = ''; + bool showTfchainSecret = false; + bool showStellarSecret = false; + bool deleteLoading = false; + bool edit = false; + + Future _deleteWallet() async { + setState(() { + deleteLoading = true; + }); + try { + await deleteWallet(walletNameController.text); + widget.onDeleteWallet(walletNameController.text); + return true; + } catch (e) { + print('Failed to delete wallet due to $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to delete', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + return false; + } finally { + setState(() { + deleteLoading = false; + }); + } + } + + _editWallet() async { + edit = !edit; + final String newName = walletNameController.text.trim(); + if (walletName == newName) { + FocusScope.of(context).requestFocus(nameFocus); + setState(() {}); + return; + } + try { + await editWallet(walletName, newName); + widget.onEditWallet(walletName, newName); + walletName = newName; + widget.wallet.name = newName; + } catch (e) { + print('Failed to modify wallet due to $e'); + if (context.mounted) { + final loadingFarmsFailure = SnackBar( + content: Text( + 'Failed to Modify', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.errorContainer), + ), + duration: const Duration(seconds: 3), + ); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(loadingFarmsFailure); + } + } finally { + setState(() {}); + } + } + + @override + void dispose() { + stellarSecretController.dispose(); + stellarAddressController.dispose(); + tfchainSecretController.dispose(); + tfchainAddressController.dispose(); + walletNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + stellarSecretController.text = widget.wallet.stellarSecret; + stellarAddressController.text = widget.wallet.stellarAddress; + tfchainSecretController.text = widget.wallet.tfchainSecret; + tfchainAddressController.text = widget.wallet.tfchainAddress; + walletNameController.text = widget.wallet.name; + walletName = widget.wallet.name; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Addresses', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: stellarAddressController, + decoration: const InputDecoration( + labelText: 'Stellar', + )), + trailing: IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: stellarAddressController.text)); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy)), + ), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: tfchainAddressController, + decoration: const InputDecoration( + labelText: 'TFChain', + )), + trailing: IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: tfchainAddressController.text)); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy)), + ), + const SizedBox(height: 40), + Text( + 'Secrets', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ListTile( + title: TextField( + readOnly: true, + obscureText: !showStellarSecret, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: stellarSecretController, + decoration: InputDecoration( + labelText: 'Stellar', + suffixIcon: IconButton( + onPressed: () { + setState(() { + showStellarSecret = !showStellarSecret; + }); + }, + icon: Icon(showStellarSecret + ? Icons.visibility + : Icons.visibility_off)), + )), + trailing: IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: stellarSecretController.text)); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy)), + ), + ListTile( + title: TextField( + readOnly: true, + obscureText: !showTfchainSecret, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: tfchainSecretController, + decoration: InputDecoration( + labelText: 'TFChain', + suffixIcon: IconButton( + onPressed: () { + setState(() { + showTfchainSecret = !showTfchainSecret; + }); + }, + icon: Icon(showTfchainSecret + ? Icons.visibility + : Icons.visibility_off)), + )), + trailing: IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: tfchainSecretController.text)); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('Copied!'))); + }, + icon: const Icon(Icons.copy)), + ), + const SizedBox(height: 40), + ListTile( + title: TextField( + focusNode: nameFocus, + autofocus: edit, + readOnly: !edit, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: walletNameController, + decoration: const InputDecoration( + labelText: 'Wallet Name', + )), + trailing: IconButton( + onPressed: _editWallet, + icon: edit ? const Icon(Icons.save) : const Icon(Icons.edit)), + ), + const SizedBox(height: 40), + if (widget.wallet.type == WalletType.IMPORTED) + Center( + child: SizedBox( + width: MediaQuery.of(context).size.width - 40, + child: ElevatedButton( + onPressed: _showDeleteConfirmationDialog, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.errorContainer), + child: deleteLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.error, + )) + : Text( + 'Delete', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onErrorContainer, + ), + ), + ), + ), + ) + ], + ), + ), + ); + } + + void _showDeleteConfirmationDialog() { + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, + image: Icons.warning, + title: 'Are you sure?', + description: + 'If you confirm, your wallet will be removed from this device.', + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () async { + final deleted = await _deleteWallet(); + if (context.mounted) { + Navigator.pop(context); + if (deleted) Navigator.pop(context); + } + }, + //TODO: show loading when press yes + child: Text( + 'Yes', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/wallets/wallet_screen.dart b/app/lib/screens/wallets/wallet_screen.dart new file mode 100644 index 00000000..6ad569e4 --- /dev/null +++ b/app/lib/screens/wallets/wallet_screen.dart @@ -0,0 +1,119 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/layout_drawer.dart'; +import 'package:threebotlogin/widgets/wallets/add_wallet.dart'; +import 'package:threebotlogin/widgets/wallets/wallet_card.dart'; + +class WalletScreen extends StatefulWidget { + const WalletScreen({super.key}); + + @override + State createState() => _WalletScreenState(); +} + +class _WalletScreenState extends State { + bool loading = true; + List wallets = []; + + onDeleteWallet(String name) { + wallets = wallets.where((w) => w.name != name).toList(); + setState(() {}); + } + + onEditWallet(String oldName, String newName) { + for (final w in wallets) { + if (w.name == oldName) { + w.name = newName; + } + } + setState(() {}); + } + + @override + void initState() { + super.initState(); + listMyWallets(); + } + + @override + Widget build(BuildContext context) { + // TODO: handle empty wallets + Widget mainWidget; + if (loading) { + mainWidget = Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Wallets...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + )); + } else { + mainWidget = ListView( + children: [ + for (final wallet in wallets) + WalletCardWidget( + wallet: wallet, + allWallets: wallets, + onDeleteWallet: onDeleteWallet, + onEditWallet: onEditWallet, + ) + ], + ); + } + + return LayoutDrawer( + titleText: 'Wallet', + content: mainWidget, + appBarActions: loading + ? [] + : [ + IconButton( + onPressed: _openAddWalletOverlay, + icon: const Icon( + Icons.add, + )) + ], + ); + } + + listMyWallets() async { + setState(() { + loading = true; + }); + // TODO: handle empty list wallets + // TODO: show error on failure + final myWallets = await listWallets(); + wallets.addAll(myWallets); + setState(() { + loading = false; + }); + } + + _openAddWalletOverlay() { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => NewWallet( + onAddWallet: _addWallet, + wallets: wallets, + )); + } + + void _addWallet(Wallet wallet) { + wallets.add(wallet); + setState(() {}); + } +} diff --git a/app/lib/screens/wizard/page1.dart b/app/lib/screens/wizard/page1.dart new file mode 100644 index 00000000..7dce4725 --- /dev/null +++ b/app/lib/screens/wizard/page1.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/widgets/wizard/common_page.dart'; + +class Page1 extends StatelessWidget { + const Page1({super.key}); + + @override + Widget build(BuildContext context) { + return const CommonPage( + title: 'Welcome to', + subtitle: '', + imagePath: 'assets/TF_log_horizontal.svg', + widthPercentage: 0.75, + heightPercentage: 0.2, + description: + 'Threefold Connect is your main access point to the Threefold Grid and more. Please allow us to quickly show you around!', + ); + } +} diff --git a/app/lib/screens/wizard/page2.dart b/app/lib/screens/wizard/page2.dart new file mode 100644 index 00000000..0f09fbe2 --- /dev/null +++ b/app/lib/screens/wizard/page2.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/widgets/wizard/common_page.dart'; + +class Page2 extends StatelessWidget { + const Page2({super.key}); + + @override + Widget build(BuildContext context) { + return const CommonPage( + title: 'MAXIMUM', + subtitle: 'SECURITY', + imagePath: 'assets/fingerprint.svg', + widthPercentage: 0.75, + heightPercentage: 0.5, + description: + 'The app provides a secure authentication mechanism that provides your identity on the Threefold Grid.', + ); + } +} diff --git a/app/lib/screens/wizard/page3.dart b/app/lib/screens/wizard/page3.dart new file mode 100644 index 00000000..ca54e404 --- /dev/null +++ b/app/lib/screens/wizard/page3.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/widgets/wizard/common_page.dart'; + +class Page3 extends StatelessWidget { + const Page3({super.key}); + + @override + Widget build(BuildContext context) { + return const CommonPage( + title: 'THREEFOLD', + subtitle: 'WALLET', + imagePath: 'assets/tft.png', + description: + 'Access your ThreeFold Wallet and your ThreeFold Tokens (TFT). More currencies are to be added in the future.', + heightPercentage: 0.4, + widthPercentage: 0.75, + ); + } +} diff --git a/app/lib/screens/wizard/page4.dart b/app/lib/screens/wizard/page4.dart new file mode 100644 index 00000000..2622d564 --- /dev/null +++ b/app/lib/screens/wizard/page4.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/widgets/wizard/common_page.dart'; + +class Page4 extends StatelessWidget { + const Page4({super.key}); + + @override + Widget build(BuildContext context) { + return const CommonPage( + title: 'THREEFOLD', + subtitle: 'NEWS', + imagePath: 'assets/news.svg', + description: + "Stay updated with ThreeFold's latest updates via the News section within the app.", + heightPercentage: 0.5, + widthPercentage: 0.8, + ); + } +} diff --git a/app/lib/screens/wizard/page5.dart b/app/lib/screens/wizard/page5.dart new file mode 100644 index 00000000..8255fc46 --- /dev/null +++ b/app/lib/screens/wizard/page5.dart @@ -0,0 +1,154 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; +import 'package:threebotlogin/screens/main_screen.dart'; +import 'package:threebotlogin/screens/wizard/web_view.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:threebotlogin/widgets/wizard/terms_agreement.dart'; + +class Page5 extends StatefulWidget { + const Page5({super.key}); + + @override + State createState() => _Page5State(); +} + +class _Page5State extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.75, + height: MediaQuery.of(context).size.height * 0.25, + child: SvgPicture.asset( + 'assets/journey.svg', + alignment: Alignment.center, + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.02, + ), + Column(children: [ + Text( + 'STARTYOUR', + style: Theme.of(context).textTheme.displayMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'THREEFOLD', + style: Theme.of(context).textTheme.displayMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'JOURNEY', + style: Theme.of(context).textTheme.displayMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: ElevatedButton( + onPressed: () async { + final termsAgreement = + Provider.of(context, listen: false); + if (!termsAgreement.isChecked) { + termsAgreement.attemptToContinue(); + } else { + saveInitDone(); + await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const MainScreen( + initDone: true, + registered: false, + ))); + } + }, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + backgroundColor: Theme.of(context).colorScheme.primary, + ), + child: Text( + 'GET STARTED', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onPrimary), + )), + ), + Consumer( + builder: (context, termsAgreement, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: termsAgreement.isChecked, + onChanged: (bool? value) { + termsAgreement.toggleChecked(value ?? false); + }, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "I agree to Threefold's ", + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: termsAgreement + .attemptedWithoutAccepting && + !termsAgreement.isChecked + ? Theme.of(context).colorScheme.error + : Theme.of(context) + .colorScheme + .onBackground, + ), + ), + TextSpan( + text: 'Terms and conditions.', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: termsAgreement + .attemptedWithoutAccepting && + !termsAgreement.isChecked + ? Theme.of(context).colorScheme.error + : Colors.blue, + decoration: TextDecoration.underline, + decorationColor: termsAgreement + .attemptedWithoutAccepting && + !termsAgreement.isChecked + ? Theme.of(context).colorScheme.error + : Colors.blue, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const WebView()), + ); + }, + ), + ], + ), + ), + ], + ); + }) + ]) + ], + ), + ), + ); + } +} diff --git a/app/lib/screens/wizard/swipe_page.dart b/app/lib/screens/wizard/swipe_page.dart new file mode 100644 index 00000000..38b6560d --- /dev/null +++ b/app/lib/screens/wizard/swipe_page.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:threebotlogin/screens/wizard/page1.dart'; +import 'package:threebotlogin/screens/wizard/page2.dart'; +import 'package:threebotlogin/screens/wizard/page3.dart'; +import 'package:threebotlogin/screens/wizard/page4.dart'; +import 'package:threebotlogin/screens/wizard/page5.dart'; + +import '../../widgets/wizard/terms_and_conditions.dart'; + +class SwipePage extends StatefulWidget { + const SwipePage({super.key}); + + @override + State createState() => _SwipePagesState(); +} + +class _SwipePagesState extends State { + final PageController _pageController = + PageController(); // Controls the PageView + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Padding( + padding: + EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.01), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const TermsAndConditions(); + }, + ); + }, + child: Text( + 'SKIP', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + )) + ], + ), + ), + Expanded( + child: PageView( + controller: _pageController, + onPageChanged: (int index) { + setState(() {}); + }, + children: const [Page1(), Page2(), Page3(), Page4(), Page5()], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 16), + child: Column( + children: [ + SmoothPageIndicator( + controller: _pageController, + count: 5, + effect: ExpandingDotsEffect( + dotWidth: 20, + dotHeight: 20, + activeDotColor: Theme.of(context).colorScheme.primary, + dotColor: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/wizard/web_view.dart b/app/lib/screens/wizard/web_view.dart new file mode 100644 index 00000000..1815848d --- /dev/null +++ b/app/lib/screens/wizard/web_view.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebView extends StatefulWidget { + const WebView({super.key}); + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + bool isLoading = true; + late WebViewController controller; + + @override + void dispose() { + super.dispose(); + } + + @override + void initState() { + super.initState(); + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (url) { + setState(() { + isLoading = false; + }); + }, + onWebResourceError: (error) { + print('Error loading: ${error.description}'); + }, + ), + ) + ..loadRequest(Uri.parse('https://library.threefold.me/info/legal/')); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Terms & Conditions'), + ), + body: Stack( + children: [ + WebViewWidget(controller: controller), + if (isLoading) const Center(child: CircularProgressIndicator()), + ], + ), + ); + } +} diff --git a/app/lib/services/contact_service.dart b/app/lib/services/contact_service.dart new file mode 100644 index 00000000..0bc5305d --- /dev/null +++ b/app/lib/services/contact_service.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_pkid/flutter_pkid.dart'; +import 'package:threebotlogin/apps/wallet/wallet_config.dart'; +import 'package:threebotlogin/models/contact.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:convert/convert.dart'; + +Future _getPkidClient() async { + Uint8List seed = await getDerivedSeed(WalletConfig().appId()); + final mnemonic = bip39.entropyToMnemonic(hex.encode(seed)); + FlutterPkid client = await getPkidClient(seedPhrase: mnemonic); + return client; +} + +Future> getPkidContacts() async { + FlutterPkid client = await _getPkidClient(); + final pKidResult = await client.getPKidDoc('contacts'); + final result = + pKidResult.containsKey('data') && pKidResult.containsKey('success') + ? jsonDecode(pKidResult['data']) + : {}; + if (pKidResult.containsKey('success') && result.isEmpty) { + return []; + } + Map dataMap = result.asMap(); + final pkidWallets = + dataMap.values.map((e) => PkidContact.fromJson(e)).toList(); + return pkidWallets; +} + +Future addContact(String name, String address, ChainType type) async { + List contacts = await getPkidContacts(); + contacts.add(PkidContact(name: name, address: address, type: type)); + + await _saveContactsToPkid(contacts); +} + +Future editContact( + String oldName, String newName, String newAddress) async { + List contacts = await getPkidContacts(); + for (final w in contacts) { + if (w.name == oldName) { + w.name = newName; + w.address = newAddress; + break; + } + } + await _saveContactsToPkid(contacts); +} + +Future deleteContact(String walletName) async { + List contacts = await getPkidContacts(); + contacts = contacts.where((w) => w.name != walletName).toList(); + await _saveContactsToPkid(contacts); +} + +Future _saveContactsToPkid(List contacts) async { + FlutterPkid client = await _getPkidClient(); + final encodedContacts = json.encode(contacts.map((w) => w.toMap()).toList()); + await client.setPKidDoc('contacts', encodedContacts); +} diff --git a/app/lib/services/gridproxy_service.dart b/app/lib/services/gridproxy_service.dart index 3a6fa309..cb8cb75e 100644 --- a/app/lib/services/gridproxy_service.dart +++ b/app/lib/services/gridproxy_service.dart @@ -1,7 +1,9 @@ import 'package:gridproxy_client/gridproxy_client.dart'; +import 'package:gridproxy_client/models/nodes.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/main.reflectable.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:gridproxy_client/models/farms.dart'; Future getMySpending() async { initializeReflectable(); @@ -13,3 +15,51 @@ Future getMySpending() async { final spending = await gridProxyClient.twins.getConsumption(twinID: twinId); return spending.overall_consumption; } + +Future> getFarmsByTwinId(int twinId) async { + try { + initializeReflectable(); + final gridproxyUrl = Globals().gridproxyUrl; + GridProxyClient client = GridProxyClient(gridproxyUrl); + final farms = + await client.farms.list(ListFarmsQueryParameters(twin_id: twinId)); + return farms; + } catch (e) { + throw Exception('Failed to get farms due to $e'); + } +} + +Future> getFarmsByTwinIds(List twinIds) async { + final List>> farmFutures = []; + for (final twinId in twinIds) { + farmFutures.add(getFarmsByTwinId(twinId)); + } + final listFarms = await Future.wait(farmFutures); + final farms = listFarms.expand((i) => i).toList(); //flat + return farms; +} + +Future> getNodesByFarmId(int farmId) async { + try { + initializeReflectable(); + final gridproxyUrl = Globals().gridproxyUrl; + GridProxyClient client = GridProxyClient(gridproxyUrl); + final nodes = + await client.nodes.list(ListNodesQueryParamaters(farm_ids: '$farmId')); + return nodes; + } catch (e) { + throw Exception('Failed to get nodes due to $e'); + } +} + +Future isFarmNameAvailable(String name) async { + try { + initializeReflectable(); + final gridproxyUrl = Globals().gridproxyUrl; + GridProxyClient client = GridProxyClient(gridproxyUrl); + final farms = await client.farms.list(ListFarmsQueryParameters(name: name)); + return farms.isEmpty; + } catch (e) { + throw Exception('Failed to get farms due to $e'); + } +} diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index 9e208a7a..c5df0ff9 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -207,6 +207,7 @@ Future showIdentityMessage(BuildContext context, String type) async { return showDialog( context: context, builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, image: Icons.warning, title: 'Identity verify timed out', description: @@ -226,7 +227,8 @@ Future showIdentityMessage(BuildContext context, String type) async { return showDialog( context: context, builder: (BuildContext context) => CustomDialog( - image: Icons.warning, + type: DialogType.Error, + image: Icons.error, title: 'Identity verify failed', description: 'Something went wrong.\nIf this issue persist, please contact support', diff --git a/app/lib/services/stellar_service.dart b/app/lib/services/stellar_service.dart new file mode 100644 index 00000000..346e869b --- /dev/null +++ b/app/lib/services/stellar_service.dart @@ -0,0 +1,66 @@ +import 'package:stellar_client/models/vesting_account.dart'; +import 'package:stellar_client/stellar_client.dart'; +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; + +bool isValidStellarSecret(String seed) { + try { + StrKey.decodeStellarSecretSeed(seed); + return true; + } catch (e) { + print('Secret is invalid. $e'); + } + return false; +} + +bool isValidStellarAddress(String address) { + try { + StrKey.decodeStellarAccountId(address); + return true; + } catch (e) { + print('Address is invalid. $e'); + } + return false; +} + +Future getBalanceByClient(Client client) async { + try { + final stellarBalances = await client.getBalance(); + for (final balance in stellarBalances) { + if (balance.assetCode == 'TFT') { + if (double.parse(balance.balance) == 0) return '0'; + return balance.balance; + } + } + } catch (e) { + print("Couldn't load the account balance."); + } + return '-1'; +} + +Future getBalance(String secret) async { + final client = Client(NetworkType.PUBLIC, secret); + return getBalanceByClient(client); +} + +Future> listTransactions(String secret) async { + final client = Client(NetworkType.PUBLIC, secret); + final transactions = await client.getTransactions(assetCodeFilter: 'TFT'); + return transactions; +} + +Future?> listVestedAccounts(String secret) async { + final client = Client(NetworkType.PUBLIC, secret); + final accounts = await client.getVestingAccounts(); + return accounts; +} + +Future transfer( + String secret, String dest, String amount, String memo) async { + final client = Client(NetworkType.PUBLIC, secret); + await client.transferThroughThreefoldService( + destinationAddress: dest, + amount: amount, + currency: 'TFT', + memoText: memo, + ); +} diff --git a/app/lib/services/tfchain_service.dart b/app/lib/services/tfchain_service.dart index 6d238eaf..ff5d83ad 100644 --- a/app/lib/services/tfchain_service.dart +++ b/app/lib/services/tfchain_service.dart @@ -1,21 +1,26 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; +import 'package:tfchain_client/generated/dev/types/tfchain_support/types/farm.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:stellar_client/stellar_client.dart' as Stellar; import 'package:convert/convert.dart'; import 'package:tfchain_client/tfchain_client.dart' as TFChain; +import 'package:tfchain_client/models/dao.dart'; +import 'package:tfchain_client/generated/dev/types/pallet_dao/proposal/dao_votes.dart'; +import 'package:crypto/crypto.dart'; +import 'package:http/http.dart' as http; Future getMyTwinId() async { final chainUrl = Globals().chainUrl; if (chainUrl == '') return null; + // TODO: make sure we are using the correct phrase or needs to use derived seed final phrase = await getPhrase(); if (phrase != null) { - final token = RootIsolateToken.instance; - return await compute((dynamic token) async { - BackgroundIsolateBinaryMessenger.ensureInitialized(token); - final wallet = await Stellar.Client.createFromMnemonic( - Stellar.NetworkType.PUBLIC, phrase); + return await compute((void _) async { + final wallet = + await Stellar.Client.fromMnemonic(Stellar.NetworkType.PUBLIC, phrase); final privateKey = wallet.privateKey; if (privateKey != null) { final hexSecret = hex.encode(privateKey.toList().sublist(0, 32)); @@ -27,7 +32,122 @@ Future getMyTwinId() async { return twinId; } return null; - }, token); + }, null); } return null; } + +Future getBalance(String chainUrl, String address) async { + final tfchainQueryClient = TFChain.QueryClient(chainUrl); + await tfchainQueryClient.connect(); + final balances = await tfchainQueryClient.balances.get(address: address); + return balances!.data.free / BigInt.from(10).pow(7); +} + +Future getBalanceByClient(TFChain.Client client) async { + await client.connect(); + final balance = (await client.balances.getMyBalance())!.data.free; + return balance / BigInt.from(10).pow(7); +} + +Future getTwinIdByClient(TFChain.Client client) async { + await client.connect(); + final twinId = await client.twins.getMyTwinId(); + return twinId ?? 0; +} + +Future getTwinId(String seed) async { + final chainUrl = Globals().chainUrl; + final client = TFChain.Client(chainUrl, seed, 'sr25519'); + return getTwinIdByClient(client); +} + +Future>> getProposals() async { + try { + final chainUrl = Globals().chainUrl; + final client = TFChain.QueryClient(chainUrl); + await client.connect(); + final proposals = await client.dao.get(); + return proposals; + } catch (e) { + throw Exception('Failed to get dao proposals due to $e'); + } +} + +Future getProposalVotes(String hash) async { + try { + final chainUrl = Globals().chainUrl; + final client = TFChain.QueryClient(chainUrl); + await client.connect(); + final votes = await client.dao.getProposalVotes(hash: hash); + return votes; + } catch (e) { + throw Exception('Failed to get dao proposals votes due to $e'); + } +} + +Future vote(bool vote, String hash, int farmId, String seed) async { + try { + final chainUrl = Globals().chainUrl; + final client = TFChain.Client(chainUrl, seed, 'sr25519'); + client.connect(); + final daoVotes = + await client.dao.vote(farmId: farmId, hash: hash, approve: vote); + return daoVotes; + } catch (e) { + throw Exception('Failed to vote due to $e'); + } +} + +_activateAccount(String tfchainSeed) async { + final activationUrl = Globals().activationUrl; + final chainUrl = Globals().chainUrl; + final client = TFChain.Client(chainUrl, tfchainSeed, 'sr25519'); + await client.connect(); + + final activationUri = Uri.parse(activationUrl); + final activationResponse = await http + .post(activationUri, body: {'substrateAccountID': client.address}); + if (activationResponse.statusCode != 200) { + throw Exception('Failed to activate account'); + } + final documentUrl = Globals().termsAndConditionsUrl; + final documentUri = Uri.parse(documentUrl); + final response = await http.get(documentUri); + final bytes = utf8.encode(response.body); + final digest = md5.convert(bytes); + final hashString = + digest.bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + + await client.termsAndConditions + .accept(documentLink: documentUrl, documentHash: hashString.codeUnits); + final relayUrl = Globals().relayUrl; + await client.twins.create(relay: relayUrl, pk: []); +} + +Future createFarm( + String name, String tfchainSeed, String stellarAddress) async { + try { + final chainUrl = Globals().chainUrl; + final client = TFChain.Client(chainUrl, tfchainSeed, 'sr25519'); + client.connect(); + final twinId = await getTwinIdByClient(client); + if (twinId == 0) { + await _activateAccount(tfchainSeed); + } + final farmId = await client.farms.create(name: name, publicIps: []); + final farm = await client.farms.get(id: farmId!); + await client.farms + .addStellarAddress(farmId: farmId, stellarAddress: stellarAddress); + return farm; + } catch (e) { + throw Exception('Failed to create farm due to $e'); + } +} + +Future transfer(String secret, String dest, String amount) async { + final chainUrl = Globals().chainUrl; + final client = TFChain.Client(chainUrl, secret, 'sr25519'); + client.connect(); + await client.balances.transfer(address: dest, amount: BigInt.parse(amount)); +} diff --git a/app/lib/services/wallet_service.dart b/app/lib/services/wallet_service.dart new file mode 100644 index 00000000..51509396 --- /dev/null +++ b/app/lib/services/wallet_service.dart @@ -0,0 +1,184 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_pkid/flutter_pkid.dart'; +import 'package:threebotlogin/apps/wallet/wallet_config.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:stellar_client/stellar_client.dart' as Stellar; +import 'package:tfchain_client/tfchain_client.dart' as TFChain; +import 'package:bip39/bip39.dart' as bip39; +import 'package:convert/convert.dart'; +import 'package:threebotlogin/services/stellar_service.dart' as StellarService; +import 'package:threebotlogin/services/tfchain_service.dart' as TFChainService; + +Future _getPkidClient() async { + Uint8List seed = await getDerivedSeed(WalletConfig().appId()); + final mnemonic = bip39.entropyToMnemonic(hex.encode(seed)); + FlutterPkid client = await getPkidClient(seedPhrase: mnemonic); + return client; +} + +Future> _getPkidWallets() async { + FlutterPkid client = await _getPkidClient(); + final pKidResult = await client.getPKidDoc('purse'); + final result = + pKidResult.containsKey('data') && pKidResult.containsKey('success') + ? jsonDecode(pKidResult['data']) + : {}; + + if (pKidResult.containsKey('success') && result.isEmpty) { + return []; + } + + Map dataMap = result.asMap(); + final pkidWallets = + dataMap.values.map((e) => PkidWallet.fromJson(e)).toList(); + return pkidWallets; +} + +Future> listWallets() async { + List pkidWallets = await _getPkidWallets(); + final String chainUrl = Globals().chainUrl; + final List wallets = await compute((void _) async { + final List> walletFutures = []; + for (final w in pkidWallets) { + final walletFuture = loadWallet(w.name, w.seed, w.type, chainUrl); + walletFutures.add(walletFuture); + } + return await Future.wait(walletFutures); + }, null); + + return wallets; +} + +Future<(Stellar.Client, TFChain.Client)> loadWalletClients(String walletName, + String walletSeed, WalletType walletType, String chainUrl) async { + Stellar.Client stellarClient; + TFChain.Client tfchainClient; + if (' '.allMatches(walletSeed).length == 11) { + tfchainClient = TFChain.Client(chainUrl, walletSeed, "sr25519"); + final entropy = bip39.mnemonicToEntropy(walletSeed); + final seed = entropy.padRight(64, "0"); + stellarClient = + Stellar.Client.fromSecretSeedHex(Stellar.NetworkType.PUBLIC, seed); + } else if (' '.allMatches(walletSeed).length == 23) { + stellarClient = await Stellar.Client.fromMnemonic( + Stellar.NetworkType.PUBLIC, walletSeed); + final hexSecret = + hex.encode(stellarClient.privateKey!.toList().sublist(0, 32)); + tfchainClient = TFChain.Client(chainUrl, '0x$hexSecret', "sr25519"); + } else if (StellarService.isValidStellarSecret(walletSeed)) { + stellarClient = Stellar.Client(Stellar.NetworkType.PUBLIC, walletSeed); + final hexSecret = + hex.encode(stellarClient.privateKey!.toList().sublist(0, 32)); + tfchainClient = TFChain.Client(chainUrl, '0x$hexSecret', "sr25519"); + } else { + if (walletSeed.startsWith(RegExp(r'0[xX]'))) { + walletSeed = walletSeed.substring(2); + } + stellarClient = Stellar.Client.fromSecretSeedHex( + Stellar.NetworkType.PUBLIC, walletSeed); + final hexSecret = + hex.encode(stellarClient.privateKey!.toList().sublist(0, 32)); + tfchainClient = TFChain.Client(chainUrl, '0x$hexSecret', "sr25519"); + } + return (stellarClient, tfchainClient); +} + +Future loadWallet(String walletName, String walletSeed, + WalletType walletType, String chainUrl) async { + final (stellarClient, tfchainClient) = + await loadWalletClients(walletName, walletSeed, walletType, chainUrl); + final stellarBalance = await StellarService.getBalanceByClient(stellarClient); + final tfchainBalance = await TFChainService.getBalanceByClient(tfchainClient); + final wallet = Wallet( + name: walletName, + stellarSecret: stellarClient.secretSeed, + stellarAddress: stellarClient.accountId, + tfchainSecret: tfchainClient.mnemonicOrSecretSeed, + tfchainAddress: tfchainClient.address, + stellarBalance: stellarBalance, + tfchainBalance: + tfchainBalance.toString() == '0.0' ? '0' : tfchainBalance.toString(), + type: walletType, + ); + return wallet; +} + +Future addWallet(String walletName, String walletSecret) async { + List wallets = await _getPkidWallets(); + wallets.add(PkidWallet( + name: walletName, + index: -1, + seed: walletSecret, + type: WalletType.IMPORTED)); + + await _saveWalletsToPkid(wallets); +} + +Future editWallet(String oldName, String newName) async { + List wallets = await _getPkidWallets(); + for (final w in wallets) { + if (w.name == oldName) { + w.name = newName; + break; + } + } + await _saveWalletsToPkid(wallets); +} + +Future deleteWallet(String walletName) async { + List wallets = await _getPkidWallets(); + wallets = wallets.where((w) => w.name != walletName).toList(); + await _saveWalletsToPkid(wallets); +} + +Future _saveWalletsToPkid(List wallets) async { + FlutterPkid client = await _getPkidClient(); + final encodedWallets = json.encode(wallets.map((w) => w.toMap()).toList()); + await client.setPKidDoc('purse', encodedWallets); +} + +Future>> getWalletTwinId(String walletName, + String walletSeed, WalletType walletType, String chainUrl) async { + final (stellarClient, tfchainClient) = + await loadWalletClients(walletName, walletSeed, walletType, chainUrl); + final twinId = await TFChainService.getTwinIdByClient(tfchainClient); + final Map> twinIdWallet = { + twinId: { + 'tfchainSeed': tfchainClient.mnemonicOrSecretSeed, + 'name': walletName, + 'stellarAddress': stellarClient.accountId + } + }; + return twinIdWallet; +} + +Future>> getWalletsTwinIds() async { + List pkidWallets = await _getPkidWallets(); + final String chainUrl = Globals().chainUrl; + final Map> twinWallets = + await compute((void _) async { + final List>>> twinIdWalletFutures = []; + final Map> twinWallets = {}; + for (final w in pkidWallets) { + final twinIdWalletFuture = + getWalletTwinId(w.name, w.seed, w.type, chainUrl); + twinIdWalletFutures.add(twinIdWalletFuture); + } + + final twinWalletMaps = await Future.wait(twinIdWalletFutures); + twinWalletMaps.forEach((element) { + twinWallets.addAll(element); + }); + return twinWallets; + }, null); + + twinWallets.removeWhere((key, value) => key == 0); + return twinWallets; +} diff --git a/app/lib/widgets/add_farm.dart b/app/lib/widgets/add_farm.dart new file mode 100644 index 00000000..c19083fa --- /dev/null +++ b/app/lib/widgets/add_farm.dart @@ -0,0 +1,250 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/farm.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/gridproxy_service.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class NewFarm extends StatefulWidget { + const NewFarm({super.key, required this.onAddFarm, required this.wallets}); + final void Function(Farm addedFarm) onAddFarm; + final List wallets; + + @override + State createState() { + return _NewFarmState(); + } +} + +class _NewFarmState extends State { + final _nameController = TextEditingController(); + Wallet? _selectedWallet; + bool saveLoading = false; + String? nameError; + String? walletError; + Future _showDialog( + String title, String message, IconData icon, DialogType type) async { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => CustomDialog( + type: type, + image: icon, + title: title, + description: message, + ), + ); + await Future.delayed( + const Duration(seconds: 3), + () { + Navigator.pop(context); + }, + ); + } + + Future _validateSubmittedData() async { + final farmName = _nameController.text.trim(); + walletError = null; + nameError = null; + saveLoading = true; + setState(() {}); + + if (farmName.isEmpty) { + nameError = "Name can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final available = await isFarmNameAvailable(farmName); + + if (!available) { + nameError = 'Farm name is already used'; + saveLoading = false; + setState(() {}); + return; + } + + if (_selectedWallet == null) { + saveLoading = false; + walletError = 'Please select a wallet'; + setState(() {}); + return; + } + + Farm farm; + try { + final f = await createFarm(farmName, _selectedWallet!.tfchainSecret, + _selectedWallet!.stellarAddress); + farm = Farm( + name: farmName, + walletAddress: _selectedWallet!.stellarAddress, + tfchainWalletSecret: _selectedWallet!.tfchainSecret, + walletName: _selectedWallet!.name, + twinId: f!.twinId, + farmId: f.id, + nodes: []); + await _showDialog( + 'Farm Created!', + 'Farm $farmName has been added successfully', + Icons.check, + DialogType.Info); + } catch (e) { + print(e); + _showDialog('Error', 'Failed to create farm. Please try again.', + Icons.error, DialogType.Error); + saveLoading = false; + setState(() {}); + return; + } + widget.onAddFarm(farm); + saveLoading = false; + setState(() {}); + if (!context.mounted) return; + Navigator.pop(context); + } + + List> _buildDropdownMenuEntries() { + return widget.wallets.map((wallet) { + return DropdownMenuEntry( + value: wallet, + label: wallet.name, + labelWidget: Text(wallet.name, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + ); + }).toList(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final keyboardSpace = MediaQuery.of(context).viewInsets.bottom; + return LayoutBuilder(builder: (ctx, constraints) { + return SizedBox( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardSpace + 16), + child: Column( + children: [ + Text( + 'Create Farm', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decorationColor: + Theme.of(context).colorScheme.onBackground), + maxLength: 40, + decoration: InputDecoration( + label: const Text('Name'), errorText: nameError), + controller: _nameController, + ), + const SizedBox( + height: 20, + ), + if (widget.wallets.isNotEmpty) + DropdownMenu( + menuHeight: MediaQuery.sizeOf(context).height * 0.3, + enableFilter: true, + errorText: walletError, + width: MediaQuery.sizeOf(context).width * 0.92, + textStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + trailingIcon: const Icon( + CupertinoIcons.chevron_down, + size: 18, + ), + selectedTrailingIcon: const Icon( + CupertinoIcons.chevron_up, + size: 18, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: + Theme.of(context).colorScheme.secondaryContainer, + enabledBorder: UnderlineInputBorder( + borderRadius: + const BorderRadius.all(Radius.circular(4)), + borderSide: BorderSide( + color: + Theme.of(context).colorScheme.secondaryContainer, + width: 8.0, + ), + ), + ), + menuStyle: MenuStyle( + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + ), + ), + label: Text( + 'Select Wallet', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + dropdownMenuEntries: _buildDropdownMenuEntries(), + onSelected: (Wallet? value) { + if (value != null) { + _selectedWallet = value; + } + }, + ), + if (widget.wallets.isEmpty) + Text( + 'Please initiate the first wallet or import a wallet.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.error), + ), + const SizedBox( + height: 30, + ), + Row( + children: [ + const Spacer(), + ElevatedButton( + onPressed: () { + if (saveLoading) return; + Navigator.pop(context); + }, + child: const Text('Close')), + const SizedBox( + width: 5, + ), + ElevatedButton( + onPressed: _validateSubmittedData, + child: saveLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : const Text('Create')) + ], + ), + ], + ), + ), + ), + ); + }); + } +} diff --git a/app/lib/widgets/custom_dialog.dart b/app/lib/widgets/custom_dialog.dart index 20cc5f11..b57a90ed 100644 --- a/app/lib/widgets/custom_dialog.dart +++ b/app/lib/widgets/custom_dialog.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:threebotlogin/main.dart'; + +enum DialogType {Info, Warning, Error} class CustomDialog extends StatefulWidget { final String? description; @@ -7,6 +10,7 @@ class CustomDialog extends StatefulWidget { final String title; final IconData image; final dynamic hiddenAction; + final DialogType type; const CustomDialog({ super.key, @@ -16,6 +20,7 @@ class CustomDialog extends StatefulWidget { this.actions, this.image = Icons.person, this.hiddenAction, + this.type = DialogType.Info, }); show(context) { @@ -24,6 +29,7 @@ class CustomDialog extends StatefulWidget { barrierDismissible: false, builder: (BuildContext context) => CustomDialog( image: Icons.error, + type: DialogType.Error, title: title, description: description, widgetDescription: widgetDescription, @@ -50,6 +56,7 @@ class _CustomDialogState extends State { context: context, barrierDismissible: false, builder: (BuildContext context) => CustomDialog( + type: DialogType.Error, image: Icons.error, title: widget.title, description: widget.description, @@ -88,6 +95,18 @@ class _CustomDialogState extends State { circularImage(context) { int timesPressed = 0; const int timesPressedToReveal = 5; + Color backgroundColor; + Color color; + if (widget.type == DialogType.Error){ + backgroundColor = Theme.of(context).colorScheme.error; + color = Theme.of(context).colorScheme.onError; + } else if (widget.type == DialogType.Warning){ + backgroundColor = Theme.of(context).colorScheme.warning; + color = Theme.of(context).colorScheme.onWarning; + } else { + backgroundColor = Theme.of(context).colorScheme.primary; + color = Theme.of(context).colorScheme.onPrimary; + } return Positioned( left: 20.0, right: 20.0, @@ -107,12 +126,12 @@ class _CustomDialogState extends State { } }, child: CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: backgroundColor, radius: 30.0, child: Icon( widget.image, size: 42.0, - color: Theme.of(context).colorScheme.onPrimary, + color: color, ), ), ), diff --git a/app/lib/widgets/dao/dao_card.dart b/app/lib/widgets/dao/dao_card.dart new file mode 100644 index 00000000..4aeb6665 --- /dev/null +++ b/app/lib/widgets/dao/dao_card.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:tfchain_client/models/dao.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'show_result_dialog.dart'; +import 'vote_dialog.dart'; + +class DaoCard extends StatefulWidget { + final Proposal proposal; + final bool active; + + const DaoCard({ + required this.proposal, + required this.active, + super.key, + }); + + @override + State createState() => _DaoCardState(); +} + +class _DaoCardState extends State { + Future _launchUrl() async { + if (widget.proposal.link != "") { + final Uri url = Uri.parse(widget.proposal.link); + if (!await launchUrl(url)) { + const SnackBar( + content: Text( + "Can't go to proposal at this moment please try again later"), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Card( + color: Theme.of(context).cardColor, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + widget.proposal.action, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer), + textAlign: TextAlign.start, + ), + ), + Divider( + thickness: 2, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + widget.proposal.description, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer), + textAlign: TextAlign.start, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextButton( + onPressed: _launchUrl, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2), + ), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + child: SizedBox( + width: double.infinity, + child: Text( + 'Go to proposal', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'You can vote until:', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + widget.proposal.end.formatDateTime(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: + Theme.of(context).colorScheme.onSecondaryContainer), + textAlign: TextAlign.start, + ), + ), + ), + Row( + mainAxisAlignment: widget.active + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: _showVoteResult, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, + ), + child: Text('Show result', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer)), + ), + if (widget.active) + ElevatedButton( + onPressed: _showVoteDialog, + child: Text( + 'Vote', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: + Theme.of(context).colorScheme.onPrimaryContainer), + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + ), + ), + ); + } + + _showVoteResult() { + showDialog( + context: context, + builder: (_) => ShowResultDialog( + proposalHash: widget.proposal.hash, + )); + } + + _showVoteDialog() { + showDialog( + context: context, + builder: (_) => VoteDialog( + proposalHash: widget.proposal.hash, + )); + } +} diff --git a/app/lib/widgets/dao/proposals.dart b/app/lib/widgets/dao/proposals.dart new file mode 100644 index 00000000..e90c0cc2 --- /dev/null +++ b/app/lib/widgets/dao/proposals.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:tfchain_client/models/dao.dart'; + +import 'dao_card.dart'; + +class ProposalsWidget extends StatefulWidget { + final List proposals; + final bool active; + const ProposalsWidget( + {super.key, required this.proposals, this.active = false}); + + @override + State createState() => _ProposalsWidgetState(); +} + +class _ProposalsWidgetState extends State { + List proposals = []; + + @override + void initState() { + proposals = widget.proposals; + super.initState(); + } + + @override + void didUpdateWidget(covariant ProposalsWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.proposals != oldWidget.proposals) { + setState(() { + proposals = widget.proposals; + }); + } + } + + void search(String searchWord) { + setState(() { + final String filterText = searchWord.toLowerCase().trim(); + if (searchWord == '') { + setState(() { + proposals = widget.proposals; + }); + } else { + setState(() { + proposals = widget.proposals + .where((Proposal entry) => + entry.description.toLowerCase().contains(filterText)) + .toList(); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + final daoCards = _buildDaoCardList(proposals, widget.active); + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60.0), + child: Padding( + padding: const EdgeInsets.all(10), + child: SizedBox( + height: 40, + child: SearchBar( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.background), + onChanged: search, + trailing: [ + Icon( + Icons.search, + color: Theme.of(context).colorScheme.onBackground, + ) + ], + hintText: 'Search by proposal description', + hintStyle: MaterialStateProperty.all( + Theme.of(context).textTheme.bodyLarge!.copyWith( + color: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + textStyle: MaterialStateProperty.all( + Theme.of(context).textTheme.bodyLarge!.copyWith( + color: + Theme.of(context).colorScheme.onSecondaryContainer, + decorationThickness: 0, + ), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + ), + ), + body: daoCards!.isNotEmpty + ? SingleChildScrollView( + child: + Column(mainAxisSize: MainAxisSize.min, children: daoCards), + ) + : Center( + child: Text( + widget.proposals.isEmpty + ? 'No active proposal at the moment' + : 'No result was found', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + )); + } +} + +List? _buildDaoCardList(List list, bool active) { + return list.map((item) { + return DaoCard( + proposal: item, + active: active, + ); + }).toList(); +} diff --git a/app/lib/widgets/dao/show_result_dialog.dart b/app/lib/widgets/dao/show_result_dialog.dart new file mode 100644 index 00000000..7fef98ce --- /dev/null +++ b/app/lib/widgets/dao/show_result_dialog.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/services/tfchain_service.dart'; + +class ShowResultDialog extends StatefulWidget { + final String proposalHash; + const ShowResultDialog({ + required this.proposalHash, + super.key, + }); + + @override + State createState() => _ShowResultDialogState(); +} + +class _ShowResultDialogState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _noAnimation; + late Animation _yesAnimation; + late Animation _animation; + + bool loading = true; + int totalVotes = 0; + int noVotes = 0; + int yesVotes = 0; + int threshold = 1; + void getVotes() async { + setState(() { + loading = true; + }); + final votes = await getProposalVotes(widget.proposalHash); + totalVotes = votes.ayes.length + votes.nays.length; + noVotes = votes.nays.length; + yesVotes = votes.ayes.length; + threshold = votes.threshold; + setState(() { + loading = false; + }); + _animationController = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + + _noAnimation = Tween( + begin: 0.0, + end: totalVotes != 0 ? (noVotes / totalVotes * 1.0) : 0, + ).animate(_animationController); + + _yesAnimation = Tween( + begin: 0.0, + end: totalVotes != 0 ? (yesVotes / totalVotes * 1.0) : 0, + ).animate(_animationController); + + _animation = Tween( + begin: 0.0, + end: (totalVotes / threshold), + ).animate(_animationController); + + _animationController.forward(); + } + + @override + void initState() { + super.initState(); + getVotes(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget content; + if (loading) { + content = Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Votes...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + ), + ); + } else { + content = Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Stack( + alignment: Alignment.center, + children: [ + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return LinearProgressIndicator( + minHeight: 40, + value: _animation.value, + color: Theme.of(context).colorScheme.primary, + borderRadius: const BorderRadius.all(Radius.circular(5)), + ); + }, + ), + Center( + child: Text('Threshold ${totalVotes} / ${threshold}', + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + ), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Yes', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + Text( + totalVotes == 0 + ? '0%' + : '${((yesVotes / totalVotes) * 100).toStringAsFixed(0)}%', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + ], + ), + const SizedBox(height: 5), + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return LinearProgressIndicator( + value: _yesAnimation.value, + color: Theme.of(context).colorScheme.primary, + ); + }, + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('No', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + Text( + totalVotes == 0 + ? '0%' + : '${(noVotes / totalVotes * 100).toStringAsFixed(0)}%', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + ], + ), + const SizedBox(height: 5), + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return LinearProgressIndicator( + value: _noAnimation.value, + color: Theme.of(context).colorScheme.error, + ); + }, + ), + ]), + ); + } + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + backgroundColor: Theme.of(context).colorScheme.background, + child: content); + } +} diff --git a/app/lib/widgets/dao/vote_dialog.dart b/app/lib/widgets/dao/vote_dialog.dart new file mode 100644 index 00000000..571e9547 --- /dev/null +++ b/app/lib/widgets/dao/vote_dialog.dart @@ -0,0 +1,236 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:gridproxy_client/models/farms.dart'; + +import 'package:threebotlogin/services/tfchain_service.dart'; +import 'package:threebotlogin/services/gridproxy_service.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class VoteDialog extends StatefulWidget { + final String proposalHash; + const VoteDialog({ + required this.proposalHash, + super.key, + }); + + @override + State createState() => _VoteDialogState(); +} + +class _VoteDialogState extends State { + int? farmId; + final List farms = []; + Map> twinIdWallets = {}; + bool loading = true; + bool yesLoading = false; + bool noLoading = false; + + void getFarms() async { + setState(() { + loading = true; + }); + twinIdWallets = await getWalletsTwinIds(); + List farmsList = await getFarmsByTwinIds(twinIdWallets.keys.toList()); + farms.addAll(farmsList); + setState(() { + loading = false; + }); + } + + @override + void initState() { + getFarms(); + super.initState(); + } + + List> _buildDropdownMenuEntries(List farms) { + return farms.map((farm) { + return DropdownMenuEntry( + value: farm.farmID, + label: farm.name, + labelWidget: Text(farm.name, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + )), + ); + }).toList(); + } + + @override + Widget build(BuildContext context) { + Widget content; + if (loading) { + content = Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 15), + Text( + 'Loading Farms...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold), + ), + ], + ), + ); + } else { + content = Padding( + padding: const EdgeInsets.all(30), + child: Flex( + direction: Axis.vertical, + mainAxisSize: MainAxisSize.min, + children: [ + DropdownMenu( + menuHeight: MediaQuery.sizeOf(context).height * 0.3, + enableFilter: true, + width: MediaQuery.sizeOf(context).width * 0.55, + textStyle: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + trailingIcon: const Icon( + CupertinoIcons.chevron_down, + size: 18, + ), + selectedTrailingIcon: const Icon( + CupertinoIcons.chevron_up, + size: 18, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Theme.of(context).colorScheme.secondaryContainer, + enabledBorder: UnderlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(4)), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondaryContainer, + width: 8.0, + ), + ), + ), + menuStyle: MenuStyle( + shape: MaterialStateProperty.all( + const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + ), + ), + label: Text( + 'Select Farm', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + dropdownMenuEntries: _buildDropdownMenuEntries(farms), + onSelected: (int? value) { + if (value != null) { + farmId = value; + } + }, + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + _vote(true); + }, + child: yesLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : Text( + 'Yes', + style: + Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + ElevatedButton( + onPressed: () { + _vote(false); + }, + child: noLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : Text( + 'No', + style: + Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + ], + ), + ], + ), + ); + } + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: content); + } + + void _vote(bool approve) async { + if (yesLoading || noLoading || farmId == null) return; + setState(() { + approve ? (yesLoading = true) : (noLoading = true); + }); + final farm = farms.firstWhere((farm) => farm.farmID == farmId); + final twinId = farm.twinId; + final seed = twinIdWallets[twinId]!['tfchainSeed']; + try { + await vote(approve, widget.proposalHash, farmId!, seed!); + + _showDialog('Voted!', 'You have voted successfully.', Icons.check, DialogType.Info); + } catch (e) { + _showDialog('Error', 'Failed to Vote.', Icons.error, DialogType.Error); + } finally { + setState(() { + yesLoading = false; + noLoading = false; + }); + } + } + + _showDialog(String title, String description, IconData icon, DialogType type) async { + if (context.mounted) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => CustomDialog( + type: type, + image: icon, + title: title, + description: description, + ), + ); + await Future.delayed( + const Duration(seconds: 3), + () { + Navigator.pop(context); + }, + ); + } + } +} diff --git a/app/lib/widgets/email_verification_needed.dart b/app/lib/widgets/email_verification_needed.dart index 426f62cb..463cf4ef 100644 --- a/app/lib/widgets/email_verification_needed.dart +++ b/app/lib/widgets/email_verification_needed.dart @@ -8,6 +8,7 @@ emailVerificationDialog(context) { barrierDismissible: false, builder: (BuildContext context) => CustomDialog( image: Icons.error, + type: DialogType.Error, title: 'Please verify email', description: 'Please verify email before using this app', actions: [ diff --git a/app/lib/widgets/farm_item.dart b/app/lib/widgets/farm_item.dart index 6b39d5a9..38250416 100644 --- a/app/lib/widgets/farm_item.dart +++ b/app/lib/widgets/farm_item.dart @@ -34,8 +34,8 @@ class _FarmItemWidgetState extends State { walletAddressController.text = widget.farm.walletAddress; tfchainWalletSecretController.text = widget.farm.tfchainWalletSecret; walletNameController.text = widget.farm.walletName; - farmIdController.text = widget.farm.farmId; - twinIdController.text = widget.farm.twinId; + farmIdController.text = widget.farm.farmId.toString(); + twinIdController.text = widget.farm.twinId.toString(); return ExpansionTile( title: Text( diff --git a/app/lib/widgets/farm_node_item.dart b/app/lib/widgets/farm_node_item.dart index 4ffcf7dd..e3f03a23 100644 --- a/app/lib/widgets/farm_node_item.dart +++ b/app/lib/widgets/farm_node_item.dart @@ -21,7 +21,7 @@ class _FarmNodeItemWidgetState extends State { @override Widget build(BuildContext context) { - nodeIdController.text = widget.node.nodeId; + nodeIdController.text = widget.node.nodeId.toString(); final Color statusColor; if (widget.node.status == NodeStatus.Up) { @@ -59,7 +59,10 @@ class _FarmNodeItemWidgetState extends State { borderRadius: BorderRadius.all(Radius.circular(20)))), child: Text( widget.node.status.name, - style: Theme.of(context).textTheme.bodySmall!.copyWith(color: statusTextColor), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: statusTextColor), ), ), ); diff --git a/app/lib/widgets/home_card.dart b/app/lib/widgets/home_card.dart index 01f4e15e..b4f69361 100644 --- a/app/lib/widgets/home_card.dart +++ b/app/lib/widgets/home_card.dart @@ -2,23 +2,27 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/helpers/globals.dart'; class HomeCardWidget extends StatelessWidget { - const HomeCardWidget( - {super.key, - required this.name, - required this.icon, - required this.pageNumber}); + const HomeCardWidget({ + super.key, + required this.name, + required this.icon, + required this.pageNumber, + this.fullWidth = false, + }); final String name; final IconData icon; final int pageNumber; + final bool fullWidth; @override Widget build(BuildContext context) { Globals globals = Globals(); final size = MediaQuery.of(context).size.width; - return Card( - margin: const EdgeInsets.all(3), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), + const double margin = 3; + return Card( + margin: const EdgeInsets.all(margin), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), clipBehavior: Clip.hardEdge, elevation: 2, child: InkWell( @@ -29,9 +33,9 @@ class HomeCardWidget extends StatelessWidget { children: [ Container( padding: const EdgeInsets.all(10), - height: size / 4, - width: size / 4, - child: Column( + height: size / 8, + width: fullWidth ? size * 2 / 2.5 + 2 * margin : size / 2.5, + child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -39,6 +43,7 @@ class HomeCardWidget extends StatelessWidget { icon, color: Theme.of(context).colorScheme.onSecondaryContainer, ), + const SizedBox(width: 7), Text( name, style: Theme.of(context).textTheme.titleMedium!.copyWith( diff --git a/app/lib/widgets/layout_drawer.dart b/app/lib/widgets/layout_drawer.dart index d9a2abd8..701a7582 100644 --- a/app/lib/widgets/layout_drawer.dart +++ b/app/lib/widgets/layout_drawer.dart @@ -4,10 +4,14 @@ import 'package:threebotlogin/helpers/globals.dart'; class LayoutDrawer extends StatefulWidget { const LayoutDrawer( - {super.key, required this.titleText, required this.content}); + {super.key, + required this.titleText, + required this.content, + this.appBarActions}); final String titleText; final Widget content; + final List? appBarActions; @override State createState() => _LayoutDrawerState(); @@ -27,7 +31,7 @@ class _LayoutDrawerState extends State { } else if (index == 2) { globals.tabController.animateTo(3); } else if (index == 3) { - globals.tabController.animateTo(6); + globals.tabController.animateTo(7); } else { return; } @@ -36,6 +40,11 @@ class _LayoutDrawerState extends State { @override Widget build(BuildContext context) { + IconThemeData? selectedIconTheme = + BottomNavigationBarTheme.of(context).selectedIconTheme; + Color? selectedItemColor = + BottomNavigationBarTheme.of(context).selectedItemColor; + double selectedFontSize = 14; if (widget.titleText == 'Home') { currentScreenIndex = 0; } else if (widget.titleText == 'Wallet') { @@ -44,135 +53,161 @@ class _LayoutDrawerState extends State { currentScreenIndex = 2; } else if (widget.titleText == 'Settings') { currentScreenIndex = 3; + } else { + selectedIconTheme = + BottomNavigationBarTheme.of(context).unselectedIconTheme; + selectedItemColor = + BottomNavigationBarTheme.of(context).unselectedItemColor; + selectedFontSize = 12; } - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.titleText), - toolbarHeight: 60, - ), - body: widget.content, - drawer: Drawer( - elevation: 5, - width: MediaQuery.of(context).size.width * 2 / 3, - // space to fit everything. - child: Column( - children: [ - SizedBox( - height: 70, - child: DrawerHeader( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).colorScheme.primary), + return PopScope( + canPop: false, + child: Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + actions: widget.appBarActions ?? [], + title: Text(widget.titleText), + toolbarHeight: 60, + ), + body: widget.content, + drawer: Drawer( + elevation: 5, + width: MediaQuery.of(context).size.width * 2 / 3, + // space to fit everything. + child: Column( + children: [ + SizedBox( + height: 70, + child: DrawerHeader( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.primary), + ), + ), + child: SvgPicture.asset( + 'assets/TF_log_horizontal.svg', + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn), ), - ), - child: SvgPicture.asset( - 'assets/TF_log_horizontal.svg', - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onBackground, - BlendMode.srcIn), ), ), - ), - ListTile( - minLeadingWidth: 10, - leading: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.home, size: 18)), - title: const Text('Home'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(0); - }, - ), - ListTile( - minLeadingWidth: 10, - leading: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.article, size: 18)), - title: const Text('News'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(1); - }, - ), - ListTile( - minLeadingWidth: 10, - leading: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.account_balance_wallet, size: 18)), - title: const Text('Wallet'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(2); - }, - ), - if (Globals().canSeeFarmers) ListTile( minLeadingWidth: 10, leading: const Padding( padding: EdgeInsets.only(left: 10), - child: Icon(Icons.storage, size: 18)), - title: const Text('Farming'), + child: Icon(Icons.home, size: 18)), + title: const Text('Home'), onTap: () { Navigator.pop(context); - globals.tabController.animateTo(3); + globals.tabController.animateTo(0); }, - ) - else - Container(), - ListTile( - minLeadingWidth: 10, - leading: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.build, size: 18), ), - title: const Text('Support'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(4); - }, - ), - ListTile( - minLeadingWidth: 10, - leading: const Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.person, size: 18)), - title: const Text('Identity'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(5); - }, - ), - ListTile( - minLeadingWidth: 10, - leading: const Padding( + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.article, size: 18)), + title: const Text('News'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(1); + }, + ), + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.account_balance_wallet, size: 18)), + title: const Text('Wallet'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(2); + }, + ), + if (Globals().canSeeFarmers) + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.storage, size: 18)), + title: const Text('Farming'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(3); + }, + ) + else + Container(), + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.how_to_vote_outlined, size: 18)), + title: const Text('Dao'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(4); + }, + ), + ListTile( + minLeadingWidth: 10, + leading: const Padding( padding: EdgeInsets.only(left: 10), - child: Icon(Icons.settings, size: 18)), - title: const Text('Settings'), - onTap: () { - Navigator.pop(context); - globals.tabController.animateTo(6); - }, - ), + child: Icon(Icons.build, size: 18), + ), + title: const Text('Support'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(5); + }, + ), + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.person, size: 18)), + title: const Text('Identity'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(6); + }, + ), + ListTile( + minLeadingWidth: 10, + leading: const Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.settings, size: 18)), + title: const Text('Settings'), + onTap: () { + Navigator.pop(context); + globals.tabController.animateTo(7); + }, + ), + ], + ), + ), + bottomNavigationBar: BottomNavigationBar( + onTap: _selectScreen, + selectedIconTheme: selectedIconTheme, + selectedItemColor: selectedItemColor, + showUnselectedLabels: true, + selectedFontSize: selectedFontSize, + unselectedFontSize: 12, + currentIndex: currentScreenIndex, + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + BottomNavigationBarItem( + icon: Icon(Icons.account_balance_wallet), label: 'Wallet'), + BottomNavigationBarItem(icon: Icon(Icons.storage), label: 'Farming'), + BottomNavigationBarItem( + icon: Icon(Icons.settings), label: 'Settings'), ], ), ), - bottomNavigationBar: BottomNavigationBar( - onTap: _selectScreen, - currentIndex: currentScreenIndex, - items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), - BottomNavigationBarItem( - icon: Icon(Icons.account_balance_wallet), label: 'Wallet'), - BottomNavigationBarItem(icon: Icon(Icons.storage), label: 'Farms'), - BottomNavigationBarItem( - icon: Icon(Icons.settings), label: 'Settings'), - ], - ), ); } } diff --git a/app/lib/widgets/login_dialogs.dart b/app/lib/widgets/login_dialogs.dart index 9c585897..5e08bf82 100644 --- a/app/lib/widgets/login_dialogs.dart +++ b/app/lib/widgets/login_dialogs.dart @@ -26,6 +26,7 @@ Future showWrongEmojiDialog(BuildContext ctx) async { await showDialog( context: ctx, builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, image: Icons.warning, title: 'Wrong emoji', description: diff --git a/app/lib/widgets/transactions.dart b/app/lib/widgets/transactions.dart deleted file mode 100644 index 25b19e7c..00000000 --- a/app/lib/widgets/transactions.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class WalletTransactionsWidget extends StatelessWidget { - const WalletTransactionsWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - 'Transactions', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ); - } -} diff --git a/app/lib/widgets/wallet_balance.dart b/app/lib/widgets/wallet_balance.dart deleted file mode 100644 index fe30896e..00000000 --- a/app/lib/widgets/wallet_balance.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class WalletBalanceWidget extends StatelessWidget { - const WalletBalanceWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - 'Balances', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ); - } -} diff --git a/app/lib/widgets/wallet_card.dart b/app/lib/widgets/wallet_card.dart deleted file mode 100644 index eba1802c..00000000 --- a/app/lib/widgets/wallet_card.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:threebotlogin/models/wallet.dart'; -import 'package:threebotlogin/screens/wallet_details.dart'; - -class WalletCardWidget extends StatelessWidget { - const WalletCardWidget({super.key, required this.wallet}); - final Wallet wallet; - - @override - Widget build(BuildContext context) { - return Card( - child: InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => WalletDetailsScreen(wallet: wallet), - )); - }, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - wallet.name, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - ), - const SizedBox(height: 10), - Row( - children: [ - Text( - 'Stellar', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - const Spacer(), - Text( - '${wallet.stellarBalance} TFT', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ], - ), - Row( - children: [ - Text( - 'TFChain', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - const Spacer(), - Text( - '${wallet.tfchainBalance} TFT', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ], - ) - ], - ), - ), - ), - ); - } -} diff --git a/app/lib/widgets/wallet_details.dart b/app/lib/widgets/wallet_details.dart deleted file mode 100644 index c0f9fb7c..00000000 --- a/app/lib/widgets/wallet_details.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:threebotlogin/models/wallet.dart'; - -class WalletDetailsWidget extends StatefulWidget { - const WalletDetailsWidget({super.key, required this.wallet}); - final Wallet wallet; - - @override - State createState() => _WalletDetailsWidgetState(); -} - -class _WalletDetailsWidgetState extends State { - final stellarSecretController = TextEditingController(); - final stellarAddressController = TextEditingController(); - final tfchainSecretController = TextEditingController(); - final tfchainAddressController = TextEditingController(); - final walletNameController = TextEditingController(); - bool showTfchainSecret = false; - bool showStellarSecret = false; - - @override - void dispose() { - stellarSecretController.dispose(); - stellarAddressController.dispose(); - tfchainSecretController.dispose(); - tfchainAddressController.dispose(); - walletNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - stellarSecretController.text = widget.wallet.stellarSecret; - stellarAddressController.text = widget.wallet.stellarAddress; - tfchainSecretController.text = widget.wallet.tfchainSecret; - tfchainAddressController.text = widget.wallet.tfchainAddress; - walletNameController.text = widget.wallet.name; - - return Padding( - padding: const EdgeInsets.all(10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Addresses', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - controller: stellarAddressController, - decoration: const InputDecoration( - labelText: 'Stellar', - )), - trailing: IconButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: stellarAddressController.text)); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy)), - ), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - controller: tfchainAddressController, - decoration: const InputDecoration( - labelText: 'TFChain', - )), - trailing: IconButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: tfchainAddressController.text)); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy)), - ), - const SizedBox(height: 40), - Text( - 'Secrets', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ListTile( - title: TextField( - readOnly: true, - obscureText: !showStellarSecret, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - controller: stellarSecretController, - decoration: InputDecoration( - labelText: 'Stellar', - suffixIcon: IconButton( - onPressed: () { - setState(() { - showStellarSecret = !showStellarSecret; - }); - }, - icon: Icon(showStellarSecret - ? Icons.visibility - : Icons.visibility_off)), - )), - trailing: IconButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: stellarSecretController.text)); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy)), - ), - ListTile( - title: TextField( - readOnly: true, - obscureText: !showTfchainSecret, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - controller: tfchainSecretController, - decoration: InputDecoration( - labelText: 'TFChain', - suffixIcon: IconButton( - onPressed: () { - setState(() { - showTfchainSecret = !showTfchainSecret; - }); - }, - icon: Icon(showTfchainSecret - ? Icons.visibility - : Icons.visibility_off)), - )), - trailing: IconButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: tfchainSecretController.text)); - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Copied!'))); - }, - icon: const Icon(Icons.copy)), - ), - const SizedBox(height: 40), - ListTile( - title: TextField( - readOnly: true, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - controller: walletNameController, - decoration: const InputDecoration( - labelText: 'Wallet Name', - )), - trailing: IconButton( - onPressed: () { - //TODO: Edit the value - }, - icon: const Icon(Icons.edit)), - ), - const SizedBox(height: 40), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 40, - child: ElevatedButton( - onPressed: () { - //TODO: delete the wallet with warning dialog - }, - style: ElevatedButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.errorContainer), - child: Text( - 'Delete', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ), - ), - ) - ], - ), - ); - } -} diff --git a/app/lib/widgets/wallets/add_edit_contact.dart b/app/lib/widgets/wallets/add_edit_contact.dart new file mode 100644 index 00000000..f42d1db5 --- /dev/null +++ b/app/lib/widgets/wallets/add_edit_contact.dart @@ -0,0 +1,298 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:threebotlogin/models/contact.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/contact_service.dart'; +import 'package:threebotlogin/services/stellar_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class AddEditContact extends StatefulWidget { + const AddEditContact({ + super.key, + required this.contacts, + required this.chainType, + this.operation = ContactOperation.Add, + this.onAddContact, + this.name = '', + this.address = '', + this.onEditContact, + }); + + final void Function(PkidContact addedContact)? onAddContact; + final List contacts; + final ChainType chainType; + final ContactOperation operation; + final String name; + final String address; + final void Function(String oldName, String newName, String newAddress)? + onEditContact; + + @override + State createState() { + return _AddEditContactState(); + } +} + +class _AddEditContactState extends State { + final _nameController = TextEditingController(); + final _addressController = TextEditingController(); + bool saveLoading = false; + String? nameError; + String? addressError; + Future _showDialog( + String title, String message, IconData icon, DialogType type) async { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => CustomDialog( + type: type, + image: icon, + title: title, + description: message, + ), + ); + await Future.delayed( + const Duration(seconds: 3), + () { + Navigator.pop(context); + }, + ); + } + + Future _validateAndAdd() async { + final contactName = _nameController.text.trim(); + final contactAddress = _addressController.text.trim(); + nameError = null; + addressError = null; + saveLoading = true; + setState(() {}); + + if (contactName.isEmpty) { + nameError = "Name can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final c = widget.contacts.where((element) => element.name == contactName); + if (c.isNotEmpty) { + nameError = 'Name exists'; + saveLoading = false; + setState(() {}); + return; + } + if (contactAddress.isEmpty) { + addressError = "Address can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final contacts = widget.contacts.where((c) => c.address == contactAddress); + if (contacts.isNotEmpty) { + addressError = 'Address exists'; + saveLoading = false; + setState(() {}); + return; + } + if (widget.chainType == ChainType.TFChain && contactAddress.length != 48) { + addressError = 'Address length should be 48 characters'; + saveLoading = false; + setState(() {}); + return; + } + if (widget.chainType == ChainType.Stellar && + !isValidStellarAddress(contactAddress)) { + addressError = 'Invaild Stellar address'; + saveLoading = false; + setState(() {}); + return; + } + + try { + await addContact(contactName, contactAddress, widget.chainType); + await _showDialog( + 'Contact Added!', + 'Contact $contactName has been added successfully', + Icons.check, + DialogType.Info); + } catch (e) { + print(e); + _showDialog('Error', 'Failed to save contact. Please try again.', + Icons.error, DialogType.Error); + saveLoading = false; + setState(() {}); + return; + } + widget.onAddContact!(PkidContact( + name: contactName, address: contactAddress, type: widget.chainType)); + saveLoading = false; + setState(() {}); + if (!context.mounted) return; + Navigator.pop(context); + } + + Future _validateAndEdit() async { + final contactName = _nameController.text.trim(); + final contactAddress = _addressController.text.trim(); + nameError = null; + addressError = null; + saveLoading = true; + setState(() {}); + + if (contactName.isEmpty) { + nameError = "Name can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final c = widget.contacts.where((element) => element.name == contactName); + if (contactName != widget.name && c.isNotEmpty) { + nameError = 'Name is used for another contact'; + saveLoading = false; + setState(() {}); + return; + } + if (contactAddress.isEmpty) { + addressError = "Address can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final contacts = widget.contacts.where((c) => c.address == contactAddress); + if (contactAddress != widget.address && contacts.isNotEmpty) { + addressError = 'Address is used in another contact'; + saveLoading = false; + setState(() {}); + return; + } + + if (widget.chainType == ChainType.TFChain && contactAddress.length != 48) { + addressError = 'Address length should be 48 characters'; + saveLoading = false; + setState(() {}); + return; + } + if (widget.chainType == ChainType.Stellar && + !isValidStellarAddress(contactAddress)) { + addressError = 'Invaild Stellar address'; + saveLoading = false; + setState(() {}); + return; + } + + try { + await editContact(widget.name, contactAddress, contactAddress); + await _showDialog( + 'Contact Modified!', + 'Contact $contactName has been modified successfully', + Icons.check, + DialogType.Info); + } catch (e) { + print(e); + _showDialog('Error', 'Failed to modify contact. Please try again.', + Icons.error, DialogType.Error); + saveLoading = false; + setState(() {}); + return; + } + widget.onEditContact!(widget.name, contactName, contactAddress); + saveLoading = false; + setState(() {}); + if (!context.mounted) return; + Navigator.pop(context); + } + + @override + void initState() { + if (widget.operation == ContactOperation.Edit) { + _nameController.text = widget.name; + _addressController.text = widget.address; + } + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + _addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final keyboardSpace = MediaQuery.of(context).viewInsets.bottom; + return LayoutBuilder(builder: (ctx, constraints) { + return SizedBox( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardSpace + 16), + child: Column( + children: [ + Text( + widget.operation == ContactOperation.Add + ? 'Add Contact' + : 'Edit Contact', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decorationColor: + Theme.of(context).colorScheme.onBackground), + maxLength: 50, + decoration: InputDecoration( + label: const Text('Name'), errorText: nameError), + controller: _nameController, + ), + TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decorationColor: + Theme.of(context).colorScheme.onBackground), + keyboardType: TextInputType.multiline, + maxLines: null, + decoration: InputDecoration( + label: const Text('Address'), + errorText: addressError, + ), + controller: _addressController, + ), + const SizedBox( + height: 30, + ), + Row( + children: [ + const Spacer(), + ElevatedButton( + onPressed: () { + if (saveLoading) return; + Navigator.pop(context); + }, + child: const Text('Close')), + const SizedBox( + width: 5, + ), + ElevatedButton( + onPressed: widget.operation == ContactOperation.Add + ? _validateAndAdd + : _validateAndEdit, + child: saveLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : const Text('Save')) + ], + ), + ], + ), + ), + ), + ); + }); + } +} diff --git a/app/lib/widgets/wallets/add_wallet.dart b/app/lib/widgets/wallets/add_wallet.dart new file mode 100644 index 00000000..4b0c0260 --- /dev/null +++ b/app/lib/widgets/wallets/add_wallet.dart @@ -0,0 +1,213 @@ +import 'package:bip39/bip39.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/stellar_service.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:tfchain_client/src/utils.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class NewWallet extends StatefulWidget { + const NewWallet( + {super.key, required this.onAddWallet, required this.wallets}); + final void Function(Wallet addedWallet) onAddWallet; + final List wallets; + + @override + State createState() { + return _NewWalletState(); + } +} + +class _NewWalletState extends State { + final _nameController = TextEditingController(); + final _secretController = TextEditingController(); + bool saveLoading = false; + String? nameError; + String? secretError; + Future _showDialog( + String title, String message, IconData icon, DialogType type) async { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => CustomDialog( + type: type, + image: icon, + title: title, + description: message, + ), + ); + await Future.delayed( + const Duration(seconds: 3), + () { + Navigator.pop(context); + }, + ); + } + + Future _validateAddSubmitData() async { + final walletName = _nameController.text.trim(); + final walletSecret = _secretController.text.trim(); + nameError = null; + secretError = null; + saveLoading = true; + setState(() {}); + + if (walletName.isEmpty) { + nameError = "Name can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + final w = widget.wallets.where((element) => element.name == walletName); + if (w.isNotEmpty) { + nameError = 'Name exists'; + saveLoading = false; + setState(() {}); + return; + } + if (walletSecret.isEmpty) { + secretError = "Secret can't be empty"; + saveLoading = false; + setState(() {}); + return; + } + if (!(validateMnemonic(walletSecret) || + (!validateMnemonic(walletSecret) && walletSecret.contains(' ')) || + (isValidStellarSecret(walletSecret)) || + (isValidSeed(walletSecret) && + ((!walletSecret.startsWith('0x') && walletSecret.length == 64) || + (walletSecret.startsWith('0x') && + walletSecret.length == 66))))) { + secretError = 'Secret is invalid'; + saveLoading = false; + setState(() {}); + return; + } + Wallet wallet; + try { + wallet = await loadAddedWallet(walletName, walletSecret); + } catch (e) { + print(e); + _showDialog('Error', 'Failed to load wallet. Please try again.', + Icons.error, DialogType.Error); + saveLoading = false; + setState(() {}); + return; + } + try { + await addWallet(walletName, walletSecret); + await _showDialog( + 'Wallet Added!', + 'Wallet $walletName has been added successfully', + Icons.check, + DialogType.Info); + } catch (e) { + print(e); + _showDialog('Error', 'Failed to save wallet. Please try again.', + Icons.error, DialogType.Error); + saveLoading = false; + setState(() {}); + return; + } + widget.onAddWallet(wallet); + saveLoading = false; + setState(() {}); + if (!context.mounted) return; + Navigator.pop(context); + } + + @override + void dispose() { + _nameController.dispose(); + _secretController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final keyboardSpace = MediaQuery.of(context).viewInsets.bottom; + return LayoutBuilder(builder: (ctx, constraints) { + return SizedBox( + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardSpace + 16), + child: Column( + children: [ + Text( + 'Import Wallet', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decorationColor: + Theme.of(context).colorScheme.onBackground), + maxLength: 50, + decoration: InputDecoration( + label: const Text('Name'), errorText: nameError), + controller: _nameController, + ), + TextField( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + decorationColor: + Theme.of(context).colorScheme.onBackground), + keyboardType: TextInputType.multiline, + maxLines: null, + decoration: InputDecoration( + label: const Text('Secret'), + errorText: secretError, + ), + controller: _secretController, + ), + const SizedBox( + height: 30, + ), + Row( + children: [ + const Spacer(), + ElevatedButton( + onPressed: () { + if (saveLoading) return; + Navigator.pop(context); + }, + child: const Text('Close')), + const SizedBox( + width: 5, + ), + ElevatedButton( + onPressed: _validateAddSubmitData, + child: saveLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : const Text('Save')) + ], + ), + ], + ), + ), + ), + ); + }); + } +} + +Future loadAddedWallet(String walletName, String walletSecret) async { + final chainUrl = Globals().chainUrl; + final Wallet wallet = await compute((void _) async { + final wallet = await loadWallet( + walletName, walletSecret, WalletType.IMPORTED, chainUrl); + return wallet; + }, null); + return wallet; +} diff --git a/app/lib/widgets/wallets/arrow_inward.dart b/app/lib/widgets/wallets/arrow_inward.dart new file mode 100644 index 00000000..63a41b1e --- /dev/null +++ b/app/lib/widgets/wallets/arrow_inward.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class ArrowInward extends StatelessWidget { + const ArrowInward({super.key, required this.color, this.size = 24}); + final Color color; + final double size; + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: 180 * math.pi / 180, + child: Icon( + Icons.arrow_outward, + color: color, + size: size, + ), + ); + } +} diff --git a/app/lib/widgets/wallets/balance_tile.dart b/app/lib/widgets/wallets/balance_tile.dart new file mode 100644 index 00000000..e057cd8d --- /dev/null +++ b/app/lib/widgets/wallets/balance_tile.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class WalletBalanceTileWidget extends StatelessWidget { + const WalletBalanceTileWidget({ + super.key, + required this.balance, + required this.name, + required this.loading, + }); + final String balance; + final String name; + final bool loading; + + @override + Widget build(BuildContext context) { + return ListTile( + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: BorderRadius.circular(5), + ), + leading: SizedBox( + width: 25, + child: Image.asset( + 'assets/tft_icon.png', + fit: BoxFit.cover, + color: Theme.of(context).colorScheme.onBackground, + height: 50, + ), + ), + title: Text( + name, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + trailing: loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + )) + : Text( + '$balance TFT', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + ); + } +} diff --git a/app/lib/widgets/wallets/contact_card.dart b/app/lib/widgets/wallets/contact_card.dart new file mode 100644 index 00000000..9cdf5c0f --- /dev/null +++ b/app/lib/widgets/wallets/contact_card.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/services/contact_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class ContactCardWidget extends StatefulWidget { + const ContactCardWidget({ + super.key, + required this.name, + required this.address, + required this.canEditAndDelete, + this.onDeleteContact, + this.onEditContact, + }); + final String name; + final String address; + final bool canEditAndDelete; + final void Function(String name)? onDeleteContact; + final void Function(String oldName, String oldAddress)? onEditContact; + + @override + State createState() => _ContactCardWidgetState(); +} + +class _ContactCardWidgetState extends State { + bool deleteLoading = false; + _deleteWallet() async { + setState(() { + deleteLoading = true; + }); + //TODO: Show snack in case of failure + await deleteContact(widget.name); + widget.onDeleteContact!(widget.name); + + setState(() { + deleteLoading = false; + }); + } + + void _showDeleteConfirmationDialog() { + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, + image: Icons.warning, + title: 'Are you sure?', + description: + 'If you confirm, your contact will be removed from this device.', + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + onPressed: () async { + await _deleteWallet(); + if (context.mounted) { + Navigator.pop(context); + } + }, + //TODO: show loading when press yes + child: Text( + 'Yes', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.error), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Card( + // color: Theme.of(context).colorScheme.background, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(5), + // side: BorderSide(color: Theme.of(context).colorScheme.primary)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Text( + widget.name, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ), + if (widget.canEditAndDelete) + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton( + onPressed: () { + widget.onEditContact!(widget.name, widget.address); + }, + icon: const Icon( + Icons.edit, + )), + deleteLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.error, + )) + : IconButton( + onPressed: _showDeleteConfirmationDialog, + icon: Icon( + Icons.delete, + color: Theme.of(context).colorScheme.error, + )), + ], + ), + ], + ), + Text( + widget.address, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/wallets/contacts_widget.dart b/app/lib/widgets/wallets/contacts_widget.dart new file mode 100644 index 00000000..e944ed01 --- /dev/null +++ b/app/lib/widgets/wallets/contacts_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/contact.dart'; +import 'package:threebotlogin/widgets/wallets/contact_card.dart'; + +class ContactsWidget extends StatelessWidget { + const ContactsWidget({ + super.key, + required this.contacts, + required this.onSelectToAddress, + this.onDeleteContact, + this.onEditContact, + this.canEditAndDelete = false, + }); + + final List contacts; + final void Function(String address) onSelectToAddress; + final bool canEditAndDelete; + final void Function(String name)? onDeleteContact; + final void Function(String oldName, String oldAddress)? onEditContact; + + @override + Widget build(BuildContext context) { + Widget content; + if (contacts.isEmpty) { + content = Center( + child: Text( + 'No contacts yet.', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onBackground), + ), + ); + } else { + content = ListView(children: [ + for (final contact in contacts) + InkWell( + onTap: () { + onSelectToAddress(contact.address); + Navigator.of(context).pop(); + }, + child: ContactCardWidget( + name: contact.name, + address: contact.address, + canEditAndDelete: canEditAndDelete, + onDeleteContact: onDeleteContact, + onEditContact: onEditContact, + )), + ]); + } + return content; + } +} diff --git a/app/lib/widgets/wallets/send_confirmation.dart b/app/lib/widgets/wallets/send_confirmation.dart new file mode 100644 index 00000000..ec2a37d8 --- /dev/null +++ b/app/lib/widgets/wallets/send_confirmation.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/stellar_service.dart' as Stellar; +import 'package:threebotlogin/services/tfchain_service.dart' as TFChain; +import 'package:threebotlogin/widgets/custom_dialog.dart'; + +class SendConfirmationWidget extends StatefulWidget { + const SendConfirmationWidget({ + super.key, + required this.chainType, + required this.secret, + required this.from, + required this.to, + required this.amount, + required this.memo, + }); + + final ChainType chainType; + final String secret; + final String from; + final String to; + final String amount; + final String memo; + + @override + State createState() => _SendConfirmationWidgetState(); +} + +class _SendConfirmationWidgetState extends State { + final fromController = TextEditingController(); + final toController = TextEditingController(); + final amountController = TextEditingController(); + final memoController = TextEditingController(); + bool loading = false; + + @override + void initState() { + fromController.text = widget.from; + toController.text = widget.to; + amountController.text = widget.amount; + memoController.text = widget.memo; + super.initState(); + } + + @override + void dispose() { + fromController.dispose(); + toController.dispose(); + amountController.dispose(); + memoController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column(children: [ + Text( + 'Send Confirmation', + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: fromController, + decoration: const InputDecoration( + labelText: 'From', + )), + ), + const SizedBox(height: 10), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: toController, + decoration: const InputDecoration( + labelText: 'To', + )), + ), + const SizedBox(height: 10), + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + keyboardType: TextInputType.number, + controller: amountController, + decoration: const InputDecoration( + labelText: 'Amount', hintText: '100', suffixText: 'TFT')), + subtitle: Text( + 'Max Fee: ${widget.chainType == ChainType.Stellar ? 0.1 : 0.01} TFT'), + ), + const SizedBox(height: 10), + if (widget.chainType == ChainType.Stellar) + ListTile( + title: TextField( + readOnly: true, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + controller: memoController, + decoration: const InputDecoration( + labelText: 'Memo', + )), + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _send, + child: loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 3, + )) + : Text( + 'Confirm', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + ), + ]), + ), + ); + } + + _send() async { + setState(() { + loading = true; + }); + try { + if (widget.chainType == ChainType.Stellar) { + await Stellar.transfer( + widget.secret, widget.to, widget.amount, widget.memo); + } else { + await TFChain.transfer(widget.secret, widget.to, widget.amount); + } + await _showDialog( + 'Success!', 'Tokens have been transfered successfully', Icons.check, DialogType.Info); + } catch (e) { + _showDialog( + 'Error', 'Failed to transfer. Please try again.', Icons.error, DialogType.Error); + setState(() { + loading = false; + }); + return; + } + + setState(() { + loading = false; + }); + if (!context.mounted) return; + Navigator.pop(context); + } + + Future _showDialog(String title, String message, IconData icon, DialogType type) async { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => CustomDialog( + type: type, + image: icon, + title: title, + description: message, + ), + ); + await Future.delayed( + const Duration(seconds: 3), + () { + Navigator.pop(context); + }, + ); + } +} diff --git a/app/lib/widgets/wallets/transaction.dart b/app/lib/widgets/wallets/transaction.dart new file mode 100644 index 00000000..a2799ddb --- /dev/null +++ b/app/lib/widgets/wallets/transaction.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/wallets/transaction_details.dart'; +import 'package:threebotlogin/widgets/wallets/arrow_inward.dart'; + +class TransactionWidget extends StatelessWidget { + final Transaction transaction; + + const TransactionWidget({ + super.key, + required this.transaction, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionDetailsScreen( + transaction: transaction, + ), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + CircleAvatar( + backgroundColor: transaction.type == TransactionType.Receive + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + child: transaction.type == TransactionType.Receive + ? ArrowInward( + color: Theme.of(context).colorScheme.onPrimary, + ) + : Icon( + Icons.arrow_outward, + color: Theme.of(context).colorScheme.onError, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(transaction.hash, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + 'TFT ${double.parse(transaction.amount).toStringAsFixed(2)}', + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: transaction.type == + TransactionType.Receive + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + )), + ), + Text(transaction.date, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onBackground)), + ], + ), + ], + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all( + color: transaction.status + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text(transaction.status ? 'Successful' : 'Failed', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: transaction.status + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + )), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/wallets/transaction_details.dart b/app/lib/widgets/wallets/transaction_details.dart new file mode 100644 index 00000000..529e2239 --- /dev/null +++ b/app/lib/widgets/wallets/transaction_details.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; + +class TransactionDetails extends StatelessWidget { + final Transaction transaction; + + const TransactionDetails({ + super.key, + required this.transaction, + }); + + @override + Widget build(BuildContext context) { + Widget buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + label == 'Type' + ? Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + border: Border.all( + color: value == 'Receive' + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text(value, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: value == 'Receive' + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + )), + ) + : Text(value, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith( + color: Theme.of(context) + .colorScheme + .onBackground)), + ], + ), + ), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16.0), + buildDetailRow('From', transaction.from), + const Divider(), + buildDetailRow('To', transaction.to), + const Divider(), + buildDetailRow('Type', transaction.type.name), + const Divider(), + buildDetailRow('Amount', transaction.amount), + const Divider(), + buildDetailRow('Asset', transaction.asset), + const Divider(), + buildDetailRow('Date', transaction.date), + const Divider(), + buildDetailRow('Transaction Hash', transaction.hash), + const Divider(), + ], + ), + ); + } +} diff --git a/app/lib/widgets/wallets/vertical_divider.dart b/app/lib/widgets/wallets/vertical_divider.dart new file mode 100644 index 00000000..8ce8c24e --- /dev/null +++ b/app/lib/widgets/wallets/vertical_divider.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class CustomVerticalDivider extends StatelessWidget { + const CustomVerticalDivider({super.key}); + + @override + Widget build(BuildContext context) { + return const Align( + alignment: Alignment.topLeft, + child: Padding( + padding: EdgeInsets.only(left: 35), + child: SizedBox( + height: 20, + child: VerticalDivider( + color: Colors.grey, + width: 2, + ), + ), + ), + ); + } +} diff --git a/app/lib/widgets/wallets/wallet_card.dart b/app/lib/widgets/wallets/wallet_card.dart new file mode 100644 index 00000000..4b1f35f1 --- /dev/null +++ b/app/lib/widgets/wallets/wallet_card.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/wallets/wallet_details.dart'; + +class WalletCardWidget extends StatelessWidget { + const WalletCardWidget( + {super.key, + required this.wallet, + required this.allWallets, + required this.onDeleteWallet, + required this.onEditWallet}); + final Wallet wallet; + final List allWallets; + final void Function(String name) onDeleteWallet; + final void Function(String oldName, String newName) onEditWallet; + + @override + Widget build(BuildContext context) { + return Card( + child: InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => WalletDetailsScreen( + wallet: wallet, + allWallets: allWallets, + onDeleteWallet: onDeleteWallet, + onEditWallet: onEditWallet, + ), + )); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + wallet.name, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(height: 10), + if (double.parse(wallet.stellarBalance) >= 0) + Row( + children: [ + SizedBox( + width: 35, + child: Image.asset( + 'assets/tft_icon.png', + color: Theme.of(context).colorScheme.onBackground, + )), + Text( + 'Stellar', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + const Spacer(), + Text( + '${wallet.stellarBalance} TFT', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ], + ), + if (double.parse(wallet.tfchainBalance) >= 0) + Row( + children: [ + SizedBox( + width: 35, + child: Image.asset( + 'assets/tft_icon.png', + fit: BoxFit.contain, + color: Theme.of(context).colorScheme.onBackground, + )), + Text( + 'TFChain', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + const Spacer(), + Text( + '${wallet.tfchainBalance} TFT', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ], + ) + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/widgets/wizard/common_page.dart b/app/lib/widgets/wizard/common_page.dart new file mode 100644 index 00000000..37b1e3b2 --- /dev/null +++ b/app/lib/widgets/wizard/common_page.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CommonPage extends StatefulWidget { + final String title; + final String subtitle; + final String imagePath; + final String description; + final double? heightPercentage; + final double? widthPercentage; + + const CommonPage({ + Key? key, + required this.title, + required this.subtitle, + required this.imagePath, + required this.description, + this.heightPercentage = 100, + this.widthPercentage = 300, + }) : super(key: key); + + @override + State createState() => _CommonPageState(); +} + +class _CommonPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.width * 0.9, + child: Column(children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.displayMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold, + ), + ), + if (widget.subtitle.isNotEmpty) + Text( + widget.subtitle, + style: Theme.of(context).textTheme.displayMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + widget.imagePath.endsWith('.svg') + ? SizedBox( + width: widget.widthPercentage != null + ? MediaQuery.of(context).size.width * + widget.widthPercentage! + : null, + height: widget.heightPercentage != null + ? MediaQuery.of(context).size.width * + widget.heightPercentage! + : null, + child: SvgPicture.asset( + widget.imagePath, + alignment: Alignment.center, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onBackground, + BlendMode.srcIn, + ), + ), + ) + : SizedBox( + width: widget.widthPercentage != null + ? MediaQuery.of(context).size.width * + widget.widthPercentage! + : null, + height: widget.heightPercentage != null + ? MediaQuery.of(context).size.width * + widget.heightPercentage! + : null, + child: Image.asset( + widget.imagePath, + fit: BoxFit.contain, + ), + ), + ]), + ), + SizedBox( + width: MediaQuery.of(context).size.width - 100, + height: MediaQuery.of(context).size.height * 0.2, + child: Text( + widget.description, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/widgets/wizard/terms_agreement.dart b/app/lib/widgets/wizard/terms_agreement.dart new file mode 100644 index 00000000..b3f66c16 --- /dev/null +++ b/app/lib/widgets/wizard/terms_agreement.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class TermsAgreement with ChangeNotifier { + bool _isChecked = false; + bool _attemptedWithoutAccepting = false; + + bool get isChecked => _isChecked; + bool get attemptedWithoutAccepting => _attemptedWithoutAccepting; + + void toggleChecked(bool value) { + _isChecked = value; + _attemptedWithoutAccepting = false; + notifyListeners(); + } + + void attemptToContinue() { + _attemptedWithoutAccepting = true; + notifyListeners(); + } +} diff --git a/app/lib/widgets/wizard/terms_and_conditions.dart b/app/lib/widgets/wizard/terms_and_conditions.dart new file mode 100644 index 00000000..e9f45093 --- /dev/null +++ b/app/lib/widgets/wizard/terms_and_conditions.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:threebotlogin/screens/main_screen.dart'; +import 'package:threebotlogin/screens/wizard/web_view.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; +import 'package:threebotlogin/widgets/wizard/terms_agreement.dart'; + +class TermsAndConditions extends StatefulWidget { + const TermsAndConditions({Key? key}) : super(key: key); + + @override + State createState() => _TermsAndConditionsState(); +} + +class _TermsAndConditionsState extends State { + @override + Widget build(BuildContext context) { + return CustomDialog( + title: 'Accept the terms and conditions?', + widgetDescription: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Before you can start using the app, you must accept the Terms and Conditions.', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground)), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const WebView()), + ); + }, + child: Text('Terms and Conditions', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Colors.blue, + decoration: TextDecoration.underline, + decorationColor: Colors.blue)), + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.01), + Consumer(builder: (context, termsAgreement, child) { + return Row( + children: [ + Checkbox( + value: termsAgreement.isChecked, + onChanged: (bool? value) { + termsAgreement.toggleChecked(value ?? false); + }, + ), + Expanded( + child: Text('I Accept the terms and conditions.', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: termsAgreement.attemptedWithoutAccepting && + !termsAgreement.isChecked + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onBackground)), + ), + ], + ); + }) + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + final termsAgreement = + Provider.of(context, listen: false); + if (!termsAgreement.isChecked) { + termsAgreement.attemptToContinue(); + } else { + saveInitDone(); + Navigator.of(context).pop(); + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const MainScreen( + initDone: true, + registered: false, + ))); + } + }, + child: const Text('Continue'), + ), + ], + ); + } +} diff --git a/app/pubspec.lock b/app/pubspec.lock index d902b162..1531e037 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.5" async: dependency: transitive description: @@ -403,7 +403,7 @@ packages: description: path: "." ref: master - resolved-ref: "57827618dd0dda003b86ecb1d6c4ddb015fdad5f" + resolved-ref: "48123e66597f6fb448edb14183935b0b243163a6" url: "https://github.com/threefoldtech/threefold_connect_flutter_pkid_client" source: git version: "0.0.1" @@ -484,10 +484,10 @@ packages: dependency: transitive description: name: hashlib - sha256: "7431346983b7c18fa3d6107891b7f00da4fffc378eed8454a0c9e17961164ffd" + sha256: f572f2abce09fc7aee53f15927052b9732ea1053e540af8cae211111ee0b99b1 url: "https://pub.dev" source: hosted - version: "1.20.2" + version: "1.21.0" hashlib_codecs: dependency: transitive description: @@ -636,10 +636,10 @@ packages: dependency: "direct overridden" description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -656,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1+beta.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" open_filex: dependency: "direct main" description: @@ -888,6 +896,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: @@ -904,6 +920,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" qr_code_scanner: dependency: "direct main" description: @@ -912,6 +936,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" quiver: dependency: transitive description: @@ -968,6 +1000,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" + screen_brightness: + dependency: "direct main" + description: + name: screen_brightness + sha256: "7d4ac84ae26b37c01d6f5db7123a72db7933e1f2a2a8c369a51e08f81b3178d8" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_android: + dependency: transitive + description: + name: screen_brightness_android + sha256: "8c69d3ac475e4d625e7fa682a3a51a69ff59abe5b4a9e57f6ec7d830a6c69bd6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_ios: + dependency: transitive + description: + name: screen_brightness_ios + sha256: f08f70ca1ac3e30719764b5cfb8b3fe1e28163065018a41b3e6f243ab146c2f1 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_macos: + dependency: transitive + description: + name: screen_brightness_macos + sha256: "70c2efa4534e22b927e82693488f127dd4a0f008469fccf4f0eefe9061bbdd6a" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_platform_interface: + dependency: transitive + description: + name: screen_brightness_platform_interface + sha256: "9f3ebf7f22d5487e7676fe9ddaf3fc55b6ff8057707cf6dc0121c7dfda346a16" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_windows: + dependency: transitive + description: + name: screen_brightness_windows + sha256: c8e12a91cf6dd912a48bd41fcf749282a51afa17f536c3460d8d05702fb89ffa + url: "https://pub.dev" + source: hosted + version: "1.0.1" secp256k1_ecdsa: dependency: transitive description: @@ -1048,20 +1128,20 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - shuftipro_sdk: + shuftipro_onsite_sdk: dependency: "direct main" description: - name: shuftipro_sdk - sha256: fd47550bbbfe50c99ef490fdb609b5a8d5e3e770d3fb8196c984a7f68d00772e + name: shuftipro_onsite_sdk + sha256: d1329e5fe5a15cbbe5d2e75af9ab0eab1dd00a41a017eaf74225cb37d39f9e52 url: "https://pub.dev" source: hosted - version: "1.3.9" + version: "1.0.5" signer: dependency: transitive description: path: "packages/signer" ref: main - resolved-ref: "27c3fda2b6fe8c67785cbe5ff08373878a2a8470" + resolved-ref: fb8afb190f8c4fcad43692f37ea0d72cc57bcefc url: "https://github.com/codescalers/tfgrid-sdk-dart.git" source: git version: "0.1.0" @@ -1078,6 +1158,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + sha256: "3b28b0c545fa67ed9e5997d9f9720d486f54c0c607e056a1094544e36934dff3" + url: "https://pub.dev" + source: hosted + version: "1.2.0+3" socket_io_client: dependency: "direct main" description: @@ -1153,9 +1241,11 @@ packages: stellar_client: dependency: "direct main" description: - path: "../../../codescalers/tfgrid-sdk-dart/packages/stellar_client" - relative: true - source: path + path: "packages/stellar_client" + ref: tfchain_graphql_hotfix_2 + resolved-ref: "563cc0297d79dc333157fe69a7540f0190ff3496" + url: "https://github.com/codescalers/tfgrid-sdk-dart" + source: git version: "0.1.0" stellar_flutter_sdk: dependency: transitive @@ -1404,6 +1494,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.0" + validators: + dependency: "direct main" + description: + name: validators + sha256: "884515951f831a9c669a41ed6c4d3c61c2a0e8ec6bca761a4480b28e99cecf5d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" vector_graphics: dependency: transitive description: @@ -1460,6 +1558,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "6869c8786d179f929144b4a1f86e09ac0eddfe475984951ea6c634774c16b522" + url: "https://pub.dev" + source: hosted + version: "4.8.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "0d21cfc3bfdd2e30ab2ebeced66512b91134b39e72e97b43db2d47dda1c4e53a" + url: "https://pub.dev" + source: hosted + version: "3.16.3" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" + url: "https://pub.dev" + source: hosted + version: "3.14.0" win32: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 94e28f69..ad6d23cb 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -19,19 +19,19 @@ dependencies: tfchain_client: git: url: https://github.com/codescalers/tfgrid-sdk-dart - ref: main + ref: tfchain_graphql_hotfix path: packages/tfchain_client stellar_client: git: url: https://github.com/codescalers/tfgrid-sdk-dart - ref: main + ref: tfchain_graphql_hotfix_2 path: packages/stellar_client gridproxy_client: git: url: https://github.com/codescalers/tfgrid-sdk-dart - ref: main + ref: tfchain_graphql_hotfix path: packages/gridproxy_client - shuftipro_sdk: ^1.2.5 + shuftipro_onsite_sdk: ^1.0.5 flutter_svg: ^2.0.6 bip39: ^1.0.6 socket_io_client: ^1.0.2 @@ -59,6 +59,12 @@ dependencies: pinenacl: ^0.5.1 pinput: 3.0.1 build_runner: ^2.4.9 + smooth_page_indicator: ^1.2.0+3 + webview_flutter: ^4.8.0 + provider: ^6.1.2 + qr_flutter: ^4.1.0 + screen_brightness: ^1.0.1 + validators: ^3.0.0 dev_dependencies: flutter_test: