From 12ad83f11f1c811219a2fd124412024733f1c780 Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 5 Jan 2022 16:52:28 +0100 Subject: [PATCH 01/75] Add null safety supported libs - WIP -> make code null safe --- .../org/jimber/threebotlogin/MainActivity.kt | 2 +- app/lib/apps/chatbot/chatbot.dart | 2 +- app/lib/apps/free_flow_pages/ffp_widget.dart | 2 +- app/lib/apps/wallet/wallet_widget.dart | 2 +- app/lib/helpers/flags.dart | 2 +- app/lib/helpers/kyc_helpers.dart | 2 +- app/lib/main.dart | 2 +- app/lib/models/login.dart | 2 +- app/lib/screens/authentication_screen.dart | 2 +- app/lib/screens/change_pin_screen.dart | 2 +- app/lib/screens/home_screen.dart | 2 +- .../screens/identity_verification_screen.dart | 2 +- app/lib/screens/init_screen.dart | 2 +- app/lib/screens/login_screen.dart | 2 +- app/lib/screens/main_screen.dart | 4 +- .../screens/mobile_registration_screen.dart | 2 +- app/lib/screens/planetary_network_screen.dart | 2 +- app/lib/screens/preference_screen.dart | 2 +- app/lib/screens/recover_screen.dart | 2 +- app/lib/screens/registered_screen.dart | 2 +- app/lib/screens/reservation_screen.dart | 2 +- app/lib/services/3bot_service.dart | 4 +- app/lib/services/crypto_service.dart | 91 ++++----- app/lib/services/migration_service.dart | 2 +- app/lib/services/open_kyc_service.dart | 2 +- app/lib/services/pkid_service.dart | 2 +- ...ce.dart => shared_preference_service.dart} | 174 +++++++++++------- app/lib/services/socket_service.dart | 2 +- app/lib/services/uni_link_service.dart | 2 +- app/lib/widgets/layout_drawer.dart | 2 +- app/lib/widgets/phone_widget.dart | 2 +- app/lib/widgets/preference_dialog.dart | 2 +- app/pubspec.yaml | 72 +++----- 33 files changed, 205 insertions(+), 196 deletions(-) rename app/lib/services/{user_service.dart => shared_preference_service.dart} (72%) 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..c64d56e8 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 @@ -1,4 +1,4 @@ -package org.jimber.threebotlogin +package org.jimber.threebotlogin.staging import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/app/lib/apps/chatbot/chatbot.dart b/app/lib/apps/chatbot/chatbot.dart index 3c46fe03..e5867b60 100644 --- a/app/lib/apps/chatbot/chatbot.dart +++ b/app/lib/apps/chatbot/chatbot.dart @@ -3,7 +3,7 @@ import 'package:threebotlogin/app.dart'; import 'package:threebotlogin/apps/chatbot/chatbot_widget.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/go_home_event.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; class Chatbot implements App { ChatbotWidget _widget; diff --git a/app/lib/apps/free_flow_pages/ffp_widget.dart b/app/lib/apps/free_flow_pages/ffp_widget.dart index 6a987d71..d3edaad7 100644 --- a/app/lib/apps/free_flow_pages/ffp_widget.dart +++ b/app/lib/apps/free_flow_pages/ffp_widget.dart @@ -11,7 +11,7 @@ // import 'package:threebotlogin/events/events.dart'; // import 'package:threebotlogin/events/go_home_event.dart'; // import 'package:threebotlogin/services/crypto_service.dart'; -// import 'package:threebotlogin/services/user_service.dart'; +// import 'package:threebotlogin/services/shared_preference_service.dart'; // class FfpWidget extends StatefulWidget { // @override diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 6bca93b9..4c50d448 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -13,7 +13,7 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/screens/scan_screen.dart'; import 'package:threebotlogin/services/3bot_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; bool created = false; diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index 8372147c..ee077468 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -1,6 +1,6 @@ import 'package:flagsmith/flagsmith.dart'; import 'package:threebotlogin/app_config.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'globals.dart'; diff --git a/app/lib/helpers/kyc_helpers.dart b/app/lib/helpers/kyc_helpers.dart index e7d4ab11..551f6854 100644 --- a/app/lib/helpers/kyc_helpers.dart +++ b/app/lib/helpers/kyc_helpers.dart @@ -5,7 +5,7 @@ import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'globals.dart'; diff --git a/app/lib/main.dart b/app/lib/main.dart index 8cb4877b..5123b3e0 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -5,7 +5,7 @@ import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/screens/main_screen.dart'; import 'package:threebotlogin/services/logging_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; import 'helpers/flags.dart'; diff --git a/app/lib/models/login.dart b/app/lib/models/login.dart index 0ce374c4..e5e9109f 100644 --- a/app/lib/models/login.dart +++ b/app/lib/models/login.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:threebotlogin/models/scope.dart'; import 'package:threebotlogin/services/crypto_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; class Login { String doubleName; diff --git a/app/lib/screens/authentication_screen.dart b/app/lib/screens/authentication_screen.dart index 42d8fcad..d31f6c55 100644 --- a/app/lib/screens/authentication_screen.dart +++ b/app/lib/screens/authentication_screen.dart @@ -8,7 +8,7 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/services/fingerprint_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; class AuthenticationScreen extends StatefulWidget { diff --git a/app/lib/screens/change_pin_screen.dart b/app/lib/screens/change_pin_screen.dart index e16f263a..a4c4a5ad 100644 --- a/app/lib/screens/change_pin_screen.dart +++ b/app/lib/screens/change_pin_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/pin_field.dart'; class ChangePinScreen extends StatefulWidget { diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 100ccac7..44350d93 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -24,7 +24,7 @@ import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/services/socket_service.dart'; import 'package:threebotlogin/services/uni_link_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:uni_links/uni_links.dart'; diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index 0bd13d18..a4bb7833 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -20,7 +20,7 @@ import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/socket_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; diff --git a/app/lib/screens/init_screen.dart b/app/lib/screens/init_screen.dart index 65f83b2e..fd21deaa 100644 --- a/app/lib/screens/init_screen.dart +++ b/app/lib/screens/init_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:threebotlogin/app_config.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; class InitScreen extends StatefulWidget { InitScreen(); diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 9a8fbc16..e4011c2d 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -12,7 +12,7 @@ import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/image_button.dart'; import 'package:threebotlogin/widgets/preference_dialog.dart'; diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index a5695ef0..f600fb3a 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -13,7 +13,7 @@ import 'package:threebotlogin/screens/init_screen.dart'; import 'package:threebotlogin/screens/unregistered_screen.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/socket_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/error_widget.dart'; import 'package:uni_links/uni_links.dart'; @@ -177,6 +177,8 @@ class _AppState extends State { } checkIfAppIsUnderMaintenance() async { + print('HALLO'); + print(await isAppUnderMaintenance()); try { if (await isAppUnderMaintenance()) { CustomDialog dialog = CustomDialog( diff --git a/app/lib/screens/mobile_registration_screen.dart b/app/lib/screens/mobile_registration_screen.dart index 84a272c7..0784fcdf 100644 --- a/app/lib/screens/mobile_registration_screen.dart +++ b/app/lib/screens/mobile_registration_screen.dart @@ -11,7 +11,7 @@ import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/reusable_text_field_step.dart'; import 'package:threebotlogin/widgets/reusable_text_step.dart'; diff --git a/app/lib/screens/planetary_network_screen.dart b/app/lib/screens/planetary_network_screen.dart index 1444396b..7fe22369 100644 --- a/app/lib/screens/planetary_network_screen.dart +++ b/app/lib/screens/planetary_network_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_svg/svg.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/vpn_state.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:yggdrasil_plugin/yggdrasil_plugin.dart'; diff --git a/app/lib/screens/preference_screen.dart b/app/lib/screens/preference_screen.dart index 1550d96b..3bf587e6 100644 --- a/app/lib/screens/preference_screen.dart +++ b/app/lib/screens/preference_screen.dart @@ -20,7 +20,7 @@ import 'package:threebotlogin/screens/main_screen.dart'; import 'package:threebotlogin/services/fingerprint_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/socket_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; diff --git a/app/lib/screens/recover_screen.dart b/app/lib/screens/recover_screen.dart index 2be26ead..41ce93f9 100644 --- a/app/lib/screens/recover_screen.dart +++ b/app/lib/screens/recover_screen.dart @@ -14,7 +14,7 @@ import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; class RecoverScreen extends StatefulWidget { final Widget recoverScreen; diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index e12752f9..bfb0a45a 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/vpn_state.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; // import 'package:yggdrasil_plugin/yggdrasil_plugin.dart'; import 'package:flutter/services.dart'; diff --git a/app/lib/screens/reservation_screen.dart b/app/lib/screens/reservation_screen.dart index 86b3096f..7bd28e07 100644 --- a/app/lib/screens/reservation_screen.dart +++ b/app/lib/screens/reservation_screen.dart @@ -13,7 +13,7 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/models/paymentRequest.dart'; import 'package:threebotlogin/services/3bot_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 3ef46475..76047eae 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -6,7 +6,7 @@ import 'package:http/http.dart'; import 'package:package_info/package_info.dart'; import 'package:threebotlogin/app_config.dart'; import 'package:threebotlogin/services/crypto_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; @@ -80,7 +80,7 @@ Future sendProductReservation(Map data) async { body: body, headers: {'Content-type': 'application/json'}); } -Future isAppUpToDate() async { +Future isAppUpToDate() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); int currentBuildNumber = int.parse(packageInfo.buildNumber); diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index 25a3b796..f6781227 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -5,17 +5,10 @@ import 'dart:typed_data'; import 'package:bip39/bip39.dart' as bip39; import 'package:convert/convert.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:password_hash/password_hash.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:threebotlogin/main.dart'; import 'package:threebotlogin/services/3bot_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; - -Future> generateKeyPair() async { - Map keys = await Sodium.cryptoBoxKeypair(); - - return {'privateKey': base64.encode(keys['sk']), 'publicKey': base64.encode(keys['pk'])}; -} +import 'package:threebotlogin/services/shared_preference_service.dart'; Uint8List _toHex(String input) { double length = input.length / 2; @@ -29,72 +22,47 @@ Uint8List _toHex(String input) { return bytes; } -Future> generateKeysFromSeedPhrase(seedPhrase) async { - String entropy = bip39.mnemonicToEntropy(seedPhrase); - Map key = await Sodium.cryptoSignSeedKeypair(_toHex(entropy)); - - return {'publicKey': base64.encode(key['pk']).toString(), 'privateKey': base64.encode(key['sk']).toString()}; +Future generateSeedPhrase() async { + return bip39.generateMnemonic(strength: 256); } - -Future> generateKeyPairFromSeedPhrase(seedPhrase) async { +Future generateKeyPairFromSeedPhrase(seedPhrase) async { String entropy = bip39.mnemonicToEntropy(seedPhrase); - Map key = await Sodium.cryptoSignSeedKeypair(_toHex(entropy)); - - return {'publicKey': key['pk'], 'privateKey': key['sk']}; + return Sodium.cryptoSignSeedKeypair(_toHex(entropy)); } - Future generatePublicKeyFromEntropy(encodedEntropy) async { Uint8List entropy = base64.decode(encodedEntropy); - Map key = await Sodium.cryptoSignSeedKeypair(entropy); - - return base64.encode(key['pk']).toString(); + KeyPair keyPair = Sodium.cryptoSignSeedKeypair(entropy); + return base64.encode(keyPair.pk); } -Future signData(String data, String sk) async { - Uint8List private = base64.decode(sk); - Uint8List signed = await Sodium.cryptoSign(Uint8List.fromList(data.codeUnits), private); - +Future signData(String data, Uint8List sk) async { + Uint8List signed = Sodium.cryptoSign(Uint8List.fromList(data.codeUnits), sk); return base64.encode(signed); } -Future> encrypt(String data, String publicKey, String sk) async { - Uint8List nonce = await CryptoBox.generateNonce(); - Uint8List private = await Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(sk)); - Uint8List public = base64.decode(publicKey); +Future> encrypt(String data, Uint8List pk, Uint8List sk) async { + Uint8List nonce = CryptoBox.randomNonce(); + Uint8List private = Sodium.cryptoSignEd25519SkToCurve25519(sk); Uint8List message = Uint8List.fromList(data.codeUnits); - Uint8List encryptedData = await Sodium.cryptoBoxEasy(message, nonce, public, private); + Uint8List encryptedData = Sodium.cryptoBoxEasy(message, nonce, pk, private); - return {'nonce': base64.encode(nonce), 'ciphertext': base64.encode(encryptedData)}; + return {'nonce': base64.encode(nonce), 'cipher': base64.encode(encryptedData)}; } -Future decrypt(String encodedCipherText, String encodedPublicKey, String encodedSecretKey) async { +Future decrypt(String encodedCipherText, Uint8List pk, Uint8List sk) async { Uint8List cipherText = base64.decode(encodedCipherText); - Uint8List publicKey = await Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(encodedPublicKey)); - Uint8List secretKey = await Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(encodedSecretKey)); + Uint8List publicKey = Sodium.cryptoSignEd25519PkToCurve25519(pk); + Uint8List secretKey = Sodium.cryptoSignEd25519SkToCurve25519(sk); - return await Sodium.cryptoBoxSealOpen(cipherText, publicKey, secretKey); -} - -Future generateSeedPhrase() async { - return bip39.generateMnemonic(strength: 256); + Uint8List decryptedData = Sodium.cryptoBoxSealOpen(cipherText, publicKey, secretKey); + return new String.fromCharCodes(decryptedData); } -void testMe() { - String base64EncodedEntropy = "tllF+NHT24MLhLVfyCxMbtkoI/wdt+fkQHjELBW78BQ="; - - Uint8List tmp = base64.decode(base64EncodedEntropy); - - Uint8List bytes = new Uint8List.view(tmp.buffer); - String asHex = hex.encode(bytes); - - String words = bip39.entropyToMnemonic(asHex); - logger.log(words); -} Future generateDerivedSeed(String appId) async { - String privateKey = await getPrivateKey(); + Uint8List privateKey = await getPrivateKey(); PBKDF2 generator = new PBKDF2(); List hashKey = generator.generateKey(privateKey, appId, 1000, 32); @@ -135,5 +103,22 @@ Future> generateDerivedKeypair(String appId, String doubleNa prefs.setString("${appId.toString()}.dsk", derivedPrivateKey); } - return {'appId': appId, 'derivedPublicKey': derivedPublicKey, 'derivedPrivateKey': derivedPrivateKey}; + return { + 'appId': appId, + 'derivedPublicKey': derivedPublicKey, + 'derivedPrivateKey': derivedPrivateKey + }; } + + +void testMe() { + String base64EncodedEntropy = "tllF+NHT24MLhLVfyCxMbtkoI/wdt+fkQHjELBW78BQ="; + + Uint8List tmp = base64.decode(base64EncodedEntropy); + + Uint8List bytes = new Uint8List.view(tmp.buffer); + String asHex = hex.encode(bytes); + + String words = bip39.entropyToMnemonic(asHex); + logger.log(words); +} \ No newline at end of file diff --git a/app/lib/services/migration_service.dart b/app/lib/services/migration_service.dart index 4d5cc348..80d40172 100644 --- a/app/lib/services/migration_service.dart +++ b/app/lib/services/migration_service.dart @@ -1,6 +1,6 @@ import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:threebotlogin/services/pkid_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'crypto_service.dart'; diff --git a/app/lib/services/open_kyc_service.dart b/app/lib/services/open_kyc_service.dart index 52e17940..1c521483 100644 --- a/app/lib/services/open_kyc_service.dart +++ b/app/lib/services/open_kyc_service.dart @@ -5,7 +5,7 @@ import 'package:http/http.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:threebotlogin/app_config.dart'; import 'package:threebotlogin/services/crypto_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; String openKycApiUrl = AppConfig().openKycApiUrl(); String threeBotApiUrl = AppConfig().threeBotApiUrl(); diff --git a/app/lib/services/pkid_service.dart b/app/lib/services/pkid_service.dart index b3a1d241..fb2e214b 100644 --- a/app/lib/services/pkid_service.dart +++ b/app/lib/services/pkid_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter_pkid/flutter_pkid.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'crypto_service.dart'; diff --git a/app/lib/services/user_service.dart b/app/lib/services/shared_preference_service.dart similarity index 72% rename from app/lib/services/user_service.dart rename to app/lib/services/shared_preference_service.dart index 4e0c808d..c2417a1a 100644 --- a/app/lib/services/user_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -1,13 +1,11 @@ -import 'dart:async'; import 'dart:convert'; import 'dart:core'; +import 'dart:typed_data'; import 'package:convert/convert.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/services/3bot_service.dart'; @@ -15,54 +13,63 @@ import 'package:threebotlogin/services/crypto_service.dart'; import '../app_config.dart'; -String pkidUrl = AppConfig().pKidUrl(); +String pKidUrl = AppConfig().pKidUrl(); -Future savePin(pin) async { +Future savePin(String pin) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('pin'); prefs.setString('pin', pin); } -Future getPin() async { +Future getPin() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('pin'); } -Future savePublicKey(key) async { +Future savePublicKey(Uint8List publicKey) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('publickey'); - prefs.setString('publickey', key); + prefs.remove('publicKey'); + + String encodedPublicKey = base64.encode(publicKey); + prefs.setString('publicKey', encodedPublicKey); } -Future getPublicKey() async { +Future getPublicKey() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - if (!(await getIsPublicKeyFixed())) { - var userInfoResponse = await getUserInfo(await getDoubleName()); + bool? isPublicKeyFixed = await getIsPublicKeyFixed(); - if (userInfoResponse.statusCode != 200) { - throw new Exception('User not found'); - } + if (isPublicKeyFixed == true) { + String? encodedPublicKey = prefs.getString('publicKey'); + return base64.decode(encodedPublicKey!); + } + + var userInfoResponse = await getUserInfo(await getDoubleName()); + if (userInfoResponse.statusCode != 200) { + throw new Exception('User not found'); + } - var userInfo = json.decode(userInfoResponse.body); - var done = await prefs.setString("publickey", userInfo['publicKey']); + var userInfo = json.decode(userInfoResponse.body); + var done = await prefs.setString("publicKey", userInfo['publicKey']); - if (done && prefs.getString('publickey') == userInfo['publicKey']) { - setPublicKeyFixed(); - } + if (done && prefs.getString('publicKey') == userInfo['publicKey']) { + setPublicKeyFixed(); } - return prefs.getString('publickey'); + String? encodedPublicKey = prefs.getString('publicKey'); + return base64.decode(encodedPublicKey!); } Future> getEdCurveKeys() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String pkEd = prefs.getString('publickey'); - final String skEd = prefs.getString('privatekey'); + final String? pkEd = prefs.getString('publicKey'); + final String? skEd = prefs.getString('privateKey'); - final String pkCurve = base64.encode(await Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd))); - final String skCurve = base64.encode(await Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd))); + final String pkCurve = base64 + .encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); + final String skCurve = base64 + .encode(Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd!))); return { 'signingPublicKey': hex.encode(base64.decode(pkEd)), @@ -74,43 +81,46 @@ Future> getEdCurveKeys() async { Future setPublicKeyFixed() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool('ispublickeyfixed', true); + prefs.setBool('isPublicKeyFixed', true); } -Future getIsPublicKeyFixed() async { - try { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - - if (prefs.getBool('ispublickeyfixed') == null) { - return false; - } +Future getIsPublicKeyFixed() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('ispublickeyfixed'); - } catch (_) { + if (prefs.getBool('isPublicKeyFixed') == null) { return false; } + + return prefs.getBool('isPublicKeyFixed'); } -Future savePrivateKey(key) async { +Future savePrivateKey(Uint8List privateKey) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('privatekey'); - prefs.setString('privatekey', key); + prefs.remove('privateKey'); + + String encodedPrivateKey = base64.encode(privateKey); + prefs.setString('privateKey', encodedPrivateKey); } -Future getPrivateKey() async { +Future getPrivateKey() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getString('privatekey'); + + String? privateKey = prefs.getString('privateKey'); + Uint8List decodedPrivateKey = base64.decode(privateKey!); + + return decodedPrivateKey; } -Future savePhrase(phrase) async { +Future savePhrase(String phrase) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('phrase'); - prefs.setString('phrase', phrase); + prefs.remove('seedPhrase'); + + prefs.setString('seedPhrase', phrase); } -Future getPhrase() async { +Future getPhrase() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getString('phrase'); + return prefs.getString('seedPhrase'); } Future saveLocationId(String locationId) async { @@ -130,22 +140,22 @@ Future> getLocationIdList() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); try { - String locationIdListAsJson = prefs.getString('locationIdList'); - List locationIdList = jsonDecode(locationIdListAsJson); + String? locationIdListAsJson = prefs.getString('locationIdList'); + List locationIdList = jsonDecode(locationIdListAsJson!); return locationIdList; } catch (_) { - return new List(); + return []; } } -Future saveDoubleName(doubleName) async { +Future saveDoubleName(String doubleName) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('doubleName'); prefs.setString('doubleName', doubleName); } -Future getDoubleName() async { +Future getDoubleName() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('doubleName'); } @@ -157,7 +167,7 @@ Future removeEmail() async { prefs.remove('emailVerified'); } -Future saveEmail(String email, String signedEmailIdentifier) async { +Future saveEmail(String email, String signedEmailIdentifier) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('email'); @@ -169,9 +179,12 @@ Future saveEmail(String email, String signedEmailIdentifier) async { if (signedEmailIdentifier != null) { prefs.setString('signedEmailIdentifier', signedEmailIdentifier); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); + Map keyPair = + await generateKeyPairFromSeedPhrase(await getPhrase()); + var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('email', json.encode({'email': email, 'sei': signedEmailIdentifier}), keyPair); + client.setPKidDoc('email', + json.encode({'email': email, 'sei': signedEmailIdentifier}), keyPair); } Globals().emailVerified.value = (signedEmailIdentifier != null); @@ -181,15 +194,20 @@ Future> getIdentity() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return { 'identityName': prefs.getString('identityName'), - 'signedIdentityNameIdentifier': prefs.getString('signedIdentityNameIdentifier'), + 'signedIdentityNameIdentifier': + prefs.getString('signedIdentityNameIdentifier'), 'identityCountry': prefs.getString('identityCountry'), - 'signedIdentityCountryIdentifier': prefs.getString('signedIdentityCountryIdentifier'), + 'signedIdentityCountryIdentifier': + prefs.getString('signedIdentityCountryIdentifier'), 'identityDOB': prefs.getString('identityDOB'), - 'signedIdentityDOBIdentifier': prefs.getString('signedIdentityDOBIdentifier'), + 'signedIdentityDOBIdentifier': + prefs.getString('signedIdentityDOBIdentifier'), 'identityDocumentMeta': prefs.getString('identityDocumentMeta'), - 'signedIdentityDocumentMetaIdentifier': prefs.getString('signedIdentityDocumentMetaIdentifier'), + 'signedIdentityDocumentMetaIdentifier': + prefs.getString('signedIdentityDocumentMetaIdentifier'), 'identityGender': prefs.getString('identityGender'), - 'signedIdentityGenderIdentifier': prefs.getString('signedIdentityGenderIdentifier'), + 'signedIdentityGenderIdentifier': + prefs.getString('signedIdentityGenderIdentifier'), }; } @@ -218,15 +236,21 @@ Future saveIdentity( prefs.setString('identityGender', identityGender); prefs.setString('signedIdentityNameIdentifier', signedIdentityNameIdentifier); - prefs.setString('signedIdentityCountryIdentifier', - signedIdentityCountryIdentifier == 'null' ? 'test' : signedIdentityCountryIdentifier); + prefs.setString( + 'signedIdentityCountryIdentifier', + signedIdentityCountryIdentifier == 'null' + ? 'test' + : signedIdentityCountryIdentifier); prefs.setString('signedIdentityDOBIdentifier', signedIdentityDOBIdentifier); - prefs.setString('signedIdentityDocumentMetaIdentifier', signedIdentityDocumentMetaIdentifier); - prefs.setString('signedIdentityGenderIdentifier', signedIdentityGenderIdentifier); + prefs.setString('signedIdentityDocumentMetaIdentifier', + signedIdentityDocumentMetaIdentifier); + prefs.setString( + 'signedIdentityGenderIdentifier', signedIdentityGenderIdentifier); prefs.remove('identityVerified'); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); + Map keyPair = + await generateKeyPairFromSeedPhrase(await getPhrase()); var client = FlutterPkid(pkidUrl, keyPair); client.setPKidDoc( 'identity', @@ -238,7 +262,8 @@ Future saveIdentity( 'identityDOB': identityDOB, 'signedIdentityDOBIdentifier': signedIdentityDOBIdentifier, 'identityDocumentMeta': jsonEncode(identityDocumentMeta), - 'signedIdentityDocumentMetaIdentifier': signedIdentityDocumentMetaIdentifier, + 'signedIdentityDocumentMetaIdentifier': + signedIdentityDocumentMetaIdentifier, 'identityGender': identityGender, 'signedIdentityGenderIdentifier': signedIdentityGenderIdentifier }), @@ -304,9 +329,11 @@ Future savePhone(String phone, String signedPhoneIdentifier) async { if (signedPhoneIdentifier != null) { prefs.setString('signedPhoneIdentifier', signedPhoneIdentifier); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); + Map keyPair = + await generateKeyPairFromSeedPhrase(await getPhrase()); var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('phone', json.encode({'phone': phone, 'spi': signedPhoneIdentifier}), keyPair); + client.setPKidDoc('phone', + json.encode({'phone': phone, 'spi': signedPhoneIdentifier}), keyPair); } Globals().phoneVerified.value = (signedPhoneIdentifier != null); @@ -314,12 +341,18 @@ Future savePhone(String phone, String signedPhoneIdentifier) async { Future> getEmail() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return {'email': prefs.getString('email'), 'sei': prefs.getString('signedEmailIdentifier')}; + return { + 'email': prefs.getString('email'), + 'sei': prefs.getString('signedEmailIdentifier') + }; } Future> getPhone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return {'phone': prefs.getString('phone'), 'spi': prefs.getString('signedPhoneIdentifier')}; + return { + 'phone': prefs.getString('phone'), + 'spi': prefs.getString('signedPhoneIdentifier') + }; } Future> getKeys(String appId, String doubleName) async { @@ -359,7 +392,8 @@ Future getScopePermissions() async { return prefs.getString('scopePermissions'); } -Future savePreviousScopePermissions(String appId, String scopePermissions) async { +Future savePreviousScopePermissions( + String appId, String scopePermissions) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove('$appId-scopePreviousPermissions'); await prefs.setString('$appId-scopePreviousPermissions', scopePermissions); @@ -426,7 +460,7 @@ Future> getWallets() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); var string = prefs.getString('walletData'); - if(string == null) { + if (string == null) { return []; } diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index 045b1ed1..d673e9b2 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -16,7 +16,7 @@ import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/screens/login_screen.dart'; import 'package:threebotlogin/screens/warning_screen.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; class BackendConnection { diff --git a/app/lib/services/uni_link_service.dart b/app/lib/services/uni_link_service.dart index b2a4ee6e..86061db7 100644 --- a/app/lib/services/uni_link_service.dart +++ b/app/lib/services/uni_link_service.dart @@ -9,7 +9,7 @@ import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/models/scope.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/screens/login_screen.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; class UniLinkService { static void handleUniLink(UniLinkEvent e) async { diff --git a/app/lib/widgets/layout_drawer.dart b/app/lib/widgets/layout_drawer.dart index ed4e30ad..ada0b28f 100644 --- a/app/lib/widgets/layout_drawer.dart +++ b/app/lib/widgets/layout_drawer.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/crypto_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:convert/convert.dart'; import '../app_config.dart'; diff --git a/app/lib/widgets/phone_widget.dart b/app/lib/widgets/phone_widget.dart index 04c92361..3c0d8600 100644 --- a/app/lib/widgets/phone_widget.dart +++ b/app/lib/widgets/phone_widget.dart @@ -9,7 +9,7 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/phone_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'custom_dialog.dart'; diff --git a/app/lib/widgets/preference_dialog.dart b/app/lib/widgets/preference_dialog.dart index 8f16343b..08badd13 100644 --- a/app/lib/widgets/preference_dialog.dart +++ b/app/lib/widgets/preference_dialog.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/apps/wallet/wallet_config.dart'; import 'package:threebotlogin/models/scope.dart'; import 'package:threebotlogin/models/wallet_data.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; class PreferenceDialog extends StatefulWidget { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c7b73727..510ccae0 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -6,66 +6,54 @@ publish_to: "none" version: 3.4.3+140 environment: - sdk: ">=2.10.0 <3.0.0" + sdk: ">=2.12.0<3.0.0" dependencies: - gson: ^0.1.4 flutter: sdk: flutter - qr_code_scanner: - git: - url: https://github.com/jimbertools/qr_code_scanner - flutter_sodium: - git: - url: https://github.com/jimbertools/flutter_sodium - uni_links: - git: - url: https://github.com/jimbertools/uni_links - flutter_inappwebview: ^5.2.0 - keyboard_visibility: - git: - url: https://github.com/jimbertools/flutter_keyboard_visibility - redirection: + country_picker: git: - url: https://github.com/jimbertools/redirection + url: git@github.com:jimbertools/flutter-country-picker.git + ref: main - shuftipro_flutter_sdk: + yggdrasil_plugin: git: - url: git@github.com:jimbertools/shufti-jimber.git - ref: main_integration-dependencies + url: https://github.com/threefoldtech/yggdrasil_flutter + ref: main_null-safety flutter_pkid: git: url: git@github.com:threefoldtech/flutter-pkid-client.git - ref: main_integration-dependencies + ref: main_null-safety - country_picker: + shuftipro_flutter_sdk: git: - url: git@github.com:jimbertools/flutter-country-picker.git - ref: main + url: git@github.com:jimbertools/shufti-jimber.git + ref: main_null-safety - yggdrasil_plugin: + redirection: git: - url: https://github.com/threefoldtech/yggdrasil_flutter + url: https://github.com/jimbertools/redirection + ref: main_null-safety - flutter_svg: ^0.19.3 - crypto: ^2.1.5 - bip39: ^1.0.3 - uuid: ^2.2.0 - socket_io_client: ^1.0.1 - local_auth: ^1.1.6 - url_launcher: ^6.0.2 + flutter_svg: ^1.0.0 + bip39: ^1.0.6 + socket_io_client: ^1.0.2 + local_auth: ^1.1.7 + url_launcher: ^6.0.10 intl: ^0.17.0 - password_hash: ^2.0.0 - shared_preferences: ^0.5.12+4 - http: ^0.12.2 - package_info: ^0.4.1 - cupertino_icons: ^1.0.2 - google_fonts: ^1.1.1 - international_phone_input: ^1.0.4 - intl_phone_field: ^1.4.2 - flagsmith: ^1.0.1 + shared_preferences: ^2.0.7 + http: ^0.13.3 + package_info: ^2.0.2 + cupertino_icons: ^1.0.4 + google_fonts: ^2.1.1 + intl_phone_field: ^2.1.0 + flagsmith: ^2.0.0-nullsafety.0 + cryptography: ^2.0.2 + flutter_inappwebview: ^5.3.2 + flutter_sodium: ^0.2.0 + uni_links: ^0.5.1 dev_dependencies: flutter_test: From 0dd8255271258e83a3b83597356a6c8f87a6b277 Mon Sep 17 00:00:00 2001 From: Lennert Date: Thu, 6 Jan 2022 00:22:31 +0100 Subject: [PATCH 02/75] Update crypto_service to null safety --- app/lib/main.dart | 2 - app/lib/screens/login_screen.dart | 13 +-- app/lib/services/crypto_service.dart | 85 +++++-------------- app/lib/services/logging_service.dart | 33 ------- .../services/shared_preference_service.dart | 5 +- app/pubspec.yaml | 2 +- 6 files changed, 29 insertions(+), 111 deletions(-) delete mode 100644 app/lib/services/logging_service.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index 5123b3e0..c935ed44 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -3,7 +3,6 @@ import 'package:flutter/services.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/screens/main_screen.dart'; -import 'package:threebotlogin/services/logging_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -11,7 +10,6 @@ import 'package:google_fonts/google_fonts.dart'; import 'helpers/flags.dart'; import 'helpers/kyc_helpers.dart'; -LoggingService logger; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index e4011c2d..972f892c 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:threebotlogin/apps/wallet/wallet_config.dart'; @@ -16,6 +17,7 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/image_button.dart'; import 'package:threebotlogin/widgets/preference_dialog.dart'; +import 'package:flutter_sodium/flutter_sodium.dart'; class LoginScreen extends StatefulWidget { final Login loginData; @@ -353,7 +355,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId); - var derivedSeed = (await getDerivedSeed(widget.loginData.appId)); + Uint8List derivedSeed = (await getDerivedSeed(widget.loginData.appId)); //TODO: make separate function if (scopePermissions != null) { @@ -432,13 +434,14 @@ class _LoginScreenState extends State with BlockAndRunMixin { if (selectedImageId == correctImage || isMobileCheck) { // Only update data if the correct image was chosen: - print("derivedSeed: " + derivedSeed); + print("derivedSeed: " + base64.encode(derivedSeed)); var name = await getDoubleName(); - var digitalTwinDerivedPublicKey = await generatePublicKeyFromEntropy(derivedSeed); + KeyPair dtKeyPair = await generateKeyPairFromEntropy(derivedSeed); + String dtEncodedPublicKey = base64.encode(dtKeyPair.pk) print("name: " + name); - print("publicKey: " + digitalTwinDerivedPublicKey); - addDigitalTwinDerivedPublicKeyToBackend(name, digitalTwinDerivedPublicKey, widget.loginData.appId); + print("publicKey: " + dtEncodedPublicKey); + addDigitalTwinDerivedPublicKeyToBackend(name, dtEncodedPublicKey, widget.loginData.appId); if (Navigator.canPop(context)) { Navigator.pop(context, true); diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index f6781227..cfe3ab2a 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -2,14 +2,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; import 'package:bip39/bip39.dart' as bip39; -import 'package:convert/convert.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:threebotlogin/main.dart'; -import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:pbkdf2ns/pbkdf2ns.dart'; + +// Helper method to convert a String input to hex used for entropy Uint8List _toHex(String input) { double length = input.length / 2; Uint8List bytes = new Uint8List(length.ceil()); @@ -22,26 +22,29 @@ Uint8List _toHex(String input) { return bytes; } +// Generate a random 24 worded seed Future generateSeedPhrase() async { return bip39.generateMnemonic(strength: 256); } -Future generateKeyPairFromSeedPhrase(seedPhrase) async { +// Generate a signing keypair from a given seed +Future generateKeyPairFromSeedPhrase(String seedPhrase) async { String entropy = bip39.mnemonicToEntropy(seedPhrase); return Sodium.cryptoSignSeedKeypair(_toHex(entropy)); } -Future generatePublicKeyFromEntropy(encodedEntropy) async { - Uint8List entropy = base64.decode(encodedEntropy); - KeyPair keyPair = Sodium.cryptoSignSeedKeypair(entropy); - return base64.encode(keyPair.pk); +// Generate a signing keypair from a given entropy +Future generateKeyPairFromEntropy(Uint8List entropy) async { + return Sodium.cryptoSignSeedKeypair(entropy); } +// Sign given data with the secret signing key Future signData(String data, Uint8List sk) async { Uint8List signed = Sodium.cryptoSign(Uint8List.fromList(data.codeUnits), sk); return base64.encode(signed); } +// Encrypt given data encrypted with a keypair Future> encrypt(String data, Uint8List pk, Uint8List sk) async { Uint8List nonce = CryptoBox.randomNonce(); Uint8List private = Sodium.cryptoSignEd25519SkToCurve25519(sk); @@ -51,6 +54,7 @@ Future> encrypt(String data, Uint8List pk, Uint8List sk) asy return {'nonce': base64.encode(nonce), 'cipher': base64.encode(encryptedData)}; } +// Decrypt given ciphertext with a keypair Future decrypt(String encodedCipherText, Uint8List pk, Uint8List sk) async { Uint8List cipherText = base64.decode(encodedCipherText); Uint8List publicKey = Sodium.cryptoSignEd25519PkToCurve25519(pk); @@ -61,64 +65,13 @@ Future decrypt(String encodedCipherText, Uint8List pk, Uint8List sk) asy } -Future generateDerivedSeed(String appId) async { +// Generate a new seed combined with a random salt => appId +Future generateDerivedSeed(String appId) async { Uint8List privateKey = await getPrivateKey(); + String encodedPrivateKey = base64.encode(privateKey); - PBKDF2 generator = new PBKDF2(); - List hashKey = generator.generateKey(privateKey, appId, 1000, 32); - - Uint8List derivedSeed = new Uint8List.fromList(hashKey); - - return base64.encode(derivedSeed); -} - -Future> generateDerivedKeypair(String appId, String doubleName) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - - String derivedPublicKey = prefs.getString("${appId.toString()}.dpk"); - String derivedPrivateKey = prefs.getString("${appId.toString()}.dsk"); - - String privateKey = await getPrivateKey(); - - PBKDF2 generator = new PBKDF2(); - List hashKey = generator.generateKey(privateKey, appId, 1000, 32); - - Map key = await Sodium.cryptoBoxSeedKeypair(new Uint8List.fromList(hashKey)); - - if (derivedPublicKey == null || derivedPublicKey == "") { - derivedPublicKey = base64.encode(key['pk']); - prefs.setString("${appId.toString()}.dpk", derivedPublicKey); - - Map data = { - 'doubleName': doubleName, - 'signedDerivedPublicKey': await signData(derivedPublicKey, privateKey), - 'signedAppId': await signData(appId, privateKey) - }; - - sendPublicKey(data); - } - - if (derivedPrivateKey == null || derivedPrivateKey == "") { - derivedPrivateKey = base64.encode(key['sk']); - prefs.setString("${appId.toString()}.dsk", derivedPrivateKey); - } - - return { - 'appId': appId, - 'derivedPublicKey': derivedPublicKey, - 'derivedPrivateKey': derivedPrivateKey - }; -} - - -void testMe() { - String base64EncodedEntropy = "tllF+NHT24MLhLVfyCxMbtkoI/wdt+fkQHjELBW78BQ="; - - Uint8List tmp = base64.decode(base64EncodedEntropy); - - Uint8List bytes = new Uint8List.view(tmp.buffer); - String asHex = hex.encode(bytes); + PBKDF2NS generator = PBKDF2NS(hash: sha256); + List hashKey = generator.generateKey(encodedPrivateKey, appId, 1000, 32); - String words = bip39.entropyToMnemonic(asHex); - logger.log(words); + return new Uint8List.fromList(hashKey); } \ No newline at end of file diff --git a/app/lib/services/logging_service.dart b/app/lib/services/logging_service.dart deleted file mode 100644 index 8584405c..00000000 --- a/app/lib/services/logging_service.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:intl/intl.dart'; - -class LoggingService { - bool debug = true; - bool writeToFile = false; - - void log(Object s, [Object o, Object o2]) { - if (debug) { - print("[" + - getDateTime() + - "]: " + - s.toString() + - ((o != null) ? ", " + o.toString() : "") + - ((o2 != null) ? ", " + o2.toString() : "")); - } - - if (writeToFile) { - // Write logging to file. - } - } - - String getDateTime() { - DateTime now = new DateTime.now(); - DateFormat formatter = new DateFormat('yyyy-MM-dd hh:mm:ss'); - String formatted = formatter.format(now); - - return formatted; - } - - void shareLogToTelegram() { - // Functionality to share your debug information with us. - } -} diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index c2417a1a..2c34a866 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -355,11 +355,8 @@ Future> getPhone() async { }; } -Future> getKeys(String appId, String doubleName) async { - return await generateDerivedKeypair(appId, doubleName); -} -Future getDerivedSeed(String appId) async { +Future getDerivedSeed(String appId) async { return await generateDerivedSeed(appId); } diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 510ccae0..975a37f5 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -42,7 +42,6 @@ dependencies: socket_io_client: ^1.0.2 local_auth: ^1.1.7 url_launcher: ^6.0.10 - intl: ^0.17.0 shared_preferences: ^2.0.7 http: ^0.13.3 package_info: ^2.0.2 @@ -54,6 +53,7 @@ dependencies: flutter_inappwebview: ^5.3.2 flutter_sodium: ^0.2.0 uni_links: ^0.5.1 + pbkdf2ns: ^0.0.2 dev_dependencies: flutter_test: From b1be5c7fea2dbcf2bf3b2385d8e4737210968183 Mon Sep 17 00:00:00 2001 From: Lennert Date: Thu, 6 Jan 2022 14:32:05 +0100 Subject: [PATCH 03/75] Update shared preference methods --- app/lib/services/pkid_service.dart | 14 + .../services/shared_preference_service.dart | 449 +++++++++--------- 2 files changed, 243 insertions(+), 220 deletions(-) diff --git a/app/lib/services/pkid_service.dart b/app/lib/services/pkid_service.dart index fb2e214b..0f68c3df 100644 --- a/app/lib/services/pkid_service.dart +++ b/app/lib/services/pkid_service.dart @@ -1,12 +1,26 @@ import 'dart:convert'; import 'package:flutter_pkid/flutter_pkid.dart'; +import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; +import '../app_config.dart'; import 'crypto_service.dart'; +Future getPkidClient() async { + String pKidUrl = AppConfig().pKidUrl(); + + String? phrase = await getPhrase(); + KeyPair keyPair = await generateKeyPairFromSeedPhrase(phrase!); + + return FlutterPkid(pKidUrl, keyPair); +} + + + + Future saveEmailToPKidForMigration() async { Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); var client = FlutterPkid(pkidUrl, keyPair); diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index 2c34a866..f779a915 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -10,29 +10,13 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; -import '../app_config.dart'; - -String pKidUrl = AppConfig().pKidUrl(); - -Future savePin(String pin) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('pin'); - prefs.setString('pin', pin); -} - -Future getPin() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getString('pin'); -} - -Future savePublicKey(Uint8List publicKey) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('publicKey'); - - String encodedPublicKey = base64.encode(publicKey); - prefs.setString('publicKey', encodedPublicKey); -} +/// +/// +/// Methods for encryption / signing +/// +/// Future getPublicKey() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -60,23 +44,22 @@ Future getPublicKey() async { return base64.decode(encodedPublicKey!); } -Future> getEdCurveKeys() async { +Future savePublicKey(Uint8List publicKey) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove('publicKey'); - final String? pkEd = prefs.getString('publicKey'); - final String? skEd = prefs.getString('privateKey'); + String encodedPublicKey = base64.encode(publicKey); + prefs.setString('publicKey', encodedPublicKey); +} - final String pkCurve = base64 - .encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); - final String skCurve = base64 - .encode(Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd!))); +Future getIsPublicKeyFixed() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); - return { - 'signingPublicKey': hex.encode(base64.decode(pkEd)), - 'signingPrivateKey': hex.encode(base64.decode(skEd)), - 'encryptionPublicKey': hex.encode(base64.decode(pkCurve)), - 'encryptionPrivateKey': hex.encode(base64.decode(skCurve)) - }; + if (prefs.getBool('isPublicKeyFixed') == null) { + return false; + } + + return prefs.getBool('isPublicKeyFixed'); } Future setPublicKeyFixed() async { @@ -84,14 +67,13 @@ Future setPublicKeyFixed() async { prefs.setBool('isPublicKeyFixed', true); } -Future getIsPublicKeyFixed() async { +Future getPrivateKey() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - if (prefs.getBool('isPublicKeyFixed') == null) { - return false; - } + String? privateKey = prefs.getString('privateKey'); + Uint8List decodedPrivateKey = base64.decode(privateKey!); - return prefs.getBool('isPublicKeyFixed'); + return decodedPrivateKey; } Future savePrivateKey(Uint8List privateKey) async { @@ -102,13 +84,23 @@ Future savePrivateKey(Uint8List privateKey) async { prefs.setString('privateKey', encodedPrivateKey); } -Future getPrivateKey() async { +Future> getEdCurveKeys() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - String? privateKey = prefs.getString('privateKey'); - Uint8List decodedPrivateKey = base64.decode(privateKey!); + final String? pkEd = prefs.getString('publicKey'); + final String? skEd = prefs.getString('privateKey'); - return decodedPrivateKey; + final String pkCurve = + base64.encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); + final String skCurve = + base64.encode(Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd!))); + + return { + 'signingPublicKey': hex.encode(base64.decode(pkEd)), + 'signingPrivateKey': hex.encode(base64.decode(skEd)), + 'encryptionPublicKey': hex.encode(base64.decode(pkCurve)), + 'encryptionPrivateKey': hex.encode(base64.decode(skCurve)) + }; } Future savePhrase(String phrase) async { @@ -123,91 +115,121 @@ Future getPhrase() async { return prefs.getString('seedPhrase'); } -Future saveLocationId(String locationId) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - - List locationIdList = await getLocationIdList(); +/// +/// +/// Email methods in Shared Preferences +/// +/// - locationIdList.add(locationId); +Future setIsEmailVerified(bool isEmailVerified) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool('isEmailVerified', isEmailVerified); +} - String locationIdListAsJson = jsonEncode(locationIdList); +Future getIsEmailVerified() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('isEmailVerified'); +} - prefs.remove('locationIdList'); - prefs.setString('locationIdList', locationIdListAsJson); +Future> getEmail() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return {'email': prefs.getString('email'), 'sei': prefs.getString('signedEmailIdentifier')}; } -Future> getLocationIdList() async { +Future saveEmail(String email, String? signedEmailIdentifier) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - try { - String? locationIdListAsJson = prefs.getString('locationIdList'); - List locationIdList = jsonDecode(locationIdListAsJson!); + prefs.remove('email'); + prefs.remove('signedEmailIdentifier'); + prefs.remove('emailVerified'); - return locationIdList; - } catch (_) { - return []; + prefs.setString('email', email); + + FlutterPkid client = await getPkidClient(); + + if (signedEmailIdentifier != null) { + Globals().emailVerified.value = true; + prefs.setString('signedEmailIdentifier', signedEmailIdentifier); + client.setPKidDoc('email', json.encode({'email': email, 'sei': signedEmailIdentifier})); + return; } + + Globals().emailVerified.value = false; + client.setPKidDoc('email', json.encode({'email': email})); } -Future saveDoubleName(String doubleName) async { +/// +/// +/// Phone methods in Shared Preferences +/// +/// + +Future setIsPhoneVerified(bool isPhoneVerified) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('doubleName'); - prefs.setString('doubleName', doubleName); + prefs.setBool('isPhoneVerified', isPhoneVerified); } -Future getDoubleName() async { +Future getIsPhoneVerified() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getString('doubleName'); + return prefs.getBool('isPhoneVerified'); } -Future removeEmail() async { +Future> getPhone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - - prefs.remove('email'); - prefs.remove('emailVerified'); + return {'phone': prefs.getString('phone'), 'spi': prefs.getString('signedPhoneIdentifier')}; } -Future saveEmail(String email, String signedEmailIdentifier) async { +Future savePhone(String phone, String? signedPhoneIdentifier) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('email'); - prefs.remove('signedEmailIdentifier'); - prefs.remove('emailVerified'); - - prefs.setString('email', email); + prefs.remove('phone'); + prefs.remove('phoneVerified'); + prefs.remove('signedPhoneIdentifier'); - if (signedEmailIdentifier != null) { - prefs.setString('signedEmailIdentifier', signedEmailIdentifier); + prefs.setString('phone', phone); - Map keyPair = - await generateKeyPairFromSeedPhrase(await getPhrase()); + FlutterPkid client = await getPkidClient(); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('email', - json.encode({'email': email, 'sei': signedEmailIdentifier}), keyPair); + if (signedPhoneIdentifier != null) { + Globals().phoneVerified.value = true; + prefs.setString('signedPhoneIdentifier', signedPhoneIdentifier); + client.setPKidDoc('phone', json.encode({'phone': phone, 'spi': signedPhoneIdentifier})); + return; } - Globals().emailVerified.value = (signedEmailIdentifier != null); + Globals().phoneVerified.value = false; + client.setPKidDoc('phone', json.encode({'phone': phone})); +} + +/// +/// +/// Identity methods in Shared Preferences +/// +/// + +Future setIsIdentityVerified(bool isIdentityVerified) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setBool('isIdentityVerified', isIdentityVerified); +} + +Future getIsIdentityVerified() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('isIdentityVerified'); } Future> getIdentity() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return { 'identityName': prefs.getString('identityName'), - 'signedIdentityNameIdentifier': - prefs.getString('signedIdentityNameIdentifier'), + 'signedIdentityNameIdentifier': prefs.getString('signedIdentityNameIdentifier'), 'identityCountry': prefs.getString('identityCountry'), - 'signedIdentityCountryIdentifier': - prefs.getString('signedIdentityCountryIdentifier'), + 'signedIdentityCountryIdentifier': prefs.getString('signedIdentityCountryIdentifier'), 'identityDOB': prefs.getString('identityDOB'), - 'signedIdentityDOBIdentifier': - prefs.getString('signedIdentityDOBIdentifier'), + 'signedIdentityDOBIdentifier': prefs.getString('signedIdentityDOBIdentifier'), 'identityDocumentMeta': prefs.getString('identityDocumentMeta'), - 'signedIdentityDocumentMetaIdentifier': - prefs.getString('signedIdentityDocumentMetaIdentifier'), + 'signedIdentityDocumentMetaIdentifier': prefs.getString('signedIdentityDocumentMetaIdentifier'), 'identityGender': prefs.getString('identityGender'), - 'signedIdentityGenderIdentifier': - prefs.getString('signedIdentityGenderIdentifier'), + 'signedIdentityGenderIdentifier': prefs.getString('signedIdentityGenderIdentifier'), }; } @@ -236,22 +258,15 @@ Future saveIdentity( prefs.setString('identityGender', identityGender); prefs.setString('signedIdentityNameIdentifier', signedIdentityNameIdentifier); - prefs.setString( - 'signedIdentityCountryIdentifier', - signedIdentityCountryIdentifier == 'null' - ? 'test' - : signedIdentityCountryIdentifier); + prefs.setString('signedIdentityCountryIdentifier', signedIdentityCountryIdentifier); prefs.setString('signedIdentityDOBIdentifier', signedIdentityDOBIdentifier); - prefs.setString('signedIdentityDocumentMetaIdentifier', - signedIdentityDocumentMetaIdentifier); - prefs.setString( - 'signedIdentityGenderIdentifier', signedIdentityGenderIdentifier); + prefs.setString('signedIdentityDocumentMetaIdentifier', signedIdentityDocumentMetaIdentifier); + prefs.setString('signedIdentityGenderIdentifier', signedIdentityGenderIdentifier); prefs.remove('identityVerified'); - Map keyPair = - await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); + client.setPKidDoc( 'identity', json.encode({ @@ -262,189 +277,133 @@ Future saveIdentity( 'identityDOB': identityDOB, 'signedIdentityDOBIdentifier': signedIdentityDOBIdentifier, 'identityDocumentMeta': jsonEncode(identityDocumentMeta), - 'signedIdentityDocumentMetaIdentifier': - signedIdentityDocumentMetaIdentifier, + 'signedIdentityDocumentMetaIdentifier': signedIdentityDocumentMetaIdentifier, 'identityGender': identityGender, 'signedIdentityGenderIdentifier': signedIdentityGenderIdentifier - }), - keyPair); + })); - Globals().identityVerified.value = (signedIdentityNameIdentifier != null); + Globals().identityVerified.value = true; } -Future removeIdentity() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('identityName'); - prefs.remove('identityCountry'); - prefs.remove('identityDOB'); - prefs.remove('identityDocumentMeta'); - prefs.remove('identityGender'); - prefs.remove('identityVerified'); -} - -Future getIsEmailVerified() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('isEmailVerified'); -} - -Future getIsPhoneVerified() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('isPhoneVerified'); -} - -Future getIsIdentityVerified() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('isIdentityVerified'); -} - -Future setIsEmailVerified(bool isEmailVerified) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.setBool('isEmailVerified', isEmailVerified); -} - -Future setIsPhoneVerified(bool isPhoneVerified) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.setBool('isPhoneVerified', isPhoneVerified); -} - -Future setIsIdentityVerified(bool isIdentityVerified) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.setBool('isIdentityVerified', isIdentityVerified); -} +/// +/// +/// Methods for derived seed +/// +/// -Future removePhone() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - - prefs.remove('phone'); - prefs.remove('phoneVerified'); +Future getDerivedSeed(String appId) async { + return await generateDerivedSeed(appId); } -Future savePhone(String phone, String signedPhoneIdentifier) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('phone'); - prefs.setString('phone', phone); - - prefs.remove('phoneVerified'); - - if (signedPhoneIdentifier != null) { - prefs.setString('signedPhoneIdentifier', signedPhoneIdentifier); - - Map keyPair = - await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('phone', - json.encode({'phone': phone, 'spi': signedPhoneIdentifier}), keyPair); - } - - Globals().phoneVerified.value = (signedPhoneIdentifier != null); -} +/// +/// +/// Methods for authentication +/// +/// -Future> getEmail() async { +Future savePin(String pin) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return { - 'email': prefs.getString('email'), - 'sei': prefs.getString('signedEmailIdentifier') - }; + prefs.remove('pin'); + prefs.setString('pin', pin); } -Future> getPhone() async { +Future getPin() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return { - 'phone': prefs.getString('phone'), - 'spi': prefs.getString('signedPhoneIdentifier') - }; -} - - -Future getDerivedSeed(String appId) async { - return await generateDerivedSeed(appId); + return prefs.getString('pin'); } Future saveFingerprint(fingerprint) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('fingerprint'); - prefs.setBool('fingerprint', fingerprint); + prefs.remove('useFingerPrint'); + prefs.setBool('useFingerPrint', fingerprint); } -Future getFingerprint() async { +Future getFingerprint() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - bool result = prefs.getBool('fingerprint'); + bool? result = prefs.getBool('useFingerPrint'); if (result == null) { - await prefs.setBool('fingerprint', false); - result = prefs.getBool('fingerprint'); + prefs.setBool('useFingerPrint', false); + result = prefs.getBool('useFingerPrint'); } return result; } + +/// +/// +/// Methods for login permissions +/// +/// + Future saveScopePermissions(scopePermissions) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('scopePermissions'); prefs.setString('scopePermissions', scopePermissions); } -Future getScopePermissions() async { +Future getScopePermissions() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('scopePermissions'); } -Future savePreviousScopePermissions( - String appId, String scopePermissions) async { +Future savePreviousScopePermissions(String appId, String scopePermissions) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove('$appId-scopePreviousPermissions'); await prefs.setString('$appId-scopePreviousPermissions', scopePermissions); } -Future getPreviousScopePermissions(String appId) async { +Future getPreviousScopePermissions(String appId) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('$appId-scopePreviousPermissions'); } -Future isTrustedDevice(String appId, String trustedDevice) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - String trustedDeviceApp = prefs.getString('$appId-trusted'); - if (trustedDeviceApp == null) return false; - - return trustedDeviceApp == trustedDevice; -} - -Future saveTrustedDevice(String appId, String trustedDeviceId) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('$appId-trusted'); - prefs.setString('$appId-trusted', trustedDeviceId); -} +/// +/// +/// Methods for initialization +/// +/// Future saveInitDone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setBool('initDone', true); } -Future getInitDone() async { +Future getInitDone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - bool initDone = prefs.getBool('initDone'); + + bool? initDone = prefs.getBool('initDone'); if (initDone == null) { - initDone = false; + prefs.setBool('initDone', false); + initDone = prefs.getBool('initDone'); } + return initDone; } + +/// +/// +/// Methods for unilinks +/// +/// + Future savePreviousState(String state) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return await prefs.setString("previousState", state); } -Future getPreviousState() async { +Future getPreviousState() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString("previousState"); } -Future clearData() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - bool cleared = await prefs.clear(); - saveInitDone(); - return cleared; -} +/// +/// +/// Methods for Wallets +/// +/// Future saveWallets(List data) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -469,3 +428,53 @@ Future> getWallets() async { } return walletData; } + +/// +/// +/// Globals +/// +/// + +Future clearData() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + bool cleared = await prefs.clear(); + saveInitDone(); + return cleared; +} + +Future saveLocationId(String locationId) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + + List locationIdList = await getLocationIdList(); + + locationIdList.add(locationId); + + String locationIdListAsJson = jsonEncode(locationIdList); + + prefs.remove('locationIdList'); + prefs.setString('locationIdList', locationIdListAsJson); +} + +Future> getLocationIdList() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + + try { + String? locationIdListAsJson = prefs.getString('locationIdList'); + List locationIdList = jsonDecode(locationIdListAsJson!); + + return locationIdList; + } catch (_) { + return []; + } +} + +Future saveDoubleName(String doubleName) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove('doubleName'); + prefs.setString('doubleName', doubleName); +} + +Future getDoubleName() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString('doubleName'); +} From dd8aa60b0c84446db9daed7c78e16070f9ed9c73 Mon Sep 17 00:00:00 2001 From: Lennert Date: Fri, 7 Jan 2022 14:31:39 +0100 Subject: [PATCH 04/75] Made services widgets etc null safe - Only screens WIP --- app/lib/app_config.dart | 17 +- app/lib/apps/chatbot/chatbot.dart | 7 +- app/lib/apps/chatbot/chatbot_config.dart | 2 +- app/lib/apps/chatbot/chatbot_widget.dart | 25 +- app/lib/apps/free_flow_pages/ffp_config.dart | 2 +- app/lib/apps/free_flow_pages/ffp_events.dart | 2 +- app/lib/apps/news/news_config.dart | 2 +- app/lib/apps/news/news_widget.dart | 9 +- app/lib/apps/wallet/wallet_config.dart | 2 +- app/lib/apps/wallet/wallet_user_data.dart | 14 +- app/lib/apps/wallet/wallet_widget.dart | 20 +- app/lib/browser.dart | 4 +- app/lib/clipboard_hack/clipboard_hack.dart | 5 +- app/lib/events/close_auth_event.dart | 2 +- app/lib/events/events.dart | 2 +- app/lib/events/identity_callback_event.dart | 2 +- app/lib/events/new_login_event.dart | 2 +- app/lib/helpers/flags.dart | 81 ++--- app/lib/helpers/globals.dart | 24 +- app/lib/helpers/kyc_helpers.dart | 15 +- app/lib/helpers/vpn_state.dart | 3 +- app/lib/jrouter.dart | 12 +- app/lib/main.dart | 11 +- app/lib/models/login.dart | 56 ++-- app/lib/models/scope.dart | 26 +- app/lib/services/3bot_service.dart | 187 ++++++----- app/lib/services/identity_service.dart | 4 - app/lib/services/open_kyc_service.dart | 187 ++++++----- app/lib/services/phone_service.dart | 9 +- app/lib/services/pkid_service.dart | 69 ++-- .../services/push_notifications_manager.dart | 37 --- .../services/shared_preference_service.dart | 13 +- app/lib/services/socket_service.dart | 300 +++++++++--------- app/lib/services/tools_service.dart | 14 +- app/lib/services/uni_link_service.dart | 60 ++-- app/lib/widgets/custom_dialog.dart | 21 +- app/lib/widgets/error_widget.dart | 11 +- app/lib/widgets/image_button.dart | 3 +- app/lib/widgets/layout_drawer.dart | 2 +- app/lib/widgets/phone_widget.dart | 14 +- app/lib/widgets/pin_field.dart | 20 +- app/lib/widgets/preference_dialog.dart | 122 +++---- app/lib/widgets/reusable_text_field_step.dart | 14 +- app/lib/widgets/reusable_text_step.dart | 6 +- 44 files changed, 730 insertions(+), 710 deletions(-) delete mode 100644 app/lib/services/push_notifications_manager.dart diff --git a/app/lib/app_config.dart b/app/lib/app_config.dart index e1c41115..7454f475 100644 --- a/app/lib/app_config.dart +++ b/app/lib/app_config.dart @@ -2,8 +2,10 @@ import 'package:threebotlogin/app_config_local.dart'; import 'package:threebotlogin/helpers/env_config.dart'; import 'package:threebotlogin/helpers/environment.dart'; +import 'helpers/globals.dart'; + class AppConfig extends EnvConfig { - AppConfigImpl appConfig; + late AppConfigImpl appConfig; AppConfig() { if (environment == Environment.Staging) { @@ -17,7 +19,7 @@ class AppConfig extends EnvConfig { } } - String baseUrl() { + String? baseUrl() { return appConfig.baseUrl(); } @@ -177,4 +179,15 @@ class AppConfigTesting extends AppConfigImpl { 'apiKey': 'VtTsMwJwiF69QWFWHGEMKM' }; } +} + +void setFallbackConfigs() { + print("Can't connect to FlagSmith, setting default configs... "); + + Globals().isOpenKYCEnabled = false; + Globals().isYggdrasilEnabled = false; + Globals().debugMode = false; + Globals().useNewWallet = false; + Globals().newWalletUrl = ''; + Globals().redoIdentityVerification = false; } \ No newline at end of file diff --git a/app/lib/apps/chatbot/chatbot.dart b/app/lib/apps/chatbot/chatbot.dart index e5867b60..280283a9 100644 --- a/app/lib/apps/chatbot/chatbot.dart +++ b/app/lib/apps/chatbot/chatbot.dart @@ -6,11 +6,12 @@ import 'package:threebotlogin/events/go_home_event.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; class Chatbot implements App { - ChatbotWidget _widget; + late ChatbotWidget _widget; Future widget() async { - var email = await getEmail(); - this._widget = ChatbotWidget(email: email['email']); + Map email = await getEmail(); + String emailAddress = email['email'].toString(); + this._widget = ChatbotWidget(email: emailAddress); return this._widget; } diff --git a/app/lib/apps/chatbot/chatbot_config.dart b/app/lib/apps/chatbot/chatbot_config.dart index f9c04cce..a563075e 100644 --- a/app/lib/apps/chatbot/chatbot_config.dart +++ b/app/lib/apps/chatbot/chatbot_config.dart @@ -2,7 +2,7 @@ import 'package:threebotlogin/helpers/env_config.dart'; import 'package:threebotlogin/helpers/environment.dart'; class ChatbotConfig extends EnvConfig { - ChatbotConfigImpls impl; + late ChatbotConfigImpls impl; ChatbotConfig() { if (environment == Environment.Staging) { diff --git a/app/lib/apps/chatbot/chatbot_widget.dart b/app/lib/apps/chatbot/chatbot_widget.dart index c4edb14e..fa42bea1 100644 --- a/app/lib/apps/chatbot/chatbot_widget.dart +++ b/app/lib/apps/chatbot/chatbot_widget.dart @@ -7,21 +7,20 @@ import 'package:threebotlogin/widgets/layout_drawer.dart'; class ChatbotWidget extends StatefulWidget { final String email; - ChatbotWidget({this.email}); + ChatbotWidget({required this.email}); @override _ChatbotState createState() => new _ChatbotState(email: this.email); } -class _ChatbotState extends State - with AutomaticKeepAliveClientMixin { - InAppWebViewController webView; +class _ChatbotState extends State with AutomaticKeepAliveClientMixin { + InAppWebViewController? webView; ChatbotConfig config = ChatbotConfig(); - InAppWebView iaWebview; + InAppWebView? iaWebview; final String email; - _ChatbotState({this.email}) { + _ChatbotState({required this.email}) { iaWebview = InAppWebView( initialUrlRequest: URLRequest( url: Uri.parse('${config.url()}$email&cache_buster=' + @@ -32,19 +31,17 @@ class _ChatbotState extends State onWebViewCreated: (InAppWebViewController controller) { webView = controller; }, - onCreateWindow: - (InAppWebViewController controller, CreateWindowAction req) { - inAppBrowser.openUrlRequest( - urlRequest: req.request, options: InAppBrowserClassOptions()); + onCreateWindow: (InAppWebViewController controller, CreateWindowAction req) { + inAppBrowser.openUrlRequest(urlRequest: req.request, options: InAppBrowserClassOptions()); + return Future.value(true); }, - onConsoleMessage: - (InAppWebViewController controller, ConsoleMessage consoleMessage) { + onConsoleMessage: (InAppWebViewController controller, ConsoleMessage consoleMessage) { print("CB console: " + consoleMessage.message); }, - onLoadStart: (InAppWebViewController controller, Uri url) { + onLoadStart: (InAppWebViewController controller, _) { webView = controller; }, - onLoadStop: (InAppWebViewController controller, Uri url) { + onLoadStop: (InAppWebViewController controller, _) { controller.evaluateJavascript(source: """ function waitForElm(selector) { return new Promise(resolve => { diff --git a/app/lib/apps/free_flow_pages/ffp_config.dart b/app/lib/apps/free_flow_pages/ffp_config.dart index 5e7ee54f..c69a73ea 100644 --- a/app/lib/apps/free_flow_pages/ffp_config.dart +++ b/app/lib/apps/free_flow_pages/ffp_config.dart @@ -2,7 +2,7 @@ import 'package:threebotlogin/helpers/env_config.dart'; import 'package:threebotlogin/helpers/environment.dart'; class FfpConfig extends EnvConfig { - FfpConfigImpls impl; + late FfpConfigImpls impl; FfpConfig() { if (environment == Environment.Staging) { diff --git a/app/lib/apps/free_flow_pages/ffp_events.dart b/app/lib/apps/free_flow_pages/ffp_events.dart index cdbb2a58..9418eea6 100644 --- a/app/lib/apps/free_flow_pages/ffp_events.dart +++ b/app/lib/apps/free_flow_pages/ffp_events.dart @@ -1,5 +1,5 @@ class FfpBrowseEvent { - String url; + String? url; FfpBrowseEvent({this.url}); } diff --git a/app/lib/apps/news/news_config.dart b/app/lib/apps/news/news_config.dart index 36edbaf4..22dd5c2a 100644 --- a/app/lib/apps/news/news_config.dart +++ b/app/lib/apps/news/news_config.dart @@ -2,7 +2,7 @@ import 'package:threebotlogin/helpers/env_config.dart'; import 'package:threebotlogin/helpers/environment.dart'; class NewsConfig extends EnvConfig { - NewsConfigImpls impl; + late NewsConfigImpls impl; NewsConfig() { if (environment == Environment.Staging) { diff --git a/app/lib/apps/news/news_widget.dart b/app/lib/apps/news/news_widget.dart index e750f62b..3f5158e8 100644 --- a/app/lib/apps/news/news_widget.dart +++ b/app/lib/apps/news/news_widget.dart @@ -17,15 +17,16 @@ class NewsWidget extends StatefulWidget { class _NewsState extends State with AutomaticKeepAliveClientMixin { - InAppWebViewController webView; + late InAppWebViewController webView; + late InAppWebView iaWebView; + String url = ""; String initialEndsWith= ""; double progress = 0; var config = NewsConfig(); - InAppWebView iaWebView; _back(NewsBackEvent event) async { - Uri url = await webView.getUrl(); + Uri? url = await webView.getUrl(); print("URL: " + url.toString()); if (url.toString().endsWith(initialEndsWith)) { Events().emit(GoHomeEvent()); @@ -54,7 +55,7 @@ class _NewsState extends State return true; }, - onLoadStop: (InAppWebViewController controller, Uri url) async { + onLoadStop: (InAppWebViewController controller, Uri? url) async { addClipboardHandlersOnly(controller); }, onProgressChanged: (InAppWebViewController controller, int progress) { diff --git a/app/lib/apps/wallet/wallet_config.dart b/app/lib/apps/wallet/wallet_config.dart index fbe9e2e7..749dbcc2 100644 --- a/app/lib/apps/wallet/wallet_config.dart +++ b/app/lib/apps/wallet/wallet_config.dart @@ -2,7 +2,7 @@ import 'package:threebotlogin/helpers/env_config.dart'; import 'package:threebotlogin/helpers/environment.dart'; class WalletConfig extends EnvConfig { - WalletConfigImpls impl; + late WalletConfigImpls impl; WalletConfig() { if (environment == Environment.Staging) { diff --git a/app/lib/apps/wallet/wallet_user_data.dart b/app/lib/apps/wallet/wallet_user_data.dart index b8210440..b85956bd 100644 --- a/app/lib/apps/wallet/wallet_user_data.dart +++ b/app/lib/apps/wallet/wallet_user_data.dart @@ -3,10 +3,10 @@ import 'package:shared_preferences/shared_preferences.dart'; void saveImportedWallet(List params) async { String importedWallet = params[0].toString(); final prefs = await SharedPreferences.getInstance(); - List importedWallets = await getImportedWallets(); + List? importedWallets = await getImportedWallets(); if (importedWallets == null) { - importedWallets = new List(); + importedWallets = []; } if (!importedWallets.contains(importedWallet)) { @@ -17,23 +17,23 @@ void saveImportedWallet(List params) async { } } -Future> getImportedWallets() async { +Future?> getImportedWallets() async { final prefs = await SharedPreferences.getInstance(); return prefs.getStringList("importedWallets"); } Future saveAppWallet(List params) async { - String appWalletToAdd = params[0].toString(); + String? appWalletToAdd = params[0]; final prefs = await SharedPreferences.getInstance(); - List appWallets = await getAppWallets(); + List? appWallets = await getAppWallets(); if (appWalletToAdd == null) { return false; } if (appWallets == null) { - appWallets = new List(); + appWallets = []; } if (!appWallets.contains(appWalletToAdd)) { @@ -45,7 +45,7 @@ Future saveAppWallet(List params) async { return false; } -Future> getAppWallets() async { +Future?> getAppWallets() async { final prefs = await SharedPreferences.getInstance(); var wallets = prefs.getStringList("appWallets"); return wallets; diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 4c50d448..ad163482 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -24,14 +24,14 @@ class WalletWidget extends StatefulWidget { } class _WalletState extends State with AutomaticKeepAliveClientMixin { - InAppWebViewController webView; + late InAppWebViewController webView; + late InAppWebView iaWebView; double progress = 0; var config = WalletConfig(); - InAppWebView iaWebView; _back(WalletBackEvent event) async { - Uri url = await webView.getUrl(); + Uri? url = await webView.getUrl(); print(url.toString()); String endsWith = config.appId() + '/'; if (url.toString().endsWith(endsWith)) { @@ -42,7 +42,8 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi } _WalletState() { - String walletUri = Globals().useNewWallet == true ? Globals().newWalletUrl : 'https://${config.appId()}/init'; + String walletUri = + Globals().useNewWallet == true ? Globals().newWalletUrl : 'https://${config.appId()}/init'; iaWebView = InAppWebView( initialUrlRequest: URLRequest( @@ -50,14 +51,17 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi walletUri + '?cache_buster=' + new DateTime.now().millisecondsSinceEpoch.toString())), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions(), - android: AndroidInAppWebViewOptions(supportMultipleWindows: true, thirdPartyCookiesEnabled: true), + android: AndroidInAppWebViewOptions( + supportMultipleWindows: true, thirdPartyCookiesEnabled: true), ios: IOSInAppWebViewOptions()), onWebViewCreated: (InAppWebViewController controller) { webView = controller; this.addHandler(); }, - onCreateWindow: (InAppWebViewController controller, CreateWindowAction req) {}, - onLoadStop: (InAppWebViewController controller, Uri url) async { + onCreateWindow: (InAppWebViewController controller, CreateWindowAction req) { + return Future.value(true); + }, + onLoadStop: (InAppWebViewController controller, Uri? url) async { addClipboardHandlersOnly(controller); if (url.toString().contains('/init')) { initKeys(); @@ -114,7 +118,7 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi // QRCode scanner is black if we don't sleep here. bool slept = await Future.delayed(const Duration(milliseconds: 400), () => true); - String result; + String result = ''; if (slept) { result = await Navigator.push(context, MaterialPageRoute(builder: (context) => ScanScreen())); } diff --git a/app/lib/browser.dart b/app/lib/browser.dart index fa169815..356a5ac5 100644 --- a/app/lib/browser.dart +++ b/app/lib/browser.dart @@ -2,13 +2,13 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; class MyInAppBrowser extends InAppBrowser { @override - void onLoadStart(Uri url) { + void onLoadStart(Uri? url) { super.onLoadStart(url); print("\n\nStarted $url\n\n"); } @override - void onLoadStop(Uri url) { + void onLoadStop(Uri? url) { super.onLoadStop(url); print("\n\nStopped $url\n\n"); } diff --git a/app/lib/clipboard_hack/clipboard_hack.dart b/app/lib/clipboard_hack/clipboard_hack.dart index 4daf9490..720c8acd 100644 --- a/app/lib/clipboard_hack/clipboard_hack.dart +++ b/app/lib/clipboard_hack/clipboard_hack.dart @@ -8,8 +8,9 @@ void copy(List params) { Clipboard.setData(new ClipboardData(text: params[0])); } -Future paste(List params) async { - return (await Clipboard.getData('text/plain')).text.toString(); +Future paste(List params) async { + ClipboardData? data = await Clipboard.getData('text/plain'); + return data?.text.toString(); } Future addClipboardHack(InAppWebViewController webview) async { diff --git a/app/lib/events/close_auth_event.dart b/app/lib/events/close_auth_event.dart index fe7158dd..4b511100 100644 --- a/app/lib/events/close_auth_event.dart +++ b/app/lib/events/close_auth_event.dart @@ -1,5 +1,5 @@ class CloseAuthEvent { - bool close; + bool? close; CloseAuthEvent({this.close}); } diff --git a/app/lib/events/events.dart b/app/lib/events/events.dart index 99609e50..043f8611 100644 --- a/app/lib/events/events.dart +++ b/app/lib/events/events.dart @@ -11,7 +11,7 @@ class Events { onEvent(Type eventType, Function function) { if (this.eventList[eventType] == null) { - this.eventList[eventType] = new List(); + this.eventList[eventType] = []; } this.eventList[eventType].add(function); } diff --git a/app/lib/events/identity_callback_event.dart b/app/lib/events/identity_callback_event.dart index bfd2d07c..9ca1fa16 100644 --- a/app/lib/events/identity_callback_event.dart +++ b/app/lib/events/identity_callback_event.dart @@ -1,4 +1,4 @@ class IdentityCallbackEvent { - String type; + String? type; IdentityCallbackEvent({this.type}); } diff --git a/app/lib/events/new_login_event.dart b/app/lib/events/new_login_event.dart index d0e9b6d0..9aa2c217 100644 --- a/app/lib/events/new_login_event.dart +++ b/app/lib/events/new_login_event.dart @@ -1,7 +1,7 @@ import 'package:threebotlogin/models/login.dart'; class NewLoginEvent { - Login loginData; + Login? loginData; NewLoginEvent({this.loginData}); } diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index ee077468..59cd743c 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -1,63 +1,64 @@ import 'package:flagsmith/flagsmith.dart'; -import 'package:threebotlogin/app_config.dart'; -import 'package:threebotlogin/services/shared_preference_service.dart'; +import '../app_config.dart'; import 'globals.dart'; class Flags { static final Flags _singleton = new Flags._internal(); - FlagsmithClient client; + FlagsmithClient? client; - Map flagSmithConfig = AppConfig().flagSmithConfig(); + Map? flagSmithConfig = AppConfig().flagSmithConfig(); - Future initialiseFlagSmith() async { - client = await FlagsmithClient.init( - config: FlagsmithConfig( - baseURI: flagSmithConfig['url'], - ), - apiKey: flagSmithConfig['apiKey']); + Future initFlagSmith() async { + try { + client = await FlagsmithClient.init( + config: FlagsmithConfig( + baseURI: flagSmithConfig!['url'].toString(), + ), + apiKey: flagSmithConfig!['apiKey'].toString()); - - String doubleName = await getDoubleName(); - - if(doubleName != null) { - FeatureUser user = FeatureUser(identifier: doubleName); - - try { - await client.getFeatureFlags(user: user, reload: true); - } - - catch(e) { - print(e); - throw Exception(); - } + await client?.getFeatureFlags(reload: true); + } catch (e) { + print(e); + setFallbackConfigs(); } } - Future setFlagSmithDefaultValues() async { - Globals().isOpenKYCEnabled = await Flags().hasFlagValueByFeatureName('kyc'); - Globals().isYggdrasilEnabled = await Flags().hasFlagValueByFeatureName('yggdrasil'); - Globals().debugMode = await Flags().hasFlagValueByFeatureName('debug'); - Globals().useNewWallet = await Flags().hasFlagValueByFeatureName('use-new-wallet'); - Globals().newWalletUrl = await Flags().getFlagValueByFeatureName('new-wallet-url'); - Globals().redoIdentityVerification = await Flags().hasFlagValueByFeatureName('redo-identity-verification'); + Future getNewFlagValues() async { + await client?.getFeatureFlags(reload: true); } - Future hasFlagValueByFeatureName(String name) async { - if (client != null) { - return (await client.hasFeatureFlag(name)); + Future setFlagSmithDefaultValues() async { + try { + await client?.getFeatureFlags(reload: true); + + Globals().isOpenKYCEnabled = (await Flags().hasFlagValueByFeatureName('kyc'))!; + Globals().isYggdrasilEnabled = (await Flags().hasFlagValueByFeatureName('yggdrasil'))!; + Globals().debugMode = (await Flags().hasFlagValueByFeatureName('debug'))!; + Globals().useNewWallet = (await Flags().hasFlagValueByFeatureName('use-new-wallet'))!; + Globals().newWalletUrl = (await Flags().getFlagValueByFeatureName('new-wallet-url'))!; + Globals().redoIdentityVerification = + (await Flags().hasFlagValueByFeatureName('redo-identity-verification'))!; + } catch (e) { + print(e); } + } - return false; + Future hasFlagValueByFeatureName(String name) async { + return (await client?.hasFeatureFlag(name)); } - Future getFlagValueByFeatureName(String name) async { - if (client != null) { - return (await client.getFeatureFlagValue(name)); - } + Future isFlagEnabled(String name) async { + return (await client?.isFeatureFlagEnabled(name)); + } + + Future getFlagValueByFeatureName(String name) async { + return (await client?.getFeatureFlagValue(name)); + } - return ''; + Future getGlobalFlagValue(String name) async { + return (await client?.getFeatureFlagValue(name)); } factory Flags() { diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index 06e929af..24b62dec 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -1,17 +1,14 @@ -import 'dart:ffi'; - import 'package:flutter/material.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/helpers/vpn_state.dart'; -// import 'package:threebotlogin/helpers/vpn_state.dart'; import 'package:threebotlogin/jrouter.dart'; import 'package:threebotlogin/models/paymentRequest.dart'; class NoAnimationTabController extends TabController { NoAnimationTabController( {int initialIndex = 0, - @required int length, - @required TickerProvider vsync}) + required int length, + required TickerProvider vsync}) : super(initialIndex: initialIndex, length: length, vsync: vsync); @override @@ -38,21 +35,21 @@ class Globals { bool tooManySmsAttempts = false; String routeName = 'Home'; - NoAnimationTabController tabController; + late NoAnimationTabController tabController; int lockedUntill = 0; int lockedSmsUntill = 0; int loginTimeout = 120; - PaymentRequest paymentRequest; + PaymentRequest? paymentRequest; bool paymentRequestIsUsed = false; // FlagSmith configurations - bool isOpenKYCEnabled; - bool isYggdrasilEnabled; - bool useNewWallet; - String newWalletUrl; - bool redoIdentityVerification; - bool debugMode; + bool isOpenKYCEnabled = false; + bool isYggdrasilEnabled = false; + bool useNewWallet = false; + String newWalletUrl = ''; + bool redoIdentityVerification = false; + bool debugMode = false; VpnState vpnState = new VpnState(); @@ -61,7 +58,6 @@ class Globals { ValueNotifier hidePhoneButton = ValueNotifier(false); - // VpnState vpnState = new VpnState(); static final Globals _singleton = new Globals._internal(); factory Globals() { diff --git a/app/lib/helpers/kyc_helpers.dart b/app/lib/helpers/kyc_helpers.dart index 551f6854..2cab8039 100644 --- a/app/lib/helpers/kyc_helpers.dart +++ b/app/lib/helpers/kyc_helpers.dart @@ -10,18 +10,17 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import 'globals.dart'; Future fetchPKidData() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); List keyWords = ['email', 'phone', 'identity']; var futures = keyWords.map((keyword) async { - var pKidResult = await client.getPKidDoc(keyword, keyPair); + var pKidResult = await client.getPKidDoc(keyword); return pKidResult.containsKey('data') && pKidResult.containsKey('success') ? jsonDecode(pKidResult['data']) : {}; }); var pKidResult = await Future.wait(futures); - Map dataMap = pKidResult.asMap(); + Map dataMap = pKidResult.asMap(); await handleKYCData(dataMap[0], dataMap[1], dataMap[2]); } @@ -30,9 +29,9 @@ Future handleKYCData( Map emailData, Map phoneData, Map identityData) async { await saveCorrectVerificationStates(emailData, phoneData, identityData); - bool isEmailVerified = await getIsEmailVerified(); - bool isPhoneVerified = await getIsPhoneVerified(); - bool isIdentityVerified = await getIsIdentityVerified(); + bool? isEmailVerified = await getIsEmailVerified(); + bool? isPhoneVerified = await getIsPhoneVerified(); + bool? isIdentityVerified = await getIsIdentityVerified(); // This method got refactored due my mistake in one little mapping in the migration from no pkid to pkid if (isEmailVerified == false) { @@ -95,6 +94,6 @@ Future saveCorrectVerificationStates( } bool checkEmail(String email) { - String emailValue = email.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String? emailValue = email.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); return validateEmail(emailValue); } diff --git a/app/lib/helpers/vpn_state.dart b/app/lib/helpers/vpn_state.dart index 4346760f..87a12e3f 100644 --- a/app/lib/helpers/vpn_state.dart +++ b/app/lib/helpers/vpn_state.dart @@ -4,7 +4,8 @@ class VpnState { VpnState() { plugin = new YggdrasilPlugin(); } - YggdrasilPlugin plugin; + + YggdrasilPlugin plugin = new YggdrasilPlugin(); bool vpnConnected = false; String ipAddress = ""; diff --git a/app/lib/jrouter.dart b/app/lib/jrouter.dart index f576e022..2f0547ec 100644 --- a/app/lib/jrouter.dart +++ b/app/lib/jrouter.dart @@ -13,13 +13,13 @@ import 'apps/news/news.dart'; class AppInfo { Route route; - App app; + App? app; - AppInfo({this.route, this.app}); + AppInfo({required this.route, this.app}); } class JRouter { - List routes; + List routes = []; init() async { routes = [ @@ -96,14 +96,14 @@ class JRouter { bool emailMustBeVerified(int index) { if (routes[index].app != null) { - return routes[index].app.emailVerificationRequired(); + return routes[index].app!.emailVerificationRequired(); } return false; } bool pinRequired(int index) { if (routes[index].app != null) { - return routes[index].app.pinRequired(); + return routes[index].app!.pinRequired(); } return false; } @@ -138,5 +138,5 @@ class Route { final String path; final Widget view; - Route({this.path, this.name, this.icon, this.view}); + Route({required this.path, required this.name, required this.icon, required this.view}); } diff --git a/app/lib/main.dart b/app/lib/main.dart index c935ed44..626c9bb7 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -7,7 +7,6 @@ import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'helpers/flags.dart'; import 'helpers/kyc_helpers.dart'; @@ -15,7 +14,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); bool initDone = await getInitDone(); - String doubleName = await getDoubleName(); + String? doubleName = await getDoubleName(); await setGlobalValues(); @@ -30,8 +29,8 @@ Future main() async { } Future setGlobalValues() async { - Map email = await getEmail(); - Map phone = await getPhone(); + Map email = await getEmail(); + Map phone = await getPhone(); Map identity = await getIdentity(); Globals().emailVerified.value = (email['sei'] != null); @@ -41,10 +40,10 @@ Future setGlobalValues() async { } class MyApp extends StatelessWidget { - MyApp({this.initDone, this.doubleName, this.registered}); + MyApp({required this.initDone, this.doubleName, required this.registered}); final bool initDone; - final String doubleName; + final String? doubleName; final bool registered; @override diff --git a/app/lib/models/login.dart b/app/lib/models/login.dart index e5e9109f..21a52d42 100644 --- a/app/lib/models/login.dart +++ b/app/lib/models/login.dart @@ -6,19 +6,19 @@ import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; class Login { - String doubleName; - String state; - Scope scope; - String appId; - String appPublicKey; - String randomImageId; - String type; - String randomRoom; - String redirecturl; - bool isMobile; - int created; - String locationId; - bool showWarning; + String? doubleName; + String? state; + Scope? scope; + String? appId; + String? appPublicKey; + String? randomImageId; + String? type; + String? randomRoom; + String? redirectUrl; + bool? isMobile; + int? created; + String? locationId; + bool? showWarning; Login( {this.doubleName, @@ -29,7 +29,7 @@ class Login { this.randomImageId, this.type, this.randomRoom, - this.redirecturl, + this.redirectUrl, this.isMobile, this.created, this.locationId}); @@ -37,9 +37,7 @@ class Login { Login.fromJson(Map json) : doubleName = json['doubleName'], state = json['state'], - scope = (json['scope'] != null && - json['scope'] != "" && - json['scope'] != "null") + scope = (json['scope'] != null && json['scope'] != "" && json['scope'] != "null") ? Scope.fromJson(jsonDecode(json['scope'])) : null, appId = json['appId'], @@ -47,7 +45,7 @@ class Login { randomImageId = json['randomImageId'], type = json['type'], randomRoom = json['randomRoom'], - redirecturl = json['redirecturl'], + redirectUrl = json['redirecturl'], isMobile = json['mobile'] as bool, created = json['created'], locationId = json['locationId']; @@ -55,13 +53,13 @@ class Login { Map toJson() => { 'doubleName': doubleName, 'state': state, - 'scope': scope != null ? scope.toJson() : "", + 'scope': scope != null ? scope?.toJson() : "", 'appId': appId, 'appPublicKey': appPublicKey, 'randomImageId': randomImageId, 'type': type, 'randomRoom': randomRoom, - 'redirecturl': redirecturl, + 'redirecturl': redirectUrl, 'mobile': isMobile, 'created': created, 'locationId': locationId @@ -71,14 +69,11 @@ class Login { Login loginData; if (data['encryptedLoginAttempt'] != null) { - Uint8List decryptedLoginAttempt = await decrypt( - data['encryptedLoginAttempt'], - await getPublicKey(), - await getPrivateKey()); - data['encryptedLoginAttempt'] = - new String.fromCharCodes(decryptedLoginAttempt); + Uint8List pk = await getPublicKey(); + Uint8List sk = await getPrivateKey(); - var decryptedLoginAttemptMap = jsonDecode(data['encryptedLoginAttempt']); + String decryptedLoginAttempt = await decrypt(data['encryptedLoginAttempt'], pk, sk); + dynamic decryptedLoginAttemptMap = jsonDecode(decryptedLoginAttempt); decryptedLoginAttemptMap['type'] = data['type']; decryptedLoginAttemptMap['created'] = data['created']; @@ -92,12 +87,7 @@ class Login { List list = await getLocationIdList(); - if (list.contains(loginData.locationId)) { - loginData.showWarning = false; - } else { - loginData.showWarning = true; - } - + loginData.showWarning = !list.contains(loginData.locationId); return loginData; } } diff --git a/app/lib/models/scope.dart b/app/lib/models/scope.dart index f0791d2f..cee1b1eb 100644 --- a/app/lib/models/scope.dart +++ b/app/lib/models/scope.dart @@ -1,17 +1,17 @@ class Scope { - bool doubleName; - bool user; - bool email; - bool derivedSeed; - bool phone; - bool digitalTwin; - bool identityName; - bool identityDOB; - bool identityGender; - bool identityDocumentMeta; - bool identityCountry; - bool walletAddress; - String walletAddressData; + bool? doubleName; + bool? user; + bool? email; + bool? derivedSeed; + bool? phone; + bool? digitalTwin; + bool? identityName; + bool? identityDOB; + bool? identityGender; + bool? identityDocumentMeta; + bool? identityCountry; + bool? walletAddress; + String? walletAddressData; Scope({this.doubleName, this.email}); diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 76047eae..2d172b3a 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; @@ -11,85 +12,82 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; -Future sendData(String state, data, selectedImageId, - String randomRoom, String appId) async { - return http.post('$threeBotApiUrl/signedAttempt', - body: json.encode({ - 'signedAttempt': await signData( - json.encode({ - 'signedState': state, - 'data': data, - 'selectedImageId': selectedImageId, - 'doubleName': await getDoubleName(), - 'randomRoom': randomRoom, - 'appId': appId - }), - await getPrivateKey()), - 'doubleName': await getDoubleName() - }), +Future sendData( + String state, data, selectedImageId, String randomRoom, String appId) async { + Uri url = Uri.parse('$threeBotApiUrl/signedAttempt'); + print('Sending call: ${url.toString()}'); + + Uint8List sk = await getPrivateKey(); + String jsonData = json.encode({ + 'signedState': state, + 'data': data, + 'selectedImageId': selectedImageId, + 'doubleName': await getDoubleName(), + 'randomRoom': randomRoom, + 'appId': appId + }); + + String signedData = await signData(jsonData, sk); + + return http.post(url, + body: json.encode({'signedAttempt': signedData, 'doubleName': await getDoubleName()}), headers: requestHeaders); } -// Future> generateKeysFromSeedPhrase(seedPhrase) async { -// String entropy = bip39.mnemonicToEntropy(seedPhrase); -// Map key = -// await Sodium.cryptoSignSeedKeypair(_toHex(entropy)); - -// return { -// 'publicKey': base64.encode(key['pk']).toString(), -// 'privateKey': base64.encode(key['sk']).toString() -// }; -// } - -Future addDigitalTwinDerivedPublicKeyToBackend( - name, publicKey, appId) async { - return http.post('$threeBotApiUrl/users/digitaltwin/$name', - body: await signData( - json.encode({'name': name, 'public_key': publicKey, 'app_id': appId}), - await getPrivateKey()), - headers: requestHeaders); +Future addDigitalTwinDerivedPublicKeyToBackend(name, publicKey, appId) async { + Uri url = Uri.parse('$threeBotApiUrl/users/digitaltwin/$name'); + print('Sending call: ${url.toString()}'); + + Uint8List sk = await getPrivateKey(); + String encodedData = json.encode({'name': name, 'public_key': publicKey, 'app_id': appId}); + String signedData = await signData(encodedData, sk); + + return http.post(url, body: signedData, headers: requestHeaders); } Future sendPublicKey(Map data) async { + Uri url = Uri.parse('$threeBotApiUrl/savederivedpublickey'); + print('Sending call: ${url.toString()}'); + String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - String privatekey = await getPrivateKey(); + Uint8List sk = await getPrivateKey(); - Map headers = { - "timestamp": timestamp, - "intention": "post-savederivedpublickey" - }; - String signedHeaders = await signData(jsonEncode(headers), privatekey); + Map headers = {"timestamp": timestamp, "intention": "post-savederivedpublickey"}; + String signedHeaders = await signData(jsonEncode(headers), sk); Map loginRequestHeaders = { 'Content-type': 'application/json', 'Jimber-Authorization': signedHeaders }; - return http.post('$threeBotApiUrl/savederivedpublickey', - body: json.encode(data), headers: loginRequestHeaders); + return http.post(url, body: json.encode(data), headers: loginRequestHeaders); } Future sendProductReservation(Map data) async { - String privatekey = await getPrivateKey(); - String doubleName = await getDoubleName(); + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/productkey'); + print('Sending call: ${url.toString()}'); + + Uint8List sk = await getPrivateKey(); + String? doubleName = await getDoubleName(); - String signedData = await signData(jsonEncode(data), privatekey); + String signedData = await signData(jsonEncode(data), sk); var body = json.encode({"doubleName": doubleName, "data": signedData}); - return await http.put('$threeBotApiUrl/digitaltwin/productkey', - body: body, headers: {'Content-type': 'application/json'}); + return await http.put(url, body: body, headers: {'Content-type': 'application/json'}); } -Future isAppUpToDate() async { +Future isAppUpToDate() async { + Uri url = Uri.parse('$threeBotApiUrl/minimumversion'); + print('Sending call: ${url.toString()}'); + PackageInfo packageInfo = await PackageInfo.fromPlatform(); int currentBuildNumber = int.parse(packageInfo.buildNumber); int minimumBuildNumber = 0; - String jsonResponse = (await http - .get('$threeBotApiUrl/minimumversion', headers: requestHeaders) - .timeout(const Duration(seconds: 3))) - .body; + String jsonResponse = + (await http.get(url, headers: requestHeaders).timeout(const Duration(seconds: 3))).body; + Map minimumVersion = json.decode(jsonResponse); if (Platform.isAndroid) { @@ -102,10 +100,11 @@ Future isAppUpToDate() async { } Future isAppUnderMaintenance() async { - print('$threeBotApiUrl/maintenance'); - Response response = await http - .get('$threeBotApiUrl/maintenance', headers: requestHeaders) - .timeout(const Duration(seconds: 3)); + Uri url = Uri.parse('$threeBotApiUrl/maintenance'); + print('Sending call: ${url.toString()}'); + + Response response = + await http.get(url, headers: requestHeaders).timeout(const Duration(seconds: 3)); if (response.statusCode != 200) { return false; @@ -116,28 +115,25 @@ Future isAppUnderMaintenance() async { } Future cancelLogin(doubleName) { - return http.post('$threeBotApiUrl/users/$doubleName/cancel', - body: null, headers: requestHeaders); -} + Uri url = Uri.parse('$threeBotApiUrl/users/$doubleName/cancel'); + print('Sending call: ${url.toString()}'); -Future getUserInfo(doubleName) { - return http.get('$threeBotApiUrl/users/$doubleName', headers: requestHeaders); + return http.post(url, body: null, headers: requestHeaders); } -Future updateDeviceID(String doubleName, String signedDeviceId) { - return http.post('$threeBotApiUrl/users/$doubleName/deviceid', - body: json.encode({'signed_device_id': signedDeviceId}), - headers: requestHeaders); -} +Future getUserInfo(doubleName) { + Uri url = Uri.parse('$threeBotApiUrl/users/$doubleName'); + print('Sending call: ${url.toString()}'); -Future removeDeviceId(String deviceId) { - return http.delete('$threeBotApiUrl/deviceid/$deviceId', - headers: requestHeaders); + return http.get(url, headers: requestHeaders); } Future finishRegistration( String doubleName, String email, String sid, String publicKey) async { - return http.post('$threeBotApiUrl/mobileregistration', + Uri url = Uri.parse('$threeBotApiUrl/mobileregistration'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: json.encode({ 'doubleName': doubleName + '.3bot', 'sid': sid, @@ -148,47 +144,42 @@ Future finishRegistration( } Future getReservations(String doubleName) { - print('$threeBotApiUrl/digitaltwin/reserve/$doubleName'); - return http.get('$threeBotApiUrl/digitaltwin/reserve/$doubleName', - headers: requestHeaders); + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/reserve/$doubleName'); + print('Sending call: ${url.toString()}'); + + return http.get(url, headers: requestHeaders); } Future getProductKeys(String doubleName) { - print('$threeBotApiUrl/digitaltwin/productkey/$doubleName'); - return http.get('$threeBotApiUrl/digitaltwin/productkey/$doubleName', - headers: requestHeaders); + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/productkey/$doubleName'); + print('Sending call: ${url.toString()}'); + + return http.get(url, headers: requestHeaders); } Future getReservationDetails(String doubleName) { - print('$threeBotApiUrl/digitaltwin/reservation_details/$doubleName'); - return http.get('$threeBotApiUrl/digitaltwin/reservation_details/$doubleName', - headers: requestHeaders); + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/reservation_details/$doubleName'); + print('Sending call: ${url.toString()}'); + + return http.get(url, headers: requestHeaders); } Future getAllProductKeys() { - print('$threeBotApiUrl/digitaltwin/productkeys'); - return http.get('$threeBotApiUrl/digitaltwin/productkeys', - headers: requestHeaders); + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/productkeys'); + print('Sending call: ${url.toString()}'); + + return http.get(url, headers: requestHeaders); } -Future activateDigitalTwin( - String doubleName, String productKey) async { +Future activateDigitalTwin(String doubleName, String productKey) async { + Uri url = Uri.parse('$threeBotApiUrl/digitaltwin/productkey/activate'); + print('Sending call: ${url.toString()}'); + Object jsonObject = {'doubleName': doubleName, 'productKey': productKey}; - String privateKey = await getPrivateKey(); + + Uint8List privateKey = await getPrivateKey(); String signedData = await signData(jsonEncode(jsonObject), privateKey); var body = json.encode({"doubleName": doubleName, "data": signedData}); - return await http.post('$threeBotApiUrl/digitaltwin/productkey/activate', - body: body, headers: {'Content-type': 'application/json'}); + return await http.post(url, body: body, headers: {'Content-type': 'application/json'}); } - -// // TODO Please remove this function, it's only for testing -// Future postReservations(String doubleName, String reservingFor) { -// print('$threeBotApiUrl/digitaltwin/$doubleName/reservations'); -// return http.post('$threeBotApiUrl/digitaltwin/$doubleName/reservations', -// body: { -// 'tx': 'bla', -// 'ReservingUser': doubleName, -// 'ReservedDigitaltwin': reservingFor -// }); -// } diff --git a/app/lib/services/identity_service.dart b/app/lib/services/identity_service.dart index a3377fa4..5d2a7426 100644 --- a/app/lib/services/identity_service.dart +++ b/app/lib/services/identity_service.dart @@ -1,7 +1,3 @@ -import 'dart:convert'; - -import 'package:threebotlogin/helpers/globals.dart'; - String getFullNameOfObject(Map identityName) { String firstName = identityName['first_name'] != null ? identityName['first_name'] : ''; diff --git a/app/lib/services/open_kyc_service.dart b/app/lib/services/open_kyc_service.dart index 1c521483..34a18285 100644 --- a/app/lib/services/open_kyc_service.dart +++ b/app/lib/services/open_kyc_service.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:threebotlogin/app_config.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; @@ -15,48 +15,75 @@ Map requestHeaders = {'Content-type': 'application/json'}; Future getSignedEmailIdentifierFromOpenKYC(String doubleName) async { String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - String privatekey = await getPrivateKey(); + Uint8List sk = await getPrivateKey(); Map payload = {"timestamp": timestamp, "intention": "get-signedemailidentifier"}; - String signedPayload = await signData(jsonEncode(payload), privatekey); + String signedPayload = await signData(jsonEncode(payload), sk); - Map loginRequestHeaders = {'Content-type': 'application/json', 'Jimber-Authorization': signedPayload}; + Map loginRequestHeaders = { + 'Content-type': 'application/json', + 'Jimber-Authorization': signedPayload + }; - return http.get('$openKycApiUrl/verification/retrieve-sei/$doubleName', headers: loginRequestHeaders); + Uri url = Uri.parse('$openKycApiUrl/verification/retrieve-sei/$doubleName'); + print('Sending call: ${url.toString()}'); + + return http.get(url, headers: loginRequestHeaders); } Future getSignedPhoneIdentifierFromOpenKYC(String doubleName) async { String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - String privatekey = await getPrivateKey(); + Uint8List sk = await getPrivateKey(); Map payload = {"timestamp": timestamp, "intention": "get-signedphoneidentifier"}; - String signedPayload = await signData(jsonEncode(payload), privatekey); + String signedPayload = await signData(jsonEncode(payload), sk); + + Map loginRequestHeaders = { + 'Content-type': 'application/json', + 'Jimber-Authorization': signedPayload + }; - Map loginRequestHeaders = {'Content-type': 'application/json', 'Jimber-Authorization': signedPayload}; + Uri url = Uri.parse('$openKycApiUrl/verification/retrieve-spi/$doubleName'); + print('Sending call: ${url.toString()}'); - return http.get('$openKycApiUrl/verification/retrieve-spi/$doubleName', headers: loginRequestHeaders); + return http.get(url, headers: loginRequestHeaders); } Future getSignedIdentityIdentifierFromOpenKYC(String doubleName) async { String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - String privateKey = await getPrivateKey(); + Uint8List sk = await getPrivateKey(); + + Map payload = { + "timestamp": timestamp, + "intention": "get-identity-kyc-data-identifiers" + }; - Map payload = {"timestamp": timestamp, "intention": "get-identity-kyc-data-identifiers"}; + String signedPayload = await signData(jsonEncode(payload), sk); - String signedPayload = await signData(jsonEncode(payload), privateKey); + Map loginRequestHeaders = { + 'Content-type': 'application/json', + 'Jimber-Authorization': signedPayload + }; - Map loginRequestHeaders = {'Content-type': 'application/json', 'Jimber-Authorization': signedPayload}; + Uri url = Uri.parse('$openKycApiUrl/verification/retrieve-sii/$doubleName'); + print('Sending call: ${url.toString()}'); - return http.get('$openKycApiUrl/verification/retrieve-sii/$doubleName', headers: loginRequestHeaders); + return http.get(url, headers: loginRequestHeaders); } Future verifySignedEmailIdentifier(String signedEmailIdentifier) async { - return http.post('$openKycApiUrl/verification/verify-sei', + Uri url = Uri.parse('$openKycApiUrl/verification/verify-sei'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: json.encode({"signedEmailIdentifier": signedEmailIdentifier}), headers: requestHeaders); } Future verifySignedPhoneIdentifier(String signedPhoneIdentifier) async { - return http.post('$openKycApiUrl/verification/verify-spi', + Uri url = Uri.parse('$openKycApiUrl/verification/verify-spi'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: json.encode({"signedPhoneIdentifier": signedPhoneIdentifier}), headers: requestHeaders); } @@ -67,7 +94,10 @@ Future verifySignedIdentityIdentifier( String signedIdentityDocumentMetaIdentifier, String signedIdentityGenderIdentifier, String reference) async { - return http.post('$openKycApiUrl/verification/verify-sii', + Uri url = Uri.parse('$openKycApiUrl/verification/verify-sii'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: json.encode({ "signedIdentityNameIdentifier": signedIdentityNameIdentifier, "signedIdentityCountryIdentifier": signedIdentityCountryIdentifier, @@ -80,93 +110,102 @@ Future verifySignedIdentityIdentifier( } Future sendVerificationEmail() async { - print('$openKycApiUrl/verification/send-email'); - return http.post('$openKycApiUrl/verification/send-email', - body: json.encode({ - 'user_id': await getDoubleName(), - 'email': (await getEmail())['email'], - 'public_key': await getPublicKey(), - }), - headers: requestHeaders); + String encodedBody = json.encode({ + 'user_id': await getDoubleName(), + 'email': (await getEmail())['email'], + 'public_key': await getPublicKey(), + }); + + Uri url = Uri.parse('$openKycApiUrl/verification/send-email'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future sendVerificationSms() async { - print('sms send'); - return http.post('$openKycApiUrl/verification/send-sms', - body: json.encode({ - 'user_id': await getDoubleName(), - 'number': (await getPhone())['phone'], - 'public_key': await getPublicKey(), - }), - headers: requestHeaders); + String encodedBody = json.encode({ + 'user_id': await getDoubleName(), + 'number': (await getPhone())['phone'], + 'public_key': await getPublicKey(), + }); + + Uri url = Uri.parse('$openKycApiUrl/verification/send-sms'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future getShuftiAccessToken() async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - dynamic signedPhoneIdentifier = prefs.getString('signedPhoneIdentifier'); - - if (signedPhoneIdentifier == null) { + Map phoneMap = await getPhone(); + if (phoneMap['signedPhoneIdentifier'] == null) { return; } - print('Getting shufti Access token'); - print('$openKycApiUrl/verification/shufti-access-token'); - return http.post('$openKycApiUrl/verification/shufti-access-token', - body: json.encode({"signedPhoneIdentifier": signedPhoneIdentifier}), headers: requestHeaders); + String encodedBody = json.encode({"signedPhoneIdentifier": phoneMap['signedPhoneIdentifier']}); + + Uri url = Uri.parse('$openKycApiUrl/verification/shufti-access-token'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future sendVerificationIdentity() async { - print('Sending verification identity'); - print('$openKycApiUrl/verification/send-identity'); + bool? isPhoneVerified = await getIsPhoneVerified(); + bool? isEmailVerified = await getIsEmailVerified(); - bool isPhoneVerified = await getIsPhoneVerified(); - bool isEmailVerified = await getIsEmailVerified(); + int level = isPhoneVerified == true && isEmailVerified == true ? 2 : 1; - int level = isPhoneVerified && isEmailVerified ? 2 : 1; + String encodedBody = json.encode({ + 'user_id': await getDoubleName(), + 'kycLevel': (level), + 'public_key': await getPublicKey(), + }); - return http.post('$openKycApiUrl/verification/send-identity', - body: json.encode({ - 'user_id': await getDoubleName(), - 'kycLevel': (level), - 'public_key': await getPublicKey(), - }), - headers: requestHeaders); + Uri url = Uri.parse('$openKycApiUrl/verification/send-identity'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future verifyIdentity(String reference) async { print('Verify Identity'); print('$openKycApiUrl/verification/verify-identity'); + bool? isPhoneVerified = await getIsPhoneVerified(); + bool? isEmailVerified = await getIsEmailVerified(); - bool isPhoneVerified = await getIsPhoneVerified(); - bool isEmailVerified = await getIsEmailVerified(); + int level = isPhoneVerified == true && isEmailVerified == true ? 2 : 1; - int level = isPhoneVerified && isEmailVerified ? 2 : 1; + String encodedBody = json.encode({ + 'user_id': await getDoubleName(), + 'kycLevel': (level), + 'reference': reference, + }); - return http.post('$openKycApiUrl/verification/verify-identity', - body: json.encode({ - 'user_id': await getDoubleName(), - 'kycLevel': (level), - 'reference': reference, - }), - headers: requestHeaders); + Uri url = Uri.parse('$openKycApiUrl/verification/send-identity'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future updateEmailAddressOfUser() async { - print('$threeBotApiUrl/users/change-email'); - - Uri uri = Uri.parse('$threeBotApiUrl/users/change-email'); - String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - String sk = await getPrivateKey(); + Uint8List sk = await getPrivateKey(); Map payload = {"timestamp": timestamp, "intention": "change-email"}; - String signedPayload = await signData(jsonEncode(payload), sk.toString()); + String signedPayload = await signData(jsonEncode(payload), sk); + + Map email = await getEmail(); + + Map loginRequestHeaders = { + 'Content-type': 'application/json', + 'Jimber-Authorization': signedPayload + }; + + String encodedBody = jsonEncode({'username': await getDoubleName(), "email": email['email']}); - var email = await getEmail(); + Uri url = Uri.parse('$threeBotApiUrl/users/change-email'); + print('Sending call: ${url.toString()}'); - Map loginRequestHeaders = {'Content-type': 'application/json', 'Jimber-Authorization': signedPayload}; - return http.post(uri, - headers: loginRequestHeaders, - body: jsonEncode({'username': await getDoubleName(), "email": email['email'].toString()})); + return http.post(url, headers: loginRequestHeaders, body: encodedBody); } diff --git a/app/lib/services/phone_service.dart b/app/lib/services/phone_service.dart index 6fb8d3e4..9fbd3a5b 100644 --- a/app/lib/services/phone_service.dart +++ b/app/lib/services/phone_service.dart @@ -1,7 +1,10 @@ import 'package:http/http.dart' as http; import 'package:http/http.dart'; +// https://api.ipgeolocationapi.com/geolocate Future getCountry() async { - // https://api.ipgeolocationapi.com/geolocate - return await http.get('https://ipinfo.io/country'); -} \ No newline at end of file + Uri url = Uri.parse('https://ipinfo.io/country'); + print('Sending call: ${url.toString()}'); + + return await http.get(url); +} diff --git a/app/lib/services/pkid_service.dart b/app/lib/services/pkid_service.dart index 0f68c3df..b30b4a72 100644 --- a/app/lib/services/pkid_service.dart +++ b/app/lib/services/pkid_service.dart @@ -7,8 +7,6 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import '../app_config.dart'; import 'crypto_service.dart'; - - Future getPkidClient() async { String pKidUrl = AppConfig().pKidUrl(); @@ -18,77 +16,82 @@ Future getPkidClient() async { return FlutterPkid(pKidUrl, keyPair); } - - - Future saveEmailToPKidForMigration() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); - Map email = await getEmail(); - var emailPKidResult = await client.getPKidDoc('email', keyPair); + Map email = await getEmail(); + var emailPKidResult = await client.getPKidDoc('email'); if (!emailPKidResult.containsKey('success') && email['email'] != null) { if (email['sei'] != null) { - return client.setPKidDoc('email', json.encode({'email': email['email'], 'sei': email['sei']}), keyPair); + await client.setPKidDoc('email', json.encode({'email': email['email'], 'sei': email['sei']})); + return; } if (email['email'] != null) { - return client.setPKidDoc('email', json.encode({'email': email['email']}), keyPair); + await client.setPKidDoc('email', json.encode({'email': email['email']})); + return; } } } Future savePhoneToPKidForMigration() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); + + Map phone = await getPhone(); + var phonePKidResult = await client.getPKidDoc('phone'); - Map phone = await getPhone(); - var phonePKidResult = await client.getPKidDoc('phone', keyPair); if (!phonePKidResult.containsKey('success') && phone['phone'] != null) { if (phone['spi'] != null) { - return client.setPKidDoc('phone', json.encode({'phone': phone['phone'], 'spi': phone['spi']}), keyPair); + await client.setPKidDoc('phone', json.encode({'phone': phone['phone'], 'spi': phone['spi']})); + return; } if (phone['phone'] != null) { - return client.setPKidDoc('phone', json.encode({'phone': phone}), keyPair); + await client.setPKidDoc('phone', json.encode({'phone': phone})); + return; } } } Future saveEmailToPKid() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); - Map email = await getEmail(); + Map email = await getEmail(); if (email['sei'] != null) { - return client.setPKidDoc('email', json.encode({'email': email['email'], 'sei': email['sei']}), keyPair); + await client.setPKidDoc('email', json.encode({'email': email['email'], 'sei': email['sei']})); + return; } if (email['email'] != null) { - return client.setPKidDoc('email', json.encode({'email': email['email']}), keyPair); + await client.setPKidDoc('email', json.encode({'email': email['email']})); + return; } } -Future getEmailFromPKid() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - return await client.getPKidDoc('email', keyPair); -} - Future savePhoneToPKid() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + FlutterPkid client = await getPkidClient(); - Map phone = await getPhone(); + Map phone = await getPhone(); if (phone['spi'] != null) { - return client.setPKidDoc('phone', json.encode({'phone': phone['phone'], 'spi': phone['spi']}), keyPair); + await client.setPKidDoc('phone', json.encode({'phone': phone['phone'], 'spi': phone['spi']})); + return; } if (phone['phone'] != null) { - return client.setPKidDoc('phone', json.encode({'phone': phone['phone']}), keyPair); + await client.setPKidDoc('phone', json.encode({'phone': phone['phone']})); + return; } } +Future getEmailFromPKid() async { + FlutterPkid client = await getPkidClient(); + return await client.getPKidDoc('email'); +} + +Future getPhoneFromPkid() async { + FlutterPkid client = await getPkidClient(); + return await client.getPKidDoc('phone'); +} diff --git a/app/lib/services/push_notifications_manager.dart b/app/lib/services/push_notifications_manager.dart deleted file mode 100644 index 1dd4d539..00000000 --- a/app/lib/services/push_notifications_manager.dart +++ /dev/null @@ -1,37 +0,0 @@ -// import 'package:firebase_messaging/firebase_messaging.dart'; - -// import 'logging_service.dart'; - -// LoggingService logger; - -// class FirebaseNotificationListener { -// FirebaseMessaging _firebaseMessaging; -// String token; - -// FirebaseNotificationListener() { -// _firebaseMessaging = FirebaseMessaging(); -// _firebaseMessaging.configure( -// onMessage: (Map message) async { -// logger.log('On message $message'); -// }, -// onLaunch: (Map message) async { -// logger.log('On launch $message'); -// }, -// onResume: (Map message) async { -// logger.log('On resume $message'); -// }, -// ); - -// _firebaseMessaging.requestNotificationPermissions( -// const IosNotificationSettings(sound: true, badge: true, alert: true)); - -// _firebaseMessaging.onIosSettingsRegistered -// .listen((IosNotificationSettings settings) { -// logger.log("Settings registered: $settings"); -// }); -// } - -// getToken() { -// return _firebaseMessaging.getToken(); -// } -// } diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index f779a915..907c92cb 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -131,7 +131,7 @@ Future getIsEmailVerified() async { return prefs.getBool('isEmailVerified'); } -Future> getEmail() async { +Future> getEmail() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return {'email': prefs.getString('email'), 'sei': prefs.getString('signedEmailIdentifier')}; } @@ -174,7 +174,7 @@ Future getIsPhoneVerified() async { return prefs.getBool('isPhoneVerified'); } -Future> getPhone() async { +Future> getPhone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return {'phone': prefs.getString('phone'), 'spi': prefs.getString('signedPhoneIdentifier')}; } @@ -348,10 +348,10 @@ Future getScopePermissions() async { return prefs.getString('scopePermissions'); } -Future savePreviousScopePermissions(String appId, String scopePermissions) async { +Future savePreviousScopePermissions(String appId, String? scopePermissions) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.remove('$appId-scopePreviousPermissions'); - await prefs.setString('$appId-scopePreviousPermissions', scopePermissions); + await prefs.setString('$appId-scopePreviousPermissions', scopePermissions!); } Future getPreviousScopePermissions(String appId) async { @@ -370,7 +370,7 @@ Future saveInitDone() async { prefs.setBool('initDone', true); } -Future getInitDone() async { +Future getInitDone() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); bool? initDone = prefs.getBool('initDone'); @@ -379,7 +379,8 @@ Future getInitDone() async { initDone = prefs.getBool('initDone'); } - return initDone; + bool isInitDone = initDone == true; + return isInitDone; } diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index d673e9b2..1e37ec32 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -15,12 +15,13 @@ import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/screens/login_screen.dart'; import 'package:threebotlogin/screens/warning_screen.dart'; +import 'package:threebotlogin/services/fingerprint_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; class BackendConnection { - IO.Socket socket; + late IO.Socket socket; String doubleName; String threeBotSocketUrl = AppConfig().threeBotSocketUrl(); @@ -51,10 +52,10 @@ class BackendConnection { socket.on('login', (dynamic data) async { print('[login]'); - // var d = new DateTime.fromMillisecondsSinceEpoch(ts, isUtc: true); int currentTimestamp = new DateTime.now().millisecondsSinceEpoch; - if (data['created'] != null && ((currentTimestamp - data['created']) / 1000) > Globals().loginTimeout) { + if (data['created'] != null && + ((currentTimestamp - data['created']) / 1000) > Globals().loginTimeout) { print('We received an expired login attempt, ignoring it.'); return; } @@ -94,102 +95,107 @@ class BackendConnection { } Future emailVerification(BuildContext context) async { - Map email = await getEmail(); - if (email['email'] != null) { - String doubleName = (await getDoubleName()).toLowerCase(); - Response response = await getSignedEmailIdentifierFromOpenKYC(doubleName); + Map email = await getEmail(); - if (response.statusCode != 200) { - return; - } + if (email['email'] == null) { + return; + } - Map body = jsonDecode(response.body); - - dynamic signedEmailIdentifier = body["signed_email_identifier"]; - - if (signedEmailIdentifier != null && signedEmailIdentifier.isNotEmpty) { - Map vsei = jsonDecode((await verifySignedEmailIdentifier(signedEmailIdentifier)).body); - - if (vsei != null && vsei["email"] == email["email"] && vsei["identifier"] == doubleName) { - await setIsEmailVerified(true); - await saveEmail(vsei["email"], signedEmailIdentifier); - - showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.email, - title: "Email verified", - description: "Your email has been verified!", - actions: [ - FlatButton( - child: new Text("Ok"), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } else { - await saveEmail(email["email"], null); - } - } + String doubleName = (await getDoubleName())!.toLowerCase(); + + Response response = await getSignedEmailIdentifierFromOpenKYC(doubleName); + if (response.statusCode != 200) { + return; + } + + Map body = jsonDecode(response.body); + String? signedEmailIdentifier = body["signed_email_identifier"]; + + if (signedEmailIdentifier == null || signedEmailIdentifier.isEmpty) { + await saveEmail(email["email"]!, null); + } + + var vSei = jsonDecode((await verifySignedEmailIdentifier(signedEmailIdentifier!)).body); + if (vSei == null || vSei['email'] != email['email'] || vSei['identifier'] != doubleName) { + return; } + + await setIsEmailVerified(true); + await saveEmail(vSei["email"], signedEmailIdentifier); + + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + image: Icons.email, + title: "Email verified", + description: "Your email has been verified!", + actions: [ + FlatButton( + child: new Text("Ok"), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); } Future phoneVerification(BuildContext context) async { - Map phone = await getPhone(); - if (phone['phone'] != null) { - String doubleName = (await getDoubleName()).toLowerCase(); - Response response = await getSignedPhoneIdentifierFromOpenKYC(doubleName); + Map phone = await getPhone(); - if (response.statusCode != 200) { - return; - } + if (phone['phone'] == null) { + return; + } - Map body = jsonDecode(response.body); - - dynamic signedPhoneIdentifier = body["signed_phone_identifier"]; - - if (signedPhoneIdentifier != null && signedPhoneIdentifier.isNotEmpty) { - Map vspi = jsonDecode((await verifySignedPhoneIdentifier(signedPhoneIdentifier)).body); - - if (vspi != null && vspi["phone"] == phone["phone"] && vspi["identifier"] == doubleName) { - await setIsPhoneVerified(true); - await savePhone(vspi["phone"], signedPhoneIdentifier); - - showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.phone_android, - title: "Phone verified", - description: "Your phone has been verified!", - actions: [ - FlatButton( - child: new Text("Ok"), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } else { - await savePhone(phone["phone"], null); - } - } + String doubleName = (await getDoubleName())!.toLowerCase(); + Response response = await getSignedPhoneIdentifierFromOpenKYC(doubleName); + if (response.statusCode != 200) { + return; } + + Map body = jsonDecode(response.body); + String? signedPhoneIdentifier = body["signed_phone_identifier"]; + if (signedPhoneIdentifier == null || signedPhoneIdentifier.isEmpty) { + await savePhone(phone["phone"]!, null); + } + + var vSpi = jsonDecode((await verifySignedPhoneIdentifier(signedPhoneIdentifier!)).body); + if (vSpi == null || vSpi['phone'] != phone['phone'] || vSpi['identifier'] != doubleName) { + return; + } + + await setIsPhoneVerified(true); + await savePhone(vSpi["phone"], signedPhoneIdentifier); + + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + image: Icons.phone_android, + title: "Phone verified", + description: "Your phone has been verified!", + actions: [ + FlatButton( + child: new Text("Ok"), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); } Future showIdentityMessage(BuildContext context, String type) async { { - if(type == 'unauthorized') { + if (type == 'unauthorized') { return showDialog( context: context, builder: (BuildContext context) => CustomDialog( image: Icons.warning, title: "Identity verify timed out", - description: "Your verification attempt has expired, please retry and finish the flow in under 10 minutes.", + description: + "Your verification attempt has expired, please retry and finish the flow in under 10 minutes.", actions: [ FlatButton( child: new Text("Ok"), @@ -240,9 +246,7 @@ Future showIdentityMessage(BuildContext context, String type) async { } Future identityVerification(String reference) async { - print('Verifying my ID'); - - String doubleName = (await getDoubleName()).toLowerCase(); + String doubleName = (await getDoubleName())!.toLowerCase(); Response response = await getSignedIdentityIdentifierFromOpenKYC(doubleName); if (response.statusCode != 200) { @@ -251,11 +255,12 @@ Future identityVerification(String reference) async { Map identifiersData = json.decode(response.body); - dynamic signedIdentityNameIdentifier = identifiersData["signed_identity_name_identifier"]; - dynamic signedIdentityCountryIdentifier = identifiersData["signed_identity_country_identifier"]; - dynamic signedIdentityDOBIdentifier = identifiersData["signed_identity_dob_identifier"]; - dynamic signedIdentityDocumentMetaIdentifier = identifiersData["signed_identity_document_meta_identifier"]; - dynamic signedIdentityGenderIdentifier = identifiersData["signed_identity_gender_identifier"]; + String signedIdentityNameIdentifier = identifiersData["signed_identity_name_identifier"]; + String signedIdentityCountryIdentifier = identifiersData["signed_identity_country_identifier"]; + String signedIdentityDOBIdentifier = identifiersData["signed_identity_dob_identifier"]; + String signedIdentityDocumentMetaIdentifier = + identifiersData["signed_identity_document_meta_identifier"]; + String signedIdentityGenderIdentifier = identifiersData["signed_identity_gender_identifier"]; if (signedIdentityNameIdentifier.isEmpty || signedIdentityCountryIdentifier.isEmpty || @@ -274,18 +279,17 @@ Future identityVerification(String reference) async { reference)) .body); - Map verifiedSignedIdentityNameIdentifier = + var verifiedSignedIdentityNameIdentifier = jsonDecode(identifiers["signedIdentityNameIdentifierVerified"]); - Map verifiedSignedIdentityCountryIdentifier = + var verifiedSignedIdentityCountryIdentifier = jsonDecode(identifiers["signedIdentityCountryIdentifierVerified"]); - Map verifiedSignedIdentityDOBIdentifier = + var verifiedSignedIdentityDOBIdentifier = jsonDecode(identifiers["signedIdentityDOBIdentifierVerified"]); - Map verifiedSignedIdentityDocumentMetaIdentifier = + var verifiedSignedIdentityDocumentMetaIdentifier = jsonDecode(identifiers["signedIdentityDocumentMetaIdentifierVerified"]); - Map verifiedSignedIdentityGenderIdentifier = + var verifiedSignedIdentityGenderIdentifier = jsonDecode(identifiers["signedIdentityGenderIdentifierVerified"]); - if (verifiedSignedIdentityNameIdentifier == null || verifiedSignedIdentityNameIdentifier['identifier'].toString() != doubleName) { return; @@ -329,68 +333,72 @@ Future identityVerification(String reference) async { } Future openLogin(BuildContext context, Login loginData, BackendConnection backendConnection) async { - String messageType = loginData.type; + String? messageType = loginData.type; + + if (messageType == null || messageType != 'login' || loginData.isMobile == true) { + return; + } - if (messageType == 'login' && !loginData.isMobile) { - String pin = await getPin(); + String? pin = await getPin(); - Events().emit(CloseAuthEvent(close: true)); + Events().emit(CloseAuthEvent(close: true)); - bool authenticated = await Navigator.push( + bool? authenticated = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AuthenticationScreen( + correctPin: pin!, userMessage: "Sign your attempt.", loginData: loginData), + ), + ); + + if (authenticated == null || authenticated == false) { + return; + } + + if (loginData.showWarning == true) { + bool? warningScreenCompleted = await Navigator.push( context, MaterialPageRoute( - builder: (context) => - AuthenticationScreen(correctPin: pin, userMessage: "sign your attempt.", loginData: loginData), + builder: (context) => WarningScreen(), ), ); - if (authenticated != null && authenticated) { - if (loginData.showWarning) { - bool warningScreenCompleted = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => WarningScreen(), - ), - ); + if (warningScreenCompleted == null || !warningScreenCompleted) { + return; + } - if (warningScreenCompleted == null || !warningScreenCompleted) { - return; - } + await saveLocationId(loginData.locationId!); + } - await saveLocationId(loginData.locationId); - } + backendConnection.leaveRoom(loginData.doubleName); - backendConnection.leaveRoom(loginData.doubleName); + bool? loggedIn = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LoginScreen(loginData), + ), + ); - bool loggedIn = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LoginScreen(loginData), - ), - ); - - if (loggedIn != null && loggedIn) { - backendConnection.joinRoom(loginData.doubleName); - - await showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.check, - title: 'Logged in', - description: 'You are now logged in. Please return to your browser.', - actions: [ - FlatButton( - child: Text('Ok'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - } else { - backendConnection.joinRoom(loginData.doubleName); - } - } + if (loggedIn == null || loggedIn == false) { + backendConnection.joinRoom(loginData.doubleName); } + + backendConnection.joinRoom(loginData.doubleName); + + await showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + image: Icons.check, + title: 'Logged in', + description: 'You are now logged in. Please return to your browser.', + actions: [ + FlatButton( + child: Text('Ok'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); } diff --git a/app/lib/services/tools_service.dart b/app/lib/services/tools_service.dart index 7c7b45d8..dc5f20c3 100644 --- a/app/lib/services/tools_service.dart +++ b/app/lib/services/tools_service.dart @@ -15,10 +15,10 @@ String randomString(int strlen) { return result; } -bool validateEmail(String value) { +bool validateEmail(String? value) { RegExp regex = new RegExp( r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$"); - return regex.hasMatch(value); + return regex.hasMatch(value.toString()); } bool validateSeedWords(String seed, String confirmationWords) { @@ -39,12 +39,12 @@ bool validateSeedWords(String seed, String confirmationWords) { bool validateDoubleName(String value) { Pattern pattern = r'^[a-zA-Z0-9]+$'; - RegExp regex = new RegExp(pattern); + RegExp regex = new RegExp(pattern.toString()); if (!regex.hasMatch(value)) { return false; } - + return true; } @@ -60,3 +60,9 @@ bool isJson(String str) { } return true; } + +extension BoolParsing on String { + bool parseBool() { + return this.toLowerCase() == 'true'; + } +} \ No newline at end of file diff --git a/app/lib/services/uni_link_service.dart b/app/lib/services/uni_link_service.dart index 86061db7..45025132 100644 --- a/app/lib/services/uni_link_service.dart +++ b/app/lib/services/uni_link_service.dart @@ -16,51 +16,53 @@ class UniLinkService { Uri link = e.link; BuildContext context = e.context; - if (link != null) { - String jsonScope = link.queryParameters['scope']; - String state = link.queryParameters['state']; + String? jsonScope = link.queryParameters['scope']; + String? state = link.queryParameters['state']; - if (jsonScope == null && (state == null || state == "undefined")) { - return; - } + if (jsonScope == null && (state == null || state == "undefined")) { + return; } Login login = queryParametersToLogin(link.queryParameters); - String previousState = await getPreviousState(); + String? previousState = await getPreviousState(); if (login.state == previousState) { return; } - String pin = await getPin(); + String? pin = await getPin(); + + bool? authenticated = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AuthenticationScreen(correctPin: pin!, userMessage: "sign your attempt."), + ), + ); + + if (authenticated == null || authenticated == false) { + return; + } - bool authenticated = await Navigator.push( + bool? loggedIn = await Navigator.push( context, MaterialPageRoute( - builder: (context) => AuthenticationScreen( - correctPin: pin, userMessage: "sign your attempt."), + builder: (context) => LoginScreen(login), ), ); - if (authenticated != null && authenticated) { - bool loggedIn = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => LoginScreen(login), - ), - ); + if (loggedIn == null || loggedIn == false) { + return; + } - if (loggedIn != null && loggedIn) { - bool stateSaved = await savePreviousState(login.state); + bool stateSaved = await savePreviousState(login.state.toString()); - if (stateSaved) { - if (Platform.isAndroid) { - await SystemNavigator.pop(); - } else if (Platform.isIOS) { - bool didRedirect = await Redirection.redirect(); - print(didRedirect); - } - } + if (stateSaved) { + if (Platform.isAndroid) { + await SystemNavigator.pop(); + } else if (Platform.isIOS) { + bool didRedirect = await Redirection.redirect(); + print(didRedirect); } } } @@ -76,5 +78,5 @@ Login queryParametersToLogin(Map map) { : null, appId: map['appId'], appPublicKey: map['appPublicKey'], - redirecturl: map['redirecturl']); + redirectUrl: map['redirecturl']); } diff --git a/app/lib/widgets/custom_dialog.dart b/app/lib/widgets/custom_dialog.dart index 0271ec13..f70975dc 100644 --- a/app/lib/widgets/custom_dialog.dart +++ b/app/lib/widgets/custom_dialog.dart @@ -2,22 +2,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class CustomDialog extends StatefulWidget { - //@todo this is used for everything, just seems like a very bad idea. Make dialogs for the seperate things. Maybe a popup dialog with info ok/cancel and other dialogs for eg pin entry - final String description; - final Widget widgetDescription; - final List actions; + final String? description; + final Widget? widgetDescription; + final List? actions; final String title; final IconData image; final dynamic hiddenaction; CustomDialog({ - @required this.title, - @required this.description, + required this.title, + this.description, this.widgetDescription, this.actions, this.image = Icons.person, this.hiddenaction, }); + show(context) { return showDialog( context: context, @@ -120,8 +120,7 @@ class _CustomDialogState extends State { card(context) { return ConstrainedBox( - constraints: - BoxConstraints(maxHeight: double.infinity, maxWidth: double.infinity), + constraints: BoxConstraints(maxHeight: double.infinity, maxWidth: double.infinity), child: Container( padding: EdgeInsets.only(top: 30.0 + 20.0), margin: EdgeInsets.only(top: 30.0), @@ -159,14 +158,14 @@ class _CustomDialogState extends State { padding: EdgeInsets.symmetric(horizontal: 5.0), child: (widget.widgetDescription == null) ? Text( - widget.description, + widget.description!, textAlign: TextAlign.center, ) : widget.widgetDescription, ), ), SizedBox(height: 24.0), - widget.actions != null && widget.actions.length > 0 + widget.actions != null && widget.actions!.length > 0 ? Container( decoration: new BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, @@ -184,7 +183,7 @@ class _CustomDialogState extends State { ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: widget.actions, + children: widget.actions!, ), ) : Container() diff --git a/app/lib/widgets/error_widget.dart b/app/lib/widgets/error_widget.dart index e68e0c57..105ed9e9 100644 --- a/app/lib/widgets/error_widget.dart +++ b/app/lib/widgets/error_widget.dart @@ -12,10 +12,6 @@ Widget getErrorWidget(BuildContext context, FlutterErrorDetails error) { padding: const EdgeInsets.all(16.0), child: Text( "Oops something went wrong.", - style: Theme.of(context) - .textTheme - .title - .copyWith(color: Colors.white), ), ), ), @@ -24,15 +20,10 @@ Widget getErrorWidget(BuildContext context, FlutterErrorDetails error) { ), Center( child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), child: Text( 'Please restart the application. If this error persists, please contact support.', textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .title - .copyWith(color: Colors.white), ), ), ), diff --git a/app/lib/widgets/image_button.dart b/app/lib/widgets/image_button.dart index 22d968c3..48b70581 100644 --- a/app/lib/widgets/image_button.dart +++ b/app/lib/widgets/image_button.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class ImageButton extends StatefulWidget { final imageId; final selectedImageId; final callback; - ImageButton(this.imageId, this.selectedImageId, this.callback, {Key key}) + ImageButton(this.imageId, this.selectedImageId, this.callback, {required Key key}) : super(key: key); _ImageButtonState createState() => _ImageButtonState(); diff --git a/app/lib/widgets/layout_drawer.dart b/app/lib/widgets/layout_drawer.dart index ada0b28f..52bfba23 100644 --- a/app/lib/widgets/layout_drawer.dart +++ b/app/lib/widgets/layout_drawer.dart @@ -12,7 +12,7 @@ import 'package:convert/convert.dart'; import '../app_config.dart'; class LayoutDrawer extends StatefulWidget { - LayoutDrawer({@required this.titleText, @required this.content}); + LayoutDrawer({required this.titleText, required this.content}); final String titleText; final Widget content; diff --git a/app/lib/widgets/phone_widget.dart b/app/lib/widgets/phone_widget.dart index 3c0d8600..7ee3b6fa 100644 --- a/app/lib/widgets/phone_widget.dart +++ b/app/lib/widgets/phone_widget.dart @@ -9,6 +9,7 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/phone_service.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'custom_dialog.dart'; @@ -50,7 +51,7 @@ phoneSendDialog(context) { class PhoneAlertDialog extends StatefulWidget { final String defaultCountryCode; - const PhoneAlertDialog({Key key, this.defaultCountryCode}) : super(key: key); + const PhoneAlertDialog({Key? key, required this.defaultCountryCode}) : super(key: key); @override State createState() { @@ -59,8 +60,8 @@ class PhoneAlertDialog extends StatefulWidget { } class PhoneAlertDialogState extends State { - bool valid; - String verificationPhoneNumber; + bool valid = false; + String verificationPhoneNumber = ''; @override void initState() { @@ -128,7 +129,7 @@ class PhoneAlertDialogState extends State { } Future wantToVerifyNow() async { - return showDialog( + return await showDialog( context: context, barrierDismissible: false, builder: (BuildContext dialogContext) => CustomDialog( @@ -160,9 +161,8 @@ class PhoneAlertDialogState extends State { savePhone(verificationPhoneNumber, null); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('phone', json.encode({'phone': verificationPhoneNumber}), keyPair); + FlutterPkid client = await getPkidClient(); + client.setPKidDoc('phone', json.encode({'phone': verificationPhoneNumber})); wantToVerifyNow(); } diff --git a/app/lib/widgets/pin_field.dart b/app/lib/widgets/pin_field.dart index d9116e52..4b1a5bd9 100644 --- a/app/lib/widgets/pin_field.dart +++ b/app/lib/widgets/pin_field.dart @@ -6,10 +6,10 @@ class PinField extends StatefulWidget { final int pinLength = 4; final callback; final callbackParam; - final Function callbackFunction; + final Function? callbackFunction; PinField( - {Key key, + {Key? key, @required this.callback, this.callbackParam, this.callbackFunction}) @@ -22,11 +22,11 @@ class _PinFieldState extends State { void initState() { super.initState(); if (widget.callbackFunction != null) { - widget.callbackFunction(); + widget.callbackFunction!(); } } - List input = List(); + List input = []; Widget buildTextField(int i, BuildContext context) { const double maxSize = 7; @@ -48,9 +48,9 @@ class _PinFieldState extends State { double height = MediaQuery.of(context).size.height; if (buttonText == 'OK') - onPressedMethod = input.length >= widget.pinLength ? () => onOk() : null; + onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : null)!; if (buttonText == 'C') - onPressedMethod = input.length >= 1 ? () => onClear() : null; + onPressedMethod = (input.length >= 1 ? () => onClear() : null)!; return Container( padding: EdgeInsets.only(top: height / 136, bottom: height / 136), child: Center( @@ -86,12 +86,12 @@ class _PinFieldState extends State { if (buttonText == 'C') return buildNumberPin(possibleInput[i], context, backgroundColor: - input.length >= 1 ? Colors.yellow[700] : Colors.yellow[200]); + input.length >= 1 ? Colors.yellow.shade700 : Colors.yellow.shade200); else if (buttonText == 'OK') return buildNumberPin(possibleInput[i], context, backgroundColor: input.length >= widget.pinLength - ? Colors.green[600] - : Colors.green[100]); + ? Colors.green.shade600 + : Colors.green.shade100); else return buildNumberPin(possibleInput[i], context, backgroundColor: HexColor("#0a73b8")); }); @@ -152,7 +152,7 @@ class _PinFieldState extends State { widget.callback(pin); } setState(() { - input = List(); + input = []; }); } diff --git a/app/lib/widgets/preference_dialog.dart b/app/lib/widgets/preference_dialog.dart index 08badd13..6e70307a 100644 --- a/app/lib/widgets/preference_dialog.dart +++ b/app/lib/widgets/preference_dialog.dart @@ -6,13 +6,12 @@ import 'package:threebotlogin/apps/wallet/wallet_config.dart'; import 'package:threebotlogin/models/scope.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/widgets/custom_dialog.dart'; class PreferenceDialog extends StatefulWidget { - final Scope scope; - final String appId; - final Function callback; - final String type; + final Scope? scope; + final String? appId; + final Function? callback; + final String? type; PreferenceDialog({this.scope, this.appId, this.callback, this.type}); @@ -22,13 +21,13 @@ class PreferenceDialog extends StatefulWidget { class _PreferenceDialogState extends State { bool _canRender = false; - Map scopeAsMap; - Map previousSelectedScope; + Map scopeAsMap = {}; + Map previousSelectedScope = {}; - List wallets; - List> _menuItems; + List wallets = []; + List> _menuItems = []; - String _selectedItem; + String _selectedItem = ''; var config = WalletConfig(); @@ -49,27 +48,31 @@ class _PreferenceDialogState extends State { Future _startup() async { if (widget.scope != null) { - scopeAsMap = widget.scope.toJson(); // Scope we received from the application the users wants to log into. + scopeAsMap = widget.scope! + .toJson(); // Scope we received from the application the users wants to log into. - String previousScopePermissions = - await getPreviousScopePermissions(widget.appId); // Scope from our history based on the appId. + String? previousScopePermissions = await getPreviousScopePermissions( + widget.appId!); // Scope from our history based on the appId. Map previousScopePermissionsObject; if (previousScopePermissions != null) { previousScopePermissionsObject = jsonDecode(previousScopePermissions); } else { - previousScopePermissionsObject = widget.scope.toJson(); - await savePreviousScopePermissions(widget.appId, jsonEncode(previousScopePermissionsObject)); + previousScopePermissionsObject = widget.scope!.toJson(); + await savePreviousScopePermissions( + widget.appId!, jsonEncode(previousScopePermissionsObject)); } if (!scopeIsEqual(scopeAsMap, previousScopePermissionsObject)) { - previousScopePermissionsObject = widget.scope.toJson(); - await savePreviousScopePermissions(widget.appId, jsonEncode(previousScopePermissionsObject)); + previousScopePermissionsObject = widget.scope!.toJson(); + await savePreviousScopePermissions( + widget.appId!, jsonEncode(previousScopePermissionsObject)); } - previousSelectedScope = (previousScopePermissionsObject == null) ? scopeAsMap : previousScopePermissionsObject; + previousSelectedScope = + (previousScopePermissionsObject == null) ? scopeAsMap : previousScopePermissionsObject; } else { - await savePreviousScopePermissions(widget.appId, null); + await savePreviousScopePermissions(widget.appId!, null); } } @@ -99,7 +102,7 @@ class _PreferenceDialogState extends State { void toggleScope(String scopeItem, value) async { previousSelectedScope[scopeItem] = value; - await savePreviousScopePermissions(widget.appId, jsonEncode(previousSelectedScope)); + await savePreviousScopePermissions(widget.appId!, jsonEncode(previousSelectedScope)); setState(() {}); } @@ -107,20 +110,19 @@ class _PreferenceDialogState extends State { void initializeDropDown() { getWallets().then((value) { setState(() { - if (value != null && value.length != 0) { + if (value.length != 0) { wallets = value; - if(wallets.length != 0){ + if (wallets.length != 0) { _selectedItem = wallets[0].address; toggleScope('walletAddressData', _selectedItem); _menuItems = List.generate( wallets.length, - (i) => DropdownMenuItem( + (i) => DropdownMenuItem( value: wallets[i].address, child: Text("${wallets[i].name}"), ), ); - } - else { + } else { _menuItems = []; } } @@ -166,14 +168,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "${scopeItem.toUpperCase()}" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -201,14 +204,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "${scopeItem.toUpperCase()}" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -231,7 +235,7 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); @@ -253,14 +257,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "PHONE NUMBER" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), decoration: BoxDecoration( @@ -279,7 +284,7 @@ class _PreferenceDialogState extends State { break; case "derivedSeed": return FutureBuilder( - future: getDerivedSeed(widget.appId), + future: getDerivedSeed(widget.appId!), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Container( @@ -294,14 +299,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "${scopeItem.toUpperCase()}" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -329,14 +335,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "NAME (IDENTITY)" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -364,14 +371,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "DATE OF BIRTH (IDENTITY)" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -399,14 +407,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "GENDER (IDENTITY)" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -434,14 +443,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "DOCUMENT META DATA (IDENTITY)" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -469,14 +479,15 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( "COUNTRY (IDENTITY)" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.black), ), ), ); @@ -491,7 +502,7 @@ class _PreferenceDialogState extends State { return FutureBuilder( future: getWallets(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData || wallets == null) { + if (!snapshot.hasData) { return Container( decoration: BoxDecoration( border: Border( @@ -507,9 +518,12 @@ class _PreferenceDialogState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${scopeItem.toUpperCase()}" + (mandatory ? " *" : ""), + "${scopeItem.toUpperCase()}" + + (mandatory ? " *" : ""), style: TextStyle( - fontWeight: FontWeight.bold, color: Colors.black, fontSize: 16), + fontWeight: FontWeight.bold, + color: Colors.black, + fontSize: 16), ), Icon( Icons.warning, @@ -531,7 +545,7 @@ class _PreferenceDialogState extends State { )); } - if (wallets != null && _selectedItem != null) { + if (wallets != null) { return Container( decoration: BoxDecoration( border: Border( @@ -552,14 +566,16 @@ class _PreferenceDialogState extends State { value: (previousSelectedScope[scopeItem] == null) ? mandatory : previousSelectedScope[scopeItem], - onChanged: ((mandatory == null || mandatory == true) + onChanged: ((mandatory == true) ? null : (value) { toggleScope(scopeItem, value); }), title: Text( - "${scopeItem.toUpperCase()}" + (mandatory ? " *" : ""), - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black), + "${scopeItem.toUpperCase()}" + + (mandatory ? " *" : ""), + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.black), ), )) ], @@ -578,7 +594,7 @@ class _PreferenceDialogState extends State { onChanged: (value) { setState(() { toggleScope('walletAddressData', value); - _selectedItem = value; + _selectedItem = value!; }); }), )) diff --git a/app/lib/widgets/reusable_text_field_step.dart b/app/lib/widgets/reusable_text_field_step.dart index 143f117e..ef5e465d 100644 --- a/app/lib/widgets/reusable_text_field_step.dart +++ b/app/lib/widgets/reusable_text_field_step.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; class ReuseableTextFieldStep extends StatelessWidget { ReuseableTextFieldStep( - {@required this.titleText, - @required this.labelText, - @required this.focusNode, - @required this.controller, - @required this.typeText, - @required this.errorStepperText, + {required this.titleText, + required this.labelText, + required this.focusNode, + required this.controller, + required this.typeText, + required this.errorStepperText, this.suffixText}); final String titleText; @@ -15,7 +15,7 @@ class ReuseableTextFieldStep extends StatelessWidget { final String errorStepperText; final FocusNode focusNode; final TextEditingController controller; - final String suffixText; + final String? suffixText; final TextInputType typeText; @override diff --git a/app/lib/widgets/reusable_text_step.dart b/app/lib/widgets/reusable_text_step.dart index b338dc09..1b9e4eda 100644 --- a/app/lib/widgets/reusable_text_step.dart +++ b/app/lib/widgets/reusable_text_step.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; class ReuseableTextStep extends StatelessWidget { ReuseableTextStep( - {@required this.titleText, - @required this.extraText, - @required this.errorStepperText}); + {required this.titleText, + required this.extraText, + required this.errorStepperText}); final String titleText; final String extraText; From 8335bd55e967fcf992e188aea7262d3897696860 Mon Sep 17 00:00:00 2001 From: Lennert Date: Fri, 7 Jan 2022 16:59:34 +0100 Subject: [PATCH 05/75] Solved screens --- app/lib/app_config.dart | 2 +- app/lib/events/uni_link_event.dart | 4 +- app/lib/screens/authentication_screen.dart | 59 +- app/lib/screens/change_pin_screen.dart | 10 +- app/lib/screens/error_screen.dart | 4 +- app/lib/screens/home_screen.dart | 67 +- .../screens/identity_verification_screen.dart | 223 ++- app/lib/screens/init_screen.dart | 20 +- app/lib/screens/login_screen.dart | 99 +- app/lib/screens/main_screen.dart | 24 +- .../screens/mobile_registration_screen.dart | 66 +- app/lib/screens/planetary_network_screen.dart | 2 +- app/lib/screens/preference_screen.dart | 42 +- app/lib/screens/recover_screen.dart | 79 +- app/lib/screens/reservation_screen.dart | 1634 ++++++++--------- app/lib/screens/successful_screen.dart | 2 +- app/lib/screens/unregistered_screen.dart | 4 +- app/lib/services/3bot_service.dart | 2 +- app/lib/services/uni_link_service.dart | 4 +- app/lib/widgets/image_button.dart | 2 +- app/pubspec.yaml | 1 + 21 files changed, 1182 insertions(+), 1168 deletions(-) diff --git a/app/lib/app_config.dart b/app/lib/app_config.dart index 7454f475..99c49ebf 100644 --- a/app/lib/app_config.dart +++ b/app/lib/app_config.dart @@ -19,7 +19,7 @@ class AppConfig extends EnvConfig { } } - String? baseUrl() { + String baseUrl() { return appConfig.baseUrl(); } diff --git a/app/lib/events/uni_link_event.dart b/app/lib/events/uni_link_event.dart index 35a0708b..26b3d0ff 100644 --- a/app/lib/events/uni_link_event.dart +++ b/app/lib/events/uni_link_event.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; class UniLinkEvent { - Uri link; - BuildContext context; + Uri? link; + BuildContext? context; UniLinkEvent(this.link, this.context); } diff --git a/app/lib/screens/authentication_screen.dart b/app/lib/screens/authentication_screen.dart index d31f6c55..c63a65e8 100644 --- a/app/lib/screens/authentication_screen.dart +++ b/app/lib/screens/authentication_screen.dart @@ -15,10 +15,11 @@ class AuthenticationScreen extends StatefulWidget { final int pinLength = 4; final String correctPin; final String userMessage; - final Login loginData; + final Login? loginData; @override - AuthenticationScreen({this.correctPin, this.userMessage, this.loginData}); + AuthenticationScreen( + {required this.correctPin, required this.userMessage, this.loginData}); @override AuthenticationScreenState createState() => AuthenticationScreenState(); @@ -27,7 +28,7 @@ class AuthenticationScreen extends StatefulWidget { class AuthenticationScreenState extends State { int timeout = 30000; Globals globals = Globals(); - Timer timer; + late Timer timer; void initState() { super.initState(); @@ -38,7 +39,7 @@ class AuthenticationScreenState extends State { } }); - if (widget.loginData != null && !widget.loginData.isMobile) { + if (widget.loginData != null && widget.loginData!.isMobile == false) { const oneSec = const Duration(seconds: 1); print('Starting timer ... '); @@ -47,7 +48,7 @@ class AuthenticationScreenState extends State { }); } - WidgetsBinding.instance.addPostFrameCallback((_) => checkFingerprint()); + WidgetsBinding.instance?.addPostFrameCallback((_) => checkFingerprint()); } timeoutTimer() async { @@ -56,11 +57,10 @@ class AuthenticationScreenState extends State { return; } - int created = widget.loginData.created; + int? created = widget.loginData!.created; int currentTimestamp = new DateTime.now().millisecondsSinceEpoch; - if (created != null && - ((currentTimestamp - created) / 1000) > Globals().loginTimeout) { + if (created != null && ((currentTimestamp - created) / 1000) > Globals().loginTimeout) { timer.cancel(); await showDialog( @@ -68,8 +68,7 @@ class AuthenticationScreenState extends State { builder: (BuildContext context) => CustomDialog( image: Icons.timer, title: 'Login attempt expired', - description: - 'Your login attempt has expired, please request a new one in your browser.', + description: 'Your login attempt has expired, please request a new one in your browser.', actions: [ FlatButton( child: Text('Ok'), @@ -92,9 +91,9 @@ class AuthenticationScreenState extends State { } checkFingerprint() async { - bool isFingerprintEnabled = await getFingerprint(); + bool? isFingerprintEnabled = await getFingerprint(); - if (isFingerprintEnabled) { + if (isFingerprintEnabled == true) { bool isAuthenticated = await authenticate(); if (isAuthenticated) { @@ -103,7 +102,7 @@ class AuthenticationScreenState extends State { } } - List input = List(); + List input = []; Widget buildTextField(int i, BuildContext context) { const double maxSize = 7; @@ -125,9 +124,8 @@ class AuthenticationScreenState extends State { double height = MediaQuery.of(context).size.height; if (buttonText == 'OK') - onPressedMethod = input.length >= widget.pinLength ? () => onOk() : null; - if (buttonText == 'C') - onPressedMethod = input.length >= 1 ? () => onClear() : null; + onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : null)!; + if (buttonText == 'C') onPressedMethod = (input.length >= 1 ? () => onClear() : null)!; return Container( padding: EdgeInsets.only(top: height / 136, bottom: height / 136), child: Center( @@ -144,31 +142,16 @@ class AuthenticationScreenState extends State { } Widget generateNumbers(BuildContext context) { - List possibleInput = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - 'C', - '0', - 'OK' - ]; + List possibleInput = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', 'OK']; List pins = List.generate(possibleInput.length, (int i) { String buttonText = possibleInput[i]; if (buttonText == 'C') return buildNumberPin(possibleInput[i], context, - backgroundColor: - input.length >= 1 ? Colors.yellow[700] : Colors.yellow[200]); + backgroundColor: input.length >= 1 ? Colors.yellow.shade700 : Colors.yellow.shade200); else if (buttonText == 'OK') return buildNumberPin(possibleInput[i], context, - backgroundColor: input.length >= widget.pinLength - ? Colors.green[600] - : Colors.green[100]); + backgroundColor: + input.length >= widget.pinLength ? Colors.green.shade600 : Colors.green.shade100); else return buildNumberPin(possibleInput[i], context, backgroundColor: HexColor("#0a73b8")); }); @@ -254,8 +237,7 @@ class AuthenticationScreenState extends State { int currentTime = new DateTime.now().millisecondsSinceEpoch; if (globals.incorrectPincodeAttempts >= 3 && - (globals.tooManyAuthenticationAttempts && - globals.lockedUntill < currentTime)) { + (globals.tooManyAuthenticationAttempts && globals.lockedUntill < currentTime)) { globals.tooManyAuthenticationAttempts = false; globals.lockedUntill = 0; globals.incorrectPincodeAttempts = 0; @@ -274,8 +256,7 @@ class AuthenticationScreenState extends State { var dialog; if (globals.incorrectPincodeAttempts >= 3 || - (globals.tooManyAuthenticationAttempts && - globals.lockedUntill >= currentTime)) { + (globals.tooManyAuthenticationAttempts && globals.lockedUntill >= currentTime)) { if (!globals.tooManyAuthenticationAttempts) { globals.tooManyAuthenticationAttempts = true; globals.lockedUntill = currentTime + timeout; diff --git a/app/lib/screens/change_pin_screen.dart b/app/lib/screens/change_pin_screen.dart index a4c4a5ad..73d56e23 100644 --- a/app/lib/screens/change_pin_screen.dart +++ b/app/lib/screens/change_pin_screen.dart @@ -5,7 +5,7 @@ import 'package:threebotlogin/widgets/pin_field.dart'; class ChangePinScreen extends StatefulWidget { final currentPin; - final bool hideBackButton; + final bool? hideBackButton; ChangePinScreen({this.currentPin, this.hideBackButton}); @@ -15,8 +15,8 @@ class ChangePinScreen extends StatefulWidget { enum _State { CurrentPin, CurrentPinWrong, NewPinWrong, NewPin, Confirm, Done } class _ChangePinScreenState extends State { - String newPin; - _State state; + String newPin = ''; + _State? state; _ChangePinScreenState() { state = _State.NewPin; @@ -46,7 +46,7 @@ class _ChangePinScreenState extends State { ? Text("Choose your pincode") : Text("Change pincode"), elevation: 0.0, - automaticallyImplyLeading: !widget.hideBackButton), + automaticallyImplyLeading: widget.hideBackButton == false), body: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, @@ -65,7 +65,7 @@ class _ChangePinScreenState extends State { ), ), onWillPop: () { - if (state != _State.Done && widget.hideBackButton) { + if (state != _State.Done && widget.hideBackButton == true) { return Future(() => false); } return Future(() => true); diff --git a/app/lib/screens/error_screen.dart b/app/lib/screens/error_screen.dart index 95fb73e0..895a7174 100644 --- a/app/lib/screens/error_screen.dart +++ b/app/lib/screens/error_screen.dart @@ -3,10 +3,10 @@ import 'package:package_info/package_info.dart'; import 'package:threebotlogin/helpers/globals.dart'; class ErrorScreen extends StatefulWidget { - final Widget errorScreen; + final Widget? errorScreen; final String errorMessage; - ErrorScreen({Key key, this.errorScreen, this.errorMessage = ''}) + ErrorScreen({Key? key, this.errorScreen, this.errorMessage = ''}) : super(key: key); _ErrorScreenState createState() => _ErrorScreenState(); diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 44350d93..cb2eebc8 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -28,41 +28,42 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:uni_links/uni_links.dart'; -/* Screen shows tabbar and all pages defined in router.dart */ +/* Screen shows tab bar and all pages defined in router.dart */ class HomeScreen extends StatefulWidget { - final String initialLink; - final BackendConnection backendConnection; + final String? initialLink; + final BackendConnection? backendConnection; HomeScreen({this.initialLink, this.backendConnection}); _HomeScreenState createState() => _HomeScreenState(); } -class _HomeScreenState extends State with WidgetsBindingObserver, SingleTickerProviderStateMixin { +class _HomeScreenState extends State + with WidgetsBindingObserver, SingleTickerProviderStateMixin { Globals globals = Globals(); - StreamSubscription _sub; - String initialLink; + StreamSubscription? _sub; + String? initialLink; bool timeoutExpiredInBackground = true; bool pinCheckOpen = false; int lastCheck = 0; final int pinCheckTimeout = 60000 * 5; _HomeScreenState() { - globals.tabController = - NoAnimationTabController(initialIndex: 0, length: Globals().router.routes.length, vsync: this); + globals.tabController = NoAnimationTabController( + initialIndex: 0, length: Globals().router.routes.length, vsync: this); //Events().onEvent(FfpBrowseEvent().runtimeType, activateFfpTab); globals.tabController.addListener(_handleTabSelection); } void checkPinAndNavigateIfSuccess(int indexIfAuthIsSuccess) async { - String pin = await getPin(); + String? pin = await getPin(); pinCheckOpen = true; - bool authenticated = await Navigator.push( + bool? authenticated = await Navigator.push( context, MaterialPageRoute( builder: (context) => AuthenticationScreen( - correctPin: pin, + correctPin: pin!, userMessage: "access the wallet.", ), ), @@ -82,14 +83,17 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si return; } - if (Globals().router.pinRequired(globals.tabController.index) && timeoutExpiredInBackground && !pinCheckOpen) { + if (Globals().router.pinRequired(globals.tabController.index) && + timeoutExpiredInBackground && + !pinCheckOpen) { int authenticatedAppIndex = globals.tabController.index; globals.tabController.animateTo(globals.tabController.previousIndex); checkPinAndNavigateIfSuccess(authenticatedAppIndex); } - if (Globals().router.emailMustBeVerified(globals.tabController.index) && !Globals().emailVerified.value) { + if (Globals().router.emailMustBeVerified(globals.tabController.index) && + !Globals().emailVerified.value) { globals.tabController.animateTo(globals.tabController.previousIndex); await emailVerificationDialog(context); } @@ -152,7 +156,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si }); Events().onEvent(NewLoginEvent().runtimeType, (NewLoginEvent event) { - openLogin(context, event.loginData, widget.backendConnection); + openLogin(context, event.loginData!, widget.backendConnection!); }); Events().onEvent(EmailEvent().runtimeType, (EmailEvent event) { @@ -162,7 +166,7 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si Events().onEvent(IdentityCallbackEvent().runtimeType, (IdentityCallbackEvent event) async { Future(() { globals.tabController.animateTo(0, duration: Duration(seconds: 0)); - showIdentityMessage(context, event.type); + showIdentityMessage(context, event.type!); }); }); @@ -170,13 +174,13 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si phoneVerification(context); }); - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance?.addObserver(this); } @override void dispose() { - _sub.cancel(); - WidgetsBinding.instance.removeObserver(this); + _sub?.cancel(); + WidgetsBinding.instance?.removeObserver(this); super.dispose(); } @@ -208,13 +212,13 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si initialLink = widget.initialLink; if (initialLink != null) { - Events().emit(UniLinkEvent(Uri.parse(initialLink), context)); + Events().emit(UniLinkEvent(Uri.parse(initialLink!), context)); } - _sub = getLinksStream().listen((String incomingLink) { + _sub = getLinksStream().listen((String? incomingLink) { if (!mounted) { return; } - Events().emit(UniLinkEvent(Uri.parse(incomingLink), context)); + Events().emit(UniLinkEvent(Uri.parse(incomingLink!), context)); }); } @@ -248,21 +252,6 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si )), ], ), - // bottomNavigationBar: Container( - // color: HexColor("#0A73B8"), - // //@todo theme obj - // padding: EdgeInsets.all(0.0), - // height: 80, - // margin: EdgeInsets.all(0.0), - // child: TabBar( - // controller: _tabController, - // isScrollable: false, - // indicatorSize: TabBarIndicatorSize.tab, - // tabs: Globals().router.getAppButtons(), - // labelPadding: EdgeInsets.all(0.0), - // indicatorPadding: EdgeInsets.all(0.0), - // ), - // ), ), onWillPop: onWillPop, ), @@ -278,7 +267,11 @@ class _HomeScreenState extends State with WidgetsBindingObserver, Si if (Globals().router.routes[globals.tabController.index].app == null) { Events().emit(GoHomeEvent()); // if not an app, eg settings, go home } - Globals().router.routes[globals.tabController.index].app.back(); // if app ask app to handle back event + Globals() + .router + .routes[globals.tabController.index] + .app! + .back(); // if app ask app to handle back event return Future(() => false); } diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index a4bb7833..62553b25 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -9,7 +9,6 @@ import 'package:http/http.dart'; import 'package:shuftipro_flutter_sdk/ShuftiPro.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/identity_callback_event.dart'; -import 'package:threebotlogin/helpers/flags.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/hex_color.dart'; import 'package:threebotlogin/helpers/kyc_helpers.dart'; @@ -22,7 +21,6 @@ import 'package:threebotlogin/services/socket_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; -import 'package:threebotlogin/widgets/email_verification_needed.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:country_picker/country_picker.dart'; import 'package:threebotlogin/widgets/phone_widget.dart'; @@ -74,7 +72,7 @@ class _IdentityVerificationScreenState extends State }; // Template for Shufti API verification object - Map verificationObj = { + Map verificationObj = { "face": {}, "background_checks": {}, "phone": {}, @@ -170,7 +168,8 @@ class _IdentityVerificationScreenState extends State } checkPhoneStatus() { - if (Globals().smsSentOn + (Globals().smsMinutesCoolDown * 60 * 1000) > new DateTime.now().millisecondsSinceEpoch) { + if (Globals().smsSentOn + (Globals().smsMinutesCoolDown * 60 * 1000) > + new DateTime.now().millisecondsSinceEpoch) { return Globals().hidePhoneButton.value = true; } @@ -180,13 +179,13 @@ class _IdentityVerificationScreenState extends State void getUserValues() { getDoubleName().then((dn) { setState(() { - doubleName = dn; + doubleName = dn!; }); }); getEmail().then((emailMap) { setState(() { if (emailMap['email'] != null) { - email = emailMap['email']; + email = emailMap['email']!; changeEmailController.text = email; emailVerified = (emailMap['sei'] != null); } @@ -195,7 +194,7 @@ class _IdentityVerificationScreenState extends State getPhone().then((phoneMap) { setState(() { if (phoneMap['phone'] != null) { - phone = phoneMap['phone']; + phone = phoneMap['phone']!; phoneVerified = (phoneMap['spi'] != null); } }); @@ -243,50 +242,74 @@ class _IdentityVerificationScreenState extends State child: Column( children: [ AnimatedBuilder( - animation: Listenable.merge( - [Globals().emailVerified, Globals().phoneVerified, Globals().identityVerified]), + animation: Listenable.merge([ + Globals().emailVerified, + Globals().phoneVerified, + Globals().identityVerified + ]), builder: (BuildContext context, _) { return Container( child: Column( children: [ // Step one: verify email - _fillCard(getCorrectState(1, emailVerified, phoneVerified, identityVerified), 1, - email, Icons.email), + _fillCard( + getCorrectState( + 1, emailVerified, phoneVerified, identityVerified), + 1, + email, + Icons.email), // Step two: verify phone - _fillCard(getCorrectState(2, emailVerified, phoneVerified, identityVerified), 2, - phone, Icons.phone), + _fillCard( + getCorrectState( + 2, emailVerified, phoneVerified, identityVerified), + 2, + phone, + Icons.phone), // Step three: verify identity Globals().isOpenKYCEnabled ? _fillCard( - getCorrectState(3, emailVerified, phoneVerified, identityVerified), + getCorrectState(3, emailVerified, phoneVerified, + identityVerified), 3, extract3Bot(doubleName), Icons.perm_identity) : Container(), - Globals().redoIdentityVerification && identityVerified == true + Globals().redoIdentityVerification && + identityVerified == true ? ElevatedButton( onPressed: () async { await verifyIdentityProcess(); }, child: Text('Redo identity verification')) : Container(), - Globals().debugMode == true ? ElevatedButton( - onPressed: () async { - bool isEmailVerified = await getIsEmailVerified(); - bool isPhoneVerified = await getIsPhoneVerified(); - bool isIdentityVerified = await getIsIdentityVerified(); - - kycLogs = ''; - kycLogs += 'Email verified: ' + isEmailVerified.toString() + '\n'; - kycLogs += 'Phone verified: ' + isPhoneVerified.toString() + '\n'; - kycLogs += 'Identity verified: ' + isIdentityVerified.toString() + '\n'; - - setState(() {}); - }, - child: Text('KYC Status')) : Container(), + Globals().debugMode == true + ? ElevatedButton( + onPressed: () async { + bool? isEmailVerified = + await getIsEmailVerified(); + bool? isPhoneVerified = + await getIsPhoneVerified(); + bool? isIdentityVerified = + await getIsIdentityVerified(); + + kycLogs = ''; + kycLogs += 'Email verified: ' + + isEmailVerified.toString() + + '\n'; + kycLogs += 'Phone verified: ' + + isPhoneVerified.toString() + + '\n'; + kycLogs += 'Identity verified: ' + + isIdentityVerified.toString() + + '\n'; + + setState(() {}); + }, + child: Text('KYC Status')) + : Container(), Text(kycLogs), ], ), @@ -554,11 +577,14 @@ class _IdentityVerificationScreenState extends State mainAxisAlignment: MainAxisAlignment.center, children: [ Text('0' + step.toString(), - style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)) + style: TextStyle( + color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)) ], ), decoration: new BoxDecoration( - border: Border.all(color: Colors.blue, width: 2), shape: BoxShape.circle, color: Colors.white), + border: Border.all(color: Colors.blue, width: 2), + shape: BoxShape.circle, + color: Colors.white), ), Padding(padding: EdgeInsets.only(left: 20)), Icon( @@ -569,7 +595,8 @@ class _IdentityVerificationScreenState extends State Padding(padding: EdgeInsets.only(left: 15)), Flexible( child: Container( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + child: + Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Expanded( @@ -594,7 +621,8 @@ class _IdentityVerificationScreenState extends State Padding(padding: EdgeInsets.only(left: 5)), Text( 'Not verified', - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12), + style: + TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12), ) ], ), @@ -625,7 +653,7 @@ class _IdentityVerificationScreenState extends State return; } - String phoneNumber = phoneMap['phone']; + String? phoneNumber = phoneMap['phone']; if (phoneNumber == null || phoneNumber.isEmpty) { return; } @@ -634,9 +662,8 @@ class _IdentityVerificationScreenState extends State phone = phoneNumber; }); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('phone', json.encode({'phone': phone}), keyPair); + FlutterPkid client = await getPkidClient(); + client.setPKidDoc('phone', json.encode({'phone': phone})); if (phone.isEmpty) { return; @@ -662,11 +689,14 @@ class _IdentityVerificationScreenState extends State mainAxisAlignment: MainAxisAlignment.center, children: [ Text('0' + step.toString(), - style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)) + style: TextStyle( + color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)) ], ), decoration: new BoxDecoration( - border: Border.all(color: Colors.blue, width: 2), shape: BoxShape.circle, color: Colors.white), + border: Border.all(color: Colors.blue, width: 2), + shape: BoxShape.circle, + color: Colors.white), ), Padding(padding: EdgeInsets.only(left: 15)), Icon( @@ -689,36 +719,41 @@ class _IdentityVerificationScreenState extends State minWidth: MediaQuery.of(context).size.width * 0.6, maxWidth: MediaQuery.of(context).size.width * 0.6), padding: EdgeInsets.all(10), - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Text( - text == '' ? 'Unknown' : text, - overflow: TextOverflow.clip, - style: TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold), - ), - ) - ], - ), - step == 2 && Globals().hidePhoneButton.value == true - ? SizedBox( - height: 5, - ) - : Container(), - step == 2 && Globals().hidePhoneButton.value == true - ? Row( - children: [ - Text( - 'SMS sent, retry in ${calculateMinutes()} minute${calculateMinutes() == '1' ? '' : 's'}', + Row( + children: [ + Expanded( + child: Text( + text == '' ? 'Unknown' : text, overflow: TextOverflow.clip, style: - TextStyle(color: Colors.orange, fontWeight: FontWeight.bold, fontSize: 12), + TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold), + ), + ) + ], + ), + step == 2 && Globals().hidePhoneButton.value == true + ? SizedBox( + height: 5, + ) + : Container(), + step == 2 && Globals().hidePhoneButton.value == true + ? Row( + children: [ + Text( + 'SMS sent, retry in ${calculateMinutes()} minute${calculateMinutes() == '1' ? '' : 's'}', + overflow: TextOverflow.clip, + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + fontSize: 12), + ) + ], ) - ], - ) - : Container(), - ]))), + : Container(), + ]))), Globals().hidePhoneButton.value == true && step == 2 ? Container() : ElevatedButton( @@ -836,7 +871,8 @@ class _IdentityVerificationScreenState extends State children: [ Text( 'Verified', - style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold), ) ], ) @@ -880,7 +916,8 @@ class _IdentityVerificationScreenState extends State builder: (BuildContext context) => CustomDialog( image: Icons.warning, title: "Maximum requests Reached", - description: "You already had 5 requests in last 24 hours. \nPlease try again in 24 hours.", + description: + "You already had 5 requests in last 24 hours. \nPlease try again in 24 hours.", actions: [ FlatButton( child: new Text("Ok"), @@ -904,7 +941,8 @@ class _IdentityVerificationScreenState extends State builder: (BuildContext context) => CustomDialog( image: Icons.warning, title: "Couldn't setup verification process", - description: "Something went wrong. Please contact support if this issue persists.", + description: + "Something went wrong. Please contact support if this issue persists.", actions: [ FlatButton( child: new Text("Ok"), @@ -916,18 +954,18 @@ class _IdentityVerificationScreenState extends State )); } - Map details = jsonDecode(accessTokenResponse.body); + Map details = jsonDecode(accessTokenResponse.body); authObject['access_token'] = details['access_token']; Response identityResponse = await sendVerificationIdentity(); - Map identityDetails = jsonDecode(identityResponse.body); + Map identityDetails = jsonDecode(identityResponse.body); String verificationCode = identityDetails['verification_code']; reference = verificationCode; createdPayload["reference"] = reference; - createdPayload["document"] = verificationObj['document']; - createdPayload["face"] = verificationObj['face']; + createdPayload["document"] = verificationObj['document']!; + createdPayload["face"] = verificationObj['face']!; createdPayload["verification_mode"] = "image_only"; setState(() { @@ -959,7 +997,7 @@ class _IdentityVerificationScreenState extends State } } - Future showIdentityDetails() { + Future showIdentityDetails() { return showDialog( context: context, builder: (BuildContext context) => Dialog( @@ -1034,7 +1072,9 @@ class _IdentityVerificationScreenState extends State ), Row( children: [ - Text(snapshot.data['identityDOB'] != 'None' ? snapshot.data['identityDOB'] : 'Unknown') + Text(snapshot.data['identityDOB'] != 'None' + ? snapshot.data['identityDOB'] + : 'Unknown') ], ) ], @@ -1105,7 +1145,7 @@ class _IdentityVerificationScreenState extends State )); } - Future resendEmailDialog(context) { + Future resendEmailDialog(context) { return showDialog( context: context, barrierDismissible: false, @@ -1129,7 +1169,7 @@ class _IdentityVerificationScreenState extends State TextEditingController controller = new TextEditingController(); bool validEmail = false; - String errorEmail; + String? errorEmail; Text statusMessage = const Text(''); showDialog( @@ -1173,12 +1213,13 @@ class _IdentityVerificationScreenState extends State onPressed: () async { _loadingDialog(); - String emailValue = controller.text.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); + String emailValue = + controller.text.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); bool isValidEmail = validateEmail(emailValue); var oldEmail = await getEmail(); - if(oldEmail['email'] == emailValue) { + if (oldEmail['email'] == emailValue) { validEmail = false; errorEmail = "Please enter a different email"; setCustomState(() {}); @@ -1204,7 +1245,6 @@ class _IdentityVerificationScreenState extends State throw Exception(); } - sendVerificationEmail(); email = emailValue; @@ -1221,7 +1261,7 @@ class _IdentityVerificationScreenState extends State print(e); Navigator.pop(context); - await saveEmail(oldEmail['email'], oldEmail['sei']); + await saveEmail(oldEmail['email']!, oldEmail['sei']); await saveEmailToPKid(); statusMessage = Text('Something went wrong', @@ -1238,11 +1278,10 @@ class _IdentityVerificationScreenState extends State }); } - Future showEmailChangeDialog() async { - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); + Future showEmailChangeDialog() async { + FlutterPkid client = await getPkidClient(); - var emailPKidResult = await client.getPKidDoc('email', keyPair); + var emailPKidResult = await client.getPKidDoc('email'); print(emailPKidResult); return showDialog( context: context, @@ -1258,7 +1297,8 @@ class _IdentityVerificationScreenState extends State TextField( controller: changeEmailController, decoration: InputDecoration( - labelText: 'Email', errorText: emailInputValidated ? null : 'Please enter a valid email'), + labelText: 'Email', + errorText: emailInputValidated ? null : 'Please enter a valid email'), ), ], ), @@ -1282,9 +1322,9 @@ class _IdentityVerificationScreenState extends State await saveEmail(changeEmailController.text, null); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('email', json.encode({'email': email}), keyPair); + FlutterPkid client = await getPkidClient(); + + client.setPKidDoc('email', json.encode({'email': email})); Navigator.of(context).pop(); }, @@ -1320,7 +1360,7 @@ class _IdentityVerificationScreenState extends State if (phoneMap.isEmpty || !phoneMap.containsKey('phone')) { return; } - String phoneNumber = phoneMap['phone']; + String? phoneNumber = phoneMap['phone']; if (phoneNumber == null || phoneNumber.isEmpty) { return; } @@ -1329,9 +1369,8 @@ class _IdentityVerificationScreenState extends State phone = phoneNumber; }); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('phone', json.encode({'phone': phone}), keyPair); + FlutterPkid client = await getPkidClient(); + client.setPKidDoc('phone', json.encode({'phone': phone})); if (phone.isEmpty) { return; diff --git a/app/lib/screens/init_screen.dart b/app/lib/screens/init_screen.dart index fd21deaa..24368932 100644 --- a/app/lib/screens/init_screen.dart +++ b/app/lib/screens/init_screen.dart @@ -11,9 +11,8 @@ class InitScreen extends StatefulWidget { } class _InitState extends State { - InAppWebViewController webView; - - InAppWebView iaWebView; + late InAppWebViewController webView; + late InAppWebView iaWebView; finish(List params) async { print("**** LOAD DONE "); @@ -27,8 +26,10 @@ class _InitState extends State { _InitState() { iaWebView = InAppWebView( - initialUrlRequest: URLRequest(url:Uri.parse(AppConfig().wizardUrl() + '?cache_buster=' + new DateTime.now().millisecondsSinceEpoch.toString())), - + initialUrlRequest: URLRequest( + url: Uri.parse(AppConfig().wizardUrl() + + '?cache_buster=' + + new DateTime.now().millisecondsSinceEpoch.toString())), initialOptions: InAppWebViewGroupOptions( android: AndroidInAppWebViewOptions(supportMultipleWindows: true), ), @@ -36,10 +37,11 @@ class _InitState extends State { webView = controller; addHandler(); }, - onCreateWindow: - (InAppWebViewController controller, CreateWindowAction req) {}, - onLoadStart: (InAppWebViewController controller, Uri url) {}, - onLoadStop: (InAppWebViewController controller, Uri url) async {}, + 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) {}, ); } diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 972f892c..68f93137 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -32,7 +32,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { String scopeTextMobile = 'Please select the data you want to share and press Accept'; String scopeText = 'Please select the data you want to share and press the corresponding emoji'; - List imageList = new List(); + List imageList = []; int selectedImageId = -1; int correctImage = -1; @@ -43,12 +43,12 @@ class _LoginScreenState extends State with BlockAndRunMixin { String emitCode = randomString(10); - Timer timer; + late Timer timer; int timeLeft = Globals().loginTimeout; - int created; - int currentTimestamp; + int? created = 0; + int? currentTimestamp = 0; final GlobalKey _scaffoldKey = GlobalKey(); @@ -56,17 +56,17 @@ class _LoginScreenState extends State with BlockAndRunMixin { void initState() { super.initState(); Events().onEvent(PopAllLoginEvent("").runtimeType, close); - isMobileCheck = widget.loginData.isMobile; + isMobileCheck = widget.loginData.isMobile == true; generateEmojiImageList(); - if (widget.loginData != null && !widget.loginData.isMobile) { + if (widget.loginData.isMobile == false) { const oneSec = const Duration(seconds: 1); print('Starting timer ... '); created = widget.loginData.created; currentTimestamp = new DateTime.now().millisecondsSinceEpoch; - timeLeft = Globals().loginTimeout - ((currentTimestamp - created) / 1000).round(); + timeLeft = Globals().loginTimeout - ((currentTimestamp! - created!) / 1000).round(); timer = new Timer.periodic(oneSec, (Timer t) async { timeoutTimer(); @@ -83,10 +83,10 @@ class _LoginScreenState extends State with BlockAndRunMixin { currentTimestamp = new DateTime.now().millisecondsSinceEpoch; setState(() { - timeLeft = Globals().loginTimeout - ((currentTimestamp - created) / 1000).round(); + timeLeft = Globals().loginTimeout - ((currentTimestamp! - created!) / 1000).round(); }); - if (created != null && ((currentTimestamp - created) / 1000) > Globals().loginTimeout) { + if (created != null && ((currentTimestamp! - created!) / 1000) > Globals().loginTimeout) { timer.cancel(); await showDialog( @@ -136,7 +136,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { scope: widget.loginData.scope, appId: widget.loginData.appId, callback: cancelIt, - type: widget.loginData.isMobile ? 'mobilelogin' : 'login', + type: widget.loginData.isMobile == true ? 'mobilelogin' : 'login', ), ), ), @@ -179,14 +179,16 @@ class _LoginScreenState extends State with BlockAndRunMixin { ), ), Visibility( - visible: !widget.loginData.isMobile, + visible: widget.loginData.isMobile == false, child: Expanded( flex: 1, child: Center( child: Padding( padding: const EdgeInsets.only(right: 24.0, left: 24.0), child: Text( - "Attempt expires in " + ((timeLeft >= 0) ? timeLeft.toString() : "0") + " second(s).", + "Attempt expires in " + + ((timeLeft >= 0) ? timeLeft.toString() : "0") + + " second(s).", style: TextStyle(fontSize: 12), textAlign: TextAlign.center, ), @@ -263,7 +265,8 @@ class _LoginScreenState extends State with BlockAndRunMixin { builder: (BuildContext context) => CustomDialog( image: Icons.warning, title: 'Wrong emoji', - description: 'You selected the wrong emoji, please check your browser for the new one.', + description: + 'You selected the wrong emoji, please check your browser for the new one.', actions: [ FlatButton( child: Text('Retry'), @@ -280,7 +283,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { } } } else { - _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text('Please select an emoji'))); + _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text('Please select an emoji'))); } }); } @@ -304,11 +307,11 @@ class _LoginScreenState extends State with BlockAndRunMixin { sendIt(bool includeData) async { var config = WalletConfig(); - String state = widget.loginData.state; - String randomRoom = widget.loginData.randomRoom; + String? state = widget.loginData.state; + String? randomRoom = widget.loginData.randomRoom; - if (widget.loginData != null && !widget.loginData.isMobile) { - int created = widget.loginData.created; + if (widget.loginData.isMobile == false) { + int? created = widget.loginData.created; int currentTimestamp = new DateTime.now().millisecondsSinceEpoch; if (created != null && ((currentTimestamp - created) / 1000) > Globals().loginTimeout) { @@ -329,7 +332,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { ), ); - await sendData(state, null, selectedImageId, null, widget.loginData.appId); + await sendData(state!, null, selectedImageId, null, widget.loginData.appId!); if (Navigator.canPop(context)) { Navigator.pop(context, false); @@ -338,12 +341,12 @@ class _LoginScreenState extends State with BlockAndRunMixin { } } - String publicKey = widget.loginData.appPublicKey?.replaceAll(" ", "+"); + String publicKey = widget.loginData.appPublicKey!.replaceAll(" ", "+"); - bool stateCheck = RegExp(r"[^A-Za-z0-9]+").hasMatch(state); + bool stateCheck = RegExp(r"[^A-Za-z0-9]+").hasMatch(state!); if (stateCheck) { - _scaffoldKey.currentState.showSnackBar( + _scaffoldKey.currentState?.showSnackBar( SnackBar( content: Text('States can only be alphanumeric [^A-Za-z0-9]'), ), @@ -353,9 +356,9 @@ class _LoginScreenState extends State with BlockAndRunMixin { Map scope = Map(); - var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId); + var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId!); - Uint8List derivedSeed = (await getDerivedSeed(widget.loginData.appId)); + Uint8List derivedSeed = (await getDerivedSeed(widget.loginData.appId!)); //TODO: make separate function if (scopePermissions != null) { @@ -370,32 +373,38 @@ class _LoginScreenState extends State with BlockAndRunMixin { scope['phone'] = (await getPhone()); } - if (scopePermissionsDecoded['derivedSeed'] != null && scopePermissionsDecoded['derivedSeed']) { + if (scopePermissionsDecoded['derivedSeed'] != null && + scopePermissionsDecoded['derivedSeed']) { scope['derivedSeed'] = derivedSeed; } - if (scopePermissionsDecoded['digitalTwin'] != null && scopePermissionsDecoded['digitalTwin']) { + if (scopePermissionsDecoded['digitalTwin'] != null && + scopePermissionsDecoded['digitalTwin']) { scope['digitalTwin'] = 'ok'; } - if (scopePermissionsDecoded['identityName'] != null && scopePermissionsDecoded['identityName']) { + if (scopePermissionsDecoded['identityName'] != null && + scopePermissionsDecoded['identityName']) { scope['identityName'] = { 'identityName': ((await getIdentity())['identityName']), 'signedIdentityNameIdentifier': ((await getIdentity())['signedIdentityNameIdentifier']) }; } - if (scopePermissionsDecoded['identityDOB'] != null && scopePermissionsDecoded['identityDOB']) { + if (scopePermissionsDecoded['identityDOB'] != null && + scopePermissionsDecoded['identityDOB']) { scope['identityDOB'] = { 'identityDOB': ((await getIdentity())['identityDOB']), 'signedIdentityDOBIdentifier': ((await getIdentity())['signedIdentityDOBIdentifier']) }; } - if (scopePermissionsDecoded['identityCountry'] != null && scopePermissionsDecoded['identityCountry']) { + if (scopePermissionsDecoded['identityCountry'] != null && + scopePermissionsDecoded['identityCountry']) { scope['identityCountry'] = { 'identityCountry': ((await getIdentity())['identityCountry']), - 'signedIdentityCountryIdentifier': ((await getIdentity())['signedIdentityCountryIdentifier']) + 'signedIdentityCountryIdentifier': + ((await getIdentity())['signedIdentityCountryIdentifier']) }; } @@ -403,18 +412,22 @@ class _LoginScreenState extends State with BlockAndRunMixin { scopePermissionsDecoded['identityDocumentMeta']) { scope['identityDocumentMeta'] = { 'identityDocumentMeta': ((await getIdentity())['identityDocumentMeta']), - 'signedIdentityDocumentMetaIdentifier': ((await getIdentity())['signedIdentityDocumentMetaIdentifier']) + 'signedIdentityDocumentMetaIdentifier': + ((await getIdentity())['signedIdentityDocumentMetaIdentifier']) }; } - if (scopePermissionsDecoded['identityGender'] != null && scopePermissionsDecoded['identityGender']) { + if (scopePermissionsDecoded['identityGender'] != null && + scopePermissionsDecoded['identityGender']) { scope['identityGender'] = { 'identityGender': ((await getIdentity())['identityGender']), - 'signedIdentityGenderIdentifier': ((await getIdentity())['signedIdentityGenderIdentifier']) + 'signedIdentityGenderIdentifier': + ((await getIdentity())['signedIdentityGenderIdentifier']) }; } - if (scopePermissionsDecoded['walletAddress'] != null && scopePermissionsDecoded['walletAddress']) { + if (scopePermissionsDecoded['walletAddress'] != null && + scopePermissionsDecoded['walletAddress']) { scope['walletAddressData'] = { 'address': scopePermissionsDecoded['walletAddressData'], }; @@ -422,14 +435,16 @@ class _LoginScreenState extends State with BlockAndRunMixin { } } - Map encryptedScopeData = await encrypt(jsonEncode(scope), publicKey, await getPrivateKey()); + Map encryptedScopeData = + await encrypt(jsonEncode(scope), base64.decode(publicKey), await getPrivateKey()); //push to backend with signed if (!includeData) { - await sendData( - state, null, selectedImageId, null, widget.loginData.appId); // temp fix send empty data for regenerate emoji + await sendData(state, null, selectedImageId, null, + widget.loginData.appId!); // temp fix send empty data for regenerate emoji } else { - await sendData(state, encryptedScopeData, selectedImageId, randomRoom, widget.loginData.appId); + await sendData( + state, encryptedScopeData, selectedImageId, randomRoom, widget.loginData.appId!); } if (selectedImageId == correctImage || isMobileCheck) { @@ -438,8 +453,8 @@ class _LoginScreenState extends State with BlockAndRunMixin { var name = await getDoubleName(); KeyPair dtKeyPair = await generateKeyPairFromEntropy(derivedSeed); - String dtEncodedPublicKey = base64.encode(dtKeyPair.pk) - print("name: " + name); + String dtEncodedPublicKey = base64.encode(dtKeyPair.pk); + print("name: " + name!); print("publicKey: " + dtEncodedPublicKey); addDigitalTwinDerivedPublicKeyToBackend(name, dtEncodedPublicKey, widget.loginData.appId); @@ -451,7 +466,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { } } - int parseImageId(String imageId) { + int parseImageId(String? imageId) { if (imageId == null || imageId == '') { return 1; } @@ -459,7 +474,7 @@ class _LoginScreenState extends State with BlockAndRunMixin { } void generateEmojiImageList() { - correctImage = parseImageId(widget.loginData.randomImageId); + correctImage = parseImageId(widget.loginData.randomImageId!); imageList.add(correctImage); diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index f600fb3a..51921ca5 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -19,8 +19,8 @@ import 'package:threebotlogin/widgets/error_widget.dart'; import 'package:uni_links/uni_links.dart'; class MainScreen extends StatefulWidget { - final bool initDone; - final bool registered; + final bool? initDone; + final bool? registered; MainScreen({this.initDone, this.registered}); @@ -29,17 +29,17 @@ class MainScreen extends StatefulWidget { } class _AppState extends State { - StreamSubscription _sub; - String initialLink; + StreamSubscription? _sub; + String? initialLink; // FirebaseNotificationListener _listener; - BackendConnection _backendConnection; + late BackendConnection _backendConnection; @override void initState() { super.initState(); Events().reset(); // _listener = FirebaseNotificationListener(); - WidgetsBinding.instance.addPostFrameCallback((_) => pushScreens()); + WidgetsBinding.instance?.addPostFrameCallback((_) => pushScreens()); } @override @@ -64,7 +64,7 @@ class _AppState extends State { await checkIfAppIsUpToDate(); try { - await Flags().initialiseFlagSmith(); + await Flags().initFlagSmith(); await Flags().setFlagSmithDefaultValues(); } catch (e) { @@ -81,7 +81,7 @@ class _AppState extends State { } } - if (widget.initDone != null && !widget.initDone) { + if (widget.initDone != null && !widget.initDone!) { InitScreen init = InitScreen(); bool accepted = false; while (!accepted) { @@ -89,19 +89,19 @@ class _AppState extends State { } } - if (!widget.registered) { + if (!widget.registered!) { await Navigator.push(context, MaterialPageRoute(builder: (context) => UnregisteredScreen())); } await Globals().router.init(); - _backendConnection = BackendConnection(await getDoubleName()); + _backendConnection = BackendConnection((await getDoubleName())!); _backendConnection.init(); await initUniLinks(); if (_sub != null) { - _sub.cancel(); + _sub?.cancel(); } await Navigator.pushReplacement( @@ -287,7 +287,7 @@ class _AppState extends State { initialLink = await getInitialLink(); // Doesn't seem needed in this scenario. Might be removed in the future. - _sub = getLinksStream().listen((String incomingLink) { + _sub = getLinksStream().listen((String? incomingLink) { if (!mounted) { return; } diff --git a/app/lib/screens/mobile_registration_screen.dart b/app/lib/screens/mobile_registration_screen.dart index 0784fcdf..1abdf007 100644 --- a/app/lib/screens/mobile_registration_screen.dart +++ b/app/lib/screens/mobile_registration_screen.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; +import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:http/http.dart'; import 'package:threebotlogin/helpers/flags.dart'; import 'package:threebotlogin/helpers/globals.dart'; @@ -10,6 +11,7 @@ import 'package:threebotlogin/helpers/kyc_helpers.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; @@ -17,12 +19,11 @@ import 'package:threebotlogin/widgets/reusable_text_field_step.dart'; import 'package:threebotlogin/widgets/reusable_text_step.dart'; class MobileRegistrationScreen extends StatefulWidget { - final String doubleName; + final String? doubleName; MobileRegistrationScreen({this.doubleName}); - _MobileRegistrationScreenState createState() => - _MobileRegistrationScreenState(); + _MobileRegistrationScreenState createState() => _MobileRegistrationScreenState(); } enum _State { DoubleName, Email, SeedPhrase, ConfirmSeedPhrase, Finish } @@ -31,7 +32,7 @@ class RegistrationData { String doubleName = ''; String phrase = ''; String email = ''; - Map keys; + late KeyPair keyPair; } class _MobileRegistrationScreenState extends State { @@ -39,7 +40,6 @@ class _MobileRegistrationScreenState extends State { final emailController = TextEditingController(); final seedConfirmationController = TextEditingController(); _State state = _State.DoubleName; - // FirebaseNotificationListener _listener; bool isVisible = false; @@ -56,7 +56,7 @@ class _MobileRegistrationScreenState extends State { void initState() { if (widget.doubleName != null) { setState(() { - doubleNameController.text = widget.doubleName; + doubleNameController.text = widget.doubleName!; }); } // _listener = FirebaseNotificationListener(); @@ -68,7 +68,8 @@ class _MobileRegistrationScreenState extends State { } checkEmail() async { - String emailValue = emailController.text?.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String? emailValue = + emailController.text.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); setState(() { emailController.text = emailValue; @@ -87,7 +88,8 @@ class _MobileRegistrationScreenState extends State { } checkDoubleName() async { - String doubleNameValue = doubleNameController.text?.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String? doubleNameValue = + doubleNameController.text.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); setState(() { doubleNameController.text = doubleNameValue; @@ -123,19 +125,20 @@ class _MobileRegistrationScreenState extends State { setState(() { state = _State.ConfirmSeedPhrase; }); - _registrationData.keys = - await generateKeysFromSeedPhrase(_registrationData.phrase); + _registrationData.keyPair = await generateKeyPairFromSeedPhrase(_registrationData.phrase); } checkConfirm() { - String seedCheckValue = seedConfirmationController.text?.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String? seedCheckValue = + seedConfirmationController.text.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); setState(() { seedConfirmationController.text = seedCheckValue; }); - bool seedWordConfirmationValidation = validateSeedWords(_registrationData.phrase, seedConfirmationController.text); - + bool seedWordConfirmationValidation = + validateSeedWords(_registrationData.phrase, seedConfirmationController.text); + if (seedWordConfirmationValidation) { setState(() { state = _State.Finish; @@ -152,11 +155,8 @@ class _MobileRegistrationScreenState extends State { // String deviceId = await _listener.getToken(); // String signedDeviceId = // await (signData(deviceId, _registrationData.keys['privateKey'])); - Response response = await finishRegistration( - doubleNameController.text, - emailController.text, - 'random', - _registrationData.keys['publicKey']); + Response response = await finishRegistration(doubleNameController.text, emailController.text, + 'random', base64.encode(_registrationData.keyPair.pk)); if (response.statusCode == 200) { saveRegistration(); @@ -171,8 +171,7 @@ class _MobileRegistrationScreenState extends State { builder: (BuildContext context) => CustomDialog( image: Icons.error, title: 'Error', - description: - 'Something went wrong when trying to create your account.', + description: 'Something went wrong when trying to create your account.', actions: [ FlatButton( child: Text('Ok'), @@ -239,18 +238,17 @@ class _MobileRegistrationScreenState extends State { } void saveRegistration() async { - savePrivateKey(_registrationData.keys['privateKey']); - savePublicKey(_registrationData.keys['publicKey']); + savePrivateKey(_registrationData.keyPair.sk); + savePublicKey(_registrationData.keyPair.pk); saveFingerprint(false); saveEmail(_registrationData.email, null); saveDoubleName(_registrationData.doubleName); savePhrase(_registrationData.phrase); - Map keyPair = await generateKeyPairFromSeedPhrase(await getPhrase()); - var client = FlutterPkid(pkidUrl, keyPair); - client.setPKidDoc('email', json.encode({'email': _registrationData.email }), keyPair); + FlutterPkid client = await getPkidClient(); + client.setPKidDoc('email', json.encode({'email': _registrationData.email})); - await Flags().initialiseFlagSmith(); + await Flags().initFlagSmith(); await Flags().setFlagSmithDefaultValues(); await fetchPKidData(); @@ -290,8 +288,7 @@ class _MobileRegistrationScreenState extends State { accentColor: Globals.color, ), child: Stepper( - controlsBuilder: (BuildContext context, - {VoidCallback onStepContinue, VoidCallback onStepCancel}) { + controlsBuilder: (BuildContext context, ControlsDetails details) { return Column( children: [ Row( @@ -300,7 +297,7 @@ class _MobileRegistrationScreenState extends State { children: [ FlatButton( onPressed: () { - onStepCancel(); + details.onStepCancel!(); }, child: Text( state == _State.DoubleName ? 'CANCEL' : 'PREVIOUS', @@ -310,7 +307,7 @@ class _MobileRegistrationScreenState extends State { ), FlatButton( onPressed: () { - onStepContinue(); + details.onStepContinue!(); }, child: Text( state == _State.Finish ? 'FINISH' : 'NEXT', @@ -363,7 +360,7 @@ class _MobileRegistrationScreenState extends State { ), controller: doubleNameController, inputFormatters: [ - WhitelistingTextInputFormatter(RegExp("[a-zA-Z0-9]")) + FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]")) ], enableSuggestions: false, autocorrect: false, @@ -397,9 +394,7 @@ class _MobileRegistrationScreenState extends State { ? StepState.editing : StepState.disabled, title: Text('Email'), - subtitle: state.index > _State.Email.index - ? Text(emailController.text) - : null, + subtitle: state.index > _State.Email.index ? Text(emailController.text) : null, content: Card( child: Padding( padding: const EdgeInsets.all(16.0), @@ -447,8 +442,7 @@ class _MobileRegistrationScreenState extends State { padding: const EdgeInsets.all(16.0), child: ReuseableTextFieldStep( focusNode: seedFocus, - titleText: - 'Type 3 random words from your seed phrase, separated by a space.', + titleText: 'Type 3 random words from your seed phrase, separated by a space.', labelText: 'Seed phrase words', typeText: TextInputType.text, errorStepperText: errorStepperText, diff --git a/app/lib/screens/planetary_network_screen.dart b/app/lib/screens/planetary_network_screen.dart index 7fe22369..32b41b00 100644 --- a/app/lib/screens/planetary_network_screen.dart +++ b/app/lib/screens/planetary_network_screen.dart @@ -17,7 +17,7 @@ class _PlanetaryNetworkScreenState extends State { VpnState _vpnState = new VpnState(); bool _vpnTimeoutRunning = false; - Text _ipText; + Text _ipText = Text(''); Text _statusMessage = Text(''); bool _isSwitched = Globals().vpnState.vpnConnected; diff --git a/app/lib/screens/preference_screen.dart b/app/lib/screens/preference_screen.dart index 3bf587e6..6b7d1208 100644 --- a/app/lib/screens/preference_screen.dart +++ b/app/lib/screens/preference_screen.dart @@ -27,7 +27,7 @@ import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:url_launcher/url_launcher.dart'; class PreferenceScreen extends StatefulWidget { - PreferenceScreen({Key key}) : super(key: key); + PreferenceScreen({Key? key}) : super(key: key); @override _PreferenceScreenState createState() => _PreferenceScreenState(); @@ -35,7 +35,7 @@ class PreferenceScreen extends StatefulWidget { class _PreferenceScreenState extends State { // FirebaseNotificationListener _listener; - Map email; + Map email = {}; String doubleName = ''; String phrase = ''; bool showAdvancedOptions = false; @@ -45,13 +45,13 @@ class _PreferenceScreenState extends State { String phoneAdress = ''; String identity = ''; - BuildContext preferenceContext; + BuildContext? preferenceContext; bool biometricsCheck = false; bool finger = false; String version = ''; String buildNumber = ''; - String biometricDeviceName = ""; + Object? biometricDeviceName; Globals globals = Globals(); @@ -75,11 +75,11 @@ class _PreferenceScreenState extends State { } showChangePin() async { - String pin = await getPin(); + String? pin = await getPin(); Navigator.push( context, MaterialPageRoute( - builder: (context) => AuthenticationScreen(correctPin: pin), + builder: (context) => AuthenticationScreen(correctPin: pin!, userMessage: 'Enter your pincode',), )); } @@ -140,10 +140,10 @@ class _PreferenceScreenState extends State { return CheckboxListTile( secondary: Icon(Icons.fingerprint), value: finger, - title: Text(snapshot.data), + title: Text(snapshot.data.toString()), activeColor: Theme.of(context).accentColor, - onChanged: (bool newValue) async { - _toggleFingerprint(newValue); + onChanged: (bool? newValue) async { + _toggleFingerprint(newValue!); }, ); } else { @@ -270,7 +270,7 @@ class _PreferenceScreenState extends State { context, MaterialPageRoute(builder: (context) => MainScreen(initDone: true, registered: false))); } else { showDialog( - context: preferenceContext, + context: preferenceContext!, builder: (BuildContext context) => CustomDialog( title: 'Error', description: 'Something went wrong when trying to remove your account.', @@ -326,12 +326,12 @@ class _PreferenceScreenState extends State { void getUserValues() { getDoubleName().then((dn) { setState(() { - doubleName = dn; + doubleName = dn!; }); }); getPhrase().then((seedPhrase) { setState(() { - phrase = seedPhrase; + phrase = seedPhrase!; }); }); getFingerprint().then((fingerprint) { @@ -346,13 +346,13 @@ class _PreferenceScreenState extends State { } void _showPhrase() async { - String pin = await getPin(); + String? pin = await getPin(); - bool authenticated = await Navigator.push( + bool? authenticated = await Navigator.push( context, MaterialPageRoute( builder: (context) => AuthenticationScreen( - correctPin: pin, + correctPin: pin!, userMessage: "show your phrase.", ), )); @@ -383,14 +383,14 @@ class _PreferenceScreenState extends State { } void _toggleFingerprint(bool newFingerprintValue) async { - String pin = await getPin(); + String? pin = await getPin(); - bool authenticated = await Navigator.push( + bool? authenticated = await Navigator.push( context, MaterialPageRoute( builder: (context) => AuthenticationScreen( - correctPin: pin, - userMessage: "toggle " + biometricDeviceName + ".", + correctPin: pin!, + userMessage: "toggle " + biometricDeviceName.toString() + ".", ), ), ); @@ -403,8 +403,8 @@ class _PreferenceScreenState extends State { } void _changePincode() async { - String pin = await getPin(); - bool authenticated = false; + String? pin = await getPin(); + bool? authenticated = false; if (pin == null || pin.isEmpty) { authenticated = true; // In case the pin wasn't set. diff --git a/app/lib/screens/recover_screen.dart b/app/lib/screens/recover_screen.dart index 41ce93f9..f5738ed5 100644 --- a/app/lib/screens/recover_screen.dart +++ b/app/lib/screens/recover_screen.dart @@ -1,51 +1,42 @@ import 'dart:convert'; import 'dart:core'; +import 'dart:typed_data'; -import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; +import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:http/http.dart'; -import 'package:shuftipro_flutter_sdk/ShuftiProVerifications.dart'; import 'package:threebotlogin/helpers/kyc_helpers.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; -import 'package:threebotlogin/services/open_kyc_service.dart'; -import 'package:threebotlogin/services/tools_service.dart'; +import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; class RecoverScreen extends StatefulWidget { - final Widget recoverScreen; + final Widget? recoverScreen; - RecoverScreen({Key key, this.recoverScreen}) : super(key: key); + RecoverScreen({Key? key, this.recoverScreen}) : super(key: key); _RecoverScreenState createState() => _RecoverScreenState(); } class _RecoverScreenState extends State { final TextEditingController doubleNameController = TextEditingController(); - final TextEditingController seedPhrasecontroller = TextEditingController(); + final TextEditingController seedPhraseController = TextEditingController(); final GlobalKey _formKey = GlobalKey(); - bool _autoValidate = false; - String doubleName = ''; String seedPhrase = ''; String error = ''; - String privateKey; String errorStepperText = ''; checkSeedPhrase(doubleName, seedPhrase) async { checkSeedLength(seedPhrase); - Map keys = await generateKeysFromSeedPhrase(seedPhrase); - - setState(() { - privateKey = keys['privateKey']; - }); + KeyPair keyPair = await generateKeyPairFromSeedPhrase(seedPhrase); Response userInfoResult = await getUserInfo(doubleName); @@ -55,29 +46,29 @@ class _RecoverScreenState extends State { Map body = json.decode(userInfoResult.body); - if (body['publicKey'] != keys['publicKey']) { + if (body['publicKey'] != base64.encode(keyPair.pk)) { throw new Exception('Seed phrase does not match with $doubleName'); } } continueRecoverAccount() async { try { - Map keys = await generateKeysFromSeedPhrase(seedPhrase); - await savePrivateKey(keys['privateKey']); - await savePublicKey(keys['publicKey']); - - Map keyPair = await generateKeyPairFromSeedPhrase(seedPhrase); - var client = FlutterPkid(pkidUrl, keyPair); + KeyPair keyPair = await generateKeyPairFromSeedPhrase(seedPhrase); + await savePrivateKey(keyPair.sk); + await savePublicKey(keyPair.pk); + FlutterPkid client = await getPkidClient(); List keyWords = ['email', 'phone', 'identity']; var futures = keyWords.map((keyword) async { - var pKidResult = await client.getPKidDoc(keyword, keyPair); - return pKidResult.containsKey('data') && pKidResult.containsKey('success') ? jsonDecode(pKidResult['data']) : {}; + var pKidResult = await client.getPKidDoc(keyword); + return pKidResult.containsKey('data') && pKidResult.containsKey('success') + ? jsonDecode(pKidResult['data']) + : {}; }); var pKidResult = await Future.wait(futures); - Map dataMap = pKidResult.asMap(); + Map dataMap = pKidResult.asMap(); await savePhrase(seedPhrase); await saveFingerprint(false); @@ -87,13 +78,10 @@ class _RecoverScreenState extends State { await migrateToNewSystem(); // await sendVerificationEmail(); - } - - catch(e) { + } catch (e) { print(e); throw Exception('Something went wrong'); } - } checkSeedLength(seedPhrase) { @@ -112,7 +100,7 @@ class _RecoverScreenState extends State { @override void dispose() { doubleNameController.dispose(); - seedPhrasecontroller.dispose(); + seedPhraseController.dispose(); super.dispose(); } @@ -135,7 +123,6 @@ class _RecoverScreenState extends State { Widget recoverForm() { return new Form( key: _formKey, - autovalidate: _autoValidate, child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -158,8 +145,8 @@ class _RecoverScreenState extends State { suffixStyle: TextStyle(fontWeight: FontWeight.bold), ), controller: doubleNameController, - validator: (value) { - if (value.isEmpty) { + validator: (String? value) { + if (value!.isEmpty) { return 'Please enter your Name'; } return null; @@ -171,9 +158,9 @@ class _RecoverScreenState extends State { keyboardType: TextInputType.multiline, maxLines: null, decoration: InputDecoration(border: OutlineInputBorder(), labelText: 'SEED PHRASE'), - controller: seedPhrasecontroller, - validator: (value) { - if (value.isEmpty) { + controller: seedPhraseController, + validator: (String? value) { + if (value!.isEmpty) { return 'Please enter your Seed phrase'; } return null; @@ -204,19 +191,21 @@ class _RecoverScreenState extends State { FocusScope.of(context).requestFocus(new FocusNode()); - String doubleNameValue = - doubleNameController.text?.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); - String seedPhraseValue = - seedPhrasecontroller.text?.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String doubleNameValue = doubleNameController.text + .toLowerCase() + .trim() + .replaceAll(new RegExp(r"\s+"), " "); + String seedPhraseValue = seedPhraseController.text + .toLowerCase() + .trim() + .replaceAll(new RegExp(r"\s+"), " "); setState(() { doubleNameController.text = doubleNameValue; - seedPhrasecontroller.text = seedPhraseValue; - - _autoValidate = true; + seedPhraseController.text = seedPhraseValue; doubleName = doubleNameController.text + '.3bot'; - seedPhrase = seedPhrasecontroller.text; + seedPhrase = seedPhraseController.text; }); try { diff --git a/app/lib/screens/reservation_screen.dart b/app/lib/screens/reservation_screen.dart index 7bd28e07..21c96e55 100644 --- a/app/lib/screens/reservation_screen.dart +++ b/app/lib/screens/reservation_screen.dart @@ -1,817 +1,817 @@ -import 'dart:convert'; -import 'dart:ffi'; - -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:http/http.dart'; -import 'package:threebotlogin/events/events.dart'; -import 'package:threebotlogin/events/go_wallet_event.dart'; -import 'package:threebotlogin/helpers/globals.dart'; -import 'package:threebotlogin/helpers/hex_color.dart'; -import 'package:threebotlogin/models/paymentRequest.dart'; -import 'package:threebotlogin/services/3bot_service.dart'; -import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/widgets/custom_dialog.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ReservationScreen extends StatefulWidget { - ReservationScreen({Key key}) : super(key: key); - - @override - _ReservationScreenState createState() => _ReservationScreenState(); -} - -class _ReservationScreenState extends State { - String doubleName = ''; - bool _isLoading = false; - - Map _allProductKeys; - - List _activatedProductKeys = []; - List _unActivatedProductKeys = []; - - TextEditingController productKeyController = TextEditingController(); - bool _isValid = false; - bool _layoutInputValid = true; - bool _isDigitalTwinActive = true; - bool disableReserveNowButton = false; - - Future _getReservationDetails() async { - if (doubleName.isEmpty) { - String value = await getDoubleName(); - doubleName = value; - setState(() { - doubleName = value; - }); - } - - Response reservationDetailsResult = await getReservationDetails(doubleName); - Map reservationDetails = - jsonDecode(reservationDetailsResult.body); - - if (reservationDetails['details'] == null) { - return; - } - - return reservationDetails['details']; - } - - Future _checkReservations() async { - if (doubleName.isEmpty) { - String value = await getDoubleName(); - doubleName = value; - setState(() { - doubleName = value; - }); - if (!_isLoading) { - _loadingDialog(); - setState(() { - _isLoading = true; - }); - } - } - - Response reservationsResult = await getReservations(doubleName); - - if (reservationsResult.statusCode != 200) { - // TODO let user know there was an error - _isDigitalTwinActive = false; - return {"active": false}; - } - - await _fillProductKeys(); - - _isDigitalTwinActive = jsonDecode(reservationsResult.body)['active']; - - Response allProductKeysResult = await getAllProductKeys(); - Map allProductKeys = jsonDecode(allProductKeysResult.body); - _allProductKeys = allProductKeys; - - if (_isLoading) { - Navigator.pop(context); // Remove loading screen - setState(() { - _isLoading = false; - }); - } - } - - Future _checkIfProductKeyIsValid(String productKey) async { - if (_allProductKeys.isEmpty) return false; - if (_allProductKeys['productkeys'] == null) return false; - - for (var item in _allProductKeys['productkeys']) { - if (item['key'] == productKey) return true; - } - return false; - } - - _fillProductKeys() async { - Response productKeysResult = await getProductKeys(doubleName); - Map productKeys = jsonDecode(productKeysResult.body); - - _unActivatedProductKeys = []; - _activatedProductKeys = []; - - if (productKeys == null) { - // There are no product keys available - return; - } - - if (productKeys['productkeys'] == null) return []; - - for (var item in productKeys['productkeys']) { - if (item['status'] == 1) { - _unActivatedProductKeys.add(item); - } else { - _activatedProductKeys.add(item); - } - } - - return productKeys['productkeys']; - } - - Future _showActivatedKeys() async { - return _activatedProductKeys; - } - - Future _showUnActivatedKeys() async { - return _unActivatedProductKeys; - } - - _activateProductKey(String productKey) async { - bool isValid = await _checkIfProductKeyIsValid(productKey); - - if (isValid == false) { - return; - } - - activateDigitalTwin(doubleName, productKey); - _successDialog(); - productKeyController.text = ''; - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!_isLoading) { - return LayoutDrawer( - titleText: 'Reservations', - content: Stack( - children: [ - SvgPicture.asset( - 'assets/bg.svg', - alignment: Alignment.center, - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - ), - SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - FutureBuilder( - future: _checkReservations(), - builder: (BuildContext context, - AsyncSnapshot snapshot) { - Widget box = Container(); - box = _isDigitalTwinActive - ? _reserved() - : _notReservedYet(); - - return Container( - padding: EdgeInsets.all(25.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - box, - SizedBox( - height: 50.0, - ), - _reserveForLovedOnes(), - SizedBox( - height: 50.0, - ), - _productKeysItem(), - ], - ), - ); - }, - ), - ], - ), - ), - ], - ), - ); - } - return Container(); - } - - Widget _notReservedYet() { - return _card( - title: 'Reserve your digital twin for life', - body: Column( - children: [ - RichText( - text: TextSpan( - style: new TextStyle( - fontSize: 14.0, - color: Colors.black, - ), - children: [ - TextSpan( - text: - 'With Digital Twin seamless experiences, grant yourself with a lifetime digital freedom and privacy for only 1000 TFT. \n \n', - ), - TextSpan(text: 'Visit '), - TextSpan( - style: new TextStyle( - color: Theme.of(context).primaryColor, - ), - text: 'Digital Twin website', - recognizer: TapGestureRecognizer() - ..onTap = () async { - final url = 'https://mydigitaltwin.io/'; - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: false, - ); - } - }, - ), - TextSpan(text: ' for more info. '), - ], - )), - SizedBox( - height: 10, - ), - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - ElevatedButton( - onPressed: disableReserveNowButton - ? null - : () { - redirectToWallet(activatedDirectly: true); - }, - child: Text('Reserve Now'), - ), - ]), - ], - ), - ); - } - - Widget _reserved() { - return _card( - title: 'You Have Reserved Your Digital Twin', - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - children: [ - RichText( - text: TextSpan( - style: new TextStyle( - fontSize: 14.0, - color: Colors.black, - ), - children: [ - TextSpan( - text: 'Digital Twin for Life is coming soon. Go to \n', - ), - TextSpan( - style: new TextStyle( - color: Theme.of(context).primaryColor, - ), - text: 'Digital Twin Website', - recognizer: TapGestureRecognizer() - ..onTap = () async { - final url = 'https://mydigitaltwin.io/'; - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: false, - ); - } - }, - ), - TextSpan( - text: - ' and subscribe to our Telegram Channel for news and updates.'), - ], - )), - ], - ), - SizedBox( - height: 10.0, - ), - TextButton.icon( - onPressed: () async { - final url = 'https://t.me/joinchat/JnJfqY9tfAU1NTY0'; - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: false, - ); - } - }, - label: Text('Digital Twin Telegram Channel'), - icon: Icon(Icons.open_in_new), - ), - ElevatedButton( - onPressed: () { - _showReservation(); - }, - child: Text('My Digital Twin Reservation')) - ], - ), - ); - } - - Widget _reserveForLovedOnes() { - return _card( - title: "Reserve Digital Twin for Life for Your Loved Ones", - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - style: new TextStyle( - fontSize: 14.0, - color: Colors.black, - ), - children: [ - TextSpan( - text: - 'Grant a Digital Twin for Life to your loved ones for only 1000 TFT. All you need is their 3Bot ID. \n \n', - ), - TextSpan(text: 'Visit '), - TextSpan( - style: new TextStyle( - color: Theme.of(context).primaryColor, - ), - text: 'Digital Twin website', - recognizer: TapGestureRecognizer() - ..onTap = () async { - final url = 'https://mydigitaltwin.io/'; - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: false, - ); - } - }, - ), - TextSpan(text: ' for more info. '), - ], - )), - SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () { - redirectToWallet( - reservingFor: 'loved.3bot', activatedDirectly: false); - }, - child: Text('Buy Product Key'), - ) - ], - ), - ], - ), - ); - } - - // TODO: make this code more performant - Widget _productKeysItem() { - return _card( - title: "Product keys", - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // FutureBuilder( - // future: _fillProductKeys(), - // builder: (context, snapshot) { - // return Container(); - // }), - Row( - children: [ - Text('Unclaimed', - style: - new TextStyle(fontWeight: FontWeight.bold, fontSize: 18), - textAlign: TextAlign.left) - ], - ), - Row( - children: [ - FutureBuilder( - future: _showUnActivatedKeys(), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data.length > 0) { - return Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: _unActivatedProductKeys.length, - itemBuilder: (context, index) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _unActivatedProductKeys.length <= 0 - ? Container() - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8, - right: 8, - bottom: 8), - child: Text('Product key ' + - (index + 1).toString() + - ': ' + - _unActivatedProductKeys[ - index]['key'] - .toString()), - ), - new GestureDetector( - onTap: () async { - if (snapshot.hasData) { - Clipboard.setData( - new ClipboardData( - text: snapshot - .data[index] - ['key'] - .toString())); - - final snackBar = SnackBar( - content: Text( - 'Product key copied to clipboard'), - ); - - ScaffoldMessenger.of( - context) - .showSnackBar(snackBar); - } - }, - child: Icon( - Icons.content_copy, - size: 14, - ), - ), - ], - ) - ]); - })); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: - Container(child: Text('No product keys available')), - ); - } - }, - ), - ], - ), - SizedBox( - height: 10, - ), - Row( - children: [ - Text('Activated', - style: - new TextStyle(fontWeight: FontWeight.bold, fontSize: 18), - textAlign: TextAlign.left) - ], - ), - Row( - children: [ - FutureBuilder( - future: _showActivatedKeys(), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data.length > 0) { - return Expanded( - child: ListView.builder( - shrinkWrap: true, - itemCount: _activatedProductKeys.length, - itemBuilder: (context, index) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _activatedProductKeys.length <= 0 - ? Container() - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 8, right: 8), - child: Text( - _activatedProductKeys[index] - ['double_name'] - .toString()), - ), - Text( - 'Activated', - style: TextStyle( - color: Colors.green, - fontWeight: - FontWeight.bold), - ) - ], - ) - ]); - })); - } else { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container(child: Text('No users activated')), - ); - } - }, - ), - ], - ), - SizedBox( - height: 10, - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _isDigitalTwinActive - ? Container() - : Flexible( - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 25), - child: TextFormField( - controller: productKeyController, - decoration: InputDecoration( - border: UnderlineInputBorder(), - labelText: 'Enter product key', - errorText: _layoutInputValid - ? '' - : 'Enter a valid product key', - errorBorder: _layoutInputValid - ? new OutlineInputBorder( - borderSide: new BorderSide( - color: Colors.transparent, - width: 0.0)) - : new OutlineInputBorder( - borderSide: new BorderSide( - color: Colors.red, width: 1), - ), - ), - ), - )), - ElevatedButton( - onPressed: () async { - bool isValidated = await _checkIfProductKeyIsValid( - productKeyController.text); - setState(() => _layoutInputValid = isValidated); - - if (!_layoutInputValid) return; - _activateProductKey(productKeyController.text); - }, - child: Text('Activate'), - ), - ], - )) - ], - ), - ], - ), - ); - } - - Future redirectToWallet( - {String reservingFor, bool activatedDirectly}) async { - setState(() { - disableReserveNowButton = true; - }); - - if (reservingFor == null) { - reservingFor = doubleName; - } - - Map data = { - 'doubleName': doubleName, - 'reservationBy': await getDoubleName(), - 'activated_directly': activatedDirectly, - }; - - Response res = await sendProductReservation(data); - - Map decode = json.decode(res.body); - - Globals().paymentRequest = PaymentRequest.fromJson(decode); - Globals().paymentRequestIsUsed = false; - - Events().emit(GoWalletEvent()); - - setState(() { - disableReserveNowButton = false; - }); - } - - Future _loadingDialog() { - return showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) { - return WillPopScope( - onWillPop: () => Future.value(false), - child: Dialog( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 10, - ), - new CircularProgressIndicator(), - SizedBox( - height: 10, - ), - new Text("One moment please"), - SizedBox( - height: 10, - ), - ], - ), - ), - ); - }, - ); - } - - Future _successDialog() { - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.check, - title: "Successfully activated", - description: "The product key was successfully activated", - actions: [ - FlatButton( - child: new Text("Ok"), - onPressed: () { - Navigator.pop(context); - setState(() {}); - }, - ), - ], - )); - } - - Future _showReservation() { - return showDialog( - context: context, - builder: (BuildContext context) => Dialog( - child: FutureBuilder( - future: _getReservationDetails(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 10, - ), - new Text( - 'My Digital Twin', - style: TextStyle( - fontSize: 20.0, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - new Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 10, - ), - new CircularProgressIndicator(), - SizedBox( - height: 10, - ), - new Text("Loading"), - SizedBox( - height: 10, - ), - ], - ), - ], - ); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 10, - ), - new Text( - 'My Digital Twin', - style: TextStyle( - fontSize: 20.0, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - SizedBox( - height: 10, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: new Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - new Text('Reserved for:', - style: TextStyle(fontWeight: FontWeight.bold)), - new Text(snapshot.data['double_name']) - ], - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: new Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - new Text('Product Key:', - style: TextStyle(fontWeight: FontWeight.bold)), - new Text(snapshot.data['key']) - ], - ), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: new Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - new Text('Status:', - style: TextStyle(fontWeight: FontWeight.bold)), - new Text('Activated', - style: TextStyle( - color: Colors.green, - fontWeight: FontWeight.bold)) - ], - ), - ), - new ElevatedButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: Text('OK')), - SizedBox( - height: 10, - ), - ], - ); - }, - ), - )); - } - - Widget _card({String title, Widget body}) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - boxShadow: [ - BoxShadow( - offset: Offset(1, 2), - blurRadius: 2.0, - spreadRadius: 0.0, - color: Colors.grey.shade300), - ], - ), - child: Container( - padding: EdgeInsets.all(25.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), - SizedBox( - height: 20, - ), - body, - ], - ), - ), - ); - } -} +// import 'dart:convert'; +// import 'dart:ffi'; +// +// import 'package:flutter/gestures.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter/widgets.dart'; +// import 'package:flutter_svg/svg.dart'; +// import 'package:http/http.dart'; +// import 'package:threebotlogin/events/events.dart'; +// import 'package:threebotlogin/events/go_wallet_event.dart'; +// import 'package:threebotlogin/helpers/globals.dart'; +// import 'package:threebotlogin/helpers/hex_color.dart'; +// import 'package:threebotlogin/models/paymentRequest.dart'; +// import 'package:threebotlogin/services/3bot_service.dart'; +// import 'package:threebotlogin/services/shared_preference_service.dart'; +// import 'package:threebotlogin/widgets/custom_dialog.dart'; +// import 'package:threebotlogin/widgets/layout_drawer.dart'; +// import 'package:url_launcher/url_launcher.dart'; +// +// class ReservationScreen extends StatefulWidget { +// ReservationScreen({Key key}) : super(key: key); +// +// @override +// _ReservationScreenState createState() => _ReservationScreenState(); +// } +// +// class _ReservationScreenState extends State { +// String doubleName = ''; +// bool _isLoading = false; +// +// Map _allProductKeys; +// +// List _activatedProductKeys = []; +// List _unActivatedProductKeys = []; +// +// TextEditingController productKeyController = TextEditingController(); +// bool _isValid = false; +// bool _layoutInputValid = true; +// bool _isDigitalTwinActive = true; +// bool disableReserveNowButton = false; +// +// Future _getReservationDetails() async { +// if (doubleName.isEmpty) { +// String value = await getDoubleName(); +// doubleName = value; +// setState(() { +// doubleName = value; +// }); +// } +// +// Response reservationDetailsResult = await getReservationDetails(doubleName); +// Map reservationDetails = +// jsonDecode(reservationDetailsResult.body); +// +// if (reservationDetails['details'] == null) { +// return; +// } +// +// return reservationDetails['details']; +// } +// +// Future _checkReservations() async { +// if (doubleName.isEmpty) { +// String value = await getDoubleName(); +// doubleName = value; +// setState(() { +// doubleName = value; +// }); +// if (!_isLoading) { +// _loadingDialog(); +// setState(() { +// _isLoading = true; +// }); +// } +// } +// +// Response reservationsResult = await getReservations(doubleName); +// +// if (reservationsResult.statusCode != 200) { +// // TODO let user know there was an error +// _isDigitalTwinActive = false; +// return {"active": false}; +// } +// +// await _fillProductKeys(); +// +// _isDigitalTwinActive = jsonDecode(reservationsResult.body)['active']; +// +// Response allProductKeysResult = await getAllProductKeys(); +// Map allProductKeys = jsonDecode(allProductKeysResult.body); +// _allProductKeys = allProductKeys; +// +// if (_isLoading) { +// Navigator.pop(context); // Remove loading screen +// setState(() { +// _isLoading = false; +// }); +// } +// } +// +// Future _checkIfProductKeyIsValid(String productKey) async { +// if (_allProductKeys.isEmpty) return false; +// if (_allProductKeys['productkeys'] == null) return false; +// +// for (var item in _allProductKeys['productkeys']) { +// if (item['key'] == productKey) return true; +// } +// return false; +// } +// +// _fillProductKeys() async { +// Response productKeysResult = await getProductKeys(doubleName); +// Map productKeys = jsonDecode(productKeysResult.body); +// +// _unActivatedProductKeys = []; +// _activatedProductKeys = []; +// +// if (productKeys == null) { +// // There are no product keys available +// return; +// } +// +// if (productKeys['productkeys'] == null) return []; +// +// for (var item in productKeys['productkeys']) { +// if (item['status'] == 1) { +// _unActivatedProductKeys.add(item); +// } else { +// _activatedProductKeys.add(item); +// } +// } +// +// return productKeys['productkeys']; +// } +// +// Future _showActivatedKeys() async { +// return _activatedProductKeys; +// } +// +// Future _showUnActivatedKeys() async { +// return _unActivatedProductKeys; +// } +// +// _activateProductKey(String productKey) async { +// bool isValid = await _checkIfProductKeyIsValid(productKey); +// +// if (isValid == false) { +// return; +// } +// +// activateDigitalTwin(doubleName, productKey); +// _successDialog(); +// productKeyController.text = ''; +// } +// +// @override +// void dispose() { +// super.dispose(); +// } +// +// @override +// Widget build(BuildContext context) { +// if (!_isLoading) { +// return LayoutDrawer( +// titleText: 'Reservations', +// content: Stack( +// children: [ +// SvgPicture.asset( +// 'assets/bg.svg', +// alignment: Alignment.center, +// width: MediaQuery.of(context).size.width, +// height: MediaQuery.of(context).size.height, +// ), +// SingleChildScrollView( +// child: Column( +// mainAxisSize: MainAxisSize.max, +// children: [ +// FutureBuilder( +// future: _checkReservations(), +// builder: (BuildContext context, +// AsyncSnapshot snapshot) { +// Widget box = Container(); +// box = _isDigitalTwinActive +// ? _reserved() +// : _notReservedYet(); +// +// return Container( +// padding: EdgeInsets.all(25.0), +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// box, +// SizedBox( +// height: 50.0, +// ), +// _reserveForLovedOnes(), +// SizedBox( +// height: 50.0, +// ), +// _productKeysItem(), +// ], +// ), +// ); +// }, +// ), +// ], +// ), +// ), +// ], +// ), +// ); +// } +// return Container(); +// } +// +// Widget _notReservedYet() { +// return _card( +// title: 'Reserve your digital twin for life', +// body: Column( +// children: [ +// RichText( +// text: TextSpan( +// style: new TextStyle( +// fontSize: 14.0, +// color: Colors.black, +// ), +// children: [ +// TextSpan( +// text: +// 'With Digital Twin seamless experiences, grant yourself with a lifetime digital freedom and privacy for only 1000 TFT. \n \n', +// ), +// TextSpan(text: 'Visit '), +// TextSpan( +// style: new TextStyle( +// color: Theme.of(context).primaryColor, +// ), +// text: 'Digital Twin website', +// recognizer: TapGestureRecognizer() +// ..onTap = () async { +// final url = 'https://mydigitaltwin.io/'; +// if (await canLaunch(url)) { +// await launch( +// url, +// forceSafariVC: false, +// ); +// } +// }, +// ), +// TextSpan(text: ' for more info. '), +// ], +// )), +// SizedBox( +// height: 10, +// ), +// Row(mainAxisAlignment: MainAxisAlignment.end, children: [ +// ElevatedButton( +// onPressed: disableReserveNowButton +// ? null +// : () { +// redirectToWallet(activatedDirectly: true); +// }, +// child: Text('Reserve Now'), +// ), +// ]), +// ], +// ), +// ); +// } +// +// Widget _reserved() { +// return _card( +// title: 'You Have Reserved Your Digital Twin', +// body: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Wrap( +// children: [ +// RichText( +// text: TextSpan( +// style: new TextStyle( +// fontSize: 14.0, +// color: Colors.black, +// ), +// children: [ +// TextSpan( +// text: 'Digital Twin for Life is coming soon. Go to \n', +// ), +// TextSpan( +// style: new TextStyle( +// color: Theme.of(context).primaryColor, +// ), +// text: 'Digital Twin Website', +// recognizer: TapGestureRecognizer() +// ..onTap = () async { +// final url = 'https://mydigitaltwin.io/'; +// if (await canLaunch(url)) { +// await launch( +// url, +// forceSafariVC: false, +// ); +// } +// }, +// ), +// TextSpan( +// text: +// ' and subscribe to our Telegram Channel for news and updates.'), +// ], +// )), +// ], +// ), +// SizedBox( +// height: 10.0, +// ), +// TextButton.icon( +// onPressed: () async { +// final url = 'https://t.me/joinchat/JnJfqY9tfAU1NTY0'; +// if (await canLaunch(url)) { +// await launch( +// url, +// forceSafariVC: false, +// ); +// } +// }, +// label: Text('Digital Twin Telegram Channel'), +// icon: Icon(Icons.open_in_new), +// ), +// ElevatedButton( +// onPressed: () { +// _showReservation(); +// }, +// child: Text('My Digital Twin Reservation')) +// ], +// ), +// ); +// } +// +// Widget _reserveForLovedOnes() { +// return _card( +// title: "Reserve Digital Twin for Life for Your Loved Ones", +// body: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// RichText( +// text: TextSpan( +// style: new TextStyle( +// fontSize: 14.0, +// color: Colors.black, +// ), +// children: [ +// TextSpan( +// text: +// 'Grant a Digital Twin for Life to your loved ones for only 1000 TFT. All you need is their 3Bot ID. \n \n', +// ), +// TextSpan(text: 'Visit '), +// TextSpan( +// style: new TextStyle( +// color: Theme.of(context).primaryColor, +// ), +// text: 'Digital Twin website', +// recognizer: TapGestureRecognizer() +// ..onTap = () async { +// final url = 'https://mydigitaltwin.io/'; +// if (await canLaunch(url)) { +// await launch( +// url, +// forceSafariVC: false, +// ); +// } +// }, +// ), +// TextSpan(text: ' for more info. '), +// ], +// )), +// SizedBox( +// height: 10, +// ), +// Row( +// mainAxisAlignment: MainAxisAlignment.end, +// children: [ +// ElevatedButton( +// onPressed: () { +// redirectToWallet( +// reservingFor: 'loved.3bot', activatedDirectly: false); +// }, +// child: Text('Buy Product Key'), +// ) +// ], +// ), +// ], +// ), +// ); +// } +// +// // TODO: make this code more performant +// Widget _productKeysItem() { +// return _card( +// title: "Product keys", +// body: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // FutureBuilder( +// // future: _fillProductKeys(), +// // builder: (context, snapshot) { +// // return Container(); +// // }), +// Row( +// children: [ +// Text('Unclaimed', +// style: +// new TextStyle(fontWeight: FontWeight.bold, fontSize: 18), +// textAlign: TextAlign.left) +// ], +// ), +// Row( +// children: [ +// FutureBuilder( +// future: _showUnActivatedKeys(), +// builder: (context, snapshot) { +// if (snapshot.hasData && snapshot.data.length > 0) { +// return Expanded( +// child: ListView.builder( +// shrinkWrap: true, +// itemCount: _unActivatedProductKeys.length, +// itemBuilder: (context, index) { +// return Column( +// mainAxisAlignment: MainAxisAlignment.start, +// children: [ +// _unActivatedProductKeys.length <= 0 +// ? Container() +// : Row( +// mainAxisAlignment: +// MainAxisAlignment.spaceBetween, +// children: [ +// Padding( +// padding: const EdgeInsets.only( +// top: 8, +// right: 8, +// bottom: 8), +// child: Text('Product key ' + +// (index + 1).toString() + +// ': ' + +// _unActivatedProductKeys[ +// index]['key'] +// .toString()), +// ), +// new GestureDetector( +// onTap: () async { +// if (snapshot.hasData) { +// Clipboard.setData( +// new ClipboardData( +// text: snapshot +// .data[index] +// ['key'] +// .toString())); +// +// final snackBar = SnackBar( +// content: Text( +// 'Product key copied to clipboard'), +// ); +// +// ScaffoldMessenger.of( +// context) +// .showSnackBar(snackBar); +// } +// }, +// child: Icon( +// Icons.content_copy, +// size: 14, +// ), +// ), +// ], +// ) +// ]); +// })); +// } else { +// return Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: +// Container(child: Text('No product keys available')), +// ); +// } +// }, +// ), +// ], +// ), +// SizedBox( +// height: 10, +// ), +// Row( +// children: [ +// Text('Activated', +// style: +// new TextStyle(fontWeight: FontWeight.bold, fontSize: 18), +// textAlign: TextAlign.left) +// ], +// ), +// Row( +// children: [ +// FutureBuilder( +// future: _showActivatedKeys(), +// builder: (context, snapshot) { +// if (snapshot.hasData && snapshot.data.length > 0) { +// return Expanded( +// child: ListView.builder( +// shrinkWrap: true, +// itemCount: _activatedProductKeys.length, +// itemBuilder: (context, index) { +// return Column( +// mainAxisAlignment: MainAxisAlignment.start, +// children: [ +// _activatedProductKeys.length <= 0 +// ? Container() +// : Row( +// mainAxisAlignment: +// MainAxisAlignment.spaceBetween, +// children: [ +// Padding( +// padding: const EdgeInsets.only( +// top: 8, right: 8), +// child: Text( +// _activatedProductKeys[index] +// ['double_name'] +// .toString()), +// ), +// Text( +// 'Activated', +// style: TextStyle( +// color: Colors.green, +// fontWeight: +// FontWeight.bold), +// ) +// ], +// ) +// ]); +// })); +// } else { +// return Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: Container(child: Text('No users activated')), +// ); +// } +// }, +// ), +// ], +// ), +// SizedBox( +// height: 10, +// ), +// Row( +// mainAxisAlignment: MainAxisAlignment.end, +// children: [ +// _isDigitalTwinActive +// ? Container() +// : Flexible( +// child: Row( +// children: [ +// Expanded( +// child: Padding( +// padding: const EdgeInsets.only(right: 25), +// child: TextFormField( +// controller: productKeyController, +// decoration: InputDecoration( +// border: UnderlineInputBorder(), +// labelText: 'Enter product key', +// errorText: _layoutInputValid +// ? '' +// : 'Enter a valid product key', +// errorBorder: _layoutInputValid +// ? new OutlineInputBorder( +// borderSide: new BorderSide( +// color: Colors.transparent, +// width: 0.0)) +// : new OutlineInputBorder( +// borderSide: new BorderSide( +// color: Colors.red, width: 1), +// ), +// ), +// ), +// )), +// ElevatedButton( +// onPressed: () async { +// bool isValidated = await _checkIfProductKeyIsValid( +// productKeyController.text); +// setState(() => _layoutInputValid = isValidated); +// +// if (!_layoutInputValid) return; +// _activateProductKey(productKeyController.text); +// }, +// child: Text('Activate'), +// ), +// ], +// )) +// ], +// ), +// ], +// ), +// ); +// } +// +// Future redirectToWallet( +// {String reservingFor, bool activatedDirectly}) async { +// setState(() { +// disableReserveNowButton = true; +// }); +// +// if (reservingFor == null) { +// reservingFor = doubleName; +// } +// +// Map data = { +// 'doubleName': doubleName, +// 'reservationBy': await getDoubleName(), +// 'activated_directly': activatedDirectly, +// }; +// +// Response res = await sendProductReservation(data); +// +// Map decode = json.decode(res.body); +// +// Globals().paymentRequest = PaymentRequest.fromJson(decode); +// Globals().paymentRequestIsUsed = false; +// +// Events().emit(GoWalletEvent()); +// +// setState(() { +// disableReserveNowButton = false; +// }); +// } +// +// Future _loadingDialog() { +// return showDialog( +// barrierDismissible: false, +// context: context, +// builder: (BuildContext context) { +// return WillPopScope( +// onWillPop: () => Future.value(false), +// child: Dialog( +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 10, +// ), +// new CircularProgressIndicator(), +// SizedBox( +// height: 10, +// ), +// new Text("One moment please"), +// SizedBox( +// height: 10, +// ), +// ], +// ), +// ), +// ); +// }, +// ); +// } +// +// Future _successDialog() { +// return showDialog( +// context: context, +// builder: (BuildContext context) => CustomDialog( +// image: Icons.check, +// title: "Successfully activated", +// description: "The product key was successfully activated", +// actions: [ +// FlatButton( +// child: new Text("Ok"), +// onPressed: () { +// Navigator.pop(context); +// setState(() {}); +// }, +// ), +// ], +// )); +// } +// +// Future _showReservation() { +// return showDialog( +// context: context, +// builder: (BuildContext context) => Dialog( +// child: FutureBuilder( +// future: _getReservationDetails(), +// builder: +// (BuildContext context, AsyncSnapshot snapshot) { +// if (!snapshot.hasData) { +// return Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 10, +// ), +// new Text( +// 'My Digital Twin', +// style: TextStyle( +// fontSize: 20.0, fontWeight: FontWeight.bold), +// textAlign: TextAlign.center, +// ), +// new Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 10, +// ), +// new CircularProgressIndicator(), +// SizedBox( +// height: 10, +// ), +// new Text("Loading"), +// SizedBox( +// height: 10, +// ), +// ], +// ), +// ], +// ); +// } +// return Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox( +// height: 10, +// ), +// new Text( +// 'My Digital Twin', +// style: TextStyle( +// fontSize: 20.0, fontWeight: FontWeight.bold), +// textAlign: TextAlign.center, +// ), +// SizedBox( +// height: 10, +// ), +// Padding( +// padding: const EdgeInsets.all(16.0), +// child: new Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// new Text('Reserved for:', +// style: TextStyle(fontWeight: FontWeight.bold)), +// new Text(snapshot.data['double_name']) +// ], +// ), +// ), +// Padding( +// padding: const EdgeInsets.all(16.0), +// child: new Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// new Text('Product Key:', +// style: TextStyle(fontWeight: FontWeight.bold)), +// new Text(snapshot.data['key']) +// ], +// ), +// ), +// Padding( +// padding: const EdgeInsets.all(16.0), +// child: new Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// new Text('Status:', +// style: TextStyle(fontWeight: FontWeight.bold)), +// new Text('Activated', +// style: TextStyle( +// color: Colors.green, +// fontWeight: FontWeight.bold)) +// ], +// ), +// ), +// new ElevatedButton( +// onPressed: () { +// Navigator.pop(context, true); +// }, +// child: Text('OK')), +// SizedBox( +// height: 10, +// ), +// ], +// ); +// }, +// ), +// )); +// } +// +// Widget _card({String title, Widget body}) { +// return Container( +// decoration: BoxDecoration( +// color: Colors.white, +// borderRadius: BorderRadius.circular(5), +// boxShadow: [ +// BoxShadow( +// offset: Offset(1, 2), +// blurRadius: 2.0, +// spreadRadius: 0.0, +// color: Colors.grey.shade300), +// ], +// ), +// child: Container( +// padding: EdgeInsets.all(25.0), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// title, +// style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), +// ), +// SizedBox( +// height: 20, +// ), +// body, +// ], +// ), +// ), +// ); +// } +// } diff --git a/app/lib/screens/successful_screen.dart b/app/lib/screens/successful_screen.dart index eaf220bf..0906a824 100644 --- a/app/lib/screens/successful_screen.dart +++ b/app/lib/screens/successful_screen.dart @@ -6,7 +6,7 @@ class SuccessfulScreen extends StatefulWidget { final String title; final String text; - SuccessfulScreen({this.title, this.text}); + SuccessfulScreen({required this.title, required this.text}); _SuccessfulScreenState createState() => _SuccessfulScreenState(); } diff --git a/app/lib/screens/unregistered_screen.dart b/app/lib/screens/unregistered_screen.dart index 97d823e1..1d96f3aa 100644 --- a/app/lib/screens/unregistered_screen.dart +++ b/app/lib/screens/unregistered_screen.dart @@ -34,7 +34,7 @@ class _UnregisteredScreenState extends State } Future startRecovery() async { - final bool registered = await Navigator.push( + final bool? registered = await Navigator.push( context, MaterialPageRoute(builder: (context) => RecoverScreen())); if (registered != null && registered) { await Navigator.push( @@ -49,7 +49,7 @@ class _UnregisteredScreenState extends State text: "Your account has been recovered."))); - await Flags().initialiseFlagSmith(); + await Flags().initFlagSmith(); await Flags().setFlagSmithDefaultValues(); Navigator.pop(context); diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 2d172b3a..77f77696 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -13,7 +13,7 @@ String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; Future sendData( - String state, data, selectedImageId, String randomRoom, String appId) async { + String state, Map? data, selectedImageId, String? randomRoom, String appId) async { Uri url = Uri.parse('$threeBotApiUrl/signedAttempt'); print('Sending call: ${url.toString()}'); diff --git a/app/lib/services/uni_link_service.dart b/app/lib/services/uni_link_service.dart index 45025132..54023149 100644 --- a/app/lib/services/uni_link_service.dart +++ b/app/lib/services/uni_link_service.dart @@ -13,8 +13,8 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; class UniLinkService { static void handleUniLink(UniLinkEvent e) async { - Uri link = e.link; - BuildContext context = e.context; + Uri link = e.link!; + BuildContext context = e.context!; String? jsonScope = link.queryParameters['scope']; String? state = link.queryParameters['state']; diff --git a/app/lib/widgets/image_button.dart b/app/lib/widgets/image_button.dart index 48b70581..847dc125 100644 --- a/app/lib/widgets/image_button.dart +++ b/app/lib/widgets/image_button.dart @@ -5,7 +5,7 @@ class ImageButton extends StatefulWidget { final selectedImageId; final callback; - ImageButton(this.imageId, this.selectedImageId, this.callback, {required Key key}) + ImageButton(this.imageId, this.selectedImageId, this.callback, {Key? key}) : super(key: key); _ImageButtonState createState() => _ImageButtonState(); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 975a37f5..92e13972 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: flutter_sodium: ^0.2.0 uni_links: ^0.5.1 pbkdf2ns: ^0.0.2 + qr_code_scanner: ^0.6.1 dev_dependencies: flutter_test: From 94ba37295b98d5b05e103a8f16ed91a18164923e Mon Sep 17 00:00:00 2001 From: Lennert Date: Fri, 14 Jan 2022 17:10:41 +0100 Subject: [PATCH 06/75] WIP --- app/android/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- app/lib/helpers/kyc_helpers.dart | 2 +- app/lib/main.dart | 3 + app/lib/models/login.dart | 5 +- app/lib/models/scope.dart | 26 +-- app/lib/screens/authentication_screen.dart | 5 +- .../screens/identity_verification_screen.dart | 195 +++++++++--------- app/lib/screens/login_screen.dart | 9 +- app/lib/screens/preference_screen.dart | 2 +- app/lib/services/3bot_service.dart | 29 ++- app/lib/services/crypto_service.dart | 2 + .../services/shared_preference_service.dart | 37 ++-- app/lib/widgets/pin_field.dart | 4 +- app/pubspec.yaml | 8 +- 15 files changed, 173 insertions(+), 160 deletions(-) diff --git a/app/android/build.gradle b/app/android/build.gradle index 8c0798cc..b50f2cbb 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.31' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.google.gms:google-services:4.3.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index bc24dcf0..493072b3 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-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/app/lib/helpers/kyc_helpers.dart b/app/lib/helpers/kyc_helpers.dart index 2cab8039..ce2cd243 100644 --- a/app/lib/helpers/kyc_helpers.dart +++ b/app/lib/helpers/kyc_helpers.dart @@ -94,6 +94,6 @@ Future saveCorrectVerificationStates( } bool checkEmail(String email) { - String? emailValue = email.toLowerCase()?.trim()?.replaceAll(new RegExp(r"\s+"), " "); + String? emailValue = email.toLowerCase().trim().replaceAll(new RegExp(r"\s+"), " "); return validateEmail(emailValue); } diff --git a/app/lib/main.dart b/app/lib/main.dart index 626c9bb7..95fb3778 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -7,6 +7,7 @@ import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'app_config.dart'; import 'helpers/kyc_helpers.dart'; @@ -18,6 +19,8 @@ Future main() async { await setGlobalValues(); + print(AppConfig().threeBotSocketUrl()); + bool registered = doubleName != null; if (await getPhrase() != null) { diff --git a/app/lib/models/login.dart b/app/lib/models/login.dart index 21a52d42..bfa440da 100644 --- a/app/lib/models/login.dart +++ b/app/lib/models/login.dart @@ -46,7 +46,7 @@ class Login { type = json['type'], randomRoom = json['randomRoom'], redirectUrl = json['redirecturl'], - isMobile = json['mobile'] as bool, + isMobile = json['mobile'] as bool?, created = json['created'], locationId = json['locationId']; @@ -75,6 +75,9 @@ class Login { String decryptedLoginAttempt = await decrypt(data['encryptedLoginAttempt'], pk, sk); dynamic decryptedLoginAttemptMap = jsonDecode(decryptedLoginAttempt); + print('Decrypted login attempt'); + print(decryptedLoginAttempt); + decryptedLoginAttemptMap['type'] = data['type']; decryptedLoginAttemptMap['created'] = data['created']; diff --git a/app/lib/models/scope.dart b/app/lib/models/scope.dart index cee1b1eb..eb8bca90 100644 --- a/app/lib/models/scope.dart +++ b/app/lib/models/scope.dart @@ -16,19 +16,19 @@ class Scope { Scope({this.doubleName, this.email}); Scope.fromJson(Map json) - : doubleName = json['doubleName'] as bool, - user = json['user'] as bool, - email = json['email'] as bool, - derivedSeed = json['derivedSeed'] as bool, - digitalTwin = json['digitalTwin'] as bool, - phone = json['phone'] as bool, - identityName = json['identityName'] as bool, - identityDOB = json['identityDOB'] as bool, - identityGender = json['identityGender'] as bool, - identityDocumentMeta = json['identityDocumentMeta'] as bool, - identityCountry = json['identityCountry'] as bool, - walletAddress = json['walletAddress'] as bool, - walletAddressData = json['walletAddressData'] as String; + : doubleName = json['doubleName'] as bool?, + user = json['user'] as bool?, + email = json['email'] as bool?, + derivedSeed = json['derivedSeed'] as bool?, + digitalTwin = json['digitalTwin'] as bool?, + phone = json['phone'] as bool?, + identityName = json['identityName'] as bool?, + identityDOB = json['identityDOB'] as bool?, + identityGender = json['identityGender'] as bool?, + identityDocumentMeta = json['identityDocumentMeta'] as bool?, + identityCountry = json['identityCountry'] as bool?, + walletAddress = json['walletAddress'] as bool?, + walletAddressData = json['walletAddressData'] as String?; Map toJson() => { 'doubleName': doubleName, diff --git a/app/lib/screens/authentication_screen.dart b/app/lib/screens/authentication_screen.dart index c63a65e8..3c375be6 100644 --- a/app/lib/screens/authentication_screen.dart +++ b/app/lib/screens/authentication_screen.dart @@ -124,11 +124,12 @@ class AuthenticationScreenState extends State { double height = MediaQuery.of(context).size.height; if (buttonText == 'OK') - onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : null)!; - if (buttonText == 'C') onPressedMethod = (input.length >= 1 ? () => onClear() : null)!; + onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : (){}); + if (buttonText == 'C') onPressedMethod = (input.length >= 1 ? () => onClear() : (){}); return Container( padding: EdgeInsets.only(top: height / 136, bottom: height / 136), child: Center( + child: RawMaterialButton( padding: EdgeInsets.all(12), child: Text( diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index 62553b25..acbe018b 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart'; -import 'package:shuftipro_flutter_sdk/ShuftiPro.dart'; +// import 'package:shuftipro_flutter_sdk/ShuftiPro.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; @@ -432,102 +432,103 @@ class _IdentityVerificationScreenState extends State Widget _inShuftiVerificationProcess() { print(createdPayload); - return Container( - child: new ShuftiPro( - authObject: authObject, - createdPayload: createdPayload, - async: false, - callback: (res) async { - // For some reason, Shufti returns bad JSON in case when request is canceled - // "verification_process_closed", "1","message", "User cancel the verification process" - - try { - if (!isJson(res)) { - String resData = res.toString(); - - if (resData.contains('verification_process_closed')) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) => CustomDialog( - image: Icons.close, - title: "Request canceled", - description: "Verification process has been canceled.", - actions: [ - FlatButton( - onPressed: () { - Navigator.pop(dialogContext); - }, - child: Text('OK')) - ], - ), - ); - } - - if (resData.contains('internet.connection.problem')) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) => CustomDialog( - image: Icons.close, - title: "Request canceled", - description: "Please make sure your internet connection is stable.", - actions: [ - FlatButton( - onPressed: () { - Navigator.pop(dialogContext); - }, - child: Text('OK')) - ], - ), - ); - } - } - - Map data = jsonDecode(res); - switch (data['event']) { - // AUTHORIZATION IS WRONG - case 'request.unauthorized': - { - Events().emit(IdentityCallbackEvent(type: 'unauthorized')); - break; - } - // NO BALANCE - case 'request.invalid': - // DECLINED - case 'verification.declined': - // TIME OUT - case 'request.timeout': - { - Events().emit(IdentityCallbackEvent(type: 'failed')); - break; - } - - // ACCEPTED - case 'verification.accepted': - { - await verifyIdentity(reference); - await identityVerification(reference).then((value) { - if (value == null) { - return Events().emit(IdentityCallbackEvent(type: 'failed')); - } - Events().emit(IdentityCallbackEvent(type: 'success')); - }); - break; - } - default: - { - return; - } - break; - } - } catch (e) { - print(e); - } finally { - dispose(); - } - }, - homeClass: HomeScreen())); + return Container(); + // return Container( + // child: new ShuftiPro( + // authObject: authObject, + // createdPayload: createdPayload, + // async: false, + // callback: (res) async { + // // For some reason, Shufti returns bad JSON in case when request is canceled + // // "verification_process_closed", "1","message", "User cancel the verification process" + // + // try { + // if (!isJson(res)) { + // String resData = res.toString(); + // + // if (resData.contains('verification_process_closed')) { + // return showDialog( + // context: context, + // barrierDismissible: false, + // builder: (BuildContext dialogContext) => CustomDialog( + // image: Icons.close, + // title: "Request canceled", + // description: "Verification process has been canceled.", + // actions: [ + // FlatButton( + // onPressed: () { + // Navigator.pop(dialogContext); + // }, + // child: Text('OK')) + // ], + // ), + // ); + // } + // + // if (resData.contains('internet.connection.problem')) { + // return showDialog( + // context: context, + // barrierDismissible: false, + // builder: (BuildContext dialogContext) => CustomDialog( + // image: Icons.close, + // title: "Request canceled", + // description: "Please make sure your internet connection is stable.", + // actions: [ + // FlatButton( + // onPressed: () { + // Navigator.pop(dialogContext); + // }, + // child: Text('OK')) + // ], + // ), + // ); + // } + // } + // + // Map data = jsonDecode(res); + // switch (data['event']) { + // // AUTHORIZATION IS WRONG + // case 'request.unauthorized': + // { + // Events().emit(IdentityCallbackEvent(type: 'unauthorized')); + // break; + // } + // // NO BALANCE + // case 'request.invalid': + // // DECLINED + // case 'verification.declined': + // // TIME OUT + // case 'request.timeout': + // { + // Events().emit(IdentityCallbackEvent(type: 'failed')); + // break; + // } + // + // // ACCEPTED + // case 'verification.accepted': + // { + // await verifyIdentity(reference); + // await identityVerification(reference).then((value) { + // if (value == null) { + // return Events().emit(IdentityCallbackEvent(type: 'failed')); + // } + // Events().emit(IdentityCallbackEvent(type: 'success')); + // }); + // break; + // } + // default: + // { + // return; + // } + // break; + // } + // } catch (e) { + // print(e); + // } finally { + // dispose(); + // } + // }, + // homeClass: HomeScreen())); } Widget _fillCard(String phase, int step, String text, IconData icon) { diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 68f93137..4237d313 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -305,11 +305,10 @@ class _LoginScreenState extends State with BlockAndRunMixin { } sendIt(bool includeData) async { - var config = WalletConfig(); - String? state = widget.loginData.state; String? randomRoom = widget.loginData.randomRoom; + print('HALLO22'); if (widget.loginData.isMobile == false) { int? created = widget.loginData.created; int currentTimestamp = new DateTime.now().millisecondsSinceEpoch; @@ -332,6 +331,9 @@ class _LoginScreenState extends State with BlockAndRunMixin { ), ); + print('IK KOM HIER'); + print(state); + print(widget.loginData.appId); await sendData(state!, null, selectedImageId, null, widget.loginData.appId!); if (Navigator.canPop(context)) { @@ -438,11 +440,14 @@ class _LoginScreenState extends State with BlockAndRunMixin { Map encryptedScopeData = await encrypt(jsonEncode(scope), base64.decode(publicKey), await getPrivateKey()); + print(widget.loginData.appId); + print(encryptedScopeData); //push to backend with signed if (!includeData) { await sendData(state, null, selectedImageId, null, widget.loginData.appId!); // temp fix send empty data for regenerate emoji } else { + print('IK KOM IN DEZE'); await sendData( state, encryptedScopeData, selectedImageId, randomRoom, widget.loginData.appId!); } diff --git a/app/lib/screens/preference_screen.dart b/app/lib/screens/preference_screen.dart index 6b7d1208..e76bb612 100644 --- a/app/lib/screens/preference_screen.dart +++ b/app/lib/screens/preference_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart'; import 'package:package_info/package_info.dart'; -import 'package:shuftipro_flutter_sdk/ShuftiPro.dart'; import 'package:threebotlogin/app_config.dart'; import 'package:threebotlogin/apps/free_flow_pages/ffp_events.dart'; import 'package:threebotlogin/events/close_socket_event.dart'; @@ -347,6 +346,7 @@ class _PreferenceScreenState extends State { void _showPhrase() async { String? pin = await getPin(); + print(pin); bool? authenticated = await Navigator.push( context, diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 77f77696..75b95727 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -15,23 +15,22 @@ Map requestHeaders = {'Content-type': 'application/json'}; Future sendData( String state, Map? data, selectedImageId, String? randomRoom, String appId) async { Uri url = Uri.parse('$threeBotApiUrl/signedAttempt'); - print('Sending call: ${url.toString()}'); - - Uint8List sk = await getPrivateKey(); - String jsonData = json.encode({ - 'signedState': state, - 'data': data, - 'selectedImageId': selectedImageId, - 'doubleName': await getDoubleName(), - 'randomRoom': randomRoom, - 'appId': appId - }); - - String signedData = await signData(jsonData, sk); - return http.post(url, - body: json.encode({'signedAttempt': signedData, 'doubleName': await getDoubleName()}), + body: json.encode({ + 'signedAttempt': await signData( + json.encode({ + 'signedState': state, + 'data': data, + 'selectedImageId': selectedImageId, + 'doubleName': await getDoubleName(), + 'randomRoom': randomRoom, + 'appId': appId + }), + await getPrivateKey()), + 'doubleName': await getDoubleName() + }), headers: requestHeaders); + } Future addDigitalTwinDerivedPublicKeyToBackend(name, publicKey, appId) async { diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index cfe3ab2a..fef1fa95 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -60,6 +60,8 @@ Future decrypt(String encodedCipherText, Uint8List pk, Uint8List sk) asy Uint8List publicKey = Sodium.cryptoSignEd25519PkToCurve25519(pk); Uint8List secretKey = Sodium.cryptoSignEd25519SkToCurve25519(sk); + print(base64.encode(publicKey)); + print(base64.encode(secretKey)); Uint8List decryptedData = Sodium.cryptoBoxSealOpen(cipherText, publicKey, secretKey); return new String.fromCharCodes(decryptedData); } diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index 907c92cb..7171d81d 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -24,7 +24,7 @@ Future getPublicKey() async { bool? isPublicKeyFixed = await getIsPublicKeyFixed(); if (isPublicKeyFixed == true) { - String? encodedPublicKey = prefs.getString('publicKey'); + String? encodedPublicKey = prefs.getString('publickey'); return base64.decode(encodedPublicKey!); } @@ -34,22 +34,22 @@ Future getPublicKey() async { } var userInfo = json.decode(userInfoResponse.body); - var done = await prefs.setString("publicKey", userInfo['publicKey']); + var done = await prefs.setString("publickey", userInfo['publicKey']); - if (done && prefs.getString('publicKey') == userInfo['publicKey']) { + if (done && prefs.getString('publickey') == userInfo['publicKey']) { setPublicKeyFixed(); } - String? encodedPublicKey = prefs.getString('publicKey'); + String? encodedPublicKey = prefs.getString('publickey'); return base64.decode(encodedPublicKey!); } Future savePublicKey(Uint8List publicKey) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('publicKey'); + prefs.remove('publickey'); String encodedPublicKey = base64.encode(publicKey); - prefs.setString('publicKey', encodedPublicKey); + prefs.setString('publickey', encodedPublicKey); } Future getIsPublicKeyFixed() async { @@ -70,7 +70,7 @@ Future setPublicKeyFixed() async { Future getPrivateKey() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - String? privateKey = prefs.getString('privateKey'); + String? privateKey = prefs.getString('privatekey'); Uint8List decodedPrivateKey = base64.decode(privateKey!); return decodedPrivateKey; @@ -78,22 +78,23 @@ Future getPrivateKey() async { Future savePrivateKey(Uint8List privateKey) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('privateKey'); + prefs.remove('privatekey'); String encodedPrivateKey = base64.encode(privateKey); - prefs.setString('privateKey', encodedPrivateKey); + prefs.setString('privatekey', encodedPrivateKey); } Future> getEdCurveKeys() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? pkEd = prefs.getString('publicKey'); - final String? skEd = prefs.getString('privateKey'); + final String? pkEd = prefs.getString('publickey'); + final String? skEd = prefs.getString('privatekey'); + final String pkCurve = - base64.encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); + base64.encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); final String skCurve = - base64.encode(Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd!))); + base64.encode(Sodium.cryptoSignEd25519SkToCurve25519(base64.decode(skEd!))); return { 'signingPublicKey': hex.encode(base64.decode(pkEd)), @@ -105,14 +106,14 @@ Future> getEdCurveKeys() async { Future savePhrase(String phrase) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('seedPhrase'); + prefs.remove('phrase'); - prefs.setString('seedPhrase', phrase); + prefs.setString('phrase', phrase); } Future getPhrase() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getString('seedPhrase'); + return prefs.getString('phrase'); } /// @@ -330,14 +331,13 @@ Future getFingerprint() async { return result; } - /// /// /// Methods for login permissions /// /// -Future saveScopePermissions(scopePermissions) async { +Future saveScopePermissions(String scopePermissions) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('scopePermissions'); prefs.setString('scopePermissions', scopePermissions); @@ -383,7 +383,6 @@ Future getInitDone() async { return isInitDone; } - /// /// /// Methods for unilinks diff --git a/app/lib/widgets/pin_field.dart b/app/lib/widgets/pin_field.dart index 4b1a5bd9..6c03f4fc 100644 --- a/app/lib/widgets/pin_field.dart +++ b/app/lib/widgets/pin_field.dart @@ -48,9 +48,9 @@ class _PinFieldState extends State { double height = MediaQuery.of(context).size.height; if (buttonText == 'OK') - onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : null)!; + onPressedMethod = (input.length >= widget.pinLength ? () => onOk() : (){}); if (buttonText == 'C') - onPressedMethod = (input.length >= 1 ? () => onClear() : null)!; + onPressedMethod = (input.length >= 1 ? () => onClear() : (){}); return Container( padding: EdgeInsets.only(top: height / 136, bottom: height / 136), child: Center( diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 92e13972..29f3dac6 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,10 +27,10 @@ dependencies: url: git@github.com:threefoldtech/flutter-pkid-client.git ref: main_null-safety - shuftipro_flutter_sdk: - git: - url: git@github.com:jimbertools/shufti-jimber.git - ref: main_null-safety +# shuftipro_flutter_sdk: +# git: +# url: git@github.com:jimbertools/shufti-jimber.git +# ref: main_null-safety redirection: git: From d68a5c7d72e0c151cfa829544ec06f8dae58cbe4 Mon Sep 17 00:00:00 2001 From: Lennert Date: Mon, 17 Jan 2022 14:21:01 +0100 Subject: [PATCH 07/75] Buildable version --- app/lib/apps/wallet/wallet_widget.dart | 2 +- app/lib/screens/login_screen.dart | 11 ++++-- app/lib/services/3bot_service.dart | 35 ++++++++++--------- app/lib/services/crypto_service.dart | 2 +- .../services/shared_preference_service.dart | 11 ++---- app/pubspec.yaml | 10 +++--- 6 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index ad163482..ac7d6247 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -32,7 +32,7 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi _back(WalletBackEvent event) async { Uri? url = await webView.getUrl(); - print(url.toString()); + String endsWith = config.appId() + '/'; if (url.toString().endsWith(endsWith)) { Events().emit(GoHomeEvent()); diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index 4237d313..ca4ac93e 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -28,7 +28,6 @@ class LoginScreen extends StatefulWidget { } class _LoginScreenState extends State with BlockAndRunMixin { - String helperText = ''; String scopeTextMobile = 'Please select the data you want to share and press Accept'; String scopeText = 'Please select the data you want to share and press the corresponding emoji'; @@ -344,6 +343,8 @@ class _LoginScreenState extends State with BlockAndRunMixin { } String publicKey = widget.loginData.appPublicKey!.replaceAll(" ", "+"); + print('Public key'); + print(publicKey); bool stateCheck = RegExp(r"[^A-Za-z0-9]+").hasMatch(state!); @@ -357,7 +358,6 @@ class _LoginScreenState extends State with BlockAndRunMixin { } Map scope = Map(); - var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId!); Uint8List derivedSeed = (await getDerivedSeed(widget.loginData.appId!)); @@ -366,6 +366,9 @@ class _LoginScreenState extends State with BlockAndRunMixin { if (scopePermissions != null) { var scopePermissionsDecoded = jsonDecode(scopePermissions); + + + if (scopePermissions != null && scopePermissions != "") { if (scopePermissionsDecoded['email'] != null && scopePermissionsDecoded['email']) { scope['email'] = (await getEmail()); @@ -437,6 +440,10 @@ class _LoginScreenState extends State with BlockAndRunMixin { } } + + print('Encoded scope'); + print(jsonEncode(scope)); + Map encryptedScopeData = await encrypt(jsonEncode(scope), base64.decode(publicKey), await getPrivateKey()); diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 75b95727..c6117afe 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -12,25 +12,26 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; -Future sendData( - String state, Map? data, selectedImageId, String? randomRoom, String appId) async { +Future sendData(String state, Map? data, selectedImageId, + String? randomRoom, String appId) async { Uri url = Uri.parse('$threeBotApiUrl/signedAttempt'); - return http.post(url, - body: json.encode({ - 'signedAttempt': await signData( - json.encode({ - 'signedState': state, - 'data': data, - 'selectedImageId': selectedImageId, - 'doubleName': await getDoubleName(), - 'randomRoom': randomRoom, - 'appId': appId - }), - await getPrivateKey()), - 'doubleName': await getDoubleName() - }), - headers: requestHeaders); + print('Sending call: ${url.toString()}'); + String encodedBody = json.encode({ + 'signedAttempt': await signData( + json.encode({ + 'signedState': state, + 'data': data, + 'selectedImageId': selectedImageId, + 'doubleName': await getDoubleName(), + 'randomRoom': randomRoom, + 'appId': appId + }), + await getPrivateKey()), + 'doubleName': await getDoubleName() + }); + + return http.post(url, body: encodedBody, headers: requestHeaders); } Future addDigitalTwinDerivedPublicKeyToBackend(name, publicKey, appId) async { diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index fef1fa95..db08e8bd 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -51,7 +51,7 @@ Future> encrypt(String data, Uint8List pk, Uint8List sk) asy Uint8List message = Uint8List.fromList(data.codeUnits); Uint8List encryptedData = Sodium.cryptoBoxEasy(message, nonce, pk, private); - return {'nonce': base64.encode(nonce), 'cipher': base64.encode(encryptedData)}; + return {'nonce': base64.encode(nonce), 'ciphertext': base64.encode(encryptedData)}; } // Decrypt given ciphertext with a keypair diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index 7171d81d..d871a7f3 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -90,7 +90,6 @@ Future> getEdCurveKeys() async { final String? pkEd = prefs.getString('publickey'); final String? skEd = prefs.getString('privatekey'); - final String pkCurve = base64.encode(Sodium.cryptoSignEd25519PkToCurve25519(base64.decode(pkEd!))); final String skCurve = @@ -458,14 +457,10 @@ Future saveLocationId(String locationId) async { Future> getLocationIdList() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); - try { - String? locationIdListAsJson = prefs.getString('locationIdList'); - List locationIdList = jsonDecode(locationIdListAsJson!); + String? locationIdListAsJson = prefs.getString('locationIdList'); + List locationIdList = jsonDecode(locationIdListAsJson!); - return locationIdList; - } catch (_) { - return []; - } + return locationIdList; } Future saveDoubleName(String doubleName) async { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 29f3dac6..4acff152 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -27,16 +27,16 @@ dependencies: url: git@github.com:threefoldtech/flutter-pkid-client.git ref: main_null-safety -# shuftipro_flutter_sdk: -# git: -# url: git@github.com:jimbertools/shufti-jimber.git -# ref: main_null-safety - redirection: git: url: https://github.com/jimbertools/redirection ref: main_null-safety + shuftipro_flutter_sdk: + git: + url: https://github.com/jimbertools/shufti-jimber + ref: main_null-safety + flutter_svg: ^1.0.0 bip39: ^1.0.6 socket_io_client: ^1.0.2 From 09c444e40bc9fa2a69429d7e2b0ad292de011300 Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 18 Jan 2022 12:56:28 +0100 Subject: [PATCH 08/75] Small rework of login --- app/lib/helpers/login_helpers.dart | 135 +++++++++++++ app/lib/screens/login_screen.dart | 276 +++++++-------------------- app/lib/services/socket_service.dart | 28 +-- app/lib/services/tools_service.dart | 4 +- app/lib/widgets/login_dialogs.dart | 60 ++++++ 5 files changed, 268 insertions(+), 235 deletions(-) create mode 100644 app/lib/helpers/login_helpers.dart create mode 100644 app/lib/widgets/login_dialogs.dart diff --git a/app/lib/helpers/login_helpers.dart b/app/lib/helpers/login_helpers.dart new file mode 100644 index 00000000..cc4545ab --- /dev/null +++ b/app/lib/helpers/login_helpers.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter_sodium/flutter_sodium.dart'; +import 'package:threebotlogin/services/3bot_service.dart'; +import 'package:threebotlogin/services/crypto_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; + +int parseImageId(String imageId) { + if (imageId == '') { + return 1; + } + return int.parse(imageId); +} + +// Only update data if the correct image was chosen: +void addDigitalTwinToBackend(Uint8List derivedSeed, String appId) async { + String? doubleName = await getDoubleName(); + + KeyPair dtKeyPair = await generateKeyPairFromEntropy(derivedSeed); + String dtEncodedPublicKey = base64.encode(dtKeyPair.pk); + + print("Derived Seed: " + base64.encode(derivedSeed)); + print("Username: " + doubleName!); + print("Public key: " + dtEncodedPublicKey); + + addDigitalTwinDerivedPublicKeyToBackend(doubleName, dtEncodedPublicKey, appId); +} + +Future?> readScopeAsObject(String? scopePermissions, Uint8List dSeed) async { + Map? scopePermissionsDecoded = jsonDecode(scopePermissions!); + + Map scope = {}; + + if (scopePermissionsDecoded == null) { + return null; + } + + if (scopePermissionsDecoded['email'] == true) { + scope['email'] = (await getEmail()); + } + + if (scopePermissionsDecoded['phone'] == true) { + scope['phone'] = (await getPhone()); + } + + if (scopePermissionsDecoded['derivedSeed'] == true) { + scope['derivedSeed'] = base64Encode(dSeed); + } + + if (scopePermissionsDecoded['digitalTwin'] == true) { + scope['digitalTwin'] = 'OK'; + } + + if (scopePermissionsDecoded['identityName'] == true) { + Map identityDetails = await getIdentity(); + + String identityName = identityDetails['identityName']; + String sIdentityName = identityDetails['signedIdentityNameIdentifier']; + + scope['identityName'] = { + 'identityName': identityName, + 'signedIdentityNameIdentifier': sIdentityName + }; + } + + if (scopePermissionsDecoded['identityDOB'] == true) { + Map identityDetails = await getIdentity(); + + String identityDOB = identityDetails['identityDOB']; + String sIdentityDOB = identityDetails['signedIdentityDOBIdentifier']; + + scope['identityDOB'] = {'identityDOB': identityDOB, 'signedIdentityDOB': sIdentityDOB}; + } + + if (scopePermissionsDecoded['identityCountry'] == true) { + Map identityDetails = await getIdentity(); + + String identityCountry = identityDetails['identityCountry']; + String sIdentityCountryIdentifier = identityDetails['signedIdentityCountryIdentifier']; + + scope['identityCountry'] = { + 'identityCountry': identityCountry, + 'signedIdentityCountryIdentifier': sIdentityCountryIdentifier + }; + } + + if (scopePermissionsDecoded['identityCountry'] == true) { + Map identityDetails = await getIdentity(); + + String identityCountry = identityDetails['identityCountry']; + String sIdentityCountryIdentifier = identityDetails['signedIdentityCountryIdentifier']; + + scope['identityCountry'] = { + 'identityCountry': identityCountry, + 'signedIdentityCountryIdentifier': sIdentityCountryIdentifier + }; + } + + if (scopePermissionsDecoded['identityDocumentMeta'] == true) { + Map identityDetails = await getIdentity(); + + String identityDocumentMeta = identityDetails['identityDocumentMeta']; + String sIdentityDocumentMeta = identityDetails['signedIdentityCountryIdentifier']; + + scope['identityDocumentMeta'] = { + 'identityDocumentMeta': identityDocumentMeta, + 'signedIdentityDocumentMeta': sIdentityDocumentMeta + }; + } + + if (scopePermissionsDecoded['identityGender'] == true) { + Map identityDetails = await getIdentity(); + + String identityGender = identityDetails['identityGender']; + String sIdentityGender = identityDetails['signedIdentityGender']; + + scope['identityGender'] = { + 'identityGender': identityGender, + 'signedIdentityGender': sIdentityGender + }; + } + + if (scopePermissionsDecoded['walletAddress'] == true) { + scope['walletAddressData'] = {'address': scopePermissionsDecoded['walletAddressData']}; + } + + return scope; +} + +Future> encryptLoginData (String publicKey, Map? scopeData) async { + Uint8List sk = await getPrivateKey(); + Uint8List pk = base64.decode(publicKey); + + return await encrypt(jsonEncode(scopeData), pk, sk); +} diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index ca4ac93e..e93235b5 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -4,20 +4,19 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:threebotlogin/apps/wallet/wallet_config.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/pop_all_login_event.dart'; import 'package:threebotlogin/helpers/block_and_run_mixin.dart'; import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/login_helpers.dart'; import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/widgets/custom_dialog.dart'; import 'package:threebotlogin/widgets/image_button.dart'; +import 'package:threebotlogin/widgets/login_dialogs.dart'; import 'package:threebotlogin/widgets/preference_dialog.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class LoginScreen extends StatefulWidget { final Login loginData; @@ -58,19 +57,21 @@ class _LoginScreenState extends State with BlockAndRunMixin { isMobileCheck = widget.loginData.isMobile == true; generateEmojiImageList(); - if (widget.loginData.isMobile == false) { - const oneSec = const Duration(seconds: 1); - print('Starting timer ... '); + if (widget.loginData.isMobile == true) { + return; + } - created = widget.loginData.created; - currentTimestamp = new DateTime.now().millisecondsSinceEpoch; + const oneSec = const Duration(seconds: 1); + print('Starting timer ... '); - timeLeft = Globals().loginTimeout - ((currentTimestamp! - created!) / 1000).round(); + created = widget.loginData.created; + currentTimestamp = new DateTime.now().millisecondsSinceEpoch; - timer = new Timer.periodic(oneSec, (Timer t) async { - timeoutTimer(); - }); - } + timeLeft = Globals().loginTimeout - ((currentTimestamp! - created!) / 1000).round(); + + timer = new Timer.periodic(oneSec, (Timer t) async { + timeoutTimer(); + }); } timeoutTimer() async { @@ -85,28 +86,13 @@ class _LoginScreenState extends State with BlockAndRunMixin { timeLeft = Globals().loginTimeout - ((currentTimestamp! - created!) / 1000).round(); }); - if (created != null && ((currentTimestamp! - created!) / 1000) > Globals().loginTimeout) { - timer.cancel(); - - await showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.timer, - title: 'Login attempt expired', - description: 'Your login attempt has expired, please request a new one in your browser.', - actions: [ - FlatButton( - child: Text('Ok'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - - Navigator.pop(context, false); + if (created == null || ((currentTimestamp! - created!) / 1000) < Globals().loginTimeout) { + return; } + + timer.cancel(); + await showExpiredDialog(context); + Navigator.pop(context, false); } Widget scopeEmojiView() { @@ -253,36 +239,22 @@ class _LoginScreenState extends State with BlockAndRunMixin { selectedImageId = imageId; }); - if (selectedImageId != -1) { - if (selectedImageId == correctImage) { - await sendIt(true); - } else { - await sendIt(false); - - await showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.warning, - title: 'Wrong emoji', - description: - 'You selected the wrong emoji, please check your browser for the new one.', - actions: [ - FlatButton( - child: Text('Retry'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); + if (selectedImageId == -1) { + print('No image selected'); + return; + } - if (Navigator.canPop(context)) { - Navigator.pop(context, false); - } - } - } else { - _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text('Please select an emoji'))); + if (selectedImageId == correctImage) { + await sendIt(true); + return; + } + + await sendIt(false); + print(context); + await showWrongEmojiDialog(context); + + if (Navigator.canPop(context)) { + Navigator.pop(context, false); } }); } @@ -292,197 +264,77 @@ class _LoginScreenState extends State with BlockAndRunMixin { return; } - if (mounted) { - if (Navigator.canPop(context)) { - Navigator.pop(context, false); - } + if (!mounted) { + return; + } + + if (Navigator.canPop(context)) { + Navigator.pop(context, false); } } cancelIt() async { - cancelLogin(await getDoubleName()); + String? doubleName = await getDoubleName(); + cancelLogin(doubleName!); } sendIt(bool includeData) async { String? state = widget.loginData.state; String? randomRoom = widget.loginData.randomRoom; - print('HALLO22'); if (widget.loginData.isMobile == false) { int? created = widget.loginData.created; int currentTimestamp = new DateTime.now().millisecondsSinceEpoch; if (created != null && ((currentTimestamp - created) / 1000) > Globals().loginTimeout) { - await showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.timer, - title: 'Login attempt expired', - description: 'We cannot sign this login attempt because it has expired.', - actions: [ - FlatButton( - child: Text('Ok'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - - print('IK KOM HIER'); - print(state); - print(widget.loginData.appId); + await showExpiredDialog(context); await sendData(state!, null, selectedImageId, null, widget.loginData.appId!); if (Navigator.canPop(context)) { Navigator.pop(context, false); } + return; } } - String publicKey = widget.loginData.appPublicKey!.replaceAll(" ", "+"); - print('Public key'); - print(publicKey); - + // If the state is not passed through the regEx bool stateCheck = RegExp(r"[^A-Za-z0-9]+").hasMatch(state!); - if (stateCheck) { - _scaffoldKey.currentState?.showSnackBar( - SnackBar( - content: Text('States can only be alphanumeric [^A-Za-z0-9]'), - ), - ); + print('States can only be alphanumeric [^A-Za-z0-9]'); return; } - Map scope = Map(); - var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId!); - - Uint8List derivedSeed = (await getDerivedSeed(widget.loginData.appId!)); - - //TODO: make separate function - if (scopePermissions != null) { - var scopePermissionsDecoded = jsonDecode(scopePermissions); - - - - - if (scopePermissions != null && scopePermissions != "") { - if (scopePermissionsDecoded['email'] != null && scopePermissionsDecoded['email']) { - scope['email'] = (await getEmail()); - } - - if (scopePermissionsDecoded['phone'] != null && scopePermissionsDecoded['phone']) { - scope['phone'] = (await getPhone()); - } - - if (scopePermissionsDecoded['derivedSeed'] != null && - scopePermissionsDecoded['derivedSeed']) { - scope['derivedSeed'] = derivedSeed; - } - - if (scopePermissionsDecoded['digitalTwin'] != null && - scopePermissionsDecoded['digitalTwin']) { - scope['digitalTwin'] = 'ok'; - } - - if (scopePermissionsDecoded['identityName'] != null && - scopePermissionsDecoded['identityName']) { - scope['identityName'] = { - 'identityName': ((await getIdentity())['identityName']), - 'signedIdentityNameIdentifier': ((await getIdentity())['signedIdentityNameIdentifier']) - }; - } - - if (scopePermissionsDecoded['identityDOB'] != null && - scopePermissionsDecoded['identityDOB']) { - scope['identityDOB'] = { - 'identityDOB': ((await getIdentity())['identityDOB']), - 'signedIdentityDOBIdentifier': ((await getIdentity())['signedIdentityDOBIdentifier']) - }; - } - - if (scopePermissionsDecoded['identityCountry'] != null && - scopePermissionsDecoded['identityCountry']) { - scope['identityCountry'] = { - 'identityCountry': ((await getIdentity())['identityCountry']), - 'signedIdentityCountryIdentifier': - ((await getIdentity())['signedIdentityCountryIdentifier']) - }; - } - - if (scopePermissionsDecoded['identityDocumentMeta'] != null && - scopePermissionsDecoded['identityDocumentMeta']) { - scope['identityDocumentMeta'] = { - 'identityDocumentMeta': ((await getIdentity())['identityDocumentMeta']), - 'signedIdentityDocumentMetaIdentifier': - ((await getIdentity())['signedIdentityDocumentMetaIdentifier']) - }; - } - - if (scopePermissionsDecoded['identityGender'] != null && - scopePermissionsDecoded['identityGender']) { - scope['identityGender'] = { - 'identityGender': ((await getIdentity())['identityGender']), - 'signedIdentityGenderIdentifier': - ((await getIdentity())['signedIdentityGenderIdentifier']) - }; - } - - if (scopePermissionsDecoded['walletAddress'] != null && - scopePermissionsDecoded['walletAddress']) { - scope['walletAddressData'] = { - 'address': scopePermissionsDecoded['walletAddressData'], - }; - } - } - } - + String appId = widget.loginData.appId!; + String publicKey = widget.loginData.appPublicKey!.replaceAll(" ", "+"); + Uint8List derivedSeed = await getDerivedSeed(appId); - print('Encoded scope'); - print(jsonEncode(scope)); + // Get the selected scope permissions and get the required data + var scopePermissions = await getPreviousScopePermissions(widget.loginData.appId!); + Map? scopeData = await readScopeAsObject(scopePermissions, derivedSeed); - Map encryptedScopeData = - await encrypt(jsonEncode(scope), base64.decode(publicKey), await getPrivateKey()); + // Encrypt the scope data + Map encryptedScopeData = await encryptLoginData(publicKey, scopeData); - print(widget.loginData.appId); - print(encryptedScopeData); - //push to backend with signed if (!includeData) { - await sendData(state, null, selectedImageId, null, - widget.loginData.appId!); // temp fix send empty data for regenerate emoji + await sendData(state, null, selectedImageId, null, widget.loginData.appId!); } else { - print('IK KOM IN DEZE'); await sendData( state, encryptedScopeData, selectedImageId, randomRoom, widget.loginData.appId!); } - if (selectedImageId == correctImage || isMobileCheck) { - // Only update data if the correct image was chosen: - print("derivedSeed: " + base64.encode(derivedSeed)); - var name = await getDoubleName(); - KeyPair dtKeyPair = await generateKeyPairFromEntropy(derivedSeed); + // If the image is wrong, quit here and don't add the digital twin to the table + if (selectedImageId != correctImage) { + return; + } - String dtEncodedPublicKey = base64.encode(dtKeyPair.pk); - print("name: " + name!); - print("publicKey: " + dtEncodedPublicKey); - addDigitalTwinDerivedPublicKeyToBackend(name, dtEncodedPublicKey, widget.loginData.appId); + addDigitalTwinToBackend(derivedSeed, widget.loginData.appId!); - if (Navigator.canPop(context)) { - Navigator.pop(context, true); - } - - Events().emit(PopAllLoginEvent(emitCode)); + if (Navigator.canPop(context)) { + Navigator.pop(context, true); } - } - int parseImageId(String? imageId) { - if (imageId == null || imageId == '') { - return 1; - } - return int.parse(imageId); + Events().emit(PopAllLoginEvent(emitCode)); } void generateEmojiImageList() { diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index 1e37ec32..6608c177 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -19,6 +19,7 @@ import 'package:threebotlogin/services/fingerprint_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; +import 'package:threebotlogin/widgets/login_dialogs.dart'; class BackendConnection { late IO.Socket socket; @@ -332,7 +333,7 @@ Future identityVerification(String reference) async { return 'Verified'; } -Future openLogin(BuildContext context, Login loginData, BackendConnection backendConnection) async { +Future openLogin(BuildContext ctx, Login loginData, BackendConnection backendConnection) async { String? messageType = loginData.type; if (messageType == null || messageType != 'login' || loginData.isMobile == true) { @@ -344,7 +345,7 @@ Future openLogin(BuildContext context, Login loginData, BackendConnection backen Events().emit(CloseAuthEvent(close: true)); bool? authenticated = await Navigator.push( - context, + ctx, MaterialPageRoute( builder: (context) => AuthenticationScreen( correctPin: pin!, userMessage: "Sign your attempt.", loginData: loginData), @@ -357,7 +358,7 @@ Future openLogin(BuildContext context, Login loginData, BackendConnection backen if (loginData.showWarning == true) { bool? warningScreenCompleted = await Navigator.push( - context, + ctx, MaterialPageRoute( builder: (context) => WarningScreen(), ), @@ -373,7 +374,7 @@ Future openLogin(BuildContext context, Login loginData, BackendConnection backen backendConnection.leaveRoom(loginData.doubleName); bool? loggedIn = await Navigator.push( - context, + ctx, MaterialPageRoute( builder: (context) => LoginScreen(loginData), ), @@ -381,24 +382,9 @@ Future openLogin(BuildContext context, Login loginData, BackendConnection backen if (loggedIn == null || loggedIn == false) { backendConnection.joinRoom(loginData.doubleName); + return; } backendConnection.joinRoom(loginData.doubleName); - - await showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - image: Icons.check, - title: 'Logged in', - description: 'You are now logged in. Please return to your browser.', - actions: [ - FlatButton( - child: Text('Ok'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); + await showLoggedInDialog(ctx); } diff --git a/app/lib/services/tools_service.dart b/app/lib/services/tools_service.dart index dc5f20c3..03ca464d 100644 --- a/app/lib/services/tools_service.dart +++ b/app/lib/services/tools_service.dart @@ -4,11 +4,11 @@ import 'dart:math'; const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; -String randomString(int strlen) { +String randomString(int len) { Random rnd = new Random(new DateTime.now().millisecondsSinceEpoch); String result = ""; - for (int i = 0; i < strlen; i++) { + for (int i = 0; i < len; i++) { result += chars[rnd.nextInt(chars.length)]; } diff --git a/app/lib/widgets/login_dialogs.dart b/app/lib/widgets/login_dialogs.dart new file mode 100644 index 00000000..54cde94b --- /dev/null +++ b/app/lib/widgets/login_dialogs.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import 'custom_dialog.dart'; + +Future showExpiredDialog(BuildContext ctx) async { + return await showDialog( + context: ctx, + builder: (BuildContext context) => CustomDialog( + image: Icons.timer, + title: 'Login attempt expired', + description: 'Your login attempt has expired, please request a new one in your browser.', + actions: [ + FlatButton( + child: Text('Ok'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); +} + +Future showWrongEmojiDialog(BuildContext ctx) async { + await showDialog( + context: ctx, + builder: (BuildContext context) => CustomDialog( + image: Icons.warning, + title: 'Wrong emoji', + description: 'You selected the wrong emoji, please check your browser for the new one.', + actions: [ + FlatButton( + child: Text('Retry'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); +} + +Future showLoggedInDialog(BuildContext ctx) async { + await showDialog( + context: ctx, + builder: (BuildContext context) => CustomDialog( + image: Icons.check, + title: 'Logged in', + description: 'You are now logged in. Please return to your browser.', + actions: [ + FlatButton( + child: Text('Ok'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); +} From cb90831cbeb92dafc0a158c5723c668362c4b227 Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 18 Jan 2022 13:02:47 +0100 Subject: [PATCH 09/75] Fixed typo --- frontend/src/views/Login/Login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Login/Login.html b/frontend/src/views/Login/Login.html index 0ea217bd..7985dda7 100644 --- a/frontend/src/views/Login/Login.html +++ b/frontend/src/views/Login/Login.html @@ -53,7 +53,7 @@

- f you do not yet have the ThreeFold Connect app on your device, you can download it on the Google Play / Apple App store. + If you do not yet have the ThreeFold Connect app on your device, you can download it on the Google Play / Apple App store.
- Have you already created an account already but it is not active on your device? Click the ‘recover account’ button in the app and you will be instructed on how to regain access. + Have you already created an account but it is not active on your device? Click the ‘recover account’ button in the app and you will be instructed on how to regain access. Close Window From 48c4a9efcf5280c36dba04f4dcefc9b8012c7ae8 Mon Sep 17 00:00:00 2001 From: Jonas Wijne Date: Wed, 19 Jan 2022 22:41:50 +0100 Subject: [PATCH 10/75] fix typo --- app/lib/apps/wallet/wallet_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 6bca93b9..a2b27491 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -91,7 +91,7 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi var appWallets = await getAppWallets(); var jsStartApp = Globals().useNewWallet == true - ? "window.init'('$doubleName', '$seed')" + ? "window.init('$doubleName', '$seed')" : "window.vueInstance.startWallet('$doubleName', '$seed', '$importedWallets', '$appWallets');"; if (Globals().paymentRequest != null && !Globals().useNewWallet) { From edce7c63074cfd9daa20e4859e285fa9ca16c1d9 Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 19 Jan 2022 22:45:25 +0100 Subject: [PATCH 11/75] WIP --- app/lib/app_config.dart | 1 + app/lib/apps/wallet/wallet_widget.dart | 9 +++++++- app/lib/helpers/flags.dart | 1 + app/lib/helpers/globals.dart | 1 + app/lib/helpers/kyc_helpers.dart | 1 - app/lib/main.dart | 21 +++++++++--------- app/lib/screens/recover_screen.dart | 8 ++++--- app/lib/screens/unregistered_screen.dart | 4 ++-- app/lib/services/migration_service.dart | 14 +++++++++--- app/lib/services/pkid_service.dart | 4 ++-- .../services/shared_preference_service.dart | 22 +++++++++++++++++++ 11 files changed, 63 insertions(+), 23 deletions(-) diff --git a/app/lib/app_config.dart b/app/lib/app_config.dart index 99c49ebf..dd02eb3f 100644 --- a/app/lib/app_config.dart +++ b/app/lib/app_config.dart @@ -190,4 +190,5 @@ void setFallbackConfigs() { Globals().useNewWallet = false; Globals().newWalletUrl = ''; Globals().redoIdentityVerification = false; + Globals().timeOutSeconds = 5; } \ No newline at end of file diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index ac7d6247..447ae4cf 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -89,15 +89,22 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi } initKeys() async { - var seed = await getDerivedSeed(config.appId()); + var seed = base64.encode(await getDerivedSeed(config.appId())); var doubleName = await getDoubleName(); var importedWallets = await getImportedWallets(); var appWallets = await getAppWallets(); + print(seed); + print(doubleName); + print(importedWallets); + print(appWallets); + var jsStartApp = Globals().useNewWallet == true ? "window.init'('$doubleName', '$seed')" : "window.vueInstance.startWallet('$doubleName', '$seed', '$importedWallets', '$appWallets');"; + + print(jsStartApp); if (Globals().paymentRequest != null && !Globals().useNewWallet) { String paymentRequestString = Globals().paymentRequest.toString(); diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index 59cd743c..aedc8fe3 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -33,6 +33,7 @@ class Flags { try { await client?.getFeatureFlags(reload: true); + Globals().timeOutSeconds = int.parse((await Flags().getFlagValueByFeatureName('timeout-seconds'))!); Globals().isOpenKYCEnabled = (await Flags().hasFlagValueByFeatureName('kyc'))!; Globals().isYggdrasilEnabled = (await Flags().hasFlagValueByFeatureName('yggdrasil'))!; Globals().debugMode = (await Flags().hasFlagValueByFeatureName('debug'))!; diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index 24b62dec..72f44606 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -50,6 +50,7 @@ class Globals { String newWalletUrl = ''; bool redoIdentityVerification = false; bool debugMode = false; + int timeOutSeconds = 5; VpnState vpnState = new VpnState(); diff --git a/app/lib/helpers/kyc_helpers.dart b/app/lib/helpers/kyc_helpers.dart index ce2cd243..b90307ce 100644 --- a/app/lib/helpers/kyc_helpers.dart +++ b/app/lib/helpers/kyc_helpers.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter_pkid/flutter_pkid.dart'; -import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; diff --git a/app/lib/main.dart b/app/lib/main.dart index 95fb3778..b677ff67 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -10,24 +10,27 @@ import 'package:google_fonts/google_fonts.dart'; import 'app_config.dart'; import 'helpers/kyc_helpers.dart'; - Future main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + bool initDone = await getInitDone(); String? doubleName = await getDoubleName(); await setGlobalValues(); - print(AppConfig().threeBotSocketUrl()); + String? seedPhrase = await getPhrase(); - bool registered = doubleName != null; + if (seedPhrase != null && + (await isPKidMigrationIssueSolved() == false || await isPKidMigrationIssueSolved() == null)) { + fixPkidMigration(); + } if (await getPhrase() != null) { - await migrateToNewSystem(); await fetchPKidData(); } + bool registered = doubleName != null; runApp(MyApp(initDone: initDone, registered: registered)); } @@ -38,8 +41,7 @@ Future setGlobalValues() async { Globals().emailVerified.value = (email['sei'] != null); Globals().phoneVerified.value = (phone['spi'] != null); - Globals().identityVerified.value = - (identity['signedIdentityNameIdentifier'] != null); + Globals().identityVerified.value = (identity['signedIdentityNameIdentifier'] != null); } class MyApp extends StatelessWidget { @@ -63,12 +65,9 @@ class MyApp extends StatelessWidget { primaryColor: HexColor("#0a73b8"), accentColor: HexColor("#57BE8E"), textTheme: textTheme, - tabBarTheme: - TabBarTheme(labelStyle: textStyle, unselectedLabelStyle: textStyle), + tabBarTheme: TabBarTheme(labelStyle: textStyle, unselectedLabelStyle: textStyle), appBarTheme: AppBarTheme( - color: Colors.white, - textTheme: accentTextTheme, - brightness: Brightness.dark), + color: Colors.white, textTheme: accentTextTheme, brightness: Brightness.dark), ), home: MainScreen(initDone: initDone, registered: registered), ); diff --git a/app/lib/screens/recover_screen.dart b/app/lib/screens/recover_screen.dart index f5738ed5..f6b7c982 100644 --- a/app/lib/screens/recover_screen.dart +++ b/app/lib/screens/recover_screen.dart @@ -57,7 +57,9 @@ class _RecoverScreenState extends State { await savePrivateKey(keyPair.sk); await savePublicKey(keyPair.pk); - FlutterPkid client = await getPkidClient(); + print('A'); + FlutterPkid client = await getPkidClient(seedPhrase: seedPhrase); + print(client); List keyWords = ['email', 'phone', 'identity']; var futures = keyWords.map((keyword) async { @@ -76,8 +78,8 @@ class _RecoverScreenState extends State { await handleKYCData(dataMap[0], dataMap[1], dataMap[2]); - await migrateToNewSystem(); - // await sendVerificationEmail(); + await fixPkidMigration(); + } catch (e) { print(e); throw Exception('Something went wrong'); diff --git a/app/lib/screens/unregistered_screen.dart b/app/lib/screens/unregistered_screen.dart index 1d96f3aa..bc6f3a8d 100644 --- a/app/lib/screens/unregistered_screen.dart +++ b/app/lib/screens/unregistered_screen.dart @@ -41,6 +41,7 @@ class _UnregisteredScreenState extends State context, MaterialPageRoute( builder: (context) => ChangePinScreen(hideBackButton: true))); + await Navigator.push( context, MaterialPageRoute( @@ -48,11 +49,10 @@ class _UnregisteredScreenState extends State title: "Recovered", text: "Your account has been recovered."))); + Navigator.pop(context); await Flags().initFlagSmith(); await Flags().setFlagSmithDefaultValues(); - - Navigator.pop(context); } } diff --git a/app/lib/services/migration_service.dart b/app/lib/services/migration_service.dart index 80d40172..148904e5 100644 --- a/app/lib/services/migration_service.dart +++ b/app/lib/services/migration_service.dart @@ -4,9 +4,17 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import 'crypto_service.dart'; -Future migrateToNewSystem() async { - await saveEmailToPKidForMigration(); - await savePhoneToPKidForMigration(); +Future fixPkidMigration() async { + try { + print('Doing the migration... '); + await saveEmailToPKidForMigration(); + await savePhoneToPKidForMigration(); + + await setPKidMigrationIssueSolved(true); + } + catch(e) { + await setPKidMigrationIssueSolved(false); + } } Future saveEmailInCorrectFormatPKid(Map emailData) async { diff --git a/app/lib/services/pkid_service.dart b/app/lib/services/pkid_service.dart index b30b4a72..f4fea9cc 100644 --- a/app/lib/services/pkid_service.dart +++ b/app/lib/services/pkid_service.dart @@ -7,10 +7,10 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import '../app_config.dart'; import 'crypto_service.dart'; -Future getPkidClient() async { +Future getPkidClient({String seedPhrase = ''}) async { String pKidUrl = AppConfig().pKidUrl(); - String? phrase = await getPhrase(); + String? phrase = seedPhrase != '' ? seedPhrase : await getPhrase(); KeyPair keyPair = await generateKeyPairFromSeedPhrase(phrase!); return FlutterPkid(pKidUrl, keyPair); diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index d871a7f3..0dc73029 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -473,3 +473,25 @@ Future getDoubleName() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('doubleName'); } + + +/// +/// +/// Migration problems +/// +/// + + +// In the past there was a mapping mistake by Lennert in the initial migration to PKID +// This has been solved in a second patch but we want to make sure all the users get the right fix +Future isPKidMigrationIssueSolved() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('isPkidMigrationIssueSolved'); +} + +Future setPKidMigrationIssueSolved(bool isFixed) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isPkidMigrationIssueSolved', isFixed); +} + + From 1931360ebca8257537de874b673c91ca9d4a2e47 Mon Sep 17 00:00:00 2001 From: Lennert Date: Thu, 20 Jan 2022 11:18:58 +0100 Subject: [PATCH 12/75] Global improvements --- app/lib/apps/wallet/wallet_widget.dart | 2 +- app/lib/helpers/flags.dart | 80 +++++++++++++------------- app/lib/main.dart | 2 - app/lib/screens/main_screen.dart | 24 ++++---- app/lib/screens/recover_screen.dart | 2 - app/lib/services/3bot_service.dart | 9 ++- app/lib/services/open_kyc_service.dart | 6 +- app/test/flagsmith_tests.dart | 23 ++++++++ 8 files changed, 88 insertions(+), 60 deletions(-) create mode 100644 app/test/flagsmith_tests.dart diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 447ae4cf..1475fd85 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -100,7 +100,7 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi print(appWallets); var jsStartApp = Globals().useNewWallet == true - ? "window.init'('$doubleName', '$seed')" + ? "window.init('$doubleName', '$seed')" : "window.vueInstance.startWallet('$doubleName', '$seed', '$importedWallets', '$appWallets');"; diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index aedc8fe3..41bdede7 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -1,65 +1,67 @@ import 'package:flagsmith/flagsmith.dart'; +import 'package:threebotlogin/app_config.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; -import '../app_config.dart'; import 'globals.dart'; class Flags { static final Flags _singleton = new Flags._internal(); - FlagsmithClient? client; + late FlagsmithClient client; - Map? flagSmithConfig = AppConfig().flagSmithConfig(); + Map flagSmithConfig = AppConfig().flagSmithConfig(); Future initFlagSmith() async { - try { - client = await FlagsmithClient.init( - config: FlagsmithConfig( - baseURI: flagSmithConfig!['url'].toString(), - ), - apiKey: flagSmithConfig!['apiKey'].toString()); + client = await FlagsmithClient.init( + config: FlagsmithConfig( + baseURI: flagSmithConfig['url']!, + ), + apiKey: flagSmithConfig['apiKey']!); - await client?.getFeatureFlags(reload: true); - } catch (e) { - print(e); - setFallbackConfigs(); - } - } - - Future getNewFlagValues() async { - await client?.getFeatureFlags(reload: true); - } + String? doubleName = await getDoubleName(); - Future setFlagSmithDefaultValues() async { try { - await client?.getFeatureFlags(reload: true); - - Globals().timeOutSeconds = int.parse((await Flags().getFlagValueByFeatureName('timeout-seconds'))!); - Globals().isOpenKYCEnabled = (await Flags().hasFlagValueByFeatureName('kyc'))!; - Globals().isYggdrasilEnabled = (await Flags().hasFlagValueByFeatureName('yggdrasil'))!; - Globals().debugMode = (await Flags().hasFlagValueByFeatureName('debug'))!; - Globals().useNewWallet = (await Flags().hasFlagValueByFeatureName('use-new-wallet'))!; - Globals().newWalletUrl = (await Flags().getFlagValueByFeatureName('new-wallet-url'))!; - Globals().redoIdentityVerification = - (await Flags().hasFlagValueByFeatureName('redo-identity-verification'))!; + if (doubleName != null) { + Identity user = Identity(identifier: doubleName); + await client.getFeatureFlags(user: user, reload: true); + return; + } + await client.getFeatureFlags(reload: true); } catch (e) { + print('Error in init flagsmith'); print(e); + throw Exception(); } } - Future hasFlagValueByFeatureName(String name) async { - return (await client?.hasFeatureFlag(name)); + Future setFlagSmithDefaultValues() async { + Globals().timeOutSeconds = + int.parse((await Flags().getFlagValueByFeatureName('timeout-seconds'))!); + Globals().isOpenKYCEnabled = (await Flags().hasFlagValueByFeatureName('kyc')); + Globals().isYggdrasilEnabled = (await Flags().hasFlagValueByFeatureName('yggdrasil')); + Globals().debugMode = (await Flags().hasFlagValueByFeatureName('debug')); + Globals().useNewWallet = (await Flags().hasFlagValueByFeatureName('use-new-wallet')); + Globals().newWalletUrl = (await Flags().getFlagValueByFeatureName('new-wallet-url'))!; + Globals().redoIdentityVerification = + (await Flags().hasFlagValueByFeatureName('redo-identity-verification')); } - Future isFlagEnabled(String name) async { - return (await client?.isFeatureFlagEnabled(name)); + Future hasFlagValueByFeatureName(String name) async { + String? doubleName = await getDoubleName(); + if (doubleName != null) { + Identity user = Identity(identifier: doubleName); + return (await client.hasFeatureFlag(name, user: user)); + } + return (await client.hasFeatureFlag(name)); } Future getFlagValueByFeatureName(String name) async { - return (await client?.getFeatureFlagValue(name)); - } - - Future getGlobalFlagValue(String name) async { - return (await client?.getFeatureFlagValue(name)); + String? doubleName = await getDoubleName(); + if (doubleName != null) { + Identity user = Identity(identifier: doubleName); + return (await client.getFeatureFlagValue(name, user: user)); + } + return (await client.getFeatureFlagValue(name)); } factory Flags() { diff --git a/app/lib/main.dart b/app/lib/main.dart index b677ff67..f688b183 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -6,8 +6,6 @@ import 'package:threebotlogin/screens/main_screen.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:google_fonts/google_fonts.dart'; - -import 'app_config.dart'; import 'helpers/kyc_helpers.dart'; Future main() async { diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index 51921ca5..14c9bfa0 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -57,16 +57,15 @@ class _AppState extends State { } pushScreens() async { - print("checking internet connection now"); - await checkInternetConnection(); - await checkInternetConnectionWithOurServers(); - await checkIfAppIsUnderMaintenance(); - await checkIfAppIsUpToDate(); - try { await Flags().initFlagSmith(); await Flags().setFlagSmithDefaultValues(); + print("Checking internet connection now"); + await checkInternetConnection(); + await checkInternetConnectionWithOurServers(); + await checkIfAppIsUnderMaintenance(); + await checkIfAppIsUpToDate(); } catch (e) { print(e); CustomDialog dialog = CustomDialog( @@ -113,11 +112,14 @@ class _AppState extends State { checkInternetConnection() async { try { final List result = - await InternetAddress.lookup('google.com').timeout(const Duration(seconds: 3)); + await InternetAddress.lookup('google.com').timeout(Duration(seconds: Globals().timeOutSeconds)); + if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { - print('connected to the internet'); + print('Connected to the internet'); } + } on TimeoutException catch (_) { + print(_); CustomDialog dialog = CustomDialog( title: "No internet connection available", description: "Please enable your internet connection to use this app.", @@ -129,10 +131,12 @@ class _AppState extends State { exit(1); } } on Exception catch (_) { + print(_); CustomDialog dialog = CustomDialog( title: "No internet connection available", description: "Please enable your internet connection to use this app.", ); + await dialog.show(context); if (Platform.isAndroid) { SystemNavigator.pop(); @@ -147,7 +151,7 @@ class _AppState extends State { try { String baseUrl = AppConfig().baseUrl(); final List result = - await InternetAddress.lookup('$baseUrl').timeout(const Duration(seconds: 3)); + await InternetAddress.lookup('$baseUrl').timeout(Duration(seconds: Globals().timeOutSeconds)); if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { print('connected to the internet'); } @@ -177,8 +181,6 @@ class _AppState extends State { } checkIfAppIsUnderMaintenance() async { - print('HALLO'); - print(await isAppUnderMaintenance()); try { if (await isAppUnderMaintenance()) { CustomDialog dialog = CustomDialog( diff --git a/app/lib/screens/recover_screen.dart b/app/lib/screens/recover_screen.dart index f6b7c982..cf4e9790 100644 --- a/app/lib/screens/recover_screen.dart +++ b/app/lib/screens/recover_screen.dart @@ -57,9 +57,7 @@ class _RecoverScreenState extends State { await savePrivateKey(keyPair.sk); await savePublicKey(keyPair.pk); - print('A'); FlutterPkid client = await getPkidClient(seedPhrase: seedPhrase); - print(client); List keyWords = ['email', 'phone', 'identity']; var futures = keyWords.map((keyword) async { diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index c6117afe..a85ca132 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -6,6 +6,7 @@ import 'package:http/http.dart' as http; import 'package:http/http.dart'; import 'package:package_info/package_info.dart'; import 'package:threebotlogin/app_config.dart'; +import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; @@ -85,8 +86,12 @@ Future isAppUpToDate() async { int currentBuildNumber = int.parse(packageInfo.buildNumber); int minimumBuildNumber = 0; + + print('Count of timeout seconds'); + print(Globals().timeOutSeconds); + String jsonResponse = - (await http.get(url, headers: requestHeaders).timeout(const Duration(seconds: 3))).body; + (await http.get(url, headers: requestHeaders).timeout(Duration(seconds: Globals().timeOutSeconds))).body; Map minimumVersion = json.decode(jsonResponse); @@ -104,7 +109,7 @@ Future isAppUnderMaintenance() async { print('Sending call: ${url.toString()}'); Response response = - await http.get(url, headers: requestHeaders).timeout(const Duration(seconds: 3)); + await http.get(url, headers: requestHeaders).timeout(Duration(seconds: Globals().timeOutSeconds)); if (response.statusCode != 200) { return false; diff --git a/app/lib/services/open_kyc_service.dart b/app/lib/services/open_kyc_service.dart index 34a18285..7b8a2bd0 100644 --- a/app/lib/services/open_kyc_service.dart +++ b/app/lib/services/open_kyc_service.dart @@ -113,7 +113,7 @@ Future sendVerificationEmail() async { String encodedBody = json.encode({ 'user_id': await getDoubleName(), 'email': (await getEmail())['email'], - 'public_key': await getPublicKey(), + 'public_key': base64.encode(await getPublicKey()), }); Uri url = Uri.parse('$openKycApiUrl/verification/send-email'); @@ -126,7 +126,7 @@ Future sendVerificationSms() async { String encodedBody = json.encode({ 'user_id': await getDoubleName(), 'number': (await getPhone())['phone'], - 'public_key': await getPublicKey(), + 'public_key': base64.encode(await getPublicKey()), }); Uri url = Uri.parse('$openKycApiUrl/verification/send-sms'); @@ -158,7 +158,7 @@ Future sendVerificationIdentity() async { String encodedBody = json.encode({ 'user_id': await getDoubleName(), 'kycLevel': (level), - 'public_key': await getPublicKey(), + 'public_key': base64.encode(await getPublicKey()), }); Uri url = Uri.parse('$openKycApiUrl/verification/send-identity'); diff --git a/app/test/flagsmith_tests.dart b/app/test/flagsmith_tests.dart new file mode 100644 index 00000000..61e947d2 --- /dev/null +++ b/app/test/flagsmith_tests.dart @@ -0,0 +1,23 @@ +import 'package:flagsmith/flagsmith.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:threebotlogin/app_config.dart'; + +void main() { + test('Check if FlagSmith is correctly configured', () async { + Map flagSmithConfig = AppConfig().flagSmithConfig(); + + final flagSmithClient = await FlagsmithClient.init( + config: FlagsmithConfig( + baseURI: flagSmithConfig['url'].toString(), + ), + apiKey: flagSmithConfig['apiKey'].toString()); + + + String? doubleName = 'HALLODITISEENIDENTIFIER'; + Identity user = Identity(identifier: doubleName); + + print(await flagSmithClient.getFeatureFlags(user: user, reload: true)); + + expect(1, 1); + }); +} From e49f3ba009628f3335cfeb66cf9a3cb506843135 Mon Sep 17 00:00:00 2001 From: Lennert Date: Thu, 20 Jan 2022 13:02:20 +0100 Subject: [PATCH 13/75] LocationId fix --- app/lib/services/shared_preference_service.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index 0dc73029..f5007283 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -458,7 +458,15 @@ Future> getLocationIdList() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); String? locationIdListAsJson = prefs.getString('locationIdList'); - List locationIdList = jsonDecode(locationIdListAsJson!); + + List locationIdList = []; + + if(locationIdListAsJson != null) { + locationIdList = jsonDecode(locationIdListAsJson); + } + else { + locationIdList = []; + } return locationIdList; } From 877da2736460f50b1f93a4983f30651d2399e278 Mon Sep 17 00:00:00 2001 From: Lennert Date: Thu, 20 Jan 2022 16:25:25 +0100 Subject: [PATCH 14/75] WIP - SIGN --- .../org/jimber/threebotlogin/MainActivity.kt | 2 +- backend/services/socket.py | 15 ++++ example/yarn.lock | 4 ++ frontend/src/main.js | 2 +- frontend/src/router.js | 13 ++-- frontend/src/views/Sign/index.vue | 3 + frontend/src/views/Sign/sign.html | 43 ++++++++++++ frontend/src/views/Sign/sign.js | 68 +++++++++++++++++++ frontend/src/views/Sign/sign.scss | 0 9 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 example/yarn.lock create mode 100644 frontend/src/views/Sign/index.vue create mode 100644 frontend/src/views/Sign/sign.html create mode 100644 frontend/src/views/Sign/sign.js create mode 100644 frontend/src/views/Sign/sign.scss 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 c64d56e8..ece5499c 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 @@ -1,4 +1,4 @@ -package org.jimber.threebotlogin.staging +package org.jimber.threebotlogin.local import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/backend/services/socket.py b/backend/services/socket.py index 14e08599..394e8a77 100644 --- a/backend/services/socket.py +++ b/backend/services/socket.py @@ -103,6 +103,21 @@ def on_login(data): emitOrQueue("login", data, room=user["double_name"]) +@sio.on("sign") +def on_sign(data): + logger.debug("/sign %s", data) + print(data) + # double_name = data.get("doubleName") + # + # data["type"] = "login" + # milli_sec = int(round(time.time() * 1000)) + # data["created"] = milli_sec + # + # user = db.get_user_by_double_name(double_name) + # if user: + # logger.debug("[login]: User found %s", user["double_name"]) + # emitOrQueue("login", data, room=user["double_name"]) + def emitOrQueue(event, data, room): logger.debug("Emit or queue data %s", data) if not room in usersInRoom or usersInRoom[room] == 0: diff --git a/example/yarn.lock b/example/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/example/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/frontend/src/main.js b/frontend/src/main.js index 1b29db30..87cb9214 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -8,7 +8,7 @@ Vue.config.productionTip = false router.beforeEach((to, from, next) => { console.log(`to.name == ${to.name}`) - if ((to.name !== 'initial' && to.name !== 'error' && to.name !== 'verifyemail' && to.name !== 'verifysms') && !store.state.doubleName) { + if ((to.name !== 'initial' && to.name !== 'error' && to.name !== 'verifyemail' && to.name !== 'verifysms' && to.name !== 'sign') && !store.state.doubleName) { next({ name: 'initial' }) diff --git a/frontend/src/router.js b/frontend/src/router.js index ac4e024d..bb1d90ab 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -24,8 +24,13 @@ export default new Router({ name: 'verifysms', component: () => import(/* webpackChunkName: "verifysms-page" */ './views/VerifySms') }, { - path: '/error', - name: 'error', - component: () => import(/* webpackChunkName: "error-page" */ './views/Errorpage') - } ] + path: '/sign', + name: 'sign', + component: () => import(/* webpackChunkName: "sign-page" */ './views/Sign') + }, + { + path: '/error', + name: 'error', + component: () => import(/* webpackChunkName: "error-page" */ './views/Errorpage') + }] }) diff --git a/frontend/src/views/Sign/index.vue b/frontend/src/views/Sign/index.vue new file mode 100644 index 00000000..8e16b1d7 --- /dev/null +++ b/frontend/src/views/Sign/index.vue @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/views/Sign/sign.html b/frontend/src/views/Sign/sign.html new file mode 100644 index 00000000..a6eb69a0 --- /dev/null +++ b/frontend/src/views/Sign/sign.html @@ -0,0 +1,43 @@ + diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js new file mode 100644 index 00000000..1fe8099b --- /dev/null +++ b/frontend/src/views/Sign/sign.js @@ -0,0 +1,68 @@ +import { + mapActions, + mapGetters +} from 'vuex' + +export default { + name: 'initial', + components: {}, + props: [], + data () { + return { + appid: '', + doubleName: '', + valid: false, + nameRegex: new RegExp(/^(\w+)$/), + nameRules: [ + v => !!v || 'Name is required', + v => this.nameRegex.test(v) || 'Name can only contain alphanumeric characters.', + v => v.length <= 50 || 'Name must be less than 50 characters.' + ], + url: '', + nameCheckerTimeOut: null + } + }, + mounted () {}, + computed: { + ...mapGetters([ + 'nameCheckStatus', + 'signedAttempt', + 'redirectUrl', + 'firstTime', + 'randomImageId', + 'cancelLoginUp', + '_state', + 'scope', + 'appId', + 'appPublicKey' + ]) + }, + methods: { + ...mapActions([ + 'setDoubleName', + 'loginUser', + 'loginUserMobile', + 'setScope', + 'setAppId', + 'setAppPublicKey', + 'checkName', + 'clearCheckStatus', + 'setAttemptCanceled', + 'setRandomRoom' + ]), + onSignIn () { + console.log('HOI') + }, + checkNameAvailability () { + this.clearCheckStatus() + if (this.doubleName) { + if (this.nameCheckerTimeOut != null) clearTimeout(this.nameCheckerTimeOut) + this.nameCheckerTimeOut = setTimeout(() => { + this.checkName(this.doubleName) + }, 500) + } + } + }, + cancelLoginUp (val) { + } +} diff --git a/frontend/src/views/Sign/sign.scss b/frontend/src/views/Sign/sign.scss new file mode 100644 index 00000000..e69de29b From 4ce285020ab8309414f10dfb24bb165782bb45af Mon Sep 17 00:00:00 2001 From: Lennert Date: Mon, 24 Jan 2022 21:12:02 +0100 Subject: [PATCH 15/75] WIP --- app/lib/events/go_sign_event.dart | 7 ++++ app/lib/models/sign.dart | 54 ++++++++++++++++++++++++++ app/lib/services/socket_service.dart | 7 ++++ app/pubspec.yaml | 4 ++ backend/services/socket.py | 19 ++++----- frontend/src/store.js | 58 +++++++++++++++++++++++++--- frontend/src/views/Sign/sign.js | 23 +++++++++-- 7 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 app/lib/events/go_sign_event.dart create mode 100644 app/lib/models/sign.dart diff --git a/app/lib/events/go_sign_event.dart b/app/lib/events/go_sign_event.dart new file mode 100644 index 00000000..14a9374b --- /dev/null +++ b/app/lib/events/go_sign_event.dart @@ -0,0 +1,7 @@ +import 'package:threebotlogin/models/sign.dart'; + +class NewSignEvent { + Sign? signData; + NewSignEvent({ this.signData}); +} + diff --git a/app/lib/models/sign.dart b/app/lib/models/sign.dart new file mode 100644 index 00000000..4a12fe24 --- /dev/null +++ b/app/lib/models/sign.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:threebotlogin/services/crypto_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; + +class Sign { + String? hashedDataUrl; + String? dataUrl; + String? appId; + bool? isJson; + + Sign({ + this.hashedDataUrl, + this.dataUrl, + this.isJson, + this.appId, + }); + + Sign.fromJson(Map json) + : hashedDataUrl = json['dataUrlHash'], + dataUrl = json['dataUrl'], + appId = json['appId'], + isJson = json['isJson'] as bool?; + + Map toJson() => { + 'hashedDataUrl': hashedDataUrl, + 'dataUrl': dataUrl, + 'isJson': isJson, + 'appId': appId, + }; + + static Future createAndDecryptSignObject(dynamic data) async { + Sign signData; + + if (data['encryptedSignAttempt'] != null) { + Uint8List pk = await getPublicKey(); + Uint8List sk = await getPrivateKey(); + + String encryptedSignAttempt = await decrypt(data['encryptedSignAttempt'], pk, sk); + dynamic decryptedSignAttemptMap = jsonDecode(encryptedSignAttempt); + + print('Decrypted login attempt'); + print(decryptedSignAttemptMap); + + decryptedSignAttemptMap['type'] = data['type']; + signData = Sign.fromJson(decryptedSignAttemptMap); + } else { + signData = Sign.fromJson(data); + } + + return signData; + } +} diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index 6608c177..3025e4dd 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -8,10 +8,12 @@ import 'package:threebotlogin/events/close_auth_event.dart'; import 'package:threebotlogin/events/close_socket_event.dart'; import 'package:threebotlogin/events/email_event.dart'; import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_sign_event.dart'; import 'package:threebotlogin/events/new_login_event.dart'; import 'package:threebotlogin/events/phone_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/login.dart'; +import 'package:threebotlogin/models/sign.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/screens/login_screen.dart'; import 'package:threebotlogin/screens/warning_screen.dart'; @@ -65,6 +67,11 @@ class BackendConnection { Events().emit(NewLoginEvent(loginData: loginData)); }); + socket.on('sign', (dynamic data) async { + Sign signData = await Sign.createAndDecryptSignObject(data); + Events().emit(NewSignEvent(signData: signData)); + }); + socket.on('disconnect', (_) { print('disconnect'); }); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4acff152..6b1d6277 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -37,6 +37,10 @@ dependencies: url: https://github.com/jimbertools/shufti-jimber ref: main_null-safety + keyboard_visibility: + git: + url: https://github.com/jimbertools/flutter_keyboard_visibility + flutter_svg: ^1.0.0 bip39: ^1.0.6 socket_io_client: ^1.0.2 diff --git a/backend/services/socket.py b/backend/services/socket.py index 394e8a77..7640fecf 100644 --- a/backend/services/socket.py +++ b/backend/services/socket.py @@ -106,17 +106,14 @@ def on_login(data): @sio.on("sign") def on_sign(data): logger.debug("/sign %s", data) - print(data) - # double_name = data.get("doubleName") - # - # data["type"] = "login" - # milli_sec = int(round(time.time() * 1000)) - # data["created"] = milli_sec - # - # user = db.get_user_by_double_name(double_name) - # if user: - # logger.debug("[login]: User found %s", user["double_name"]) - # emitOrQueue("login", data, room=user["double_name"]) + double_name = data.get("doubleName") + + data["type"] = "sign" + user = db.get_user_by_double_name(double_name) + if user: + logger.debug("[sign]: User found %s", user["double_name"]) + emitOrQueue("sign", data, room=user["double_name"]) + def emitOrQueue(event, data, room): logger.debug("Emit or queue data %s", data) diff --git a/frontend/src/store.js b/frontend/src/store.js index 2ed46b2f..fa2f999a 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -5,6 +5,7 @@ import cryptoService from './services/cryptoService' import userService from './services/userService' import axios from 'axios' import config from '../public/config' +import { toBoolean } from 'vue-qr/src/util' Vue.use(Vuex) @@ -42,7 +43,10 @@ export default new Vuex.Store({ loginTimestamp: 0, loginTimeleft: 120, loginTimeout: 120, - loginInterval: null + loginInterval: null, + isJson: false, + dataUrl: null, + dataUrlHash: null, }, mutations: { setNameCheckStatus (state, status) { @@ -110,6 +114,15 @@ export default new Vuex.Store({ clearInterval(this.loginInterval) } }, 1000) + }, + setDataUrl (state, dataUrl) { + state.dataUrl = dataUrl + }, + setIsJson (state, isJson) { + state.isJson = isJson + }, + setHashedDataUrl (state, dataUrlHash) { + state.dataUrlHash = dataUrlHash } }, actions: { @@ -233,6 +246,32 @@ export default new Vuex.Store({ context.commit('setSignedAttempt', data) } }, + + async signDataUser (context, data) { + await context.dispatch('setDoubleName', data.doubleName) + context.commit('setAppId', data.appId) + context.commit('setIsJson', data.isJson) + context.commit('setHashedDataUrl', data.dataUrlHash) + context.commit('setDataUrl', data.dataUrl) + let publicKey = (await userService.getUserData(context.getters.doubleName)).data.publicKey + + let encryptedSignAttempt = await cryptoService.encrypt(JSON.stringify({ + doubleName: context.getters.doubleName, + isJson: toBoolean(context.getters.isJson), + dataUrlHash: context.getters.dataUrlHash, + dataUrl: context.getters.dataUrl, + appId: context.getters.appId + }), publicKey) + + let randomRoom = generateUUID() + socketService.emit('leave', { 'room': context.getters.doubleName }) + await context.dispatch('setRandomRoom', randomRoom) + + socketService.emit('sign', { + 'doubleName': context.getters.doubleName, + 'encryptedSignAttempt': encryptedSignAttempt + }) + }, async loginUser (context, data) { console.log(`LoginUser`) context.dispatch('setDoubleName', data.doubleName) @@ -273,7 +312,10 @@ export default new Vuex.Store({ socketService.emit('leave', { 'room': context.getters.doubleName }) context.dispatch('setRandomRoom', randomRoom) - socketService.emit('login', { 'doubleName': context.getters.doubleName, 'encryptedLoginAttempt': encryptedLoginAttempt }) + socketService.emit('login', { + 'doubleName': context.getters.doubleName, + 'encryptedLoginAttempt': encryptedLoginAttempt + }) }, loginUserMobile (context, data) { context.commit('setSignedAttempt', null) @@ -312,7 +354,10 @@ export default new Vuex.Store({ socketService.emit('leave', { 'room': context.getters.randomRoom }) context.dispatch('setRandomRoom', randomRoom) context.dispatch('resetTimer') - socketService.emit('login', { 'doubleName': context.getters.doubleName, 'encryptedLoginAttempt': encryptedLoginAttempt }) + socketService.emit('login', { + 'doubleName': context.getters.doubleName, + 'encryptedLoginAttempt': encryptedLoginAttempt + }) }, sendValidationEmail (context, data) { var callbackUrl = `${window.location.protocol}//${window.location.host}/verifyemail` @@ -471,8 +516,11 @@ export default new Vuex.Store({ loginTimestamp: state => state.loginTimestamp, loginTimeleft: state => state.loginTimeleft, loginTimeout: state => state.loginTimeout, - loginInterval: state => state.loginInterval - } + loginInterval: state => state.loginInterval, + dataUrl: state => state.dataUrl, + dataUrlHash: state => state.dataUrlHash, + isJson: state => state.isJson +} }) function generateUUID () { diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index 1fe8099b..906376e7 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -22,7 +22,8 @@ export default { nameCheckerTimeOut: null } }, - mounted () {}, + mounted () { + }, computed: { ...mapGetters([ 'nameCheckStatus', @@ -48,10 +49,24 @@ export default { 'checkName', 'clearCheckStatus', 'setAttemptCanceled', - 'setRandomRoom' + 'setRandomRoom', + 'signDataUser' ]), - onSignIn () { - console.log('HOI') + async onSignIn () { + const query = this.$route.query + + const appId = query.appId + const dataHash = query.dataHash + const dataUrl = query.dataUrl + const isJson = query.isJson + await this.signDataUser({ + doubleName: this.doubleName, + appId: appId, + isJson: isJson, + dataUrlHash: dataHash, + dataUrl: dataUrl + }) + console.log('Done') }, checkNameAvailability () { this.clearCheckStatus() From 6ac374042b51cabd67b728abf8257fe46e1dac2c Mon Sep 17 00:00:00 2001 From: Lennert Date: Mon, 24 Jan 2022 21:58:04 +0100 Subject: [PATCH 16/75] add hybrid composition --- app/lib/apps/chatbot/chatbot_widget.dart | 2 +- app/lib/apps/news/news_widget.dart | 2 +- app/lib/apps/wallet/wallet_widget.dart | 2 +- app/lib/screens/init_screen.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/lib/apps/chatbot/chatbot_widget.dart b/app/lib/apps/chatbot/chatbot_widget.dart index fa42bea1..50351a47 100644 --- a/app/lib/apps/chatbot/chatbot_widget.dart +++ b/app/lib/apps/chatbot/chatbot_widget.dart @@ -27,7 +27,7 @@ class _ChatbotState extends State with AutomaticKeepAliveClientMi new DateTime.now().millisecondsSinceEpoch.toString())), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true), - android: AndroidInAppWebViewOptions(supportMultipleWindows: true)), + android: AndroidInAppWebViewOptions(supportMultipleWindows: true, useHybridComposition: true)), onWebViewCreated: (InAppWebViewController controller) { webView = controller; }, diff --git a/app/lib/apps/news/news_widget.dart b/app/lib/apps/news/news_widget.dart index 3f5158e8..bbc01eea 100644 --- a/app/lib/apps/news/news_widget.dart +++ b/app/lib/apps/news/news_widget.dart @@ -44,7 +44,7 @@ class _NewsState extends State initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions(), android: AndroidInAppWebViewOptions( - supportMultipleWindows: true, thirdPartyCookiesEnabled: true), + supportMultipleWindows: true, thirdPartyCookiesEnabled: true, useHybridComposition: true), ios: IOSInAppWebViewOptions()), onWebViewCreated: (InAppWebViewController controller) { webView = controller; diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 1475fd85..6369917d 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -52,7 +52,7 @@ class _WalletState extends State with AutomaticKeepAliveClientMixi initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions(), android: AndroidInAppWebViewOptions( - supportMultipleWindows: true, thirdPartyCookiesEnabled: true), + supportMultipleWindows: true, thirdPartyCookiesEnabled: true, useHybridComposition: true), ios: IOSInAppWebViewOptions()), onWebViewCreated: (InAppWebViewController controller) { webView = controller; diff --git a/app/lib/screens/init_screen.dart b/app/lib/screens/init_screen.dart index 24368932..e6957af0 100644 --- a/app/lib/screens/init_screen.dart +++ b/app/lib/screens/init_screen.dart @@ -31,7 +31,7 @@ class _InitState extends State { '?cache_buster=' + new DateTime.now().millisecondsSinceEpoch.toString())), initialOptions: InAppWebViewGroupOptions( - android: AndroidInAppWebViewOptions(supportMultipleWindows: true), + android: AndroidInAppWebViewOptions(supportMultipleWindows: true, useHybridComposition: true), ), onWebViewCreated: (InAppWebViewController controller) { webView = controller; From 8e4b950c83ae6c8c9ec77f9b268be8f00fc716b3 Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 26 Jan 2022 10:10:52 +0100 Subject: [PATCH 17/75] Null safe farmer --- app/lib/apps/farmers/farmers_user_data.dart | 17 +++++++-------- app/lib/apps/farmers/farmers_widget.dart | 23 +++++++++++---------- app/lib/helpers/flags.dart | 4 ++-- app/lib/helpers/globals.dart | 14 +++---------- 4 files changed, 25 insertions(+), 33 deletions(-) diff --git a/app/lib/apps/farmers/farmers_user_data.dart b/app/lib/apps/farmers/farmers_user_data.dart index b8210440..d4cf9908 100644 --- a/app/lib/apps/farmers/farmers_user_data.dart +++ b/app/lib/apps/farmers/farmers_user_data.dart @@ -3,10 +3,10 @@ import 'package:shared_preferences/shared_preferences.dart'; void saveImportedWallet(List params) async { String importedWallet = params[0].toString(); final prefs = await SharedPreferences.getInstance(); - List importedWallets = await getImportedWallets(); + List? importedWallets = await getImportedWallets(); if (importedWallets == null) { - importedWallets = new List(); + importedWallets = []; } if (!importedWallets.contains(importedWallet)) { @@ -17,23 +17,22 @@ void saveImportedWallet(List params) async { } } -Future> getImportedWallets() async { +Future?> getImportedWallets() async { final prefs = await SharedPreferences.getInstance(); - return prefs.getStringList("importedWallets"); } Future saveAppWallet(List params) async { - String appWalletToAdd = params[0].toString(); + String? appWalletToAdd = params[0]; final prefs = await SharedPreferences.getInstance(); - List appWallets = await getAppWallets(); + List? appWallets = await getAppWallets(); if (appWalletToAdd == null) { return false; } if (appWallets == null) { - appWallets = new List(); + appWallets = []; } if (!appWallets.contains(appWalletToAdd)) { @@ -45,9 +44,9 @@ Future saveAppWallet(List params) async { return false; } -Future> getAppWallets() async { +Future?> getAppWallets() async { final prefs = await SharedPreferences.getInstance(); - var wallets = prefs.getStringList("appWallets"); + List? wallets = prefs.getStringList("appWallets"); return wallets; } diff --git a/app/lib/apps/farmers/farmers_widget.dart b/app/lib/apps/farmers/farmers_widget.dart index 035ab733..36b3cf89 100644 --- a/app/lib/apps/farmers/farmers_widget.dart +++ b/app/lib/apps/farmers/farmers_widget.dart @@ -12,8 +12,7 @@ import 'package:threebotlogin/events/go_home_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/screens/scan_screen.dart'; -import 'package:threebotlogin/services/3bot_service.dart'; -import 'package:threebotlogin/services/user_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; bool created = false; @@ -24,14 +23,14 @@ class FarmersWidget extends StatefulWidget { } class _FarmersState extends State with AutomaticKeepAliveClientMixin { - InAppWebViewController webView; + late InAppWebViewController webView; double progress = 0; var config = WalletConfig(); - InAppWebView iaWebView; + late InAppWebView iaWebView; _back(WalletBackEvent event) async { - Uri url = await webView.getUrl(); + Uri? url = await webView.getUrl(); print(url.toString()); String endsWith = config.appId() + '/'; if (url.toString().endsWith(endsWith)) { @@ -57,8 +56,10 @@ class _FarmersState extends State with AutomaticKeepAliveClientMi webView = controller; this.addHandler(); }, - onCreateWindow: (InAppWebViewController controller, CreateWindowAction req) {}, - onLoadStop: (InAppWebViewController controller, Uri url) async { + onCreateWindow: (InAppWebViewController controller, CreateWindowAction req) { + return Future.value(true); + }, + onLoadStop: (InAppWebViewController controller, Uri? url) async { addClipboardHandlersOnly(controller); if (url.toString().contains('/init')) { initKeys(); @@ -86,20 +87,20 @@ class _FarmersState extends State with AutomaticKeepAliveClientMi } initKeys() async { - var seed = await getDerivedSeed(config.appId()); - var doubleName = await getDoubleName(); + String seed = base64.encode(await getDerivedSeed(config.appId())); + String? doubleName = await getDoubleName(); var jsStartApp = "window.init('$doubleName', '$seed')"; webView.evaluateJavascript(source: jsStartApp); } - scanQrCode(List params) async { + Future scanQrCode(List params) 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); - String result; + String? result; if (slept) { result = await Navigator.push(context, MaterialPageRoute(builder: (context) => ScanScreen())); } diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index 31f2dfa2..f4c54a66 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -40,8 +40,8 @@ class Flags { Globals().debugMode = await Flags().hasFlagValueByFeatureName('debug'); Globals().useNewWallet = await Flags().hasFlagValueByFeatureName('use-new-wallet'); Globals().canSeeFarmers = await Flags().hasFlagValueByFeatureName('can-see-farmers'); - Globals().newWalletUrl = await Flags().getFlagValueByFeatureName('new-wallet-url'); - Globals().farmersUrl = await Flags().getFlagValueByFeatureName('farmers-url'); + Globals().newWalletUrl = (await Flags().getFlagValueByFeatureName('new-wallet-url'))!; + Globals().farmersUrl = (await Flags().getFlagValueByFeatureName('farmers-url'))!; Globals().redoIdentityVerification = await Flags().hasFlagValueByFeatureName('redo-identity-verification'); } diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index c876e872..d37160c7 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -44,7 +44,6 @@ class Globals { bool paymentRequestIsUsed = false; // FlagSmith configurations -<<<<<<< HEAD bool isOpenKYCEnabled = false; bool isYggdrasilEnabled = false; bool useNewWallet = false; @@ -52,16 +51,9 @@ class Globals { bool redoIdentityVerification = false; bool debugMode = false; int timeOutSeconds = 5; -======= - bool isOpenKYCEnabled; - bool isYggdrasilEnabled; - bool useNewWallet; - String newWalletUrl; - String farmersUrl; - bool redoIdentityVerification; - bool debugMode; - bool canSeeFarmers; ->>>>>>> master + String farmersUrl = ''; + bool canSeeFarmers = false; + VpnState vpnState = new VpnState(); From 55fa4c3f1b7f5100f2f5a2cdf9696977ea0fc0e2 Mon Sep 17 00:00:00 2001 From: Lennert Date: Fri, 28 Jan 2022 11:30:27 +0100 Subject: [PATCH 18/75] WIP --- .../org/jimber/threebotlogin/MainActivity.kt | 2 +- app/lib/helpers/download_helper.dart | 33 ++++ app/lib/models/sign.dart | 17 +- app/lib/screens/home_screen.dart | 5 + app/lib/screens/sign_screen.dart | 160 ++++++++++++++++++ app/lib/services/3bot_service.dart | 35 +++- app/lib/services/crypto_service.dart | 11 +- app/lib/services/socket_service.dart | 49 ++++++ app/lib/widgets/layout_drawer.dart | 2 +- app/pubspec.yaml | 13 +- backend/routes/crypt.py | 25 +++ backend/services/crypt.py | 2 +- frontend/src/store.js | 35 +++- frontend/src/views/Sign/sign.js | 55 +++++- 14 files changed, 419 insertions(+), 25 deletions(-) create mode 100644 app/lib/helpers/download_helper.dart create mode 100644 app/lib/screens/sign_screen.dart 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 c64d56e8..ece5499c 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 @@ -1,4 +1,4 @@ -package org.jimber.threebotlogin.staging +package org.jimber.threebotlogin.local import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/app/lib/helpers/download_helper.dart b/app/lib/helpers/download_helper.dart new file mode 100644 index 00000000..c6e523d0 --- /dev/null +++ b/app/lib/helpers/download_helper.dart @@ -0,0 +1,33 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; + + +// https://www.youtube.com/watch?v=6tfBflFUO7s +Future openFile({required String url, String? fileName}) async { + final file = await downloadFile(url, fileName!); + + if(file == null) return; + + OpenFile.open(file.path); +} + +Future downloadFile(String url, String name) async { + try { + final appStorage = await getApplicationDocumentsDirectory(); + final file = File('${appStorage.path}/$name'); + + final response = await Dio().get(url, + options: + Options(responseType: ResponseType.bytes, followRedirects: true, receiveTimeout: 0)); + + final raf = file.openSync(mode: FileMode.WRITE); + raf.writeFromSync(response.data); + await raf.close(); + return file; + } catch (e) { + print(e); + return null; + } +} diff --git a/app/lib/models/sign.dart b/app/lib/models/sign.dart index 4a12fe24..a98dc7c3 100644 --- a/app/lib/models/sign.dart +++ b/app/lib/models/sign.dart @@ -5,29 +5,42 @@ import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; class Sign { + String? doubleName; String? hashedDataUrl; String? dataUrl; String? appId; bool? isJson; + String? type; + String? randomRoom; Sign({ + this.doubleName, this.hashedDataUrl, this.dataUrl, this.isJson, this.appId, + this.type, + this.randomRoom }); Sign.fromJson(Map json) - : hashedDataUrl = json['dataUrlHash'], + : + doubleName = json['doubleName'], + hashedDataUrl = json['dataUrlHash'], dataUrl = json['dataUrl'], appId = json['appId'], - isJson = json['isJson'] as bool?; + isJson = json['isJson'] as bool?, + type = json['type'], + randomRoom = json['randomRoom']; Map toJson() => { + 'doubleName' : doubleName, 'hashedDataUrl': hashedDataUrl, 'dataUrl': dataUrl, 'isJson': isJson, 'appId': appId, + 'type' : type, + 'randomRoom' : randomRoom }; static Future createAndDecryptSignObject(dynamic data) async { diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index cb2eebc8..4476c280 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -11,6 +11,7 @@ import 'package:threebotlogin/events/email_event.dart'; import 'package:threebotlogin/events/go_news_event.dart'; import 'package:threebotlogin/events/go_reservations_event.dart'; import 'package:threebotlogin/events/go_settings_event.dart'; +import 'package:threebotlogin/events/go_sign_event.dart'; import 'package:threebotlogin/events/go_support_event.dart'; import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/events/phone_event.dart'; @@ -159,6 +160,10 @@ class _HomeScreenState extends State openLogin(context, event.loginData!, widget.backendConnection!); }); + Events().onEvent(NewSignEvent().runtimeType, (NewSignEvent event) { + openSign(context, event.signData!, widget.backendConnection!); + }); + Events().onEvent(EmailEvent().runtimeType, (EmailEvent event) { emailVerification(context); }); diff --git a/app/lib/screens/sign_screen.dart b/app/lib/screens/sign_screen.dart new file mode 100644 index 00000000..b6462e2e --- /dev/null +++ b/app/lib/screens/sign_screen.dart @@ -0,0 +1,160 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/pop_all_login_event.dart'; +import 'package:threebotlogin/helpers/block_and_run_mixin.dart'; +import 'package:threebotlogin/helpers/download_helper.dart'; +import 'package:threebotlogin/models/sign.dart'; +import 'package:threebotlogin/services/3bot_service.dart'; +import 'package:threebotlogin/services/crypto_service.dart'; +import 'package:threebotlogin/services/shared_preference_service.dart'; + +class SignScreen extends StatefulWidget { + final Sign signData; + + SignScreen(this.signData); + + _SignScreenState createState() => _SignScreenState(); +} + +class _SignScreenState extends State with BlockAndRunMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + String updateMessage = ''; + bool isBusy = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + child: new Scaffold( + key: _scaffoldKey, + appBar: new AppBar( + // automaticallyImplyLeading: false, + backgroundColor: Theme + .of(context) + .primaryColor, + title: new Text("Sign"), + ), + body: Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery + .of(context) + .size + .height * 0.8, + maxHeight: MediaQuery + .of(context) + .size + .height * 0.8, + minWidth: MediaQuery + .of(context) + .size + .width, + maxWidth: MediaQuery + .of(context) + .size + .width, + ), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(20), + child: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: new TextSpan( + style: new TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + children: [ + TextSpan(children: [ + new TextSpan( + text: widget.signData.appId!, + style: TextStyle(fontWeight: FontWeight.bold)), + new TextSpan( + text: + ' wants you to sign a data document. The hash of the document is: \n \n'), + new TextSpan( + text: widget.signData.hashedDataUrl! + '\n \n \n', + style: TextStyle(fontSize: 10)), + new TextSpan( + text: 'You can download the document for review here') + ]), + ]), + ), + SizedBox(height: 10), + Text(widget.signData.dataUrl!), + SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + isBusy = true; + updateMessage = 'Verifying hash.. '; + setState(() {}); + verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); + + updateMessage = 'Downloading and opening file.. '; + setState(() {}); + await openFile(url: widget.signData.dataUrl!, fileName: 'test.pdf'); + updateMessage = ''; + isBusy = false; + setState(() {}); + }, + child: Text('Download')), + SizedBox( + height: 10, + ), + isBusy == true + ? Transform.scale( + scale: 0.5, + child: CircularProgressIndicator(), + ) + : Container(), + SizedBox( + height: 10, + ), + Text( + updateMessage, + style: TextStyle(color: Colors.orange), + ), + ElevatedButton(onPressed: () async { + String randomRoom = widget.signData.randomRoom!; + String appId = widget.signData.appId!; + + Uint8List sk = await getPrivateKey(); + String signedData = await signData(widget.signData.dataUrl!, sk); + + await sendSignedData(randomRoom, signedData, appId); + }, child: Text('SIGN')) + ], + ), + ), + ], + ), + ) + ], + ), + ), + onWillPop: () { + // Cancel the sign + cancelSignAttempt(); + return Future.value(true); + }, + ); + } + + cancelSignAttempt() async { + String? doubleName = await getDoubleName(); + // TODO: implement cancel + // cancelSign(doubleName!); + } +} diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index a85ca132..f6962542 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -13,6 +13,29 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; +Future sendSignedData( + String socketRoom, String signedDataIdentifier, String appId) async { + Uri url = Uri.parse('$threeBotApiUrl/signedSignDataAttempt'); + print('Sending call: ${url.toString()}'); + + String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); + + Uint8List sk = await getPrivateKey(); + String encodedBody = jsonEncode({ + 'signedSignAttempt': await signData( + json.encode({ + 'signedOn': timestamp, + 'randomRoom': socketRoom, + 'appId': appId, + 'signedData': signedDataIdentifier + }), + sk), + 'doubleName': await getDoubleName() + }); + + return http.post(url, body: encodedBody, headers: requestHeaders); +} + Future sendData(String state, Map? data, selectedImageId, String? randomRoom, String appId) async { Uri url = Uri.parse('$threeBotApiUrl/signedAttempt'); @@ -86,12 +109,13 @@ Future isAppUpToDate() async { int currentBuildNumber = int.parse(packageInfo.buildNumber); int minimumBuildNumber = 0; - print('Count of timeout seconds'); print(Globals().timeOutSeconds); - String jsonResponse = - (await http.get(url, headers: requestHeaders).timeout(Duration(seconds: Globals().timeOutSeconds))).body; + String jsonResponse = (await http + .get(url, headers: requestHeaders) + .timeout(Duration(seconds: Globals().timeOutSeconds))) + .body; Map minimumVersion = json.decode(jsonResponse); @@ -108,8 +132,9 @@ Future isAppUnderMaintenance() async { Uri url = Uri.parse('$threeBotApiUrl/maintenance'); print('Sending call: ${url.toString()}'); - Response response = - await http.get(url, headers: requestHeaders).timeout(Duration(seconds: Globals().timeOutSeconds)); + Response response = await http + .get(url, headers: requestHeaders) + .timeout(Duration(seconds: Globals().timeOutSeconds)); if (response.statusCode != 200) { return false; diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index db08e8bd..167102e0 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -9,6 +9,15 @@ import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:pbkdf2ns/pbkdf2ns.dart'; +bool verifyHash(String data, String hash) { + final List codeUnits = data.codeUnits; + final Uint8List unit8List = Uint8List.fromList(codeUnits); + + Uint8List hashedData = Sodium.cryptoHash(unit8List); + + return hash == base64.encode(hashedData); +} + // Helper method to convert a String input to hex used for entropy Uint8List _toHex(String input) { double length = input.length / 2; @@ -60,8 +69,6 @@ Future decrypt(String encodedCipherText, Uint8List pk, Uint8List sk) asy Uint8List publicKey = Sodium.cryptoSignEd25519PkToCurve25519(pk); Uint8List secretKey = Sodium.cryptoSignEd25519SkToCurve25519(sk); - print(base64.encode(publicKey)); - print(base64.encode(secretKey)); Uint8List decryptedData = Sodium.cryptoBoxSealOpen(cipherText, publicKey, secretKey); return new String.fromCharCodes(decryptedData); } diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index 3025e4dd..b7e8247d 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -16,6 +16,7 @@ import 'package:threebotlogin/models/login.dart'; import 'package:threebotlogin/models/sign.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; import 'package:threebotlogin/screens/login_screen.dart'; +import 'package:threebotlogin/screens/sign_screen.dart'; import 'package:threebotlogin/screens/warning_screen.dart'; import 'package:threebotlogin/services/fingerprint_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; @@ -340,6 +341,54 @@ Future identityVerification(String reference) async { return 'Verified'; } + +Future openSign(BuildContext ctx, Sign signData, BackendConnection backendConnection) async { + print('Open sign'); + String? messageType = signData.type; + + print(signData.toJson()); + print(messageType); + + if (messageType == null || messageType != 'sign') { + return; + } + + String? pin = await getPin(); + Events().emit(CloseAuthEvent(close: true)); + + bool? authenticated = await Navigator.push( + ctx, + MaterialPageRoute( + builder: (context) => AuthenticationScreen( + correctPin: pin!, userMessage: "sign your attempt"), + ), + ); + + if (authenticated == null || authenticated == false) { + return; + } + + print('Jahoo'); + + backendConnection.leaveRoom(signData.doubleName); + + bool? signAccepted = await Navigator.push( + ctx, + MaterialPageRoute( + builder: (context) => SignScreen(signData), + ), + ); + + if (signAccepted == null || signAccepted == false) { + backendConnection.joinRoom(signData.doubleName); + return; + } + + backendConnection.joinRoom(signData.doubleName); + await showLoggedInDialog(ctx); +} + + Future openLogin(BuildContext ctx, Login loginData, BackendConnection backendConnection) async { String? messageType = loginData.type; diff --git a/app/lib/widgets/layout_drawer.dart b/app/lib/widgets/layout_drawer.dart index 0d471a11..7ac7fb3e 100644 --- a/app/lib/widgets/layout_drawer.dart +++ b/app/lib/widgets/layout_drawer.dart @@ -111,7 +111,7 @@ class _LayoutDrawerState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Padding(padding: const EdgeInsets.only(left: 30)), - Icon(Icons.computer_sharp, color: Colors.black, size: 18) + Icon(Icons.account_tree_outlined, color: Colors.black, size: 18) ], ), title: Text('Farmers'), diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 2059b332..7d2ee66b 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -37,9 +37,10 @@ dependencies: url: https://github.com/jimbertools/shufti-jimber ref: main_null-safety - keyboard_visibility: - git: - url: https://github.com/jimbertools/flutter_keyboard_visibility +# keyboard_visibility: +# git: +# url: https://github.com/jimbertools/flutter_keyboard_visibility + flutter_svg: ^1.0.0 bip39: ^1.0.6 @@ -60,6 +61,12 @@ dependencies: pbkdf2ns: ^0.0.2 qr_code_scanner: ^0.6.1 + permission_handler: ^8.3.0 + path_provider: ^2.0.2 + dio: ^4.0.4 + open_file: ^3.2.1 + + dev_dependencies: flutter_test: sdk: flutter diff --git a/backend/routes/crypt.py b/backend/routes/crypt.py index a314407f..212e764d 100644 --- a/backend/routes/crypt.py +++ b/backend/routes/crypt.py @@ -36,6 +36,31 @@ def sign_attempt_handler(): return Response("Ok") +@api_crypt.route("/signedSignDataAttempt", methods=["POST"]) +def sign_data_attempt_handler(): + data = request.get_json() + + double_name = data["doubleName"].lower() + verified_data = verify_signed_data(double_name, data["signedSignAttempt"]) + + if not verified_data: + return Response("Missing signature", status=400) + + body = json.loads(verified_data) + + logger.debug("/sign: %s", body) + logger.debug("body.get('doubleName'): %s", body.get("doubleName")) + + random_room = body.get("randomRoom") + if random_room is None: + random_room = body.get("doubleName") + + random_room = random_room.lower() + + logger.debug("roomToSendTo %s", random_room) + sio.emit("signedSignDataAttempt", data, room=random_room) + return Response("Ok") + @api_crypt.route("/mobileregistration", methods=["POST"]) def mobile_registration_handler(): logger.debug("/mobile_registration_handler ") diff --git a/backend/services/crypt.py b/backend/services/crypt.py index 58998951..a841b746 100644 --- a/backend/services/crypt.py +++ b/backend/services/crypt.py @@ -11,7 +11,7 @@ config.read("config.ini") -def verify_signed_data(double_name, data): +def verify_signed_data(double_name, data): if 'DEBUG_PLAIN_SIGNED_DATA' in config["DEFAULT"] and int(config["DEFAULT"]["DEBUG_PLAIN_SIGNED_DATA"]) == 1: return json.dumps(data).encode('utf8') diff --git a/frontend/src/store.js b/frontend/src/store.js index fa2f999a..454322d5 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -47,6 +47,7 @@ export default new Vuex.Store({ isJson: false, dataUrl: null, dataUrlHash: null, + signedSignAttempt: null }, mutations: { setNameCheckStatus (state, status) { @@ -70,6 +71,9 @@ export default new Vuex.Store({ setCancelLoginUp (state, cancelLoginUp) { state.cancelLoginUp = cancelLoginUp }, + setSignedSignAttempt (state, signedSignAttempt) { + state.signedSignAttempt = signedSignAttempt + }, setSignedAttempt (state, signedAttempt) { state.signedAttempt = signedAttempt }, @@ -188,6 +192,21 @@ export default new Vuex.Store({ console.log('f') context.commit('setCancelLoginUp', true) }, + + async SOCKET_signedSignDataAttempt (context, data) { + console.log('signedSignDataAttempt', data.signedSignAttempt) + console.log('signedSignDataAttempt', data.doubleName) + + let publicKey = (await userService.getUserData(data.doubleName)).data.publicKey + var signedAttempt = await cryptoService.validateSignedAttempt(data.signedSignAttempt, publicKey) + console.log('decoded', signedAttempt) + var string = new TextDecoder().decode(signedAttempt) + + + context.commit('setSignedSignAttempt', data) + console.log(data) + }, + async SOCKET_signedAttempt (context, data) { console.log('signedAttempt', data.signedAttempt) console.log('signedAttempt', data.doubleName) @@ -254,19 +273,18 @@ export default new Vuex.Store({ context.commit('setHashedDataUrl', data.dataUrlHash) context.commit('setDataUrl', data.dataUrl) let publicKey = (await userService.getUserData(context.getters.doubleName)).data.publicKey - + let randomRoom = generateUUID() + socketService.emit('leave', { 'room': context.getters.doubleName }) + await context.dispatch('setRandomRoom', randomRoom) let encryptedSignAttempt = await cryptoService.encrypt(JSON.stringify({ doubleName: context.getters.doubleName, isJson: toBoolean(context.getters.isJson), dataUrlHash: context.getters.dataUrlHash, dataUrl: context.getters.dataUrl, - appId: context.getters.appId + appId: context.getters.appId, + randomRoom: randomRoom }), publicKey) - let randomRoom = generateUUID() - socketService.emit('leave', { 'room': context.getters.doubleName }) - await context.dispatch('setRandomRoom', randomRoom) - socketService.emit('sign', { 'doubleName': context.getters.doubleName, 'encryptedSignAttempt': encryptedSignAttempt @@ -519,8 +537,9 @@ export default new Vuex.Store({ loginInterval: state => state.loginInterval, dataUrl: state => state.dataUrl, dataUrlHash: state => state.dataUrlHash, - isJson: state => state.isJson -} + isJson: state => state.isJson, + signedSignAttempt: state => state.signedSignAttempt + } }) function generateUUID () { diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index 906376e7..180466a0 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -27,7 +27,7 @@ export default { computed: { ...mapGetters([ 'nameCheckStatus', - 'signedAttempt', + 'signedSignAttempt', 'redirectUrl', 'firstTime', 'randomImageId', @@ -78,6 +78,57 @@ export default { } } }, - cancelLoginUp (val) { + watch: { + signedSignAttempt (val) { + if (!val) { + console.log('Missing data') + return + } + + try { + console.log('signedAttemptObject: ', val) + console.log('signedAttemptObject: ', JSON.stringify(val)) + window.localStorage.setItem('username', this.doubleName) + + var data = encodeURIComponent(JSON.stringify(val)) + console.log('data', data) + + if (data) { + var union = '?' + if (this.redirectUrl.indexOf('?') >= 0) { + union = '&' + } + + var safeRedirectUri + // Otherwise evil app could do appid+redirecturl = wallet.com + .evil.com = wallet.com.evil.com + // Now its wallet.com/.evil.com + if (this.redirectUrl[0] === '/') { + safeRedirectUri = this.redirectUrl + } else { + safeRedirectUri = '/' + this.redirectUrl + } + + console.log('!!!! this.doubleName: ', this.doubleName) + var url = `//${this.appId}${safeRedirectUri}${union}signedAttempt=${data}` + + if (!this.isRedirecting) { + this.isRedirecting = true + console.log('Changing href: ', url) + window.location.href = url + } + } + } + else + { + console.log('Val was null') + } + } catch (e) { + console.log('Something went wrong ... ', e) + } } } +, +cancelLoginUp(val) +{ +} +} From 2f549d22b55be3e3a9c30fff5e01622afe1ec08c Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 8 Feb 2022 14:07:43 +0100 Subject: [PATCH 19/75] WIP --- app/lib/models/sign.dart | 10 +++++++--- app/pubspec.yaml | 2 +- frontend/src/store.js | 1 + frontend/src/views/Sign/sign.js | 19 ++++++++----------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/lib/models/sign.dart b/app/lib/models/sign.dart index a98dc7c3..d72ad34b 100644 --- a/app/lib/models/sign.dart +++ b/app/lib/models/sign.dart @@ -12,6 +12,7 @@ class Sign { bool? isJson; String? type; String? randomRoom; + String? redirectUrl; Sign({ this.doubleName, @@ -20,7 +21,8 @@ class Sign { this.isJson, this.appId, this.type, - this.randomRoom + this.randomRoom, + this.redirectUrl }); Sign.fromJson(Map json) @@ -31,7 +33,8 @@ class Sign { appId = json['appId'], isJson = json['isJson'] as bool?, type = json['type'], - randomRoom = json['randomRoom']; + randomRoom = json['randomRoom'], + redirectUrl = json['redirectUrl']; Map toJson() => { 'doubleName' : doubleName, @@ -40,7 +43,8 @@ class Sign { 'isJson': isJson, 'appId': appId, 'type' : type, - 'randomRoom' : randomRoom + 'randomRoom' : randomRoom, + 'redirectUrl' : redirectUrl }; static Future createAndDecryptSignObject(dynamic data) async { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7d2ee66b..7b91e8f6 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -51,7 +51,7 @@ dependencies: http: ^0.13.3 package_info: ^2.0.2 cupertino_icons: ^1.0.4 - google_fonts: ^2.1.1 + google_fonts: 2.2.0 intl_phone_field: ^2.1.0 flagsmith: ^2.0.0-nullsafety.0 cryptography: ^2.0.2 diff --git a/frontend/src/store.js b/frontend/src/store.js index 454322d5..f61615dd 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -202,6 +202,7 @@ export default new Vuex.Store({ console.log('decoded', signedAttempt) var string = new TextDecoder().decode(signedAttempt) + console.log('in string', string) context.commit('setSignedSignAttempt', data) console.log(data) diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index 180466a0..cb5d35d0 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -95,6 +95,8 @@ export default { if (data) { var union = '?' + console.log('redirect url: ') + console.log(this.redirectUrl) if (this.redirectUrl.indexOf('?') >= 0) { union = '&' } @@ -116,19 +118,14 @@ export default { console.log('Changing href: ', url) window.location.href = url } + } else { + console.log('Val was null') } + } catch (e) { + console.log('Something went wrong ... ', e) } - else - { - console.log('Val was null') - } - } catch (e) { - console.log('Something went wrong ... ', e) } + }, + cancelLoginUp (val) { } } -, -cancelLoginUp(val) -{ -} -} From 50b99e8d45a915e8f752939480b09e6c2804ea03 Mon Sep 17 00:00:00 2001 From: Lennert Date: Fri, 11 Feb 2022 09:47:15 +0100 Subject: [PATCH 20/75] WIP --- app/android/app/build_local | 2 +- app/android/app/build_production | 2 +- app/android/app/build_staging | 2 +- app/android/app/build_testing | 2 +- app/lib/helpers/globals.dart | 4 +- app/lib/models/sign.dart | 13 +- app/lib/screens/home_screen.dart | 5 +- app/lib/screens/main_screen.dart | 23 +-- app/lib/screens/sign_screen.dart | 256 ++++++++++++++++++----------- app/lib/services/3bot_service.dart | 8 +- app/pubspec.yaml | 1 + backend/routes/crypt.py | 2 +- backend/routes/users.py | 100 +++++++++++ frontend/src/store.js | 14 +- frontend/src/views/Sign/sign.js | 9 +- 15 files changed, 317 insertions(+), 126 deletions(-) diff --git a/app/android/app/build_local b/app/android/app/build_local index 74f78484..ed6ba2ad 100644 --- a/app/android/app/build_local +++ b/app/android/app/build_local @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/app/android/app/build_production b/app/android/app/build_production index ea1ef6ba..a0bd05bf 100644 --- a/app/android/app/build_production +++ b/app/android/app/build_production @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/app/android/app/build_staging b/app/android/app/build_staging index 9ffc7e9a..be2c6bb6 100644 --- a/app/android/app/build_staging +++ b/app/android/app/build_staging @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 ndkVersion "20.1.5948944" sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/app/android/app/build_testing b/app/android/app/build_testing index b6a08b99..5781dc73 100644 --- a/app/android/app/build_testing +++ b/app/android/app/build_testing @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index d37160c7..4deee8a2 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -13,9 +13,9 @@ class NoAnimationTabController extends TabController { @override void animateTo(int value, - {Duration duration = kTabScrollDuration, Curve curve = Curves.ease}) { + {Duration? duration = kTabScrollDuration, Curve curve = Curves.ease}) { super.animateTo(value, - duration: const Duration(milliseconds: 0), curve: curve); + duration: const Duration(milliseconds: 1000), curve: curve); } } diff --git a/app/lib/models/sign.dart b/app/lib/models/sign.dart index d72ad34b..32d28f7a 100644 --- a/app/lib/models/sign.dart +++ b/app/lib/models/sign.dart @@ -13,6 +13,7 @@ class Sign { String? type; String? randomRoom; String? redirectUrl; + String? state; Sign({ this.doubleName, @@ -22,7 +23,8 @@ class Sign { this.appId, this.type, this.randomRoom, - this.redirectUrl + this.redirectUrl, + this.state }); Sign.fromJson(Map json) @@ -34,7 +36,8 @@ class Sign { isJson = json['isJson'] as bool?, type = json['type'], randomRoom = json['randomRoom'], - redirectUrl = json['redirectUrl']; + redirectUrl = json['redirectUrl'], + state = json['state']; Map toJson() => { 'doubleName' : doubleName, @@ -44,7 +47,8 @@ class Sign { 'appId': appId, 'type' : type, 'randomRoom' : randomRoom, - 'redirectUrl' : redirectUrl + 'redirectUrl' : redirectUrl, + 'state' : state }; static Future createAndDecryptSignObject(dynamic data) async { @@ -62,6 +66,9 @@ class Sign { decryptedSignAttemptMap['type'] = data['type']; signData = Sign.fromJson(decryptedSignAttemptMap); + + print('This is the signData'); + print(signData); } else { signData = Sign.fromJson(data); } diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 4476c280..16aed796 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -50,10 +50,12 @@ class _HomeScreenState extends State final int pinCheckTimeout = 60000 * 5; _HomeScreenState() { + print('YO'); + print(mounted); globals.tabController = NoAnimationTabController( initialIndex: 0, length: Globals().router.routes.length, vsync: this); - //Events().onEvent(FfpBrowseEvent().runtimeType, activateFfpTab); globals.tabController.addListener(_handleTabSelection); + print('YO 2'); } void checkPinAndNavigateIfSuccess(int indexIfAuthIsSuccess) async { @@ -118,6 +120,7 @@ class _HomeScreenState extends State @override void initState() { + print('INITIALIZATION'); super.initState(); initUniLinks(); diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index 14c9bfa0..1347baac 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -31,6 +31,7 @@ class MainScreen extends StatefulWidget { class _AppState extends State { StreamSubscription? _sub; String? initialLink; + // FirebaseNotificationListener _listener; late BackendConnection _backendConnection; @@ -84,7 +85,8 @@ class _AppState extends State { InitScreen init = InitScreen(); bool accepted = false; while (!accepted) { - accepted = !(await Navigator.push(context, MaterialPageRoute(builder: (context) => init)) == null); + accepted = + !(await Navigator.push(context, MaterialPageRoute(builder: (context) => init)) == null); } } @@ -102,22 +104,21 @@ class _AppState extends State { if (_sub != null) { _sub?.cancel(); } - await Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => HomeScreen(initialLink: initialLink, backendConnection: _backendConnection))); + builder: (context) => + HomeScreen(initialLink: initialLink, backendConnection: _backendConnection))); } checkInternetConnection() async { try { - final List result = - await InternetAddress.lookup('google.com').timeout(Duration(seconds: Globals().timeOutSeconds)); + final List result = await InternetAddress.lookup('google.com') + .timeout(Duration(seconds: Globals().timeOutSeconds)); if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { print('Connected to the internet'); } - } on TimeoutException catch (_) { print(_); CustomDialog dialog = CustomDialog( @@ -150,8 +151,8 @@ class _AppState extends State { if (AppConfig().environment != Environment.Local) { try { String baseUrl = AppConfig().baseUrl(); - final List result = - await InternetAddress.lookup('$baseUrl').timeout(Duration(seconds: Globals().timeOutSeconds)); + final List result = await InternetAddress.lookup('$baseUrl') + .timeout(Duration(seconds: Globals().timeOutSeconds)); if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { print('connected to the internet'); } @@ -206,7 +207,8 @@ class _AppState extends State { } } on Exception catch (_) { CustomDialog dialog = CustomDialog( - title: "Oops", description: "Something went wrong. Please contact support if this issue persists."); + title: "Oops", + description: "Something went wrong. Please contact support if this issue persists."); await dialog.show(context); if (Platform.isAndroid) { SystemNavigator.pop(); @@ -220,7 +222,8 @@ class _AppState extends State { try { if (!await isAppUpToDate()) { CustomDialog dialog = CustomDialog( - title: "Update required", description: "The app is outdated. Please, update it to the latest version."); + title: "Update required", + description: "The app is outdated. Please, update it to the latest version."); await dialog.show(context); if (Platform.isAndroid) { diff --git a/app/lib/screens/sign_screen.dart b/app/lib/screens/sign_screen.dart index b6462e2e..99d204d9 100644 --- a/app/lib/screens/sign_screen.dart +++ b/app/lib/screens/sign_screen.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; +import 'package:flutter_json_viewer/flutter_json_viewer.dart'; +import 'package:http/http.dart' as http; +import 'package:http/http.dart'; import 'package:flutter/material.dart'; -import 'package:threebotlogin/events/events.dart'; -import 'package:threebotlogin/events/pop_all_login_event.dart'; import 'package:threebotlogin/helpers/block_and_run_mixin.dart'; import 'package:threebotlogin/helpers/download_helper.dart'; import 'package:threebotlogin/models/sign.dart'; @@ -25,9 +27,57 @@ class _SignScreenState extends State with BlockAndRunMixin { String updateMessage = ''; bool isBusy = false; + bool isDataLoading = true; + Map urlData = {}; + String? errorMessage; + @override void initState() { super.initState(); + WidgetsBinding.instance?.addPostFrameCallback((_) => fetchNecessaryData()); + } + + void fetchNecessaryData() async { + if (widget.signData.isJson == false) { + isDataLoading = false; + return; + } + + try { + Uri url = Uri.parse(widget.signData.dataUrl!); + Response r = await http.get(url); + + urlData = json.decode(r.body); + isDataLoading = false; + setState(() {}); + } + catch(e) { + errorMessage = 'Failed to load data'; + setState(() {}); + } + } + + Widget jsonLayoutContainer() { + Uri url = Uri.parse(widget.signData.dataUrl!); + + var dataObject; + + try { + var r = http.get(url).then((value) { + dataObject = value.body; + var testObject = json.decode(dataObject); + return Container( + child: JsonViewer(testObject), + ); + }); + } + catch(e) { + return Container( + child: Text('Failed to load the JSON data') + ); + } + + return Container(); } @override @@ -37,111 +87,90 @@ class _SignScreenState extends State with BlockAndRunMixin { key: _scaffoldKey, appBar: new AppBar( // automaticallyImplyLeading: false, - backgroundColor: Theme - .of(context) - .primaryColor, + backgroundColor: Theme.of(context).primaryColor, title: new Text("Sign"), ), - body: Column( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery - .of(context) - .size - .height * 0.8, - maxHeight: MediaQuery - .of(context) - .size - .height * 0.8, - minWidth: MediaQuery - .of(context) - .size - .width, - maxWidth: MediaQuery - .of(context) - .size - .width, - ), - child: Column( - children: [ - Container( - padding: EdgeInsets.all(20), - child: Column( - children: [ - RichText( - textAlign: TextAlign.center, - text: new TextSpan( - style: new TextStyle( - fontSize: 15.0, - color: Colors.black, - ), - children: [ - TextSpan(children: [ - new TextSpan( - text: widget.signData.appId!, - style: TextStyle(fontWeight: FontWeight.bold)), - new TextSpan( - text: - ' wants you to sign a data document. The hash of the document is: \n \n'), - new TextSpan( - text: widget.signData.hashedDataUrl! + '\n \n \n', - style: TextStyle(fontSize: 10)), - new TextSpan( - text: 'You can download the document for review here') - ]), - ]), - ), - SizedBox(height: 10), - Text(widget.signData.dataUrl!), - SizedBox(height: 20), - ElevatedButton( - onPressed: () async { - isBusy = true; - updateMessage = 'Verifying hash.. '; - setState(() {}); - verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); - - updateMessage = 'Downloading and opening file.. '; - setState(() {}); - await openFile(url: widget.signData.dataUrl!, fileName: 'test.pdf'); - updateMessage = ''; - isBusy = false; - setState(() {}); - }, - child: Text('Download')), - SizedBox( - height: 10, - ), - isBusy == true - ? Transform.scale( - scale: 0.5, - child: CircularProgressIndicator(), - ) - : Container(), - SizedBox( - height: 10, - ), - Text( - updateMessage, - style: TextStyle(color: Colors.orange), - ), - ElevatedButton(onPressed: () async { + body: Container( + child: Column( + children: [ + Container( + padding: EdgeInsets.all(20), + child: Column( + children: [ + RichText( + textAlign: TextAlign.center, + text: new TextSpan( + style: new TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + children: [ + TextSpan(children: [ + new TextSpan( + text: widget.signData.appId!, + style: TextStyle(fontWeight: FontWeight.bold)), + new TextSpan( + text: + ' wants you to sign a data document. The URL of the document is: \n \n'), + new TextSpan( + text: widget.signData.dataUrl! + '\n \n \n', + style: TextStyle(fontSize: 14)), + ]), + ]), + ), + isDataLoading == true ? loadContainer() : dataContainer(), + Text(errorMessage ? ), + // SizedBox(height: 10), + // Text(widget.signData.dataUrl!), + // SizedBox(height: 20), + // ElevatedButton( + // onPressed: () async { + // isBusy = true; + // updateMessage = 'Verifying hash.. '; + // setState(() {}); + // verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); + // + // updateMessage = 'Downloading and opening file.. '; + // setState(() {}); + // await openFile(url: widget.signData.dataUrl!, fileName: 'test.pdf'); + // updateMessage = ''; + // isBusy = false; + // setState(() {}); + // }, + // child: Text('Download')), + // SizedBox( + // height: 10, + // ), + // isBusy == true + // ? Transform.scale( + // scale: 0.5, + // child: CircularProgressIndicator(), + // ) + // : Container(), + // SizedBox( + // height: 10, + // ), + // Text( + // updateMessage, + // style: TextStyle(color: Colors.orange), + // ), + ElevatedButton( + onPressed: () async { String randomRoom = widget.signData.randomRoom!; String appId = widget.signData.appId!; + String state = widget.signData.state!; Uint8List sk = await getPrivateKey(); String signedData = await signData(widget.signData.dataUrl!, sk); - await sendSignedData(randomRoom, signedData, appId); - }, child: Text('SIGN')) - ], - ), - ), - ], + await sendSignedData(state, randomRoom, signedData, appId); + }, + child: Text('SIGN')) + ], + ), ), - ) - ], + ], + ), ), ), onWillPop: () { @@ -152,6 +181,37 @@ class _SignScreenState extends State with BlockAndRunMixin { ); } + Widget dataContainer() { + if (widget.signData.isJson == true) { + return jsonContainer(); + } + return Container(); + } + + loadContainer() { + return new CircularProgressIndicator(); + } + + Widget jsonContainer() { + return RawScrollbar( + isAlwaysShown: true, + thumbColor: Theme.of(context).primaryColor, + thickness: 3, + child: Container( + color: Colors.white, + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * 0.4, + maxHeight: MediaQuery.of(context).size.height * 0.4, + minWidth: MediaQuery.of(context).size.width, + maxWidth: MediaQuery.of(context).size.width, + ), + child: SingleChildScrollView( + child: JsonViewer(urlData), + ), + ), + ); + } + cancelSignAttempt() async { String? doubleName = await getDoubleName(); // TODO: implement cancel diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index f6962542..2bc29b84 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -14,7 +14,7 @@ String threeBotApiUrl = AppConfig().threeBotApiUrl(); Map requestHeaders = {'Content-type': 'application/json'}; Future sendSignedData( - String socketRoom, String signedDataIdentifier, String appId) async { + String state, String socketRoom, String signedDataIdentifier, String appId) async { Uri url = Uri.parse('$threeBotApiUrl/signedSignDataAttempt'); print('Sending call: ${url.toString()}'); @@ -22,12 +22,14 @@ Future sendSignedData( Uint8List sk = await getPrivateKey(); String encodedBody = jsonEncode({ - 'signedSignAttempt': await signData( + 'signedAttempt': await signData( json.encode({ + 'signedState': state, 'signedOn': timestamp, 'randomRoom': socketRoom, 'appId': appId, - 'signedData': signedDataIdentifier + 'signedData': signedDataIdentifier, + 'doubleName' : await getDoubleName() }), sk), 'doubleName': await getDoubleName() diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7b91e8f6..07d5984d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: uni_links: ^0.5.1 pbkdf2ns: ^0.0.2 qr_code_scanner: ^0.6.1 + flutter_json_viewer: ^1.0.1 permission_handler: ^8.3.0 path_provider: ^2.0.2 diff --git a/backend/routes/crypt.py b/backend/routes/crypt.py index 212e764d..f4559800 100644 --- a/backend/routes/crypt.py +++ b/backend/routes/crypt.py @@ -41,7 +41,7 @@ def sign_data_attempt_handler(): data = request.get_json() double_name = data["doubleName"].lower() - verified_data = verify_signed_data(double_name, data["signedSignAttempt"]) + verified_data = verify_signed_data(double_name, data["signedAttempt"]) if not verified_data: return Response("Missing signature", status=400) diff --git a/backend/routes/users.py b/backend/routes/users.py index 9f1c5ef2..5d75009c 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -188,6 +188,106 @@ def set_phone_verified_handler(doublename): return Response("Ok") +@ api_users.route("testing", methods=["get"]) +def testing(): + + user = '''{ + "ParaString": "www.apigj.com", + "ParaObject": { + "ObjectType": "Api", + "ObjectName": "Manager", + "ObjectId": "Code", + "FatherId": "Generator" + }, + "ParaLong": 6222123188092928, + "ParaInt": 5303, + "ParaFloat": -268311581425.664, + "ParaBool": false, + "ParaArrString": [ + "easy", + "fast" + ], + "ParaArrObj": [ + { + "SParaString": "Work efficiently long words long words long words long words long words long words long words long words long words long words long words long words long words ", + "SParaLong": 7996655703949312, + "SParaInt": 8429, + "SParaFloat": -67669103057.3056, + "SParaBool": false, + "SParaArrString": [ + "har", + "zezbehseh" + ], + "SParaArrLong": [ + 6141464276893696, + 2096646955466752 + ], + "SParaArrInt": [ + 1601, + 757 + ], + "SParaArrFloat": [ + -643739466439.0656, + -582978647149.7728 + ], + "SParaArrBool": [ + false, + false + ] + }, + { + "SParaString": "Let's go", + "SParaLong": 641970970034176, + "SParaInt": 37, + "SParaFloat": 556457726574.592, + "SParaBool": false, + "SParaArrString": [ + "miw", + "aweler" + ], + "SParaArrLong": [ + 3828767638159360, + 7205915801419776 + ], + "SParaArrInt": [ + 1187, + 6397 + ], + "SParaArrFloat": [ + -744659811617.9968, + 494621489417.4208 + ], + "SParaArrBool": [ + true, + false + ] + } + ], + "ParaArrLong": [ + 7607846344589312, + 7840335854043136 + ], + "ParaArrInt": [ + 2467, + 1733 + ], + "ParaArrFloat": [ + 759502472845.7216, + -157877664743.424 + ], + "ParaArrBool": [ + true, + true + ] + }''' + + response = Response( + response=user, mimetype="application/json" + ) + + return response + + @ api_users.route("/change-email", methods=["POST"]) def change_email_for_user(): body = request.get_json() diff --git a/frontend/src/store.js b/frontend/src/store.js index f61615dd..1e6c5e6d 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -194,11 +194,11 @@ export default new Vuex.Store({ }, async SOCKET_signedSignDataAttempt (context, data) { - console.log('signedSignDataAttempt', data.signedSignAttempt) + console.log('signedSignDataAttempt', data.signedAttempt) console.log('signedSignDataAttempt', data.doubleName) let publicKey = (await userService.getUserData(data.doubleName)).data.publicKey - var signedAttempt = await cryptoService.validateSignedAttempt(data.signedSignAttempt, publicKey) + var signedAttempt = await cryptoService.validateSignedAttempt(data.signedAttempt, publicKey) console.log('decoded', signedAttempt) var string = new TextDecoder().decode(signedAttempt) @@ -273,17 +273,25 @@ export default new Vuex.Store({ context.commit('setIsJson', data.isJson) context.commit('setHashedDataUrl', data.dataUrlHash) context.commit('setDataUrl', data.dataUrl) + context.commit('setRedirectUrl', data.redirectUrl) + context.commit('setState', data.state) + + console.log('THIS IS THE STATE') + console.log(data.state) + let publicKey = (await userService.getUserData(context.getters.doubleName)).data.publicKey let randomRoom = generateUUID() socketService.emit('leave', { 'room': context.getters.doubleName }) await context.dispatch('setRandomRoom', randomRoom) let encryptedSignAttempt = await cryptoService.encrypt(JSON.stringify({ + state: context.getters._state, doubleName: context.getters.doubleName, isJson: toBoolean(context.getters.isJson), dataUrlHash: context.getters.dataUrlHash, dataUrl: context.getters.dataUrl, appId: context.getters.appId, - randomRoom: randomRoom + randomRoom: randomRoom, + redirectUrl: context.getters.redirectUrl }), publicKey) socketService.emit('sign', { diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index cb5d35d0..e61ed95f 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -55,16 +55,23 @@ export default { async onSignIn () { const query = this.$route.query + console.log('QUERIES') + console.log(query) const appId = query.appId const dataHash = query.dataHash const dataUrl = query.dataUrl const isJson = query.isJson + const redirectUrl = query.redirectUrl + const state = query.state + await this.signDataUser({ doubleName: this.doubleName, appId: appId, isJson: isJson, dataUrlHash: dataHash, - dataUrl: dataUrl + dataUrl: dataUrl, + redirectUrl: redirectUrl, + state: state }) console.log('Done') }, From 0bc68179a2b360eabe856e3e26b9c8f2e1a71c53 Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 15 Feb 2022 10:21:05 +0100 Subject: [PATCH 21/75] Finished screens --- app/lib/helpers/download_helper.dart | 9 +- app/lib/screens/sign_screen.dart | 366 +++++++++++++++++++-------- 2 files changed, 264 insertions(+), 111 deletions(-) diff --git a/app/lib/helpers/download_helper.dart b/app/lib/helpers/download_helper.dart index c6e523d0..8c9c6853 100644 --- a/app/lib/helpers/download_helper.dart +++ b/app/lib/helpers/download_helper.dart @@ -5,12 +5,8 @@ import 'package:path_provider/path_provider.dart'; // https://www.youtube.com/watch?v=6tfBflFUO7s -Future openFile({required String url, String? fileName}) async { - final file = await downloadFile(url, fileName!); - - if(file == null) return; - - OpenFile.open(file.path); +Future openFile(File? file) async { + OpenFile.open(file?.path); } Future downloadFile(String url, String name) async { @@ -26,6 +22,7 @@ Future downloadFile(String url, String name) async { raf.writeFromSync(response.data); await raf.close(); return file; + } catch (e) { print(e); return null; diff --git a/app/lib/screens/sign_screen.dart b/app/lib/screens/sign_screen.dart index 99d204d9..b111ef38 100644 --- a/app/lib/screens/sign_screen.dart +++ b/app/lib/screens/sign_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_json_viewer/flutter_json_viewer.dart'; import 'package:http/http.dart' as http; @@ -12,6 +13,7 @@ import 'package:threebotlogin/models/sign.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; class SignScreen extends StatefulWidget { final Sign signData; @@ -27,6 +29,8 @@ class _SignScreenState extends State with BlockAndRunMixin { String updateMessage = ''; bool isBusy = false; + File? downloadedFile; + bool isDataLoading = true; Map urlData = {}; String? errorMessage; @@ -39,7 +43,9 @@ class _SignScreenState extends State with BlockAndRunMixin { void fetchNecessaryData() async { if (widget.signData.isJson == false) { + print('Coming here'); isDataLoading = false; + setState(() {}); return; } @@ -50,36 +56,13 @@ class _SignScreenState extends State with BlockAndRunMixin { urlData = json.decode(r.body); isDataLoading = false; setState(() {}); - } - catch(e) { + } catch (e) { errorMessage = 'Failed to load data'; + isDataLoading = false; setState(() {}); } } - Widget jsonLayoutContainer() { - Uri url = Uri.parse(widget.signData.dataUrl!); - - var dataObject; - - try { - var r = http.get(url).then((value) { - dataObject = value.body; - var testObject = json.decode(dataObject); - return Container( - child: JsonViewer(testObject), - ); - }); - } - catch(e) { - return Container( - child: Text('Failed to load the JSON data') - ); - } - - return Container(); - } - @override Widget build(BuildContext context) { return WillPopScope( @@ -94,80 +77,11 @@ class _SignScreenState extends State with BlockAndRunMixin { child: Column( children: [ Container( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * 0.85, + minWidth: MediaQuery.of(context).size.width * 0.85), padding: EdgeInsets.all(20), - child: Column( - children: [ - RichText( - textAlign: TextAlign.center, - text: new TextSpan( - style: new TextStyle( - fontSize: 15.0, - color: Colors.black, - ), - children: [ - TextSpan(children: [ - new TextSpan( - text: widget.signData.appId!, - style: TextStyle(fontWeight: FontWeight.bold)), - new TextSpan( - text: - ' wants you to sign a data document. The URL of the document is: \n \n'), - new TextSpan( - text: widget.signData.dataUrl! + '\n \n \n', - style: TextStyle(fontSize: 14)), - ]), - ]), - ), - isDataLoading == true ? loadContainer() : dataContainer(), - Text(errorMessage ? ), - // SizedBox(height: 10), - // Text(widget.signData.dataUrl!), - // SizedBox(height: 20), - // ElevatedButton( - // onPressed: () async { - // isBusy = true; - // updateMessage = 'Verifying hash.. '; - // setState(() {}); - // verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); - // - // updateMessage = 'Downloading and opening file.. '; - // setState(() {}); - // await openFile(url: widget.signData.dataUrl!, fileName: 'test.pdf'); - // updateMessage = ''; - // isBusy = false; - // setState(() {}); - // }, - // child: Text('Download')), - // SizedBox( - // height: 10, - // ), - // isBusy == true - // ? Transform.scale( - // scale: 0.5, - // child: CircularProgressIndicator(), - // ) - // : Container(), - // SizedBox( - // height: 10, - // ), - // Text( - // updateMessage, - // style: TextStyle(color: Colors.orange), - // ), - ElevatedButton( - onPressed: () async { - String randomRoom = widget.signData.randomRoom!; - String appId = widget.signData.appId!; - String state = widget.signData.state!; - - Uint8List sk = await getPrivateKey(); - String signedData = await signData(widget.signData.dataUrl!, sk); - - await sendSignedData(state, randomRoom, signedData, appId); - }, - child: Text('SIGN')) - ], - ), + child: isDataLoading == true ? loadContainer() : mainLayout(), ), ], ), @@ -181,18 +95,225 @@ class _SignScreenState extends State with BlockAndRunMixin { ); } - Widget dataContainer() { - if (widget.signData.isJson == true) { - return jsonContainer(); + Widget mainLayout() { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + leadingText(), + widget.signData.isJson! ? jsonLayout() : fileLayout(), + ], + ), + signButton(), + ], + ); + } + + Widget leadingText() { + return RichText( + textAlign: TextAlign.center, + text: new TextSpan( + style: new TextStyle( + fontSize: 15.0, + color: Colors.black, + ), + children: [ + TextSpan(children: [ + new TextSpan( + text: widget.signData.appId!, style: TextStyle(fontWeight: FontWeight.bold)), + new TextSpan( + text: ' wants you to sign a data document. The URL of the document is: \n \n'), + new TextSpan( + text: widget.signData.dataUrl! + '\n', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + ]), + ]), + ); + } + + Widget jsonLayout() { + try { + if (errorMessage == null) { + return jsonDataView(); + } + + return Container( + child: Text( + 'Failed to load the data', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red, fontSize: 15), + )); + } catch (e) { + return Container( + child: Text( + 'Failed to parse the data', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red, fontSize: 15), + )); } - return Container(); } - loadContainer() { - return new CircularProgressIndicator(); + Widget fileLayout() { + return Container( + child: Column( + children: [ + SizedBox( + height: 40, + ), + Text( + 'You can download the document for review here', + style: TextStyle(fontWeight: FontWeight.w600), + ), + SizedBox( + height: 15, + ), + downloadButton(), + SizedBox( + height: 15, + ), + isBusy ? CircularProgressIndicator() : Container(), + isBusy + ? SizedBox( + height: 10, + ) + : Container(), + Text( + updateMessage, + style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold), + ) + ], + ), + ); + } + + Widget signButton() { + return Container( + child: Column( + children: [ + SizedBox(height: 20), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: Size.fromHeight(50), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.assignment_turned_in_outlined), + Padding(padding: EdgeInsets.only(left: 20)), + Text( + 'SIGN', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold), + ) + ], + ), + onPressed: () async { + if (errorMessage != null) { + return await areYouSure(); + } + + String randomRoom = widget.signData.randomRoom!; + String appId = widget.signData.appId!; + String state = widget.signData.state!; + + Uint8List sk = await getPrivateKey(); + String signedData = await signData(widget.signData.dataUrl!, sk); + + await sendSignedData(state, randomRoom, signedData, appId); + Navigator.pop(context); + }, + ) + ], + ), + ); + } + + Widget downloadButton() { + return Container( + width: MediaQuery.of(context).size.width * 0.9, + child: Column( + children: [ + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.white), + padding: MaterialStateProperty.all(EdgeInsets.all(12))), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + downloadedFile != null ? Icons.remove_red_eye_outlined : Icons.download, + color: Colors.grey, + ), + Padding(padding: EdgeInsets.only(left: 20)), + Text( + downloadedFile != null ? 'OPEN FILE' : 'DOWNLOAD FILE', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.grey), + ) + ], + ), + onPressed: () async { + if (downloadedFile != null) { + updateMessage = 'Opening file.. '; + setState(() {}); + await openFile(downloadedFile); + updateMessage = ''; + setState(() {}); + return; + } + + isBusy = true; + updateMessage = 'Verifying hash.. '; + setState(() {}); + verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); + + updateMessage = 'Downloading file.. '; + setState(() {}); + + String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); + + try { + downloadedFile = await downloadFile(widget.signData.dataUrl!, 'tfc-$timestamp.pdf'); + + if(downloadedFile == null) { + updateMessage = 'Failed to download the file'; + isBusy = false; + setState(() {}); + return; + } + + updateMessage = ''; + isBusy = false; + setState(() {}); + } catch (e) { + updateMessage = 'Failed to download the file'; + setState(() {}); + } + }, + ) + ], + ), + ); + } + + Widget loadContainer() { + return Center( + child: Container( + constraints: BoxConstraints(minHeight: MediaQuery.of(context).size.height * 0.8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 20), + Text( + 'Loading ...', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ) + ], + ), + ), + ); } - Widget jsonContainer() { + Widget jsonDataView() { return RawScrollbar( isAlwaysShown: true, thumbColor: Theme.of(context).primaryColor, @@ -212,6 +333,41 @@ class _SignScreenState extends State with BlockAndRunMixin { ); } + Future areYouSure() async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext customContext) => CustomDialog( + image: Icons.warning, + title: "Are you sure", + description: + "Are you sure you want to sign the data, even if the data has been failed to load?", + actions: [ + FlatButton( + child: new Text("No"), + onPressed: () { + Navigator.pop(customContext); + }, + ), + FlatButton( + child: new Text("Yes"), + onPressed: () async { + String randomRoom = widget.signData.randomRoom!; + String appId = widget.signData.appId!; + String state = widget.signData.state!; + + Uint8List sk = await getPrivateKey(); + String signedData = await signData(widget.signData.dataUrl!, sk); + + await sendSignedData(state, randomRoom, signedData, appId); + Navigator.pop(customContext); + }, + ), + ], + ), + ); + } + cancelSignAttempt() async { String? doubleName = await getDoubleName(); // TODO: implement cancel From 788f16f09a4618ce3898bd9de8dd0392193c8d02 Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 15 Feb 2022 16:35:38 +0100 Subject: [PATCH 22/75] Finished signing --- app/lib/events/pop_all_sign_event.dart | 5 ++ app/lib/helpers/download_helper.dart | 5 ++ app/lib/screens/home_screen.dart | 3 -- app/lib/screens/sign_screen.dart | 64 ++++++++++++++++++++++---- app/lib/services/3bot_service.dart | 7 +++ app/lib/services/socket_service.dart | 7 +-- app/lib/widgets/login_dialogs.dart | 19 ++++++++ backend/routes/users.py | 9 ++++ frontend/src/store.js | 45 +++++++++++++++++- frontend/src/views/Sign/sign.html | 18 +++++++- frontend/src/views/Sign/sign.js | 37 +++++++++++---- 11 files changed, 190 insertions(+), 29 deletions(-) create mode 100644 app/lib/events/pop_all_sign_event.dart diff --git a/app/lib/events/pop_all_sign_event.dart b/app/lib/events/pop_all_sign_event.dart new file mode 100644 index 00000000..1e13ae5f --- /dev/null +++ b/app/lib/events/pop_all_sign_event.dart @@ -0,0 +1,5 @@ +class PopAllSignEvent { + String emitCode; + + PopAllSignEvent(this.emitCode); +} diff --git a/app/lib/helpers/download_helper.dart b/app/lib/helpers/download_helper.dart index 8c9c6853..6c4d1c7a 100644 --- a/app/lib/helpers/download_helper.dart +++ b/app/lib/helpers/download_helper.dart @@ -28,3 +28,8 @@ Future downloadFile(String url, String name) async { return null; } } + + +String extractFileName(String url) { + return url.split('/').last; +} \ No newline at end of file diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 16aed796..7cc17b71 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -50,12 +50,9 @@ class _HomeScreenState extends State final int pinCheckTimeout = 60000 * 5; _HomeScreenState() { - print('YO'); - print(mounted); globals.tabController = NoAnimationTabController( initialIndex: 0, length: Globals().router.routes.length, vsync: this); globals.tabController.addListener(_handleTabSelection); - print('YO 2'); } void checkPinAndNavigateIfSuccess(int indexIfAuthIsSuccess) async { diff --git a/app/lib/screens/sign_screen.dart b/app/lib/screens/sign_screen.dart index b111ef38..47178c74 100644 --- a/app/lib/screens/sign_screen.dart +++ b/app/lib/screens/sign_screen.dart @@ -6,13 +6,16 @@ import 'package:flutter_json_viewer/flutter_json_viewer.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import 'package:threebotlogin/events/events.dart'; import 'package:flutter/material.dart'; +import 'package:threebotlogin/events/pop_all_sign_event.dart'; import 'package:threebotlogin/helpers/block_and_run_mixin.dart'; import 'package:threebotlogin/helpers/download_helper.dart'; import 'package:threebotlogin/models/sign.dart'; import 'package:threebotlogin/services/3bot_service.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; +import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; class SignScreen extends StatefulWidget { @@ -34,10 +37,12 @@ class _SignScreenState extends State with BlockAndRunMixin { bool isDataLoading = true; Map urlData = {}; String? errorMessage; + String emitCode = randomString(10); @override void initState() { super.initState(); + Events().onEvent(PopAllSignEvent("").runtimeType, close); WidgetsBinding.instance?.addPostFrameCallback((_) => fetchNecessaryData()); } @@ -105,11 +110,31 @@ class _SignScreenState extends State with BlockAndRunMixin { widget.signData.isJson! ? jsonLayout() : fileLayout(), ], ), - signButton(), + Column( + children: [ + signButton(), + SizedBox(height: 5,), + wasNotMeButton() + ], + ) ], ); } + Widget wasNotMeButton() { + return FlatButton( + child: Text( + "It wasn\'t me - cancel", + style: TextStyle(fontSize: 16.0, color: Color(0xff0f296a)), + ), + onPressed: () { + cancelSignAttempt(); + Navigator.pop(context, false); + Events().emit(PopAllSignEvent(emitCode)); + }, + ); + } + Widget leadingText() { return RichText( textAlign: TextAlign.center, @@ -218,7 +243,9 @@ class _SignScreenState extends State with BlockAndRunMixin { String signedData = await signData(widget.signData.dataUrl!, sk); await sendSignedData(state, randomRoom, signedData, appId); - Navigator.pop(context); + + Navigator.pop(context, true); + Events().emit(PopAllSignEvent(emitCode)); }, ) ], @@ -262,15 +289,22 @@ class _SignScreenState extends State with BlockAndRunMixin { isBusy = true; updateMessage = 'Verifying hash.. '; setState(() {}); - verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); + bool verified = verifyHash(widget.signData.dataUrl!, widget.signData.hashedDataUrl!); + + if(verified == false) { + updateMessage = 'Could not verify hash '; + setState(() {}); + return; + } updateMessage = 'Downloading file.. '; setState(() {}); - String timestamp = new DateTime.now().millisecondsSinceEpoch.toString(); - try { - downloadedFile = await downloadFile(widget.signData.dataUrl!, 'tfc-$timestamp.pdf'); + + String fileName = extractFileName(widget.signData.dataUrl!); + + downloadedFile = await downloadFile(widget.signData.dataUrl!, fileName); if(downloadedFile == null) { updateMessage = 'Failed to download the file'; @@ -361,6 +395,7 @@ class _SignScreenState extends State with BlockAndRunMixin { await sendSignedData(state, randomRoom, signedData, appId); Navigator.pop(customContext); + Navigator.pop(context, true); }, ), ], @@ -368,9 +403,22 @@ class _SignScreenState extends State with BlockAndRunMixin { ); } + close(PopAllSignEvent e) { + if (e.emitCode == emitCode) { + return; + } + + if (!mounted) { + return; + } + + if (Navigator.canPop(context)) { + Navigator.pop(context, false); + } + } + cancelSignAttempt() async { String? doubleName = await getDoubleName(); - // TODO: implement cancel - // cancelSign(doubleName!); + cancelSign(doubleName!); } } diff --git a/app/lib/services/3bot_service.dart b/app/lib/services/3bot_service.dart index 2bc29b84..3cd85208 100644 --- a/app/lib/services/3bot_service.dart +++ b/app/lib/services/3bot_service.dart @@ -153,6 +153,13 @@ Future cancelLogin(doubleName) { return http.post(url, body: null, headers: requestHeaders); } +Future cancelSign(doubleName) { + Uri url = Uri.parse('$threeBotApiUrl/users/$doubleName/cancelSign'); + print('Sending call: ${url.toString()}'); + + return http.post(url, body: null, headers: requestHeaders); +} + Future getUserInfo(doubleName) { Uri url = Uri.parse('$threeBotApiUrl/users/$doubleName'); print('Sending call: ${url.toString()}'); diff --git a/app/lib/services/socket_service.dart b/app/lib/services/socket_service.dart index b7e8247d..10e7ca54 100644 --- a/app/lib/services/socket_service.dart +++ b/app/lib/services/socket_service.dart @@ -343,12 +343,8 @@ Future identityVerification(String reference) async { Future openSign(BuildContext ctx, Sign signData, BackendConnection backendConnection) async { - print('Open sign'); String? messageType = signData.type; - print(signData.toJson()); - print(messageType); - if (messageType == null || messageType != 'sign') { return; } @@ -368,7 +364,6 @@ Future openSign(BuildContext ctx, Sign signData, BackendConnection backendConnec return; } - print('Jahoo'); backendConnection.leaveRoom(signData.doubleName); @@ -385,7 +380,7 @@ Future openSign(BuildContext ctx, Sign signData, BackendConnection backendConnec } backendConnection.joinRoom(signData.doubleName); - await showLoggedInDialog(ctx); + await showSignedInDialog(ctx); } diff --git a/app/lib/widgets/login_dialogs.dart b/app/lib/widgets/login_dialogs.dart index 54cde94b..34d7c165 100644 --- a/app/lib/widgets/login_dialogs.dart +++ b/app/lib/widgets/login_dialogs.dart @@ -58,3 +58,22 @@ Future showLoggedInDialog(BuildContext ctx) async { ), ); } + +Future showSignedInDialog(BuildContext ctx) async { + await showDialog( + context: ctx, + builder: (BuildContext context) => CustomDialog( + image: Icons.check, + title: 'Successfully signed', + description: 'The data has been successfully signed. Please return to your browser.', + actions: [ + FlatButton( + child: Text('Ok'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); +} diff --git a/backend/routes/users.py b/backend/routes/users.py index 5d75009c..99b55cec 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -170,6 +170,15 @@ def cancel_login_attempt(doublename): return Response("Canceled by User") +@ api_users.route("//cancelSign", methods=["POST"]) +def cancel_sign_attempt(doublename): + logger.debug("/cancel %s", doublename) + user = db.get_user_by_double_name(doublename) + + sio.emit("cancelSign", {"scanned": True}, room=user["double_name"]) + return Response("Canceled Sign by User") + + @ api_users.route("//emailverified", methods=["post"]) def set_email_verified_handler(doublename): logger.debug("/emailverified from user %s", doublename) diff --git a/frontend/src/store.js b/frontend/src/store.js index 1e6c5e6d..93563bd2 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -32,6 +32,8 @@ export default new Vuex.Store({ }, scannedFlagUp: false, cancelLoginUp: false, + cancelSignUp: false, + signAttemptOnGoing: false, signedAttempt: null, firstTime: null, isMobile: false, @@ -71,6 +73,9 @@ export default new Vuex.Store({ setCancelLoginUp (state, cancelLoginUp) { state.cancelLoginUp = cancelLoginUp }, + setCancelSignUp (state, cancelSignUp) { + state.cancelSignUp = cancelSignUp + }, setSignedSignAttempt (state, signedSignAttempt) { state.signedSignAttempt = signedSignAttempt }, @@ -105,6 +110,9 @@ export default new Vuex.Store({ setRandomRoom (state, randomRoom) { state.randomRoom = randomRoom }, + setSignAttemptOnGoing (state, signAttemptOnGoing) { + state.signAttemptOnGoing = signAttemptOnGoing + }, resetTimer (state) { if (state.loginInterval !== undefined) { clearInterval(state.loginInterval) @@ -148,6 +156,9 @@ export default new Vuex.Store({ setAttemptCanceled (context, payload) { context.commit('setCancelLoginUp', payload) }, + setSignAttemptCanceled (context, payload) { + context.commit('setCancelSignUp', payload) + }, SOCKET_connect (context, payload) { console.log(`hi, connected with SOCKET_connect`) }, @@ -192,6 +203,11 @@ export default new Vuex.Store({ console.log('f') context.commit('setCancelLoginUp', true) }, + SOCKET_cancelSign (context) { + console.log('Cancel sign attempt') + context.commit('setCancelSignUp', true) + context.commit('setSignAttemptOnGoing', false) + }, async SOCKET_signedSignDataAttempt (context, data) { console.log('signedSignDataAttempt', data.signedAttempt) @@ -298,6 +314,8 @@ export default new Vuex.Store({ 'doubleName': context.getters.doubleName, 'encryptedSignAttempt': encryptedSignAttempt }) + + context.commit('setSignAttemptOnGoing', true) }, async loginUser (context, data) { console.log(`LoginUser`) @@ -350,6 +368,29 @@ export default new Vuex.Store({ context.commit('setRandomImageId') context.commit('setIsMobile', data.mobile) }, + async resendSignNotification (context) { + let publicKey = (await userService.getUserData(context.getters.doubleName)).data.publicKey + let randomRoom = generateUUID() + socketService.emit('leave', { 'room': context.getters.doubleName }) + await context.dispatch('setRandomRoom', randomRoom) + let encryptedSignAttempt = await cryptoService.encrypt(JSON.stringify({ + state: context.getters._state, + doubleName: context.getters.doubleName, + isJson: toBoolean(context.getters.isJson), + dataUrlHash: context.getters.dataUrlHash, + dataUrl: context.getters.dataUrl, + appId: context.getters.appId, + randomRoom: randomRoom, + redirectUrl: context.getters.redirectUrl + }), publicKey) + + socketService.emit('sign', { + 'doubleName': context.getters.doubleName, + 'encryptedSignAttempt': encryptedSignAttempt + }) + + context.commit('setSignAttemptOnGoing', true) + }, async resendNotification (context) { context.commit('setRandomImageId') @@ -530,6 +571,7 @@ export default new Vuex.Store({ redirectUrl: state => state.redirectUrl, scannedFlagUp: state => state.scannedFlagUp, cancelLoginUp: state => state.cancelLoginUp, + cancelSignUp: state => state.cancelSignUp, signedAttempt: state => state.signedAttempt, firstTime: state => state.firstTime, emailVerificationStatus: state => state.emailVerificationStatus, @@ -547,7 +589,8 @@ export default new Vuex.Store({ dataUrl: state => state.dataUrl, dataUrlHash: state => state.dataUrlHash, isJson: state => state.isJson, - signedSignAttempt: state => state.signedSignAttempt + signedSignAttempt: state => state.signedSignAttempt, + signAttemptOnGoing: state => state.signAttemptOnGoing } }) diff --git a/frontend/src/views/Sign/sign.html b/frontend/src/views/Sign/sign.html index a6eb69a0..0572e657 100644 --- a/frontend/src/views/Sign/sign.html +++ b/frontend/src/views/Sign/sign.html @@ -20,11 +20,26 @@

- + + + + + + refresh + + RESEND NOTIFICATION + + + @@ -36,6 +51,7 @@

+ diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index e61ed95f..b90747da 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -19,7 +19,8 @@ export default { v => v.length <= 50 || 'Name must be less than 50 characters.' ], url: '', - nameCheckerTimeOut: null + nameCheckerTimeOut: null, + isSignAttemptOnGoing: false } }, mounted () { @@ -31,11 +32,12 @@ export default { 'redirectUrl', 'firstTime', 'randomImageId', - 'cancelLoginUp', + 'cancelSignUp', '_state', 'scope', 'appId', - 'appPublicKey' + 'appPublicKey', + 'signAttemptOnGoing' ]) }, methods: { @@ -48,15 +50,17 @@ export default { 'setAppPublicKey', 'checkName', 'clearCheckStatus', - 'setAttemptCanceled', + 'setSignAttemptCanceled', 'setRandomRoom', - 'signDataUser' + 'signDataUser', + 'resendSignNotification' ]), + async triggerResendSignSocket () { + await this.resendSignNotification() + }, async onSignIn () { const query = this.$route.query - console.log('QUERIES') - console.log(query) const appId = query.appId const dataHash = query.dataHash const dataUrl = query.dataUrl @@ -73,7 +77,6 @@ export default { redirectUrl: redirectUrl, state: state }) - console.log('Done') }, checkNameAvailability () { this.clearCheckStatus() @@ -131,8 +134,22 @@ export default { } catch (e) { console.log('Something went wrong ... ', e) } + }, + cancelSignUp (val) { + console.log('CANCELED') + console.log(val) + var safeRedirectUri + if (this.redirectUrl[0] === '/') { + safeRedirectUri = this.redirectUrl + } else { + safeRedirectUri = '/' + this.redirectUrl + } + + var url = `//${this.appId}${safeRedirectUri}?error=CancelledByUser` + window.location.href = url + }, + signAttemptOnGoing (val) { + this.isSignAttemptOnGoing = val } - }, - cancelLoginUp (val) { } } From b82797944ce1a13ee6f761021044d79c078adf4c Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 15 Feb 2022 16:36:42 +0100 Subject: [PATCH 23/75] clean backend --- backend/routes/users.py | 100 ---------------------------------------- 1 file changed, 100 deletions(-) diff --git a/backend/routes/users.py b/backend/routes/users.py index 99b55cec..d3b123ef 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -197,106 +197,6 @@ def set_phone_verified_handler(doublename): return Response("Ok") -@ api_users.route("testing", methods=["get"]) -def testing(): - - user = '''{ - "ParaString": "www.apigj.com", - "ParaObject": { - "ObjectType": "Api", - "ObjectName": "Manager", - "ObjectId": "Code", - "FatherId": "Generator" - }, - "ParaLong": 6222123188092928, - "ParaInt": 5303, - "ParaFloat": -268311581425.664, - "ParaBool": false, - "ParaArrString": [ - "easy", - "fast" - ], - "ParaArrObj": [ - { - "SParaString": "Work efficiently long words long words long words long words long words long words long words long words long words long words long words long words long words ", - "SParaLong": 7996655703949312, - "SParaInt": 8429, - "SParaFloat": -67669103057.3056, - "SParaBool": false, - "SParaArrString": [ - "har", - "zezbehseh" - ], - "SParaArrLong": [ - 6141464276893696, - 2096646955466752 - ], - "SParaArrInt": [ - 1601, - 757 - ], - "SParaArrFloat": [ - -643739466439.0656, - -582978647149.7728 - ], - "SParaArrBool": [ - false, - false - ] - }, - { - "SParaString": "Let's go", - "SParaLong": 641970970034176, - "SParaInt": 37, - "SParaFloat": 556457726574.592, - "SParaBool": false, - "SParaArrString": [ - "miw", - "aweler" - ], - "SParaArrLong": [ - 3828767638159360, - 7205915801419776 - ], - "SParaArrInt": [ - 1187, - 6397 - ], - "SParaArrFloat": [ - -744659811617.9968, - 494621489417.4208 - ], - "SParaArrBool": [ - true, - false - ] - } - ], - "ParaArrLong": [ - 7607846344589312, - 7840335854043136 - ], - "ParaArrInt": [ - 2467, - 1733 - ], - "ParaArrFloat": [ - 759502472845.7216, - -157877664743.424 - ], - "ParaArrBool": [ - true, - true - ] - }''' - - response = Response( - response=user, mimetype="application/json" - ) - - return response - - @ api_users.route("/change-email", methods=["POST"]) def change_email_for_user(): body = request.get_json() From d6d2128a2106a1cd8fd79db475f538170b00922e Mon Sep 17 00:00:00 2001 From: Lennert Date: Tue, 15 Feb 2022 16:58:55 +0100 Subject: [PATCH 24/75] query validation --- frontend/src/views/Sign/sign.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/views/Sign/sign.js b/frontend/src/views/Sign/sign.js index b90747da..90c81e3d 100644 --- a/frontend/src/views/Sign/sign.js +++ b/frontend/src/views/Sign/sign.js @@ -24,6 +24,18 @@ export default { } }, mounted () { + const query = this.$route.query + + const appId = query.appId + const dataHash = query.dataHash + const dataUrl = query.dataUrl + const isJson = query.isJson + const redirectUrl = query.redirectUrl + const state = query.state + + if (!appId || !dataHash || !dataUrl || !isJson || !redirectUrl || !state) { + this.$router.push({ name: 'error' }) + } }, computed: { ...mapGetters([ From f2100440a4ab9b55096e32889e0f98571bac3368 Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 16 Feb 2022 11:14:14 +0100 Subject: [PATCH 25/75] fix staging configs pkid --- app/lib/app_config.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/app_config.dart b/app/lib/app_config.dart index dd02eb3f..f0e18ccf 100644 --- a/app/lib/app_config.dart +++ b/app/lib/app_config.dart @@ -133,7 +133,7 @@ class AppConfigStaging extends AppConfigImpl { } String pKidUrl() { - return 'https://pkid.staging.jimber.org/v1'; + return 'https://pkid.staging.jimber.io/v1'; } Map flagSmithConfig() { @@ -170,7 +170,7 @@ class AppConfigTesting extends AppConfigImpl { } String pKidUrl() { - return 'https://pkid.staging.jimber.org/v1'; + return 'https://pkid.staging.jimber.io/v1'; } Map flagSmithConfig() { From d0bdd4255de92c657037a18a8e0bee875bed4c5d Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 16 Feb 2022 12:20:49 +0100 Subject: [PATCH 26/75] Add verify signature --- app/lib/services/crypto_service.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/lib/services/crypto_service.dart b/app/lib/services/crypto_service.dart index 167102e0..935810d7 100644 --- a/app/lib/services/crypto_service.dart +++ b/app/lib/services/crypto_service.dart @@ -53,6 +53,19 @@ Future signData(String data, Uint8List sk) async { return base64.encode(signed); } +// Verify signed data +bool verifySignature(Uint8List signedMessage, Uint8List pk) { + try { + Uint8List data = Sodium.cryptoSignOpen(signedMessage, pk); + print(utf8.decode(data)); + return true; + } + catch(e) { + print(e); + return false; + } +} + // Encrypt given data encrypted with a keypair Future> encrypt(String data, Uint8List pk, Uint8List sk) async { Uint8List nonce = CryptoBox.randomNonce(); From 72c9d05e6a27bde4b6bd13ef1a9f319d847af177 Mon Sep 17 00:00:00 2001 From: Lennert Defauw <43674202+LennertDefauw1@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:43:26 +0100 Subject: [PATCH 27/75] Update README.md --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a76c7406..28e5d885 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,81 @@ # ThreeFold Connect -- Mobile app that serves as your main gateway to the ThreeFold Grid, ThreeFold products and services. -- Ultra secure 2FA authenticator. -- Completed with a XLM wallet to store your ThreeFold Tokens. -- Stay updated with ThreeFold News within the app. +## Introduction + +Threefold Connect is a mobile app that serves as your main gateway to the Threefold Grid, Threefold products and services. + +It has a ultra secure 2FA authenticator for authenticating through third party applications. + +Inside the app, you can manage your Threefold Tokens within a XLM wallet. + +## Features + +#### Threefold news + +Inside the app, there is a "News" section where you can find all the latest news of Threefold! + +#### Wallet + +In the Threefold Connect app, it is possible to manage your TFT tokens and managing your transaction history on the TF chain. + + +#### Farmers + +If you are in ownership of a Threefold node, you can manage your farm inside this tool. + +#### Support + +If you have Threefold related questions, we provide a support chat where we will answer the question as soon as possible! + +#### Planetary network + +It is possible to have a Yggdrasil IPv6 address by navigating to the Planetary network tab. There you can enable the Yggdrasil connection and your phone will be connected to the p2p network. + +#### Identity + +When you are using the secure 2FA authentication, some third party apps require certain information (eg. phone number). In this tab you can verify your email, phone number and identity to provide this data to the third party application. + +## Local development + +### External repositories + +Threefold News: https://github.com/threefoldtech/threefold_connect_news + +Wallet v3: https://github.com/threefoldtech/wallet-next + +Farmer: https://github.com/threefoldtech/wallet-next + +Support: https://github.com/threefoldtech/tfsupport + +## Frontend + +Make sure the correct configuration is inside config.js. After that start the frontend by doing: + +`yarn && yarn serve` + + +## Backend + +Go inside virtual environement: + +`source ./venv/bin/activate +` + +Start UWSGI backend: + +` uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file __main__.py --callable app -s 0.0.0.0:3030 +: 1643024584:0;uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file __main__.py --callable app -s 0.0.0.0:3030` + + +## App + +Make sure you have at least Flutter 2.8.1 installed. If everything is installed properly, execute the following commands: + +After this, copy the file in /lib/app_config_local.template into /lib/app_config_local.dart and change the configuration to your local IP's + +Afther that, use the build.sh script to set up the right environement + +`./build.sh --init && ./build.sh --switch --local` + +Connect your phone / start an emulator and everything should work properly. From 2138de08ea35283dde7896dabf080d11e60701d1 Mon Sep 17 00:00:00 2001 From: Lennert Defauw <43674202+LennertDefauw1@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:46:17 +0100 Subject: [PATCH 28/75] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28e5d885..db6cfc6b 100644 --- a/README.md +++ b/README.md @@ -71,9 +71,9 @@ Start UWSGI backend: Make sure you have at least Flutter 2.8.1 installed. If everything is installed properly, execute the following commands: -After this, copy the file in /lib/app_config_local.template into /lib/app_config_local.dart and change the configuration to your local IP's +Copy the file in /lib/app_config_local.template into /lib/app_config_local.dart and change the configuration to your local IP's -Afther that, use the build.sh script to set up the right environement +After that, use the build.sh script to set up the right environement `./build.sh --init && ./build.sh --switch --local` From 3fc14a980dee89abb500a40d71c3092814193bbf Mon Sep 17 00:00:00 2001 From: Ken De Moor <59569757+ken-de-moor@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:49:21 +0100 Subject: [PATCH 29/75] Update README.md English --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index db6cfc6b..f721f46c 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,37 @@ ## Introduction -Threefold Connect is a mobile app that serves as your main gateway to the Threefold Grid, Threefold products and services. +Threefold Connect is a mobile app that serves as your main gateway to the Threefold Grid and various other Threefold products and services. -It has a ultra secure 2FA authenticator for authenticating through third party applications. +It contains an ultra secure 2FA authenticator for authenticating through third party applications. -Inside the app, you can manage your Threefold Tokens within a XLM wallet. +Inside the app, you can manage your Threefold Tokens(TFT). ## Features #### Threefold news -Inside the app, there is a "News" section where you can find all the latest news of Threefold! +Inside the app, there is a "News" section where you can find all the latest Threefold news! #### Wallet -In the Threefold Connect app, it is possible to manage your TFT tokens and managing your transaction history on the TF chain. - +In the Threefold Connect app, it is possible to manage your TFT and view your transaction history on the TF chain. #### Farmers -If you are in ownership of a Threefold node, you can manage your farm inside this tool. +If you own a Threefold node, you can manage your farm here. #### Support -If you have Threefold related questions, we provide a support chat where we will answer the question as soon as possible! +If you have Threefold related questions, we provide a support chat where we will answer your questions as soon as possible! #### Planetary network -It is possible to have a Yggdrasil IPv6 address by navigating to the Planetary network tab. There you can enable the Yggdrasil connection and your phone will be connected to the p2p network. +It is possible to have a a planetary network IPv6 address. Here you can enable the planety network connection and your phone will automatically be connected to the p2p network. #### Identity -When you are using the secure 2FA authentication, some third party apps require certain information (eg. phone number). In this tab you can verify your email, phone number and identity to provide this data to the third party application. +When you are using the secure 2FA authentication, some third party apps require certain information (eg. phone number). In this tab you can verify your email, phone number and identity to provide this data to the third party application. This allows you total granular control over which data you choose to share or not share. ## Local development From 4cfc10c6b824cb83a2077565e81ca218fe5ea2c9 Mon Sep 17 00:00:00 2001 From: Lennert Date: Wed, 16 Feb 2022 19:28:36 +0100 Subject: [PATCH 30/75] Splash screen; --- .../org/jimber/threebotlogin/MainActivity.kt | 2 +- .../app/src/main/res/drawable-hdpi/splash.png | Bin 0 -> 13395 bytes .../app/src/main/res/drawable-mdpi/splash.png | Bin 0 -> 7343 bytes .../src/main/res/drawable-v21/background.png | Bin 0 -> 68 bytes .../res/drawable-v21/launch_background.xml | 15 +- .../src/main/res/drawable-xhdpi/splash.png | Bin 0 -> 18498 bytes .../src/main/res/drawable-xxhdpi/splash.png | Bin 0 -> 36321 bytes .../src/main/res/drawable-xxxhdpi/splash.png | Bin 0 -> 42760 bytes .../app/src/main/res/drawable/background.png | Bin 0 -> 68 bytes .../main/res/drawable/launch_background.xml | 15 +- .../app/src/main/res/values-v31/styles.xml | 17 ++ .../app/src/main/res/values/styles.xml | 3 +- app/assets/logo.png | Bin 18415 -> 39994 bytes .../BrandingImage.imageset/Contents.json | 23 ++ .../LaunchBackground.imageset/Contents.json | 21 ++ .../LaunchBackground.imageset/background.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/Contents.json | 26 +- .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 7343 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 18498 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 36321 bytes .../Runner/Base.lproj/LaunchScreen.storyboard | 19 +- app/ios/Runner/Info.plist | 164 +++++------ app/lib/helpers/flags.dart | 5 +- app/lib/screens/main_screen.dart | 266 ++++++++---------- app/lib/services/3bot_service.dart | 16 ++ app/pubspec.yaml | 7 + 26 files changed, 323 insertions(+), 276 deletions(-) create mode 100644 app/android/app/src/main/res/drawable-hdpi/splash.png create mode 100644 app/android/app/src/main/res/drawable-mdpi/splash.png create mode 100644 app/android/app/src/main/res/drawable-v21/background.png create mode 100644 app/android/app/src/main/res/drawable-xhdpi/splash.png create mode 100644 app/android/app/src/main/res/drawable-xxhdpi/splash.png create mode 100644 app/android/app/src/main/res/drawable-xxxhdpi/splash.png create mode 100644 app/android/app/src/main/res/drawable/background.png create mode 100644 app/android/app/src/main/res/values-v31/styles.xml create mode 100644 app/ios/Runner/Assets.xcassets/BrandingImage.imageset/Contents.json create mode 100644 app/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json create mode 100644 app/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png 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 ece5499c..c64d56e8 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 @@ -1,4 +1,4 @@ -package org.jimber.threebotlogin.local +package org.jimber.threebotlogin.staging import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine diff --git a/app/android/app/src/main/res/drawable-hdpi/splash.png b/app/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..2826abe833f5c86bf5b6c24a04afdb997ae98062 GIT binary patch literal 13395 zcmc(GV|OJ?ux@N8lT5T@+qNf~iEZ1-4kww|wr$(a#J0IJv5lMe{DiyKS?5c4f9bAT z)rF_3y2E}dNFgKOA%KB_ADm~!^&xXrkkU^A0MgAzdtyhzox1rzyN7K%$7zy}b+ z^@IJN%<-rM@$gES704tDv#JL2nut%1L_Lk3*$FcUEzBhO6ZA)>3zN4~9#0S?&%Ak= zewc`sQlx&U0SCtWY!Gvd(z7VZx=2cbLHjAh?_~}I=ZN(MI&T6*V(qa1C<@hMv)P`# zRwK9yc~QLe=w4H8PBU_)+1Xol5DPbBjWBrBkmG0X7f2{S%^m`J6nUkgQ@yLog*`Jy z-Hf}UwolOutz-Ms-gNCL5-nYGH@IkEX)^j9n_dOVOafI_`<(d=Z=7!uN2f;-DYp?B zMb!P}bzk=tJDp(U=58|s)dE%F%yuX6vS?q1GXJi07OZrJOKxe5lbEtxy)}oL-28d6 zR}&sH*=N(hJ@j{3?yK(*dr{ea;+8JJiZ0m9AfP6K{x*b-kE;>q3rUkWnxCP<%3~3s zh9K7dRMP|G$+PLCe991#K3L(GmO!S*_bXwY5cHQYB!n-+DO`~2R@;!&!`g_CO{Yi< zL;gz_`y#StC$;(sb8b=X;O#96=<2=9AMv?<;pqzu=m47&m!5Xvc8{O&3Qfg#P!Pq; zynY&)Neq82Cd{4%8KPSD9u+BeD#nCmu^(htRFS!W`x({*1|~5fyf{dEkwgW#thg4V zqXa}P!pE+O{4gE?U2pZG7wyGzit!@7#5LX4WL(D-JAX50tUrPr~t|HiuuDybI zAI&%fcdV@826_>oyo7^@eU6Q^;m!@1oQWF*kj|*N{UY+`Qb)-Mvr+UwOR?$Xopzk{ z%orLmzh4Js<}Mn2IFtNdO!pJ2n}eB=D*1U?fK$)L%ju$}#3EV)2jN@Pjy_RD30ZHo zRDAP0_3@B3A@j>(>C*?#7aC*;r2h9<+*}#*b10vj`(yn295=}olO!0>prCB>8+Sj- zX(~%_x){{d4Yv031l_>yo!Sp2Y56qz#woDzw74t0lS0-_!i65YSXMF57@qDi;#mkd zMgLp+t+sJvYui*5^MT!ORY`)L9Gt(nqZyuSII`&|6v?+u>Os~>2&GCx zkyW=zv9zosnLG|2*R7B_QWxvbZHyC+F{dGJzRj=+moie)6JEG|+vjxB4D6YZJp;Cs zkBum-t71%$Fza`Ybeo(eAF_G712k-d2dD=hU44nnLFR;d{SvD{f{Fk!HR)#bP@_AP zXA-FrH0~sa@a(|N&XhnVQsi=_wf7esrDFf8M@jKPO8VO!Z7S3fS>2IS6LcWsnPD?D zwhaB=w_Q93I{{$|RFvmEw3>q3-`wh9k3%qKULWYEA8oNGJQ`|F5CyeKzg1*O6g4Rk zD;bfMF*Fp#Q_sS%#9E2MfCQKq@CYSJ)9@)_cP1}9t2@WT_Hu1p!DIyA*PqH1OxyW9 zY*`X%qRCgn3!;(ZQ6iy|Yw<4{i@*cxsFA@*;}q%wenR?{WYYp2d^}sDP4f-*!~Ml3 z9()4yMvmY1Wl(}&F_maqC^1czGu*g8jqK&5XBaAozr!38JMX3V-n^yS7fw(^z zANr$NfmGRpy#Nk`Fe-TZfIf;uDD1A=>-2NyMq&(&0O{;f+vnhjBiGwtastsZlDl*Y zn8a7l3`uQ>hy`;e#fNQegkqBg(c!%Yi7zq$B)c9}xm|7;>Rb||#~t0I{p0J?<8~MA z(kgW+3$zeM~mqt z9o{Lzt`UM9u{FC99tYe&z(5va>e*pBmP5945Q-_t^u~@|2AYhVqWR*9FlqWd@)cCAljV*T8T)TB<*l$c!W`vHoPGpD_ChCG zP#?8G&#^(wT)Mor1#(VVut4@jm@c1JYGQ0W1j>k8(483R$ik7nJ!M^G;qUCD@3o@; zS|-d4o-f8YlJ@`o&+)_?up6lCbwymQ!g{CCbjztXYa0E}g=#!@CVxfmD2Lx~qU3&ofOt3fCBUsysB3^YQDsYeJNGDq;w@C2Ec zcz3J&7%8xC;(lvvI{U})to51n>cyCG(!S)Be|~E@GPswKcx{Vnbk9ry+W%Yu0IGd+ z+x3cnK9zuQM7Q_%XRGBS-Jhu#1wKFWYc`N8trt{6R#SXH$HUfQ^3DN!?O98zC+gBH zh?FN@@vyb)_4}vLf1Hq{^9W)c*=2OWvTA}k{^~?OrDpjV1laof-~c{{j*}F%*F70M z{cgH{>4CgX<6VuE0NTDkdGC>-Kn6zYP##3@WI7Wj@#EZ4OY8f0-HwpT-Z?8Gb|fL~ zHgh^q>L8*#cDH?CgcMk_0RP+%1{c>UO^qKocs%i}y{Zy1ioK|g+!CtuM0WrTcisnh zlb_SmX-BTJ+w6qfE|3K2a8+)O=V}*J4RX$3(cLrD*B!HoLkIf+H$5q+25~={I{JHSerEJTt`IrMXd-v7KVF>l$2&?$&o3|Nu^{;ZtKT+ z>>ty-W#*~ZV%5>*jIx?pd6^x!8MGw+)fmmwN# zbgjN36~eSm^nj|rGEeR{($G|$YmhKi8h;NCd4BAm9ZT|!OGh6BHKvt~j7fs~7OUrx z*Fwd43)?FaSY_tN!h)`AOYje{B0>vmlSc(Ki;CD!A& zk&GF2aF7;=*;UaB>K)sPC5tPY%AX?i09p;KwfXuBm}~@nL-ND~nMWhLH<>-0N4!vS zlb2)DGPBe!$F2H1{fe#S+OtE^|K17dm8unN?uhbj2+dDy!x2QEAjRT9$XC5jk3{3% z?}eqtU=#wF{FJR<4Eqz;eYV*K4=PyX%>U$S4btJlAGzw-_cS^iN(o7XRH7`lC4ed0> z4kzS8H7_0aV%_^hlKS`~8R>>BW;1`rD**)-LiOUHZ4UavpCP4p<$897MTxE+2Vj)6 zDeY&fn@KtV2H!_ub0v4Vk^l;~r$M&oHx;GL10mT(`t!qgA zB+P?^DArZ#qg>IWYUi?T@>MK&BO69+I?8SvH{_al!hYG!q+G1EQ7RwMfw1v*w zOz)EXOFX_8*!xQkU@M#_AqCK}OImoro&JNuM_oxo-s#EeAyAv=@6~Z(Sg)L{>~&3E z8_3qXD_0b2<4J!eIfAUcKAX2(1Q>@{=pKQ61l@|7L ze8eHJ8h#evNRo*G`#5g1S-4DG2|oJ8n;0^s-1-i%a}8bI!%L7@oXIoLj_t*mGJsP? z{dGi5fg~uz%1N?{sCFi4Qyuk9`ZBOE$qQyRKS%>7If@#3(;c?94@gPkGkUN^?}@g5 zRi`fs=nSS-`mXo{Bdb$OAi+T0y_HMde$zce_@#uzaE~xjGzzV2aHRjf$o!YZg8zTa zj76e7JDnR76^C{|yBo*%9veUffUdXu zp@fo=si{q))T*+ELPhO-nrt}m@JL1oUgeT{?f2tFO|Lc7SyaZe z{7QYrN!kMf)^nTE-}LxHH*Q8^*0YaYyMMg@nb(-?w0htz`tc&OHB^-e`W=y=;E;`= zCx7M9Z%>;IKXvguIGoi{d;0`5`vx2Ht;Rjdbr!oIf3Nr95`ppN*t9`mgOAhonNdJN z972{m2?-oXzWumGr_tq<+^$oLn$~%k?oC9gV(Yi;kokFQUs`ix;?G<0ivlLc>u0Cy zO!(<~OjTR=>(jLt6{Dn<(Or!3r`n;Brd9O0(!9}Lmhh8xT_82@o<06{X~Z-BWfysJ zfzt0cz1q;s)h^s`;_0r*larYnoz1dC&1gms7w>8fa zDPyG3?!jIFG5g_=#5Yj3-@d9Z^ZqB{>EY1Mi{Wr9#M!7s9hpmCcieUj%yp;d;}uUu z)y)kD?Ka}nbv7C2DYEzSCV8!ReZhG(3vR1)1K!rs!F87MdQuw3jL{U5)f}7sJA%N? zLdBH^^Yv(4Tg8A~C3j&&C>EnmYRbn^!tT#GbK>>xHzBKZDDH8})CDR5+0Bx*z!=gU z>lSU?75f^1TRF~TXa1z#?bw|ScH8ZI%`$g^T*2?lxsI1XyO#cd7e&V=2AZVi4pD6< zl5S&1em0QX_NAGBT>=GX+wfovW#6-!#?!c>8cg&;m3dYsu|EY86?3C`G=0$x6O?IX z6l!jCNw6g6IGAhS4ejQd9HzipCy`pir=XO=_j>D>pCnJhQxcS2ktUya_6^RI4^SZ? zdTfrIES=Mlz{jbLnYvVQ*`u^A8s?tsY8qHhXq0Yq8$+1>{hHn5*eqq#%2KxD*osLR z8K@A&Q!pG4R-J8vYsEysUx-O)&^g7?GCjHX%gr|tNWrHID`falAY5@G!Bn%FlgYKIK+$SxdU`c#st$|XWDX+PLl$+L z9viK69N!JD?8itmnn4$$-XGYx`p2PJd5pJYumgY`ccS}(K*r1&npYbDeUbAIc(#XK zA55m7!*<0Q*21rZm>CQed^)OfTJ$zFz#d&&DE1A<3}zy#l~k}4;ivMn{Lbj$v+Pwz ziSzDtB!;L4xkIl;jnvOgj4F4e7qw-efjPwXK10IQRx+dhL%Cx07KafmZcOfGG>yNg zM*mL036D|%6u(g?@$FH@`dQ}3b2%?$F>jepiYSEaarEl;@`ly>Q_+=hldHt@Pxv0| zIB&DUvDD^)3aRE6e>r{Xk{I=9+6s(J9vz64bXAQid{H-r^ZNX$+@WA>%42!p0k=zId(Z%LI=m~GUTlbKctqU{yuxg|hvAhMQi+JEzHQsTat!wHyXZSU5 z?s_T1yy$SF9>ebMf($ouNtCXNSe4SiHCOUb{BEa z+>5@);LvbwMEAhQ2V_H_jM~2#@|8b}0h-dCL@8YP>ch0dVt+_wlu1R6{3s}TFuni< zV`^uYWiBCdm6+25nn5!gTZuJZX=IHU@C057-M=J&RDw(Ym`-q}Gbmpi+x zoFefszy=+!;^a&=!_F=MMbgUoYHwn_zZ12D>B&v_AcqT!P8L>%K?!JEs$QeFdcNL5 z#bxS=na$|lEaPhSu-0-rzgnVeD;NJ;ZLQnC;ZlO2$HFUB!hE9J5q~${bS9n1F4ElL zQ(=qi?X{K)$6nXx{>suQViHv>J*7zH!>-9{k+DA==d?Fy0TVAWtP0ZC;~`|SYh~`L z=MxQ^+e4tUQgQ52I*a3v`@AWcV4%5%s(~WoD$Ds>Koz4l=~py77WKzgoqI06B^*l9 zuC>Qt2xOmhpKg+&8=h2gReQGHR`m|kP`2I)XYE?I8B?w~v&dNxBuqa(#OX ze#cCjQZ5C$`ZbIO0@-==L?T`-buyQqbOm}+Z=dF>Joqjf#=~>k{2u5Z{?=3@opBY| zOdn++G?%lWYduyqCBmB2Jj!p+u6s^i+&*n^kag4*%^{ZwDX(=^)NpVzK8MqTELmX= zUfVQ||2lzQV!RHloHx*G?#eVHRE@RzAeB@r{P;H>I~c13W|unXy)6u2zIb<%=|UFkPJs($v~|U98aY5fmC0Q zBiM=TnY;C-Bx2Gl)^1?h8Ek7h2njh)yPQwujL<^av{M9*nAdosSQ^Ye1{@`wty%D> z5J2Z{d1Nz;l^Cx~xP}V^)aNu4Kr%r$CXtisZ}=&hDtFYmP2;?O+`kBsG+931K3#A5 zmIYgDn1|8{dL33MyPaP~ln&$l zDah4yKd)fDDoulYN?pkv0<+VqKw@C+5A%BDbOom1p)n)VI8U4Q`MVg1(!7KUg%p3y z?X|Ii?i9IDS`j|+snpTU4Ba>_STf6ie(xd<@$wvBP_e~zE@o=mqine?p6tu%h%KZp z(7=M;Afr}O?9O<-3i4oR=0#;Gk4=4ZI6xem#*bS0MuY!!srbIxYVZo_G$4eh@g%%C zn*ovpKea1}^xFsvS{OcisMVOwgYvCU&H$=D=I3>z?z%MQ9~+$$})2$icH%}e(U(i zjmSA#gn{H8!IgC_sF`VvH@DBh+#i|g&D`a}S$hdxT|R0RprhviNw=}K_FM5y&$(u_ z%%o`x`4)Pq#l7bZ z1P#MJe~9H5v!jFId3PAY088e=H(A*klDAAtjRXW-Y{ED1?!9D_Xs4T3SA05O3@2>y zs=$1P%;!{R^Ai;N5MCt!&Y3%P1(Nc|$M>;n(-ErsnzO8g{wq$;Ni0 z!35uP)z^C$&m)7kKj=zroEC}?u=Q9$HpM(X{{=?e~1Fl@mRL5a;^FlJVa=aIC zIVmKs<7}NBkRN$f*p~_tQjW2QB*Ll$%!n!;CrggO_DWLYZsqj!{?+$(@I55oUrnff z79=Eh&zt;%)9KBSVDL+BEOxyg5JX(T=I^UoqBlCocBXr5W?CyArkHA7YYa5m^Vaka zPRh`MTW^@BfoVtoSgn`+SY>5jidWzBj+lM$p#4(hO$S4UD#y>i4NRnfHk&6J?O^(# z)aG7{6f&lb1qPQajG7A}QOBVo#4!l`8afD2^f|UhFKB2}YPwR#n-;zs?c=;igV@}1 z-gJz+MS41S7^fS>eKZqj1V`DaFsbRMc!xjJx^i!uR-PNOJYZ^O=kd{{r@7>Q~gQ6~A47>w0N3&&B1Z6m`EI1&UaO)rFRPrD^tq?vPQ zwLgl#*Pc>_igPw~2CXgr^)>4z%R&P5Aa9SYK_)|ogmC`0p8a)?nYqlg*nW+MrZMza zV=9n}9qFTT&BZSdTCC^(=KkOK0-i!94()u!VLj+Ed!gI;D)Ci(erKcNXyM|0H%oVa zd1FkWvXslAVsc;9!#awGzO~gL204)TKamN?P)8J+LZ~sPpm3y z3lA#vt~*%)yC%ESJxR4Rglxw{7+^T)q3vbM4i5vOP_{Ttl{Mb`()$m2B96TjSAt{f zAhoOTa|i=Z1e)Gva!ZwbbV_Dwdp~3%5^U-9bi&;`8xYZkE_a5Cd&8fGL$7A*m!S@d zDCA-CDtf10AJ%z`#*=?_?ZPiZd;@LXB+(+f-|-VyuW?!*^%p@PLgc8+BB>p>LeSX=uOh7&Q)tPUCa2bneI6z}Y+*~TR!i49cahpytCE|`jA7{@dnVB(!GkF6CC zOmXOOmUsCz-{G^HOvfby&qu#$6kkN?j%?Dx^%Y5U=&72@k1}X6{G8AZcgYucY_o* zp3H{E(n~Zsf4&uP1kTdEUzihid!A;!T@t-JHLY_CkaWCGwQX9NTlg?dJ@mEwjA+SF zOMMPVUN2tK)ApMYDEWRY+W1Vw`k0oY;Qv*}c^F7*<}ScczfI)8li&T^OO znFG~wINU2u-nqf`c!t&J+KB6w@`Ys2=QZ`4FISVKLow1J;?-Y$rjixw-5+1B_Z9st zt@)jzi2;WhiNuZtxeC}2It9rDisX`(bf6(x&Q<%VxW$_P_Bz5e6byY|Lr4Jdgkn^r zXnHHl&%Q_5;<-N?j;*`Z6LhGt?5NH4*M|*JWCHJMj!6qQNA+Gl;^0e3LvQwQ)Tc@V zAyg6=N0zd4$I`boy!iKe9@ue7{^x8_L8z(87m>AA8-9!5b1D4v*0EiS)8+culoj9J z-*;~`>8x9(kSqf&BaPir9?YJd;qDl^>eIuUQrO|d2UWfVgW?G} zy#)DVBzSs5u9=4R5wS6~uone?sYF{jPG zkYS{_suNsbj`Px`v?apwdvkKZ)CjonvNum$+nv=BB7=Gh+NCzolfI=mo`J=jC&w839OC9lhbhmpq*lqkzJ0(VcWu6*C_B|k}1n?g0m6c;yv`PIe(kP?X&jD6_(%X zQ?%dU@Xp$cx@vfhaINH;8C+p6)?+%DBGjP2d-!l7`c3;)%c8V2biN8~xR0SfOaC|= z$WdW)P8P6#{(XpwmuT@$Ps%Il0QyIFb9d)D{%0R2%BeCU2>+uf*+u-mSjvrgDy=SIe2A0K9rX8hFlWRoN&TN83I? z#Xm=#41_N2H_FzRGTm^`EuTWllR5G%0hEwMCJ0--s&y+P@OW1f1`j zayqpVP5D>(c&OhQ)01vnXQW~PxTa%%p-<=2)O{Y-Q-k2e^~T6|$UY)*f^NySGP^hJ zkTu?HPs_d~Vw-;q%F+|EWZ)kBl}AFde&5kr;jrTZ`dgGDT9bL5%K{2~=3YOUUaoJk z=Kngt{)fy$z3jFw4vDyaFc7H>uUZ|9vbuSN>5ye-x_%5()&TgkGkBGQ>kLBjZxMM- z4oe>uWI59q*v68-(wP(FZ#nJmRG^p^Z}!vNUv4X;^rfF)9o0M{*-Qq*@;;lk6IL{J z$A^(BbV?>?huPR($g7SLrHKiS^XA0Ll?~gahMlZ=H)qDCU*m|&C6<5F9anLnAK1Kk z`xA)$@5Nng3bx*FCu-Wpu-jl_EcB8pTk|G!-GyvhtgKtgsO}ns@$vtR2q2=M4%xPq zmmemjvUllv9B&pr_YD=gyoI16c=+aeErc5|i6Vy3bUOjOZ<@K@r z{dRadRMv*?$F$TeiAkB(VtE~_U}^H#QuFpHy1f^|GC$4L`JTYI$8s0eQ)W8kP`x|Y>_lj*U1+3jSYhF=|uQ3s50H3AU0AU)hV(aPe%5|0M(+7y$O};px?f?>v zmWmFlx2wI20rA)4iW3w;tWLT+FqOt8)yYJU0;jI@PnXA;G`BCX$b(fr!&e|=-R+=Z zn7#JMKToZqqhW4)j&?FfXcHZ!3-CJKoR_bNx^$9t4PA^1=WAb8i!ZZf zPuC&mhoc79_WWr{r=B80(@|!yY_O*KA};s#k~ulurFqg|jgGSM?-r}hyM{lCjN6qx zRqIIdeB0z!pf^L?T+oZayjC5e+xyv@@%%Z1qM{vO$`f@G&zZ?mYptx-J3o=<{}FJ|uP zDz23^=MKl}t8AYdYZRupS@+)*ch@rMtXR}4y|a|Skx!%dCMw|H4s*cXuV67#M77W1 zx|)jZK5fH%yn$K1RO{!N4_UE=*;W#cZ!x#w{}7MbSDZ#7d2q1p(jQ0Sv$&dk1ct-i zE7fUpmH+r(H8TwKV&L_{@vCzZ0YC2)4ec7bU0l1mLHKXX$G0xePPxZa>s#CVvMt#? zu|^#)nqB?7In`Ug`z^&kfMh2z66?HJry8HcZyqOP=$UCEs$Q@#*bKlI&&@cTgR|pP?Q(0ZCB&9#bbc{_Rv<{jpWv6?2o;<+;}3+3*~y*f>mwM#b-7lPk_J>j!x*4 zp{9qlqoS<6$Gts!iM`(L>1M`gfN6w_9%Xas_*&c{s3qG0QVa8Q6wUGICUycGIzuKC zG-qLMW}oo=ekvp?SWL^-u%>(cdiTuwr!zl}sJL^asab$nJ)Q%xOg7i1=x;!#r_t&@b?_b{XP7Yd^KTE@aqpZyH@@1D1^<0 z_wfXWV_g1>Rz_{OHt_HdDC)I;cpuEAf^TJ9Z%se@c|N?frbNpXj%_yAES*M5mM@E* zd@N)P1R}TR-UNqhou@UPt^JD^Jsr(7V-u%E=x{vUy$uHJI9}@PIEvYJWL_pt% ze`o9%J_J+Y5t%V0c`bqd&3(Sed~LUsz%cQ0a;9qSu>sLV{r-^cJ?`+ek)5A97K zGALCN4xzK&U4Rsy8xLnRc|}XI!Z8B z@m*~koa%O~v`~I7Z)Nq0J3RAR5G-#ySd(WuDs{pwp%yw zPbOSVgG^w<*@O_=<)jzT+e_cs6?jsycOf?9vO7m_zinrAdc3Twf~2+HU3!|k_Fi?i zQGKT`Z@D?TPCT<0RO-1nI7+t5 zS2x$YUd@~`4;p5Fa8W6@Gj?S{mD5F;OhFozza(!EKj`wJ#ZUNesSIKn}_a(xx8Rz8Q6 z&S~y&T%GcA-CelBoVUWbrna8E>-zQj9}6nbd+>&6aX~f1?K>ZdGB2a?)S6dQ+IyO<1m#ES%4FL0^G~Udk!eL zmEs7R$j!#Js$K98A1gq+>~He-w?IX<0%wz;aIV6vV{ zxz6blTw}I^mQxd&Xd=6XYZeKB%K~K@&Ns>8^9B4Vc%2yL&W*bLdG*KKL^(%SO@cZ< zlc#JJDdR1tgcSt!l%X^?R|5-KE(3; z4zWQ7irVT@Fgo3~rrp~4Lu+nspY?=UgDgJ!*|9c{c$S~T@YpWGtZi;&<6`}``a@&W zHHW*Xr0p_30Uol6y8iz3+xR+WwB$5JJnsvtYDo2bb&Qsz4S#I)%_FgQh;N_BWb!E~ z_N-p86Jz{W@(G;7m{lv^(KrJ2T3XX`2F`2LGK{5?^a z|FG*tHq$$^r@jd>>e}U#NPYtutls^q+J|EJ;{IsmfylT#=Q!Eq*vXHy_hij?(qfd8 zo#k^3AxDeDlN)YQYsa_xEp%mX_#!bKU2OAL4mbOM_S*9$?W4^+WDin-(t(m!JtYPA za}wTK_3q*x{GXkO_-W;N^Xvvki1I~k?tzWs2mZG5NbCf5 zToX)HZMqN0A7*xsLKBpJk}~eTU^lyTw(lD=rgFQc^u>XjJLX`k0Kmm66=|~>s3;qK zv8*;tf2cwXMhys&Yb?GZ{P^|#;1g)>P*Y9x{g2pFOZNeABvDE!>V7;@n!=m!L0t$F z)6J$W%V#wGTbHfJc2EK(Z8+=ipAS91l_9G-p9bX8WLMFVe`!gje{aKT^ zY^8{+@*4N)!;HqQRKG@adMoe;k+LY?wdPs^trpF#&n}B8eU4w(LGj-bh3Yk+Lb6K< zVuB((-bm{W@H*$|RmWJ?O)y4wopT49g^c? z_ny{kZ6~liiqyq>S>5D8IBp|ck$zL-;;cR$#k8U&ce+YCt8^*JPRW>_*r}bWZq)V} zJtZZnx$uO+{>LJWY*FWoL_O)Z938(E(F?1!!&~R1gDpn6IebZY{a+k6%2OM=^UKln zWX`^oFPh)We#M;}+KmAzNDkkyKb&NJQhud6P7Rlw@QPYqAw*{~km*hVM#&CBUwnjD z%mIzS22TFPHKpMio1A;u$;WA@k9LjtjSKT2dk+nu*a|$Y9L0lpmtniI+7U4neX9-- z$TO;kSCj2iksx+o+J9~Q_qsOfWS{*c5p=Q#7V5@tmGN@dmt~H*01kl-1||#!MGpq{ z8w^h4>w6A3TJ6^eL2LjH1_mYE3jqcO_xm?A7#JEDI3gGrG1&k5hF*Q$HQAJ#J(-6u PKEPxo6vV4V4FdijyFj6Z literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable-mdpi/splash.png b/app/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..4482d6a651cdd515c556584b1f109fbc2730335c GIT binary patch literal 7343 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 D$0S=n literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable-v21/background.png b/app/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e29b3b59f99290135b0cf3745bc9993ce935b27c GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blZci7-kP61+pZqKgT>LDI5tB{+ Q0fiYnUHx3vIVCg!0BB+iu>b%7 literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable-v21/launch_background.xml b/app/android/app/src/main/res/drawable-v21/launch_background.xml index 43b36635..3fe6b2e8 100644 --- a/app/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -1,10 +1,9 @@ - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/android/app/src/main/res/drawable-xhdpi/splash.png b/app/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..b1aa9453a19822be6b7aebd5c5565385b3eddb82 GIT binary patch literal 18498 zcmd3M1ydYd*Ddbu1ef6M?gWCnJHg!shhV`WxVyXS;2PY54esvlA9>#2aI0=r*VNRh z?yh~dthM$&9j>Gxg^ch80SpWb`Mb1)3K$p!;PZbc9L(ph&@M{}7??EtcL`B75Af40 zSRbr;oY1MPH4ZTcnG3iG^xx~Od0{^TF+`#Q1C2y~Q-zV|F8#bj%jqPQ{!T6_0!`*B zsRS-6sfkhC0Z~+fqJjdhZcvsF}57t#zdsvYAKQAa~`w}7} zykEwjS7ks5(Z4kox!*l~G5=QZsHBaIhyS}1ld`tf^uPPyF|?Lg|MxIF!|DGX4TQo0 z{I|p+3W3P~-`i42|37&hRjp%QNS;})C*gg_h{fkF#NtLRC9?qJsI$|4<692cGZFN} z7R(@8dctR1*Dcye4Kb&eoasqgTk7fZeC+Gl8v4A*ZT!Xr?So_-LU!&$&N8q`|7L|; zqTozu@F&e>hxH$de0Vt%H*L16L{+(l9V2O`MjgojUqJrMmt0O!8>k`oa!MU2ygnw( zsJczmMhZ)U86oi%__l6@y%b z!XTJ;Am!+nTzQF;idJ8n#yf!~RuSLIg>JAM$ZC4En;r4GFQZ$q&Y zlPjl*>n|X@KibpoU_1Pql-@I5`F0~_8CZ?&epgf$2H(7~D&H!nEnaaNV~jdrB(d(x z;_e!B7ZfoFU1kiVRuJwH%ebfQRaTR`V2AB)_t+?(zaQc>UlCHCF6R2iG~-9r^`8?+ z!Nxb4#nZ}aCjnx2~{A2BPWt`)Kta6Sc=9hZJ`!I8KboJhG@uXtjtviYO6WT&YDQ64G=*|Ix97rtxL^Tyh07jO2KBi5;h4$C8qfZ& zIHO7yAR3-GBClzbEj{iDI|~1gwgTTE6cGpiBFvd!V}%`$P`EfoIAr}P4T!YyYfqgg zK#6S3azD}h+!E(Pn?=H{2TlM75M_F(!hzPEnWk_1XBJjb;lgCtnXJE5q>c|uTw+Xz z!;)G|D7wlR0-@bA73mv~L}AXP_LnmvmVqJv-fkz4JeY9ni0D@|I}AMTU-&UziO`7r zL%d8?v({$8ROU%|T|O#9>ow~_4V&Sn3c$f&2G=Vh*lz-x9xTb)ctZ@(Jz))nQ!0!s z_JptSUxz#3hS<(58)L~tprn3_K{zmgh%Vh%ca*7#OpJ$lV$nhhnW#h5D@k-4Uk0*E zaANq68fE4Fh{;-mIWV$(r44!7_ruu>MY)su2`h)xpHfml zI9}NBx!H1NQ^F*7yf7}vsCFBq1WdJ^=y*rVqdCJX*YatVOaGqBIFolKz7qv$kr8ZG zqg6cn`-c(jPqRYMG-cuZdE3VtQ4W%YtQX}*UMy`oS0BEy6ob^4z}APYoGOjim5#t8 zD0H&=H>^Ah5%8dm9HfBA;}eBSS(Uw(^fLgD3-g#&(>W*FuHkaCTy;wdM!tn4Jtiev zXf>a9X3!K4(S^)m(>VP~TrlCh!70i&Wn+g&%qWa6YKsoIpltg0Z`w7t_*8jHBvDgE z+(jzHQv6^;5z?iDtIBom>7uH%@d_6fUs>wY$|MLn&!IbbsK5SDCP)0L^?Ee;ZqVNs z1J46>xY$lHsk$}UV#hZ)CUh!$MOWSzqq3QEv!xg1pu`&}sGS@IV8>gw;n1ouz_a>U z@+A`h$8tPWxAtpC2cGdObbBl%MmOxiHrdKg^pvCM(ib8|_^7&bpk6aj?^mcjgO&Mj zlgDv@MIOi<3-TMgG*Iq5VS=s#)ZIj`=P-<;$`#b4pHCN}4B++tg(QConIXzFlm-vWuC}P39!Y?NHA6 z4wnvIDAq?wSPJ}=#)NEGum=5&URY<}8!jr1Y`$3syMFS!F^oWSc^%c*Ayupf+^?TA zXJU8zzmpHFh8&`t>tyPKNqXT(@otixXcZ*d2>R?6i~F%*t+~h`tMb{0t$&td%ZH zaRb3pS@I%(QvsXsz*1XU^~^2l3mH_!D$F8Uo`X5@2zYD9vUg-TPQVTKcKuTgro zB-R+j^@Me*)L}31rKz8xyjE~fvg>zKY?ZIiXH|hmt=KN$_NN9&5Dnw8u1`b!W!g{i z#687yOUa8;OqbNlZ53RbzJ(BXWLp=NJ-PMTC~5VDWvM@H0qvuI(lG>qgN+z#6oR>R zz?Cz$3%*QxEB0SuVKqd@5a09HpdzCICpV2%KP*s%<$>J|&0`+kaI(<=@4h4l9z33! zsw!v??Vscj!%j!nVAL8fB1;`p(y$^Lw6hwq6L_pqq{k*MW}{P^l=$lN(`$tbrXUk` zhX{-Pi41Q~PHn(Xs=W0?Sb7({ z^iRuNGy3710&cp@!%QQd+z2+D;Wedc(ug0+hoMifkm`IorN>Q?qgLVf&(N`>T zUE9tZ8rYT#QGvU2XD-ybr6k8*ETC9PT3bW>C<%n>xkEw9;<7G%MkU>ts)VL2#vO$J zd=O1;q!y@|R#otr#(d@$IwGY8e%CrKEYitcHtz%ZD_0F2@U@}Y)cAlPqenVd315?G zsu-klAN(o&pqR0Qxrn3u;x)ugrH~0?Y^ZO&rrAIqEHDclxka>e&(BTr?@GmmHIOgl zEajUk&}RPGw^w*-`vpWxxHjK%By|)R%zNt&1xw7DGS_jR$14WRF#dL(D-pk0W5+>arp=3soJn0f|x(i=@FKQ zmU0Q}Y@q4xH=fcdrM+L)30@Ali&wspF_P@I^V2bVm+t9nD!xpVVN=>$iMYQ@3lH+0 zA;m*n)XncxSC=7=`>I(|>05q$G73`vuI3?xYy*d;#SM2mK`}&qBB`i$)LGAK6iO4~ zb1yDzT5gbevl<2lT)FR|nixcuZqon)y!-sY!0tG@5-y;;q06B6Qx>Pb%J8yS1NJyl z<1cUat5cI}OI3R$nBPnSaof$y{feda%$5#i?F)n23Xx^9YZ+68C0JA|rYG@j|1(1O zQ0&JY*!L@=gz0xgz4w5T?nA1U1AHK9;6L#_zMDJC1PcE1^hJY2lFfyc7FdQH)&CW(8rXT)&@$j7b3eiJ`4V4_0bvB|`P z&oDwC8y0=zEZHTpe)aleE)ndBO=nL;5HN^kyW)E z+a?l{s9tlyVS09Pj@K)#jk%)2YWcrf61!4Vg9(X%iiHt>edgB=M7XzqxZ#};Yg&R> zlhRqpUy`PmV47gsZ)W%|X<2sY2p?iPW6QM$_GTSmXdVuESG4rDsYkYF6kvYL7wCmC zYEWCo!@9+6d;p(tq$c7VoijNM1>XKUH0wS9-V!AsuGh=V0<%9UPEYhB5CtR7Rrx%()mN7V!{rs2L!43{Yv#HHb<&Kk&4BPZ2g2UyN3S_P;_)Hy5`eM(( zdzGdi6O6_k;N^*rF=M!Vi1@Gk0x=?h`2%4qGuCvrieno6J*|J1<81q&P!p{b*nvr( zNSJ|`&f7Jp5&QmKQRzZl?kc##?)IzNCq%QG(3-o|_*Vbx3qL7j^G8H)0baXh$eGpA zF?i#hQQ}Xd)YDuQ-p1D&^3$*dqgB1r=81ytyzy}WRr3d7>)-Gi4yg(|lNauueRc4E zaff0j35dgT!o?Ha^PIu5Lj(E$jBIA(6&H-4ovbeHYj#z=!UGi;CF0aYsu(f6ls`%+ z4c%D2tir?Vxvi|E?<9(%Na5sczV2D)9jF#UIP4a|Rg zT*^aQmdvF(g)3iD)UN-e`pn#ACc2Gzu)oPqM>?{sIcjs~rUb8TkX6v7Q-nm2ruSjq z32~A#G&{jUPPbVRKD^R>HNB%;ctIXG4M0N?TOn(PlLGd{>qM`C6-;J)U*9-!r`H5 zWRsIc_k-$!^C1Q$X7umrfSD%OoPVqGVDa!{$^{d?`^bw&N)dp6yAEt|1*bsEfhstz zL>Ch4=lAA~3sn#8C&Sfm-rocS>_Fg3(j^R%+V z%R3_<`_l7#e6sI_W?3iL2`_18atm#hf$bVu*H?I0u*-PYIMW;jG8-eaMvc)0MfP8q zOApxxnbUnI%Ggn`%hoNNg>(MPAet*kJXVFWh!tFkjGy-)p%y?ruy0$#cpP6lgP)JSY8$06rodye1+?Atwsm5O%zs)r$-u}k(m#&Y% z^~&OuGcrq8|9h|mWs<_RY~MiAf4TKHSLP){L@+L}{)g3mjL9v}QsxNCF|;ge8uCoF z+fvGDu69qjQFo~+l4T|y>bE+V?*6`Wgg(R&@rJF5M^1ROTa2RJ=U&>(xIata6QCJ6 z5o{%0gQLi1%qSh>*R&t?K{B~Yi2N8zf5pb-lFRj{RUYJ6>ic-BCxw-C#nPKxZMo7V z<#5nL5W<$69u}FLecP}VUiyBJ9f_sSj#zQ0=Kf#=VX-iI(D`zf8DLNRQHop6x^ElY zb+IhVVKbm@+cIMzv2^(A3dsf)Wh+O&&6w7a2Ki05PdLjpFhn0m3=>WN4`xiPnk*R$ z7e&afUslwo4e(6_K$0jUS8RJQ)BVX~JaVeX++`ySjYLA_fq)xT2xPO>NvF-#Jh(w^ z6@7kd&JzC9|4>T|XH=~q9$n3pPR5A@A--e)k-r_5rFRf$X&4xX?MaOX{#u?z))8R7 zcgQY3!rm|wv*f`vl$uoV;Qkc^YbnfsGP&`e+Y|Fd(?)3p@>&HMtK=aH1SETqcIA4m z?UH9KqRd0B!tePACdG?XXzUZ&gx_Y@NP~HyzVX0JtoonSGB5Z2Qv*t&2m>l*;XF#R zg}Ied$2=meUOwn%YPqwxAEMr{QFxUBme{>5_yXy9FDOxa!s$i)M+LER_ex7c_>DnE zl=;)PhsM-IqlllCVT#gDp;3j=JwioN`o0{Buo8DzOFy0Stf%ri2^c){S6_|sDb9|d zn~F^q1JDz5b6Y+|c=fNtePf+v3GuZwDybZv(|nDA@wdJz)S;MSIux9$5;^5!dEVrM z?{xRXw)?uJr?RDWs|Wck<_&#|pBGF-XRQYlbxkwhdq^dEwN}(aBSh#qD5N%?Rh`gz1+q7H+H#~Y%m(_r8h|OlTE@{munzT8?2<5ee zAk}yCAO=dPcG?QIKER+~^K&$Z_78vl4HW<1jvb?-8@1VmPLFR7X#|lX;MdegDefSc zaD!zXfg z3eo;{&lG(Zq`(YKbCv&*J^3fGgl+CN!J9sXx251*@cqzXge7XQF^5yAQexx}1b%vj zr50Y|pXt0@G;%#l`xG$6;^6{sM=*Xey=yIrb{^HDtD~DKEbm|47k>Zbxt82ik?LqT zjU__*g2C~$+AmQva3wU3M7~>L2_;#zynsK?FxKcyUmg_(aBZH5D(#!}7X`-@2>IKc z+-h){c^>5IOP&nC;;QFRTN;a-9EKquu_ ze;K&aZ(5{0$55%B;PG}E39OFI0i2GIc4{bQQHE%cmUPSN3zPTv?H4tA?Q;7+-#MI1 z%b#w}N=VJ0O%)RPye259&Z@Q42%pBx&O2CnwM;iU^E!OJ_ji)ln{_ZdAygBrCTdKb zZqaPhU`3uL(aa_|d7im5-SB&u#@!&%5cC_vC5~WNtOY624yX(9T2tU>k89!#MZb=a z92_^9$lwdg0VOe~!G;^Z@o4Es#`arbxxC$>Oruft+&wUpvg>6(TiIMWcvWZLV@tQ` z#Qtoe^)=*075=aVg)-e_TN`Ya=eO+827gr?B`Zucq}^-dH`g8i5 z5VseJYlI(vCDShWgKV~o6Q~u-YZTz7?mRNd9=mKj9SEm80){@35F(B^k{TNwnqp-; zq(Jag55SJ1x3@LxKAKJ-Lz_z%W7K~+BQB^(Z9gRH*1&X!y0$mz$J%}G5&M;TB#4;4 zM6afeqa$pzIgM1OU?}{T?3bSOot;txh*-B0rbdGzv#qtO3QX>;8cn^{)Tfd745_TT zeTHNuc1DKn2_1^(C`n`PDJzRi$px^yi5Nxs4)>ejv@t*3?x2+Az+k|IfJUWOQ3YtH zfB#YcBG_=6PUHbgBTFPf-BHdPWtFYrEs(l2P`1Kf;5=&q7y?Asw_G37dIJI_g4Td20IHlMaQT|?+ikx z{ztfd_jjnsv5E~8Ki=}mS3|w3)7h+ty({9~Ck%-3AiL1i3mqq-4AWW}+tzAHG6BQN zmoBjvi?`qUbqQapwqvHYd`pN-eD-3J=5(x>-h6!w>0j?;fnB%T@%NSy8)e0=2+AFo zQgt>|t2f#gR>RDMLS}Q{#bN((N#Hf32?A%%^aHvN4}_$?vPB0PQMs#iV~0x0uZQAB z;;zZ#0(V3HBJI@biJs8AN}*=>(Vx@n5l4$%z6tN#8z1&3WC0j9cqmM~b>A+Qf^+tZ z3|v9@p=G7mmFY@Pj&BzaeHvX~4pc#%_tL#4ob-asBI}{1vW4RIt12ouAfJ<$9b%aX zg62Lll0cYGiwZ=ZaNP3KKlg{s;h_SdM{x>FQ7!m4(-g9o{a}%6}tqT4$UoGWbwn zh6Be0Ovwu#H43BTe@A;Gh*fZ0GhP@PY( zSiS$s&C2Kaujh5XMW57L_Q&089OhHGm`Luw1F&mSzbp2l#z5trA}W||VSR;IKTWiN zE+j0gt+n#OMUTyjMWZ}TDSQq%ox4h2Etyg$9>7EBOKxHrfX1oiT&>@8+%-d8te0yGYa#pl) zN(ToStM8KCttHHLW7Ttg2{zv0OIMFo=ru*a?JqXDO{Wz*QUpo)L@^|1P?8=^bjng$ zr>0=Hi@~a;s*}6=3g@)_srDNe?O-I>?2~r89vVb7>YQNB@a@IzWlW3E`b8ys3WoGA zzy^!iLSHh)J>Rjc2&q?iSnc4WQRrMYi>GS6&S0De#UfLW#9C5fUq(rbgCXx-PavS5 zoBvGCk3wlf?Uz%g^F$5(oR4Fqg|hOk8z`mYH&~JY`RTicB4w1p$GB}#)N;9gI*$GA zAyI5@DFN|e{y8(HYqgt&-V>-3B#@28J$j^-X>yKq)=4b8;m5Q0CGA*KXkn1j8_lj_4&g7e zIR2ZFU=r%!yg+1GB$<RHzh%wzDoFhsBpuHX!+rmgbTpfACL z)226=@@cwr%XTUt=nrZ>N@+}qkiEfToK)65h44QWTAk}NccM2zb$5c=j%Otrv*grK zUxh>JevIMwoAkkIllN<{8w>~0Fym_l9CNPstSrKWx)@v)%()ANo0-U#3ijV*naQ6_ zC1=k+Uf8fh=>V-W^Nzb~`XR|rm5Nj2n*tH(M2~D$i=yv=!VjJz!cm&hb4)4HH_}AY z-0}%%XXrY#WRg}sG4mpSo_VafhYqyxeY8+8RGK5&D&ByLZVqqKG6$^PI+s2ju28Fw z3JT$hS=Sewv z)!3^h@6@CQJm8HecNyG#3|#oo`8{#T()!Jc@TYEF5xRtUzK@2*{60U7q}x326*Zc} zwJ|hURm_bSpmQh${$U+EXecqgJtyG0rNDjl4b5pX-&u@sZ$=8@h#d(+X+5)34a(Uj z_!AC7zSuf3WVc1=l{eoJUZyWMr-oJ9TM9jKH=D<$kyESgQ!zx}dn9ZmIyrX$^}dhm z!Q4fburu4v=mt}?|32C~&+a^CYoKnza85Xb@*h(T60g2hT@{c}`^w_Y3*;xZyQGCR ziYXi8uZdb>U$66c2L}scNQgZY>$UmL(jt5WHtG3u%ii_p4J}R4q45EUbOEA*yLPUje zF5|)$?(o=m665YVtpZo4Hrr@CjYHOb}N+FA#XF@T(*w zT=H^(-LQ&mwaa$6>Tf9cc4r(};$sEGxK{|*+I$;);ahJge>;roaECX5`s1afrcj2b z>2TYnzS7Db0*O_l|3nN9y4%#(TXWJsR-IjqV^3-FTZ#w8jbUc5ECOcc^sDLANMUN) zVhxX_5Vuvn387fS&WunWY;ZHU&W$ntYv_+kCi}O09nJk zA31r#VjwPH9_Q@0JRK+HDGt2q9I{Upv~yyb%Qb46H`ZBlZ;%KNf#TJ&oFAwW$2f~? z^Y`Gt@iDu+9f}gFx-30g-LGoHC^#={*_|I&o5+YAs9`t=+#dYO#JAt)R}xM8@HX1) zc6Q<7s%PBS)fkq|Tu}uYx7UwGEbcE_IN;6YTd2zfH*uWnrlgpgJMgxw58|(bqc9q9 zHm+{r9ot_$(Ee$H-*RdAC()(i>LmrW-n3rmeaH6_BF&Bm#KPQLfHv2!n9U@n)F;^b7Lyy4}rohvMbk^skMcr`gu{M+YxE zbL<;Ta--btjO#4#8Jf8IwY^Orrkb>=L1>y8AZ@XN6AaM9BVhmf;Ot6) zh%E0Fx!cx*i^}bI8Y|T1j5p?Ie0p`~%FbL_OK~Gf*P4~PSinM!pw73cM9dHtB5il{ z;U+6%eCOlLM~}VqR5_zhUWT`#;{blMxmsPjbBzsS1v1B^3ct1khWmB1v$3a}l4tK4 zvO=cQ(}=gf+VYcRvz}()N4|dflZnOcb8}Wvg6taf3ieU9j=rNp)j;UHl71B)DgXDj z0lMI~GlhIaVQ`a!9zSs6EiX%iG&kRI1Roo;AivaY{$fzF1`p^1|CV1096I%*IquS# z@xM--^F|?R>1Q4i{$y;iFR`xJ3+#IOB9J#lV1=1odq)otI)Bl{=4f{Q>e*%mBk2b% zrZf?rg^7TkK(mZ)umFQ!xFz=H_X#SAD2tJ<5b0NlV1+Q)vwLaioZgH6<_{fJWJC_3 zB)z{TvZW8(zK5pVSFKE50l3g#uKQ;OZ4wJ4D&Xe$Wgj11T}O6s)Ub^SKl^;lS&p>7 zIWifI+?@r983m1DsFe?Ybi91{l0dMW>}?rL(3Q`gz2Ande@jY+5Xmccvv8A>$NPMq z<7T zH=}-7CFm|VZuqMC2YIcfGdBBdr=SXF0MGV9nl+`T!X4Ii^9=t;zm%^3w0D=Gm7f4@ z%~u+?QaP+wO- zbln$WFjau4%P$vADlOrX%&FF$=@Y^I6CT1c{p0IKo;gy#uq*b@lYrkv7pulwRv2G0Pvd|Q|6$v7z5 zWQ0@peMZm*7zfBvdw%$6DS1-UM$XHGb&8ImA-Z>bgcot19ZpEGy5yi$YhLBd%bgog zFpwYbeQ|FjzMF$Rx%O>hBHN9-*~BL<0~)dsl*X=bTk|n(Ez?1oNL633b8d9wfcFzX z21CvC`D9}i`hfgF4$Y4PkN-Yc_~U$j9O1?>J;9`1NCaxViHzQ{fB}4c^SaAj2&+zV z{K?&9E6wN|ocMiZ{hfzYhG;yRz_0M&xa>q3!Bu4)UOfe-8c$FzxyN6-Ju@n>(AD|$ zPZH}JKXQey1z|X_o(k1Ta}5@lneZj*o!4{Cir)8z`%#;=YqwZAXQ)Q@lu^(*2+X)R zGlrn0yFxC_x#-eK3h0Bx>7`a_@TL2vxWQO|Q-`4tE-Y1T$o*erBhkF%8@5(jpS1Ny zf7T4D=TWvshzzLD`piEGudy-Ye$g)gTMC&H*I&{H!H(|4k{i|40;6>GUbTMeQHb8M z9;NgkT3-A@U2TU8St|iN?mNWmqORcNr1OyEtoj<9uIC+$mSx3*kUy53hsDs2P==>E zIqtHCFBZ~bU%DMP|JHP$0GGI| z7*P$$g^?2znLp@pH_57;Y{d+-c||9p&w~anyDL}oF5Yg(oRauCXhO&{O*H0hP%+X| z0S6_j3Y8=elgW^VJ~=gmA&#OUE1Gg;W_zscOxeP#)jB;_T%US-T5f*J0d!Or-8`gojU;!*GdJvuN@u z8pXa=-b+efX$tlfHk(e~Yu)enlOIo|J!)H99rGk=pP<()wsy|12{USuT!!;3J4gCNZO`EfqcylY6wAHs~ub}pO%yz;S z@0njd{>E$9db3+}PRJy^s2d0750bRbc510Y#3boDAoKSSv|fhd{m&4Z6Q__tp z<`qgFl&{7KvIZ1xX}_lSQ0LguRk$;jHnE>u4^)4Aw33tVagdSD4pcL3%|0*sJ~lU) zk%aqt*{xhXy>F*u@tdf=j*f_n&QZaUMWim{Zmg{fYAU|@?Wi^s6R%#%|G{y-hwhMP zG;2R@2)=1&x_+~WeIoc6UxMGlnB~OjZi{~X{6Lh)u@~jFo3Ss(ys^iYVY=u|!pNfk zJ*4M=Dk5!djFAn}VPAS~_NpzD9hd=S+%8?geVX~AOa-#Jswh#9QxS^To*X|YNAoMr z?f|K#jvi1h?-d9yXFSgnB&~3k1{rPww^UYC%xGGENbFEwJa09W+zm=`og#&epS}j8 zpxJf(4VU{!z@!dDFo-Y{!w|(Kgh$_}4j`=s%0GrN^+BAQ(7D2Y!Tyc8FvO%i5qqQA zsa;`VOYgHZaN5-lV`}Z{(DqgmLg3ff%h_GO_f*JngyNq+D(~ii_F;3Ou$f;D68r|CdB^mniILuz5XEI*1wu&y#M6)C5!cZmA!wJz1#buZ3=up&+!U_ugW}P@m4XO z0))bS5yFh}Gk~VJRfEGcr2Q1-$pPy$%#@9jlI8nRuD~`ahRt~^d9^o8hsvN8zF*fD zI&B=`=sN<`GyGK#krd;tHLBNq6--7dzafyWx=#0fS8^nyOeAgrjNYNR46 zsg`13J+Bw%<7D@UUX~k+NiZsdUa1B>qq$3{GVU{Rrr=+iOfjc1Wxmo6Wcx%MwQC=@ zp6Dfs6#`_sLrFLjUvemy<$aOdffB~}aq*1@!Re5soMDpMOazDWxj>N2}rzne{ zqvwayaBhn0y?hjDh*K<3GCHKFAHPkZZplH>>1)7Qo-8&!!)_rOH+`{dXv{&SA}(8Q zx5Dy6dNJKpVp~@bDqTNs`VJ{*EOU5H;N)b@Ia9sqZLZXTbgqWwUxwuEu@|#_!W3qg zZzM&ij#Hc}w+9`uCFz!KYEQ1o;-qQiXeLHiqY@-XeO=9=u9u+jQ#nbS`g+GiYrKQ+ z-2fZU)GxQNmJO0Ywj|0*b+n70wdqL1(`Z`#^6v{-u}*R1Yqsrtt?qmwe}y zzq|7$zMnsaw?+kX;wS>_2ImD^Z1iCe`O&Y4M)-6{W;5`55*C`DU$i%dhviE82*a5+ zQg5UPX}w9u@((H_Z0E0j@uHy9%gDS4I%K+^y7P@mtwz-du@bw^)eWO%OTu57*0Oaf z6oFH$y56*v4%8KcZfMiKW5*BV-hR1{x9L^OjaCVV?3GVq(VK3fPcxdDNbVGdm$sMM z*0w6eU(VQZk&G!EZf-4Igbt2eo>28#cRclvLX7dC#^8H*$?yQagWVK)pP@_!J7QB6 zx(VjrnI^oj)hLkPCr3)p)a$%z*C zSPLkbKj>IHByyZ(=JUYnieSDn#+O7yymSY2yFV43Gw$n!3#3o(71SPc%#l}<$b?mH zFV_mdPN)r~);|P_ZY0maz5)v0kD<%lMBNRuSNS(I zDYvt|c{^>;6+3{2oeQ)<0mVs52Ju_@XxlM#$SQ|w1KCN@Z~Q3>aIEMgbnQIQkV-~7 zdA|1ID266K7#dg>i^4Ll3xt?BToR=fs!oX2`lKsX+8MnDOh-nvuBqa$c28&N0Pord8DoS)gmR> z$Ehan%{sA%3ZbT$Va3J~x0N3a3Y=2irjN&vHndBY9V;n0LO(qd*zBFfnSJXIOwG$%c zk=9+XE{^O}VI?8gaT2U3Oxy|LU8DaB$8J6Agh4odV;zdvLaiy^*8WHZ+ba@0U0W9l zs(;H5irulOjIIBwO`TI@Y90I(B~+m>7WP%SmI5WFYFHQxnjfuFWu8qTD|X4F7TC^y z9wW86GD8~N62HfSH@_qP%QYk)u{W`Ym<4ca!`M1WGkMFg3BYme!F7 zW$gxRU|#1nXjBd_s)=uH*T4-8u|Huii^3m;HG{)X2>CPTS_2mswrX0+`v!G)GA`-f z^OFpecF^!FO0GJOphrwxp7ykma z)Kmu^Q zaapVH()>iX_*RBvbfk2yy8AcCCj46B1^d{hH^0rlyL4v;HQ*3FBA#z#*Cf_lBdy7C zBU5-$DsD*bqs=5#FwI&5LJR<<%e~On9Min>a-+!+*xZd{UR)Z$PW^~X%x*{lwjm7zBMeB&Q;b<2d$~7W!TLih zW*R@rM$5}Gg8(?GN~2<68_k2J@$(8K2bv~3L$mZRI1Gs#D(o;yy`ZzY;sSQ@if=7URA`gDQ7WocJ1QePF0?sk{F(!Drm=IC54RGke|hA7*EQ!kiR4Y1J3)$l zYHAIQgQUpu4(KT}i=(Z9eq>s-_#y(TnALaA`G)B>DEI8BrxeA$Sz)>l)w?Y1BY$sqW-PmBTIUoWJuI^@yYjv%? zPI9KWt<5>JRer!z%}++aX2bv41KY^aX<{~rZZ0EpIwz)BCx6X90J5QNXafZo5(Rt< z5|IcVp3YIz2GX>GNd$`6qknhvJ}(P&UUdC-it5*uVQ%;IdIrGJL*t*%)BC(1reu^) zM6TIQ1n-~v#cA`I+p21KxmKOT8u}2=2pM(ioVZ|p2C)j`3$n0jS<;~1A0ALw_KQ`= zOFHeCED}u8@n3~P_HT3?e>vLd`f)oIuns3NoPNYx57q}P}0NZ$mtHNCVL zAPAO%Qhw|7bxKv;N1`N;wKp~abSuwV`YizJH`auaIp9y7f82ffuQ$l~pbDR*rugA| z_MD|gx#_`!fR)7F<_A4(1SIY&&h1IO9^I| zj_M|B;JgDD?aQ}o?~6dRolW6k6V_6#f$t4P2kX$7@ef2i9;`IS%bNA!r^^tT{6;^O z2+uEd^JsR+$7{M+5$>n-U5@H?<@z>zp}U-`r<&8uJXurLJsu$SWK|QgC3Yw9`4mT; z6=^KJD;(q+pvU&Zq_-FHdHhxwl|76y_bRVh1MG-dh zc#k@q)|#i;CJ>{x27rphcmKH92$AA`eBHlPUY#`dw281wd;iW_tOAyZ@nx;v04OfB z^!6Slo7Qr^tj0Ln7Z^xXCn+X9yZ83uuEnr45+iTm(t4KYhl#959ecxC)ZWd0FSaIn z$>hezA)qwjtswFv)M=!+u?W;;U1>FXNhXkHdO&U9G4$RXnxOIARi}9Jz_K(baKhW) zNiBre=40O7sKeyl+bDWGh#%(H)AVM&I0<^Ve$R#fjy{aR#IRA>+E8-?pg;JPbAL1( zHDu&-XfLMw9%yW-?*aUo1nr4p6X{{|I;a_X4_CSdoQHvt`)>6G=wkbvj%bEDb)=zo z;fm~jNv8u83|W3|rnql06ryWXtD+-e_go5dp6pr5&=V<{tbpw1{vj)x0gQTD{(JvO zgmjLIp&MoQwAe;?^})Hw+q-q1HwUVIAog4T>&DCW*$wT(k=1W2WQO4j`HcI@R&(E7 zdLC`!+L#J~&rZMEXCeaJ_>E>(0$SncNFrjY!2cy%u~N}leGQ)HiTekw6tEwookjs z)&35D{Q$_WuDMiW$N8BMF1s9@>cjrXyBrKjF(zuH#PlTnl@f`gH4o)4-wpUXtT>Kk zJxQp9Up*=3&hy&=v9iraKUB+X(&+5~wI?hK>Ap)OGkx9aSr<@TfSeC{m9=dRC4vjj z#3d@8r+TJ`dqKdWSARa~Qn2-27DNRRTpWK1J;Z=K)spY& z%5{u0JXIzXi@T2=hW8&jZfLc|R4!agVCWv5t%u;W|LjO^t;@%~_#z;Cntf8kE9yES zb_qK5arU~F`s_4@tXX%>fT-d4{`?WP>lXpifc`6g7CZ~WlLekje@<1fur4tr4aH-E zYBY4!c;wWFOH2Y2i`kAMj$nO7apmx!W_zn(d5ps(i;vdde+GQ0PO&s} z+`Ix%n^*c4j*P=s&fXzgtm1#MJ-gqX)9n*;33r^_LqxrH)8SavY6biREDlwSuhZ{%hpsQ=oD|iW32#O%g&w zOXkJaa$@}Jj)MIbR)3B!y-25Xhb&U_ZOlJbgC7lh2p!v1( zea#c$<;Fov#*O+R?ZRIf)}PV3Q8nj}_x1)8ag&^|qyqaANQ#j+s&@!X%mpLK7$BbalQYiQa!r)`<035!09 z&Y&K`RY=rcW*u&umLq&hIcINZJmH9(maVoT#doZy{aSj?4eRxbyD1;YfJm}dcYQpF z)2YXoQ~>MgCXNlaguDfg-CLOkYK>oys1TiME*W^pS}~WY{o@(pbE4CP*eS(sTM&_# zpT6oD;k_H6;wk8xx~*+-hUbNO%Y=ME z9nTc-tUqob*j>DknluY(Le)Cz7Jfwdm91{1gOAm86oQuFw@f5_Dlz>FD1)o8?01T@ zKJ+L#r8to7O5AXUhMrT+4Ord6JVYxx>R*8+JSQR&&>LH7QR<-&MFd`=1Sm7cH>;Ol z5AUTt))qFcsA>@rH|ce?(sdkkZK@H$J|UdqBKUwO@R>8)zHi-LOUi2bWLtZS^u}mq zP*PgijO{aa*8;@fIa=r7ysxn#B+< zg5`%gg&vGHq3pJF3#xc9bQj~bchm%pGv_d+hmx@jlY*05d+g}$1Y$ZAZ3e7?X88(< z2ADI}(A#=|E&`B|=-Y6!Au&t&U)dxaHrI!%hd zkS*XeTC7;H#^VG@?j^^!{!l{7T%e2@Mx?0t8c}821T&(U!PXk#jWsye^j-P)p_as||Pj%@Q&b}1vIdYQ!kgeNv5{JEFf^Csv+g1UN zu2s>Jg5!^=U$sVT!^h<$Zy%n8_=4-ckb=b_D;01?f`u`|?se}+y~P3EQm1%%^OXd< zdEuT3wN@zl|H?V{cP11tj+4@c(B)ae9@zm8 zQ}jM3(PoyScT?q42*6#aqvICB%IdwCA0LH)-#i$~WqDoQjI!p%Pd<(;40E@5cVV5W z#&{EPKTHSs78B)HIE)+UM3Lbo`ugi8AqL58&F+hUVcFF$+H}b(m_y)Srr~bCuToEr zsevVH($WLVcIJL_Y?t1|(dT%Can}XQqokct$_l@fR**3Eig7<)vHKY{MlY3T5~1J~ znl@bCvrs845@(lVHZR{cV;T6iCgwl}^q zbzk|72e>_t;q$Ok+=F2E-sVb(^4frS%Bk2RK!OGBUO(?XxV2^Eqb`##9~O^hBojQuD-BaCoQnSI41Mjq#Y>iWj)-8euh% zUGLd#tLi3_?mJ)83O$Ia39GihI4mqQgx!pI*qzW<*{MecPQnW5$ zZuzpd`pIX<9CDQ2Xu=!bj{Deg&IbZ~WX+_nqWfs-0Edtl;P>@3b6M6&|Ahph7xaXnQd{tk5d#fE-9 zKO>x19zySK1vHi-U!F`UN+AO^UD`S9)6XhK=<7(L52ax(iQ-(d4%I?EnjV7601Nk* z{zU=9fS+ucCLOKkS4(s&YeQLP*nUw=+h_WXIbOusm~rb*f4z%BYL_Q$3!cRXYB85| zbzG-t6EbzkpvT?gEaIT(Yi8kNXZD1DUQsU>!_93HyptL53g5KIEH_>s4r^*Ta}?x_ zot1K-A+5FXdb85>Tib_LYbVTxo3d1C2)3Lp4$e|`M3eqWH#{;d4~-V4p?I48J;GuT z-DTW=W5bJQ7@nd+>SPU|Aj{uy%UalI(?TP|>)C^mxs-F)zJJVk7v;3lq191vX^=`# zT#Uq^NXgWnF{*yTA|iWFgOS*$huLkjrko!1AQ?R0AH1NwsnKXu9|B{}R8}R~O;XUh z?GPj&KhM|PL0~w)sD6)FZZuSL9X{}#S==>;w|wCCKD0flmcP4a^wA+S$Oir(C)k(# zIpFkN%LI#`W!#t?LuVz<%3_%^i;(5?^t(>jTD8G(%OG;9tD5VyRM_lY0x~Ol;h!b( zgbC(CZi!XXTCAyKt9oYzRfsf^#CDn^NV30RIUDLLuSq@d1rXhyU)?FpE5pnin|_EF zqll@y6DlQsP-h!$iMrZb&ZTPdqHgC22oVoo#x``*u|=fCx@lniL10`+0=>1?Xj3> zhCA}u62SknY)#xF%blHTN6Qj=CZ+!U91v;ViZfbm4}S_)UpW^9h0R^YK2g(COCW2* z0gwcRh(Hwdl=4TIH$XwRO%$r82*~5W|Mkd>TnSiD*?&IWiLdw#q~zm?@~DT0rTqt_ Ce-h6C literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable-xxhdpi/splash.png b/app/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..4cda529c038ec869760e05e3f8d9df51b448cce6 GIT binary patch literal 36321 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%0A`x0sM#pd9$XrnT7cFE-um(kDIeD z!R$PXy55o5((d3N$8A{0UaWA>EVVqtVba2^XSH_b-f~i_KL2a?8!Jr((>*S|?$6+5 zjn&^OgG|i>_ekX|28#=c0_JyO3y+?HK#4*4%s2Rkt59-%ebFz9ME>>laQapZX^Z+L zR$KY>Yv%N&zpN8}?KNpO?16h7^0SelC8r-gTN*8yC7Wjdt}=M@Z8c3FA0v=0Bx*Y! zr$26Ds{nM<(qWDRk1TvJY)Fj%C|ahydM*2@tVUhxY~7Rf^q-q&de1(SGI{guCeu+} zmDaDXN9W|f1Xu=Yq4%Ho-6tEvwHht+4*j$l5yKRLa#WOyFJf`7d`zg`vA*Pcrp5kR zdWr1#NIk7)u)uI*{HWb!gFJgX&t-QRjrKen6q&ogjDMN~|D6NlAnu8Rl6_+JKrTa3dL|H(6Jg#Li@s|!)BhKCsWFrnQ%JpJ^R|IPWyd5N9p zD~tzjDDqsYang$$)E})MnOGRTy_~LmFGIvM*4R(RD_KzMcj^M?H8TC&S40|QVJ{ZH zrWwiF=h@C8t4uBP1?o!ba4)*1CGYonE?FW8*MmD0T{{R}4f{MHaeHU6@P=NCWq*6A z75jG*QLJwnH@7S;`A&BnQWv^K0I64P_q&kgtzMC)q0mchXP|+>4dPk@Tf=IUp~OMM zb?eGVaf8N`3G-ePOW|KTMZUS+w1b1H)Wgm3Uz!TDSvN-ms4EX`_i{FMI&2My!7o3S zP|}VLKQ-L0&ugp}T!`PxQuL|SY;f`|Dv^s9JfsYd?YVO;qXK2 ztK&sUcV@q4p?u@xs=bMGnXQMJ-I=V&Pbsi^%InW*mh8pd)7;~1$zqRWB)ZH!rrW*} za{bav(}ybjCp(&bER1~!??fAob4IGI{qwqm)|!LYM;?}%rveE}xckLx-+4uNiC_M~ zT<($+i4QJ3uqJ)po?5dRCicSxi4eN+ekpaH?(Op{pXjkk#U%;1kLY%N@G{PQwY9hJhQsX5@zIox6$&3ih? z_l;Y%Jk@6bqvf}DEt^h-#q2UcpyE>NGqc}sU8j4x`|iJZ`rGe)O6dJoiUY;q8d6a@ zcZR^8v$U~-s^;p_w+wY2-)W~fGH26p?I!u7J#uD^^|yI{d?_DA)!NLFCG;7Eyv5Fs z#@$^`0^HC~|IrQar2HVg4e@|Fwrsh<$!A-2>_wQE;O}1IE$k;i4xllDmDhg%hr7I= z80wM>Vs0y0CENXU=3JPH-Ln>qhzWdzLY}D~}ImJOLn`&ejkeRc23|TyHAvYVDD^Deogz znS9@TcByY>%Q-=GgWM@&%NOw?d9BIGUsYF)a^q5K)Vi~9mJT|0DeEAHn7>n z(!Fc()dKt3#kNO7+Z=gR z9Y6|WCe{0Uha;ucOP0|^FzrbX!c}|8r`TzxfW6)EmA}-n<&81dQ%DH#@lx=V+PW~# z`sU%^*Z9OOiPgRLR=+KUW8Uho&2(E9McOP4j5XX?SuZhq=tYuTY08**Ma zmTePies=}3NEebj(G7QDW^RKjmAAy3KPLV8_0`0#?0TQH?`<4{9j#(GrBpOyhT8Ll zi2zY-yv)fS+`X-xvcilPzWTDRPrRytkEE02)nQ2QCDdf~y1Lm{aZAVFOj*X2b$95x z#n%4x?3Z<&Tz3Uj4=pz}^|$$z;4?EkJ7K;Xmh352vp?Qf)_7T`SK{Upz5MLlNgJIo ze@D{|SJ{Gz>B1sT%455(hI;ou`U87XG6qAZ+2*x4KcjQFFgL zrnD*NVr`dcgQ%i<&|J`0jjbDj$?rt7C-!X)s&pVlEAwwWTNr=dn)Q3)J`rlNhiJ&? zHMfxWS{dxvYo72w7^V6o`qS0P>0I@yZJ^%`wN`QJ7YU5@^p7|tq5o4j4%IBYDqL+i zer^$(erd{ZMx;^xwqQKZSqk1YeIaw4qB4Fv{8I9#t=<9h?jLB+mIw0*hx;H(^PAK` zCj6q|!Ue;h=6-`0mn7r_k=fx$@anM;l7{D2{kY{{(Q-p`utazjU#8?o#CMg;p7N8W z(^E*{?e%!DWcK3K_5IO@yp4%>3$1thvXl0*T>(pX)@BIAkF5u%T$cuBmc&FBV;snm zhHe|%T=XCCcYBYXL_5eSW(LN~PPHhQUH;Kwv%T%P3v|d4CV$OQzKr}$uHc*GqCI2s zqE3ZhVKU1q$#qt+`qsQF&T7Mv3$zzqk$y6MD^Sa_q90dQQPL1$dRBRGaAeO9w;s|e zC=w7S)SFIMnYp3Ba9Z9kZ6a7fy2be!;HRJmoVNr%T-w{*0uBZ`ZV!jcI-TT6(J`OgLt_j;A%1 z>4T6iyO8M6v+h4#ZL{N&Do5X?ceetHp|b>+v5A63ujz)yxy48!1;TQ7m5q7RFSpfV z?Kc&|HPiFsPecFwD6S9*l&13EX4s$GP2GI3tJ$*^q~+;}4(fgW6jZ+X+LY<1VD)bh z(>qJN23vA&>;@=;NpN7ud^>Bi~& z<2pjueJ-jbjG-gwg6^_=%ii0{*B+%B1wG-O;%Cqz8a>K?zU)eBeI{)G2nl-rXV~qY zAKrqbBfXVU;^pB~@H6U>_XFtVZqm%kgCrS>H*HJqPmW2Ps9>DDT;XWFcl(5Z<-)AP z(R47A^Q9!)gKrN@b?7EOTIIS2tR%qymXI~I6$Q^@>TN?nfw)ide?D|&^VIc012@%4 z;wKXaYb5>aE9(bF4S4bOk^@^_GtI;D>OEgojf+iJiziUSpeXbMWkIW4cICOfJ*eh>lTWAokodT%-^aFe zi(l!dmX@quDE@wAdJ*V~b~~0YICRitbfGG;rsyaYchx&9fXYjLC-k*y>M?Wc;+t>i zxrD0i`Hsi@43=J`8w~d5MOC6+EBd)-<^6I+2#w$w^%aW$T}>zZTK7jQGwbme*M217 zN066BLdv(^Qh@J3JNA0OdX1S(v&N4!e`8KE2~E1yHMBF?qsAmqf4dv^Ek*rW zNX62rV~L*AJ$~M=PlBTU)UUyd6VvIiUI@v9Q()RRwI7Rg`Gn5e9?LX_uBRBa5lMPN z!A1{{&&FN?@L8Xg;f2u5ORxQu>XN=x%5ik;4xE#0BfQ4G4fpDD=?m+L8pAXjTp;w& zoMNDhICpR7-uDtefDioEJzfZPbzI$@lNajS9c@^AJ>1Vldv(2%aaY-Z|ByHGx7@`3 z4zLrs7hPxfYAf&y7qWHd77rIu60xa3UohIRakB+SJIxgejdg!LSH{fU_VkySmV|k|1Jk!8d@jGe zBEDRIl)Sch3gs^(AmehB8@~B|nvUFE<77>W?Z9Xs8foFx)|^f6&eysNH%d_FIef=< zTz!OCW?KD=9-9PaE%A`iowhOj5x6d>B_8{XXDhxLi8~?s(9-`}d9fb$ruQ%Y$ARwJ zf(IUp69R$wg3UmahIWaAolK zZYQ(X+I86**qN8e_PE2@*%FFohHIEcH)309rnaU+UG4)x#uIO?FHWj76sA^4a7Cms z987dn2;A^WKD{`^YsJ%CQLM-6$Ud}TGJ;?l_nDqgGcq0(e~&HE0W^FNNO7kA5~GNe zVzPn;^P5}-zwmdXY0@kG>k3ewrV|sU&iqVg&n6CVB)A$q{4qWE@Jce*n5U0DlDzY1 zmxyxceX6|_NR}RFlWDYQRO!* z#(Rf0q(Q`$=(@+++mYLNm%ypG5ZJyNA=eCuTqk$m#$x0#ljiQcU;IR z{;i|Nh8Hp6Ov0dNrOD8R_2yd>(?&p_36bF^M+{H>ws>=!XBNI#Zzk$Y1@6RM0T$Jv znlA{}++k^v#|q24=eyz`v~>|S*n?WkmAW_hC@a!3mu<{vz6FszzQ2hk#QeBJMDe2zKsieEqSX1v;VT|%u-U-)DQVD z-|v&L?ydKY4f)+I!j|P_pL--(E{+(8^riGXcMT(eG&Et&lk2kEqhHU=_|1kjDimb@ z8d)2aUaPV<7fECD5N_B_O9QhNlF0=d{`S>U5?IwwTS>S|{1?^iG{vn^1!+LCvt3OYkqJ4h3|NKttMS-%}ndw zxhz8xDzXUhx^taCA_k@@HKQ4KSJqeo2HmAU$jv%^M{d_!-s18B20rss%QglJU3kOc z9Y1s)21Xn6(s3;+$#XpXQ)%!;HgB90!6V__l=>Wdd!}BFyQ93A!L14H=(Ghkn|-V9 zVsALZY2K#~gvo;xI%aNGi*KA*FQ(b}Po?nWz%jxyiQRnp%ossO?-)p2sHdt94}Lx` zlj=8|VplS|9nP%f9r+&D+^9?4bmLuoc|gkf+WuGjTP*mjfA-gUXTA5@e)kHuBjtHN zc{IrCUz&Z<<*EAHNxl8hWbDboeHC)rn5~><&BSz|1Lxixd)>_1m%?ySm)_XGEcUMc zQz`M?GuV@?n3J1^p# z=naS*^6_AnTwBfmL(??|*0nb4G)7}Jwi`ESoEwcC}l91>Pf+;$P}+$U3t&A#}vocXx?M{sCZ zCgSZeUU}c&jmHbT572T0h8(?5qiSoSeBFKx+Vw}{c_V2MZ-0a2kzDROKCq^{VCz;# zGeUi}T)fUxM@wPd*up=Gbb?@{9@edVL&cd-Id|+>SK-~+hBJ=`_;*~dkD5;I4!>+$J?>TJQ!K&U{l_Ol7Wz3q{~o^N{2?%sMsB ztVIzMRt1-5dc5zO)or(`Ys{^RTDyvRx^@OVE{~2ZdRGC(1(M%JD+&2TSJ ztq#vVy7#Q|TU|=m_IdR5xwQz{25rg~)wE#Y@w7Bb`}nN97yoHl9cO0l{?x3-coUA} zk9Rpy^(6G(noUt~C+L;vvQydHR=2s-cV{2}HeSmc->6_*VGwFPZ#j$7q3gm$kE0Q& zVbmP;rB}Ct27dm z6oO*(QUGb?h?Nu4Wp8oR@`2(yYYqR=jT$#iZJ<6oea!OFrQnmvtjvSV@`{etqtI@b zKq2Z4S&IM7u;nIrD2k;|0g(@%rSZ0Do37h4Q^+R-eiwZa@4(1?axiB^G(s}D$vosR z0Pd(cg-+g8tH=5Qa>dJ5cJ;W2I`+HA3I+ODZ=>CpuWL%q?cKp8fh?)o^tgsImvN1j zj+^%<9ir3yUvsQW3-xt8^IUVe=~--N={p1U8a=Jny|&>Qi>(Y+rdr-d@^L=Tyt z>Z|7Y*V?{2AEO`)Pc&0!HLjIw=4pn=)k-60ICoG?HJRyM$9uan?C^d3+o@Z#PU>oi zY-vBSR8`rCb9s_}+jhXktEKu+1;rIx&h@vIPthm%B%dYj8Hy>e9f5{f$I_03-P@4z zi=ldWkovpRlS#!q--;*C)`h?BI$4OG?UC@CL|KocvdX&X)bqQEixb!Ir3uzKDczf} z?s(EAoysMT`+GdNKyJ=*-uhiR-e&OPD=Y4+k_7)|%gK?8>(Pal>yn8}*Y$`u{X6IE z0Hl6oS-`~XjT_>~0v=8oOhHH1ceU+%7M?w_O&9W;#}d3E58UigCCjLH@ZwItt!XHi zs&@VQutn_fOA=C!_W^z@tYV&rm=d znr9R5cC=T+Z2o%_dvR1Xe%CFl8gh{O0jd{Q&+R5(dr3>JtcJRdTvt%IryP+TXVxQU zlc-Cw_+?Rmn{bolZ|(PLB7Zhdj;MPp`H04xlTQ8xtVja18eIxM?ou%~gR|N~tUxAY zKixQOtEFn-8HwO;CrisRCB~Zok$rNY9&zFH-q=%3mEFRsBi&n8%DVkQcaTbv!gA*x z=bx_btdBqY={zoDyzdgDjak7%!qx2$8$O#DmUG{0P+fM>P$GhE`SDp?y@7#$UM;NF zVfM4CjF$~gt>>8zpEDg0OdTzaJDSlw?(@B3nt8I3hlH8)JQ|OYYgoEev2x&FoX_E> zpHuXj*spL6ZEwW91WdyloYRFh$qM|UtR6bQSnd4%)v@{h?LB;C5LcsE!|pr!Ka&-m zGUV2s&F*Nk!7CI5kn4YY-4bi^HOD2(?hJxHz)WE;im{V~fLSL{ue)(FuhwGQJ`pQk z(Ma(mDK?+WuNSHSh1L5f|Mi(QM$2xe7rL#+XgMrsvdLM5Gi5W&uAf6=axevFg|)}+ z4ML9-lc$&w%TYwGq{5!_Dao3$z>#p2=tbkai>ZcVIdjOaNnAEK>2qq=L53}N zxliJ3W*_UG-P_kLd#?SJx-G=QbQTYvzNY*&=Z!dk;PDk&+=fh-kz-+nXlxe`jo7zo zu;0=pYZJ+>`SRbCo=lx8KS2=)wYcKTioiQBh{}w}y!D9{4X)h%y_7rJW0`Q0%w`Uj z(CjS!<8+g@x_rnyc;7a^ySvu&uoWZ=ZtR_~SWSCbA$@U|So0HJmfXJAheQW+!uyW9 z<_fx#5ScuX68dKpJP@DQ3aCuqVEZ@WW zcK6gEbnE$tsg_9JfyGBrSYR1EQL}Z)cu?R!Lq@{)NYiE7$a|!-dEwHe@K`F89}{@y zbI+=urO!pej@XGC@9dh;G|(|oz%%}k|F{gsTN>{M2vAbE$_*Ye?z zi${a(x#pvUr)x>g#G{{oR`pn?$PyXP0)*UV@YmgiYmGC6MTi18nRmdzyQ^b6`Ltz-;OHxF7XvNNK5ngg1YYti*6#zhm!EXO{mf<(=pe1c1Hv zWgFF~r=o>@*v#1;?BTeaf@7+|cH=!eg=+fJhtA(C=NilG7uUD5Y~JuAiuaxDTO8ff zT6m3#a){s(XQz|yNbn7c5&4GBkx6E>rU$IHyN1SUIRehcJeBTojBJ}dt z$xPqQSVPqaq2MfWqN?uK_4fLZCSDfGW|7Z;*6R~wDFp6ofHgjn_v^sC(DQN3zJrBY z*(ca$xnH(%OH`%TPp@7EhC~K0=y$cOE)aUu>lpt+cYf#Q327FbggkdXK2AeujH6fd z)RN{?B%!c(!hL2ANvN=a^s0K&dhak!y`e|hvrVB{C2$==;p411!dkDp6tJU+Yjx5?&<=o7yGt*M$ zohEaNnTB~$P0vp-ZGVFwmTJp;S^gP(`<&I|_&v@s>a3v}#dj5a6O51S z;`8Fw%qt9pJ-$mx>|c}R+=EB($B!&sK|enWoQ0FQ2ywp$LrPFOA@|8bkQTNIe{#ZI z-8#9SPxU9*X+w`ylSdtnxv_(?1*7;&P$@UZ7I26xZTy3CpM~*?42Y~M=t{WTV>m}p zcfy#CK@rCX98ZIS_srJ#*jsVm0rv`n!d2$`SYk=G1Deu(?~CqYP^9JN@~U$@2Crk{ z=-PbJM)sL;d-Q)l<~z6sm2*ds^kxnG%GlvYjjS0aRw80 zaVy$2T}*g@bw0K0nhd-fUmD~VR#wA(xb%AKA=ST&kh;YRr2hqry%m1Q*yxMiuTE74 z&zYc2YrLD45l5tK9M5H0q~~B}EuTwaYJEt1#>dAlnU~KF5;t$39W=wi!rTh&H00h; zaM4s>e$NqnR^wsECL_Q5s=wIOVUbFWY$Fg_ki0{It^fhSvxQ(lXf~keL21k1x zUjEyFXxq-HPh?@GAs8MQJ1o@i60G@KE_iFjp5Anll&tZ^bZu7IgG9ieuyhGOkxz;E9vP3WrHfpLwdfj*fV&Q0TFa7Z2Ocln+o{ zmx_1(F>^!pW{Z4{gM9y~#1GT?+Rg=rnAe)C2l=_$NtS>)=Q=BfIeT{ZDge=rn%aUc z5Cb~iG9{}&E8D_Tg?rlWIOoG0_6Y%s=GSQc(glrEReHG+eB{tp-7jAUfCoa|53h?| zDW6v^K9`F<{?P)>2RF%WiuN+U+>m9#(RO~oHL12Tc)9Vq=ecyjYZW_tg=p0gFz5T!uJB z*Qnn#O4=eP)@+#{373vOY&sAub1htPGkQk)c8_Ki>xeWOs;AM98(s}!EWo{-8l7Fs z!ue6c=ClkY9jlnsm(__av17x*4zyay6UroU#;Y#!TllshJW(Od=2Tm2bNfM!T!W1w z3ljB{r*E}|P=sJ}-_Aq8igYSL7-W!*QWSw0xfrb5(O0nNS=nY;GAo#Ky0|NgjYA2) zVn^~vrqz^5X|Lxk(0tr^Bo>!`%@L7ix=rLNt}^Gi&Fhw_odTCx$~Td_Zli&8K#Y7s zb4n}&#Q4T%DF0m#hmLUm1XIR`GtY#=EHmdy4OLPuj z8lfeYGvZ1z`iV=M-eGU}*R3;qOijdVqkm)=%Hgc(2H{#qr1%`1q`zS=y^q=#* zn-@>eX5hsrPPc-y{oVusclhYXwBEdJmm?_n*_;z80=$;9rc&2|7-VAc&Jgb!uE!uk z)%Kxu{fXtggpCRRp>nT!@vKen!8gJ+fGis>)EMmHq~b%VglKak`FmB<6nBY(;J7r} zqXsLe0^|DEGbY2-T4H;DI;T{~Y9&GIAMeSzJ|LUXDnHy`CQiP`@w>x?9GW@?$PPX` zCk|+RZlA??yc*g$@5=?afP9f?dL>N(XQ@h#Ig+CiI%wW!J13r;BLjq)Y5s7!KoIP| zGPRd{qX!6(tm`ADt8kpf-krrvfwQTBPk!G{KSOiOo17n=Bi{Pn(SjSxO>`K|>qu)| z1%y0AN7m_xc-%;7K}Gk4Bp-%j$$y2j#vY~pOr!?B`QQcykr7ryUr|u-# zq6N!iB}$1DJ{=SwxuKQwlfgbuHX5~&OJud{Il>|?ZrCsKb{t~;QS8$WkdeCkszOa` zsOE3`ztIZn>Pfm^+*+`dtI~=snDFpSpFU8|r9j<@dY?(?*2xlJ>L~;!r4pDexwm`e z3dekM4gHRFhD3RPnR{GqnSsAWqn;{J24Hn2jD6#`Z#ZPbz2Mr2HfoJ?p|!_QMYQ|$ z-w~UPbQGK-@91GOx2ehdoKf#|Z2GG;mU8$clgdr0n zqe&9B+8i`pen{V=gtEbXrUjx=__KG~NxdD>5r%BIf%7%iu4S=y%|PBy>rTRM%10eK zKK!@Om*{s=SP~+Ah}OgVAlTCy7A3cngLE zMC=d~EKWqT1OaSu9*x(jff&u0eGM+ltiK(wY-(@o&>H!o_=~dx%xNuB9x-ypo|K!{ zF7c1nzaOq15G{S_hN3lE&S-5w#vHb#D`!#bv-M_>=|JZ!OAYhnQ4D(e8T!#J!BB2a zk*$2s*)EtMFHxTEQbARRpJ9ngTv(>)8z>l|X+4-OKERPPgsq_61rWKpl5)lp0#F4c z!pf{7QHZDAjPm2h(%#I=p7kN>d|e6-Bzv23ue28lTv>3Xq|ZE|-=!5_xAl4LXO7;* zC@Qi8ZvE26} zyTg6#-Rt|3!kN_wZcaAziX!Y{pfZq>^n*M_E(5RU=>+N@{5Nm7&o2QHt$@1_%^qd` zDHID13I(S)Tgl!d++CZ?j;l3-=h;9EDcOG|T8*P{XxSUb0lwEXc5P!*Ek)BgTq!>?}bESR!85|d|`~4IA zyOhV1)0y9R;0@(G(zV*AqdQ|M~vypBx`f`)!Audj^!I^}6a>qNon@bt1S@HsZ=W^QHwp4pWA3@<)PVCkuXJ;KW_ zrVzhg|42QbINb2PC)}fsW;;pVb$!$xn|OZ6OEP$fwd`(Vv$!~)RJ-O+2dH!E4aBM{61d< zZ##5rthC$xdh!BkU1az-g{SsI^o7a=$z?k7{nlwG8Pk~W%gEa1BeswQ@23Lrnd6(%;Zv8`uwv!ceD4~GwiJZA&J}S_xnAC6 zg{B6dmuMP+o|*e57KbOu-r;1a}{(Yj;dSUn`_z^!aYmT2v7lS$ME)+Zuv%zlTKzf~SXol8i$eqg8ZbM^gG2qN zlA|h2!tibPb8v)ovVe?;9c-pF8l$EIae|xVf^8FifBMhco(qL#AGuX~_A%470oND7 zj{|rjFQ0pEwWZ=vfHs?_G^$?0E8aG*RcjVm)qp=r&rcB!K_ie@xE}aL2PV^Rd~7DyO5_d!-*4PcR=nIDKzP8!JLUXY{5Msu z{^JsB?9@4(GDR1$yDJ|ginVy?OAt;UzMg1jus@+-4%XJ<%_Ce?E1A?-8$3;fB9m&Y z`eeTsz}_hqG@;mi-ZIQVKfc-LL}C4#QZbgnwOBcVOGX1IG+yQ^*)0|%c6R_r2Dx~) zm0gnrHNvU922`LIe6T@)d*rUPuGC|3j$GRBL_3~V(U$YisZW>S#-Th140XE z{z2ySJhBpTj*C0*1I8gD+Q2dxmLEw0;?$(Tu^@Hli4bByu^98(1simr#)THi{&4nA zO7mj{GIXTj^-5ITh}nB%X$d<;It`|uPg5thP?ankZh28UC-Rch;zi&a@1h{(li|WGO+-sXj zDf#?EP*QnH*oW0R>)bOr`w8L>weRGP&cgO4~l6Mdz9eKAS-$H&m{r~Hn@z_jetBM(Ka>d#^#@s0c+Nur2*J-e|#D8$d- zG&4lXN5FjKJR`<+nyaqOC-!Qc_{-DIn4#D4y9ipp?|tJ$k&%j0jVd*Kf>#wGNF^@5 zkqDzuhy}B5`fObfg^S--vi#&EOt1>&^6aI)&*|BQ+47u%CX0ZT2?& zg%>|yxxzl_8LSOyB_*O^`|?trPYXZQU6E5j(pM-Y?SJ|ROBFcw6MI{^`|M3HcG z)#sFkYW{iLBMw>%fz2QuV$?o~uwI97$P)wWD-YMBdYxJPCrPqJ5YnyI>KQta?75V* zu8SnhKrHb6;xkL1FuiXWszMaD=A_o0&D3uAz%Q^I;PJsN^zB|6gzBwtENDxSC%2fF zf8ckt;V@+E@^X6p7bTou`^%_>Z%vwCyLrRvH(|s$>v`FhtNG-&mkvLFBhR<2g&dz- z?DG)iBzr$93V6YPf!9}=A{=rFzuJEH&x6j;SytL+)jbkE$~a+m2z@dX(9p-bX!8d6 zb+Ux^%jIS4Fr_+HF`xE&E*yBcC9?xwm+Kul%8pt-dnnB%v zkI9$^02#d8vApAsS!^kPPr9Bs|K%Lv5;+K|0FT>AntF|+uj&I4A{vqn?od_gP z;;{cKH3EXHnoly>gwSLL@SX1z)^vq)+5MV78;co((2BV;v_!ISjyE8V$wz5@w7yBA zuwM>MlK!idXd|%UW1K%7;XBjtw&ly|*$auHGe%O4&zNTSy8|U?@kL$kqU*grqxown zQ}oOY!IoPH*`|e714zS~iZjk;`}+ze0xv$|RF|{Wo~6zie#&$wSYnMm2&5|;pnd6( zN)Y;+L{##qY|)#XVsMsFO{OCy+rzs2!JL}x^mh#dRUo|?MN`5ztD`KJ&2+yv`W%f? zr~X@t-F$dB^9`izc(|@-%=|;E5F{Q|Q#RP3`qyYEi}8XveZ^)JQP{ZM2bix59|%6> zao@bJUxu1fQi_8%LIXyfadtCo^a1MxW&~2L%08zR~9)h?ET^(Zo?@tqaVKjlfi~)=mC77(+9SojzJSMrq z5g0$Nc2U60;#Z`?%x#5Pm(NHSk>cyrUZvQL$%RV7fGEsOM_oGUWH>w{8pv` zV%R|o%kQWH7yRBn148>Ed?hh{07ZPF3;LG*RFt>dLW9O9pyM2UpW`0bqG84K2fv96 z3LWz44>G!$ic^Y~0J(r4VM8s&>8yH-?jj}lK}J#ot3@q0?5D_cY(#NbOEB1(<69?X zcD=&UON&ak{7Skp4AH4&^ci}u15$3wPhB?!N3mtJa@*T45nSsmglzuv-HoRsDb61p z1>OEK>p$!v8zq;BIAq~DHS9wNePh-R-Q_O*2kcP@rBP-fingUX57VQh19Wg0GZgv;f!et-k-pAI+QRZ7&fhBKknaOR9^ zNVflpOgE&Q4SGp#Yx`uSLl3+C{Of}4&OotRx=M+s@aJ7!ts;fH&RJ<0mXS7HZhT-C zhJ@n3fKxKD1S_3<_lG-;#i)e{frVy$Y$I7p;RdF1A!69y%kQY}=#%XhuP;q)`v!eE2UR&O6i%{mUCj4r=u|G3KlM>= zzJg3<-X1Xtt-GNDY(U6tDTF(|B3~;6U2%F(z&O^f!=weWfMpMcZ!5^OVju-bzzgnw zVPRG_jsW@y8sQsqR-!xx;3KWDw914PgWx4)YmVWceO+eqg%z|2L{iK_&!QvAUJu4L zN+*$GF+6P)xH;uWSFHnKJ$GZ#%VeLd^}rP`lsOPd5?ZC;(|nBz|A7(@Tl zmFFD77r^k4KNxutgVJ8@E?*$kX&#E*QKbmyi_x*v{tttXsdonvIyDwLFP7+woXE9f ziBdZ)RWZmr8K=lxdl2GrbS7^Q| z3Hh_%cnD?EvNR;GKE`?Mpt`NNo^NAY}fSqe4V6a;$LP8|pP>&AzKK1mc#doaq= zYF4%ic3EJHGSa|#i1#q!a6>!JSA(|sy1pK?ER+=5rea9Q@~J}Dlq7V2uT+)3d@K!H z7JXxnv@5|*1*mf_c9vzhhr%5hS}Lq`PI4PuN^ae*y=2UQtQKmL(CAU>ZA2v8NRyFV z4U1|@nbJ^5vW2=glIB_08N3x&sE^5x6wKt~^E|9IwHEHnbKR{uU^Vb`jf`@{1rlH+ zcB{32jNjiM6J2#yR`H(n?}ddZ&s3qK-L)ePmjZ%S{)d7i#Tz%3j-5KR7tn^U4!##9 zNFVt7f6Q~=l9G#?+}Pf{x77WqWG2qJt}tjqP3Qm zLzn=nBQB^U{}Cg}dki^HkYwUNLBjM770{H>1dkGg=~$8=Eg>oPvmxITo5DK|-C%LX zqoG_`8}k}RrN#*C08D!f@7zqTU1bJBqgu;oZvh|*jZ6w*Ak8w|HT&#qKhuNZsD|?$en<{ z=xe#d^2pnP`oC&nV33~}!PiVV?j)Q-w;jZYf3?DvQu-nLnK zF!zvNk~o=Xc;W(kj@W2q{Yv)O#vIWK=D zzEBL7(EY1)PVt|RSS@NqooV%HItY^!S_Xpi$O(VMMCT@))U9hv+xi#PPj=RC(FprFR#@#=QQBTOv4y%cLQD zFq01`Q$s%F;;KG$#>CoX^FHH!R%v4mI`lg%4Q;uIh2+Thjmf6!XU@$n zO~@7BnHY~aKYi@cEyakoQZ#kPR&)6D{2Y$4Tl$|_pr)TV1r=s(upN>GrWUtCHm=l> z)DVO0#!q7TVDN+Jk&!SV25ThFxVISO&jc&>+XfGA#h4M1E?6zaX^utzJ!@mR()y*~ zQjE#`g^x0AJcNZ&U1^{6g5V|fta2SP%#Fs(m#7I|6cP`rR9g0_5Lj+}Wis=KKZ|zb<;DJM&qC-8>9PvkO z|1sk3-L+HmZi1u6&32~p6-2%}Kw8=3^qjKvGTi~@1+y`D25*&bOB#8_d3|%l8fg=f zbOk5%^t{$ z4dJayJf5X6j340!Kx?u%!%SLI{7IBNiveJh0=3EA!Czf6ybGSSbb@omf5pBlv9k+>c$-8b)-E8mld7~8NEcr zDMFyfg@DjJ;53D7PB8fdbK4(@F*c#)Wm~E&D791)qtQOOP)e%*93dP+FwgDnwIzd` zZfW^K)$rW;0y;HGPBcQZ2uJ0|8j~CM(9GpOXdoCAau{$9QX|n=JYu%yaAxwKB3)GI zUK&!&DJobmeff;L!1u=RsjdsnBZd<#!I?MYa`Q;;WEpe~8J`w`Dr`DIitvl;uC4h# zwpxUK?KeqGrJ#cRJYRxY4sO(`QeI>tIZQa&*R_!Li7ga6r^v-HDb3=IjP0yMUpU<5 z1&B8h)Aa$>ix{ek+>li~{}X=jjXS#QX3lxZCwVqr8LDV|jIz`CL?j_o0*&c9*% zu|X)+^|%rIIk@TMin2;)Dw`EAOBKyOk~qjtC@C|yDG{OrS>u=ZNdg%OPJ)AN-5q|J zFfRxYAf!$T&hVX*IGN!C5}%uw^GmAbg=TpP_{fdR*Mi5-4!xZXp1NQz-=1uQ9}W(O zE1}ni*Y=*Lpe=QBk%VCeridn(@213T96ETvL%;zqtsQSR+Un@XEe(}h<1{m;Li&7y zCbA$EeEGLI&Lbe0dA)vF=QG8iO5A_7E!ag;a;tmdmrPkicIxu%2K#G7->D0!YI!GJ zbiP!(8diluBAw)mohmaff?tvW80ic-uHi>J)){rOD&gPCi{Nl91mbW$HmzYEbs-`f zJ8s_Q$8l%G$*Rr31OLt{-xF<0O&!&odV1TG2DBD8pfKjw_!(W5?1wMpG!}PWrhRaU zZzi%0-RIh<3(6Sb+axwTWah|pncrz1xKY8z-KAD=b*nGC4x7sKxq_4f_+06xuFYak z>UL{6(teZX)_aU%tWSsd-0o#IksYIn&Xs^Sg4S~(IFg>@Kc+oT>fER)j=hhI%(5C} z`iCn}>oYu+ecSl!3#3XDjhZ${Ql(#?;t{OxY&NBQnIpvu2Z&d3_8Fi)KmDDrFE6fSzC{Z{Jek}fKPu)!-awi@tA55)~-(0JZOyt9h(Dd7wTK!3wh$nnTe zKxPsrOtAdED-4RiUhP%G&|t%T4h==ynGZ=t0Z}xnA`)&53`B#Syxt^UiDs4Es@6BY zF&<7mE2@6XbNSVBUzjxq`g$de1tI4Y|Dy0TH_rzvl9w5F8><2WLG`~b4db`h?SXTa zpcBHix^X|~SDZ(bk(&mT-;nmg5geh>Yi5$Ll7RgIYP=(m^K+?CHvbVF`D#vh80^Mi zSBv;v@0!PCy2UOPb4`!Q-nx3k<+Z@2gxVd^G0BCg4+WGzlPgu!9UBkVq|{r;H@<Jvpj0z)a-qz{;BS1BG%i{HD{Ii~%T)$J>#CU-A1_L6)dg9e`v z_;kw?{zLLSL*7TZ-$mR&$7}0Sm;m?Kn;gXO9JjQ2DFY<1o9?q9k4OS zxpCf^7_Msm;Wo$M=so8|OQ!#-Y@VPbr@Iun4Ms!UQ^Y=iUdu2kB$V7HRD?XR^Rnyc z{NC`H*0FMt($-<>PuJP=&VHr`)`h`Q)8lGS!=sLa=#G`AoAspyj0Zux?^-Vk$htND zGRh#zhv6swy2@4|CLOs7{`~_ApY%qCahpCT``yxmy|zDjCM@MDl0W%Ay-r_9c<*2f zr_Ec{=I4lSWzt5isJwWTaVgs)n6f}jMFP}S*i!GUs5defUW6-}&hHOQ!mNr*YEBXp zM~>~vzlZZP_dkjA;;*N^Nhc~pNpbN#yLVN7&d;?ft%CMwHkX*b89^$u*oSrN(r3}M z%u}mR-3!wTj2!sT)hBhT*R41J-gB}}nm{!Wwnp&~hDnSSl{AFd1*cUSr6~gjP@D1Of$HrXtt6zHmwY!e)jk@JhFk0C+$iAn( zHrWntV4g7)^X>V7u_F#un#SP*w`@GExiC#;QCwqB#5-r10lTv&K0y_BC8vIw$C1bI z{kKiZh_|><%?<9h7*4)P=PTp^17d#|=By$HEQJ*N{D|XspMr0VLHbRb_`vp(QAQ}P zA%L)Aw8X$_ac>iHMn-}K$2+rjU_$5y zH)KY28WCFBx{U36*K5*MpiI(Bj3YB2MA;Q{xz^(9K%O&eZ)@yY9t*zWlxRwl!`ML) z=dF~g#R!Z9Ja4n{P2o{b$beP00mPE|6UqZmH6!H_KKkxH4mBxsi2a5V|Jn!fkUSYv zO_ocjw3VF&qb8#=zkfQkw8s>T*1)rPBfms;&%q{d%`QZr9iRpHS-4p=M3w`xBc@t; z?XPsJ+P(vE3$CcENh;3ps8`X7r&j8~uyp2!aKZjtpxo*X+?dq2hg!}x()ejo08S)Y@Fos&^$9+t(A z2AQ3#mgAE3U))MiOx#1p9A0c;F<*@~yZv(#eEGHEYyEsWGqBFN4byS&qe`y0BCt-v zwVX)OKG%m1V2(x|NsQX^-Q;ogQ2m8Uwt?HiFK}I^ud%`k+!Nm(!D)LkD*39>iq;{P z)=IHT0B<+sX7?UTnlQ>=n%LA{l-c8xxBREv);$6PThlcnO0h?T6jFKIG&!-Ebznz6TzDA zF)HA>t&;3Mw%@0k6;CH7{Po-Du;3ybVN> z5IQAzl0Io2sb5b`LYc5#LnYA9U_v}BWfi`jchy?y9dp>suX_ilF6RYd%_c3yl9fMU zXy@6tPy3O?ZJBKh53H}BtQKlRD$xl1R*4e7(C$XjRE6(OfakTkSB?9UT(beR4Jf^& z@eBN|%#~uwJl77qzLtKRoy~F>A8Cy}Rp|fr0%vL8j)TpYQB|OD4XiCl(0Y`cP<3)S zb%f}cJhyEXqTo`jn=^~wVEXDl&mdLEpb8+?u1($5 zf;?`#fF5nPzjl*b=~mMsOAw7i>0u>m3k%tcxJ-fye!}v1Oc;JYIJtEU1$pPW%G4ff zd3i<1tjB81PFY+CRLbT`sC@6a$i7*A&L{B?f+|3|f}U{pMl*wz|JZzv0SwIon`@Qz z*}@cImE_9J$3>?o`7GqRNXgMG8QaWo9`LGk*Z=yeqbvezj3LoD;UrHdsHfVLWjPIx z8%QFK>@N8O+5adt^7^2z93FOk@bQ4zIw>Ob!<;RgO%@UC)3ht*m+Om43}EDUe48m0 zcq_A10Fdqq?g`ANy9#vpb8QmD%J>uEf4UD{V((D`KCDJN)_Xn)l5lDYwl7gG1|!$F zw1HH7zl`JeDiVuKTo~YKKY;F}LngQj(J3oK|MUn)v!Qu?F`)RU`aKTQKNm$4J?~9! zph;b>z{KI9q(YigB3Uzx|CEtTHME7I4|wu)fsFwmH&~#QKix{{QG0z(C%W`mEUNc_ z;{;%xAy;eC@G4q*uADmn6q%&Hg{F0-K>d^~safxY+>b=Te+@5cUh(Y;7r^+1>TH7^e|E6G=xq#2 zU_fX%;l)#+$#FH4#5!?9cm4jGNty%HH~@J9Bw&z1s2@X!ymxH{H`5gV+NgeBA_#2C z^8b$UPyXH6Cl?o(tf(>|inmUb2%$gu4LIPu_MDWidb3WhiDMCit73jd7Hb(Q_u2Y5 z(tavOS1}6T#cUnpj89Ka-iUVxN;q)g)e>(mQDpGg>1K3c+)CQ8Da zOM2^L;V@Pom#J`j$=@`hp9ssWL^`@_e8H&voF^J>ZIP%C)?{D}>>0^!Xfb72!Vi(Dr$>0m2gu4t_C3e-HC@&=I8lbI%U<+6tCAjS$L_>*J92DP;*g8$$ zSaU1Rv>aE3Nge85=4thGoszfE&Hl=?Mx4E%Z0{k3#l8{*vY*Qxs8@RajJVN`Qc4oC zT_LiB%OkSz{3p6Y%bSc9QVHd-o{4uHpGJ!%(Dm(5s5$Cg*{L@gv) zMJoM)JvmwMyQIP;OHI}K!tG$VIbE-V;ZxsiTAkZjk_OU6=k)ns=2zN2)kz#}gCVA0 zM^=YVXS*B&64!gdKf zs>h`YOTn1Eg>9_ttajJ_lQ@}eO<+)$3k$8hEB1`F5$+3_*-5_681Z-jdy~LDLyA0O4@k*9D+Q5 zPf=&{uP>DHyL{GIGW}Q-FtyNNbbiHMUFk2ep)8IKzX9~JG#ztSmD97+TTSJsL9ZGU z*!LA~O4MC4&2vEnGCriw0DU!3sBr-1c{P6@XwXwA))V^v6JwLBhLy>m{Q5S_jXDjC zH?03)0W@?M5oU~m*ZpGja`PSDYJS!6Hupv0Zzo|(Q=WBp!a(0;%5QI#D+S&Nm0ng^ z-CcqLAn$a*L1Vb74w;Y*>PdS6)Ney8B;j+TL_0AuCto*m>nT|i18mbPR zb5uQhRi-keUD4L|43gWdaSS^gkd(jv=7+Y$vze zDD1+7c)RZzh=Sg~I`#wXdFK>M=;eOUTU|A|Lmr(nFo07v#YqTrQPwj})c%h~63! zjo1PsgZ)|hJT`O|dQ!;m!&ICASYASTV*Q|8L{0DvH>7@Lh-j(fk?g|F!qkZ&7_;xO9V* zNS8>5igY7_QbVJX(lsE_u5=sgVp#qXa*U&97bPg#ZF*E}VeaFxD{ulS2U(OHb z+0V1i-tSubU9tDtZqm~*6|PXlY64`=k2Jvgg-ak_kh#*|W3x1}dPJh}jp2lsB*!Di z6TyZL95oG&k+O`QOM8NanqJh}T)?L*h0(8|f}3<>7lULo^luTrVQrYL-%(Nub2(H# zVs9p8!9zc~+Sf-^xf!8}2l9lXPvh}W``?mJ0Lcv=_Ae}^1^0M%bd-S&8oCI}lN50` zs1lXLN8-I}Ho4yc%Ce(6EQ(a7!VZ_`J5KXoXIpEZ<7|;ihDw{8`YGX!tR^l~cuEbx zlVdq2)?WYF{;W6d{c)W!#UFy&dvBTj=#L~oH(*u7velTqv^nA6*C%uR1Ycyd(Kp^_ zfS5KYFtY;Z>+$PTyz*02bG)V1Wv|h&-fZz}!vLCjX~NoH6k6cmo@K-QFc+0T(N&1m zaK+8MSdQ>j4qyMHKcfHfhhU1$tT34x}+e>D-|{5#X^~>GR)rva=0m7Npm0CZ5+;|a1{U>p=ohE zwi`P=75UXU6~;9?O?NR5C5d@TYk0^C3ciJe{K#$svPEYyKiQ|uBxA9uYR3|!obRgA z72GCi$CD(x-69t7divUsW!-b9V^}|TzQYy=8E%3dhu32#;rBcil*K%3zM;(Vg4XRG zW%#jnn5dFm6&hdl{?Tpi^>gyBB5tf3OVZ`|{%7n#r;y zb(ace)UYtvK*kKm6G^kwhzX>jF~4_N`Yr^o69?wCsobJp+T9cTdoTe4a(^OmA3zDK z8aQ5zn+{q9%YfoD9u99cqg8olG+6{GI=^(MOUW-=gL3xi-Msm$aKDbZ*P`!Tv=9bH z%E2BR<`5s<_?9!t{Brcf=L8z1zkP0T1ngzVw7c(2hozpIfY1zxGUnpiQAHR zs>5H$4Usgba#4GGOi;H+voX5aHC`qb8>>biHL)7HdX{(fD6UP!r1i0~QHS65Dd&@W zQ52k{jgcNb?_%se^9S%@JK*M|`^o<}Hf9W?$bICyyk34z)DaHVA#@!+m=s*_BM0ee zXZ3l@g$W=ZXAOzyeEGbb-WFaO?%MQ`oQnjYO8)Ff5o-rb|3~T~ty%S3cQooqyp=1j z_{WQV9;`5x%JxBLi4+fmTI>76=TZucy1~Qo79hHO8=G7DQ61FYIDx@Q8B_xzPX6p8)1NxYs2O>H_7JYn9vGi@5to^U|SWFaw!d+mB(ELxK zr2F^l^!3Nd*RlrUc@Z<)!S8B=$d0PyY|_BT&RMogU?{znxcI`8d-wmIv_2lx zezT51D=Q{tcmjBH$#;tG{?r( z`co&X;qzhsHZRem1>Vk}P^-YZinzJ&Uindlm{cB+eOYnpHfJZ zt@gCQ>V9o}+1-9gQ~ZbJd>xJxr>#ZZN6t0MzCvLw)4{bLZs=a9v0MUbVOFgJKdAlW*Rq$0-pVL-q?p?;Q5! zGFAgbl;Ic69sK?pje^@GU0=pdK+c-lf-1s8)LfdaUT+LcKcASh0j6O6J%qDeC|gFejl$=8c@BwEXH)z*xSWWU3$M$LAc0V+&FUSHlTX7rv|qyA3H);KGxj6W{i3XbF_X;7+ijNL|^* z#i_4zr2dHGm*$2xaZ7kexn#kV#5Fwfd8V>am}Gay@%w^{gZtKl0Q|8&fY9GN3mj&0 z?Kyh%kMbh%#wQ%8aF~#};puDJS6bC=&%766dkEGh|EE0!(hJK}af*$XKDjIVbs{;Q zjR8f52Gv$7vk>JtupVz5C_o{eDZpM-i;2g3VDu0axeV3Qi%7SDWIWZaMKR$xR^czREw%>fEox9*V*I*tT&GU9*F9zg zPKdjzM?{#>*^BT%?+c0Z0TMVx0pI@TleCOd)44oin$_H&VRPwolEPKc5<*P&d7`10&MSce_FmhcI zYkmaZtxWObf`3?(9f)cr)nSc1^omMt%aiponzGS$yH-Z7rT&%lllE-?JILBX$;z9b zI}+QnZPB62d*=(Hok8fe^svs)T|i!SKGOxO;{|q=Mms+wpg~)Bad>Y;RQWm`tH0uz zwEKA-WL+fux>?FSv@5z>68}X&Fm}(rNad5T$jau`H8x7`*Qqcmi|3{^H2K)5*WR># zi5}eKdsxJI=CwfmPC;(TW2&8kIh2(v=Id>Sdbflus|ayF(_NM-urMtPC4Gx-eb_73JPH!X#*F&)*4Zm&w- zCnqiSngbTvGnm7{5-3?yg@JUe!u)&Q-N;#xJsSa4?ku!rnFv&!|B&NttK_Qz_y0*x z5VJ`kVytCn^A77J zX6kS0Z`RLY%0q|TYikb_dBjeUBA|F5j9d-9r*b+cAsA?366cC=d9>j}AFieD|{+PiR@**>`!1behe9ii}T~=L7@32P^04jy^E;+Y1Z}2+S1{ukeQ$u&Pb=HL8L`5KOewHcFEdo3E+AI(Cw6$Xcrf4frt_0 zPMc64J3y!P-hIlBv6dJAD-dOj%W^r<6+r zv=ipVU3{lG(4OI?@g*E}0s>L|{K5-)Dsp*8w9n?};p>&ZLIQSn?UjJoqf%Nbo@&ilN37UkKyb(WU|Jn%x(iJL$`pEA-A70(a;e>$Cc#g1#aHgtaoy2h! zC(R!2D#pIB_nAdhlvmGxI>4h_N&Lyd73Y+}p7nlJLiKgaOZ7Q>N=LULB(7uE@=Pe( z6^^rXtP|hk@oe@*86W!%Glpan2E=Xv(lNj_EqO>dmwlVWBhD>GYO^&rqri9}xJoAHSBn1#H9FaIw|^i%O(T)@jZDZTnhLh}%QroAO{2KD z`hKZk(YG!+oHZ)$#z9y3B_q&*OQ$BS03<5d8kT`1l7Az)>1Ww$X(<<`z`vxB7X8sj z>?=2iXTMVrw{uuITi#85L^ z9clUf3(hRv*i*}}dxqkOmxa&`S>7M&7N;i;@j<7K$CO@$v%I9LDo8z+<1^qx4WzlT zMP$t1-o>aEOl!T!cdq@1IBtJ8F%|@Y_d{w;yZ(|`{)d&O#J|!0MKkC@%Gs65E?3)2 zZUEqB?USbt#yDaeR;&`W9@JG3;{?XT2(!&7yOQp`7@vTE0)V3v)x;n`;tMJU!@D}% zJTHL;`WLlV9~s9HYbW;~ew&ssR{S?0^hSS_khBn4st>dI|gm!!%w$AyRXyQ;Qz6kLK^$*+sxdBa@ zE-sEi-%f}n=a56ZslfAvyf*Dx`9Z4uH>*NxX~r354CcBxNw|6>IL|GS8T!<%xG^5S z0;k^{q-Tt+n75A?eypSCY|@$5rj0XFW=voID7o-a6uW7u6BSp%0sT25glzRX%`eWj z)@yhs6-@rk#H7d#ni}75sAYB`?XlL|AK-ia6_IT!{5ms(c$C2JSj3%Nr%bO{De7Z5 zvN(68ZO6(LkDB!-^#hmin!F&ITLQLK4r2ZJl?a|^!fi+SL@*IrZ^AfBlsxbk&>U{(y)8q+rmE-i9p)THQ&h#_Q;iV0KU$b9_YK#U z71!?sNuFq-$dVY2xXx=k^Zhy*Rdwov-N|K|6}JYdwu`-^o=OuOB(J zd_;HCKArsjrkWk=PtTEN~{8U^Gh_Bw_Efe75j| zuF7pEX}HGg5Mg|B@BM`R?g*%mfYb)U?qgYtT0N;E47UU(T!q>GCOp+vqV-PJnyqU+ zzU}Fv-QS0fq(QM^6#1~BRxit5$7xfMO+-7BJ>NHI@{`Q*yp|n`S+!6?{Hvw#A5=ay ziaAKIkg3^usl8#MWNb0h^S47ea#kPgfk8#3c`iK#;-k2Rfb~E)*OtoJ6>{lnGLz`~~E~#fUzm ze|ITnzCsrlpEDipzp9Be9my`j;XNtHrif`!22-QLi-?*d@fQ1UuX(v|+>=$6wub?} zXy4#SRzBL;tzT6GKD}e@k|cJKd!3wK3WN^he`ElrWo6fT0)ZncWlyiFTleTLsm70u zK=ZOS!*RxUedB>o@pROyyTa`j&fSs%GI+XpI9wI-19QgiRBO?24u4=OXE5tO@NJ}wYR58&c;*Dpw+>y{o9Bt}o788}9zDwF0dF6&&UN@X!t~SCyH$D3h z?bC$xrm}*G*SIx5T8X!-p=+cxP|0;>;h1NxEZ+;jKe-C!9asBCgZEy_zw}emNXr)` zNDS;@VfEMrXZ$r;#p{t32@j zUAHZ0j0>L}d%HG|>^*m#T^UTmgomAW<()Z?>>kHCHu1o$iT477rI*TOu`VGJF$#{mKM$ zf6B1IQ==eC*+U}|jC*-w8YJf{oEr*&%$|{c(Fk$wiD^75YrYtD3AbsBs=&oAk;8a` z&so&>aUuz)gLI<-Vfu010eNk8H_KLhHI!ptNy8heFEpnqBjhG$Rrm^B7Ar+^p3q;I zHZ$DvN0D_B1wI);z;w=QiR!K(H(d*EXSDDA zLCG|V>EPQ2=}?pqc(jyL&zw}}kL3O&WdQem&v3qHXeIm_VSf+YKgq@D@*R=Z#=N7R89TTKLrMlBu7&PVTWU9 z1)TON4S%?3?;DjQo4r%f$BRrLSOo0XMiA6=@WL<8z&Q(a%BbScqA|s#W{Y7Yq=&6(a`qUT<-^&ex+D04q-s?5b&L=Ni#3mO*L@( zXiJkTcZk+X6lv}pB&vluFaWebFZ~ONKx-RHlAQz`FLTu&Xe{N?&tGuZemqZy?290$ z`RYaa5pdoChaeF#SlM#5$8FRz-EKzsjXj@lqZ=)xD?!Pf5&l~0<-rdx#?4d|kL|hv z;S$5(k;2scAD=dn{*m?r54*ei04lx2P1gDh|Bb( ztHLM#`g8p-Cl(A9@mtI7f9F||%MP`=z6QH5ydq`bz?@CH*Q^bkB(-~DelL{QShPtb zb=%FXCpQ(iSNCig^I~tXbyCL`EQA~@@b=jj z8ojF%+8L!W`By&4)Qv>Nyti`uJ&l)bfv`X24tOVa471y0;1x0`8bTp&__h;VnNBVmR z(BAXqV&x~Chi(L1@eYA3-dXhfz7b5@^hQWz4js%Si$X9QGKCGzsk(RGcek?uRv85< zFD~F@(6XK`t3tgOcckpPo5gK~qKV=V=CCGdnZ_I37@Pxvv+l!|9JTCkjzs&E_yNVH z&|tS3Ga$v$b8zWM6Ub5IMNrEksZl#K%&nr zMxx#6s~M@IDa;kTr@sZDGVqrLUj%+6gn#b4z`%&t3cn6)G(^W@_Mea5bnou&Of3sJ zqob;UizbOr8Ztco5GzVuZMC`j2nE#M!t%*8Uv)Z*a0 ziD`ZfZ|ny-U#>3keyFGa#p2F(=;Jn8n=skU>$g_$p}xmvWVIhGw1bBBmJI}lGw{6h z$eL7!B|3&~{`w1DfF0$AEQSj*g(WOmqcdF_`eoNWFM{UymEhypO3qTVUKatP(Z~v` z()0c&E%OOPLpQ?<#}3vH*aZMqb6@T`JSfSZdp;^$_L2B%Wk;hsjlMa}V0bhQ&qI{* z@SjjmQ2;=o&sO`c6s7agetN2u<3u`2T3AU=(AEE8 zl)0`Te8kVx1T$Yfh+V&)4SB>SQ3&{5YIkoX{S4K$gFC%a*a;EH_+mI$DA$-{!rzRT z=>Xnoz_?Mc7d&Dim_fI6I9=2p#v66?mTY@3^+Y1KrSHTM!ezc9iWEno9#m)pa(4|D zZmr8vgXg}!JM&@WQa54>I}}q7_TeoH7Kvg>205s!TB(BT_b|2Ffi_piQ7R4MzW^## zW=kzOfo5O9vXN!wt6q)c*oP}DVP`8vD>|hgQqOw8&)HnK`{o%ZwC8Qvgy9`#Hv4}y zE$Z4?8*-%>8kf=GO?WLf%955_PKPK@?KgR@P_cQf z8&EdoTA-`7Hk_xWWw9kT-LXF=buXd2DhadhWH7%hIip)pyg!e+(e)b3Evflw^$!N5 zdpKCb&>dfQ<)$E`pzx+5Zi7eq_8Q=LR$deu7+9E%_R# z)`1ue-XvKFHv}|A`&zJDFr6($wXu{#PCE~W(Z>@G%9X2w1Lr5*?HGiQ+){M5a_CNH zdt2bC^GVM2@0A%*J3pDGNnq0_1+U%LtzTe(Cy-7E@nau*k@ z%=ZE>e@}Fnn@B6ah(Ukx~(v!~i#iyWL!2o$^(o3iRj z@E+Fk``Y|i)AfVGrqr`_b)wy1AukIw$<%1Qi7*qsG_xtE}snt4#?4#K1TK~ zu*gj_Dt>(hUI_gCmK=>0nH^`fJDVcNJHUH(?!+@867;V@>lh<;4EA}Z?nM0Woj`+a oxrFWJYWx2`|C^5gcc$f**rkaPh2I&$`R~79z0g&wP_>TuKRLPok^lez literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable/background.png b/app/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e29b3b59f99290135b0cf3745bc9993ce935b27c GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blZci7-kP61+pZqKgT>LDI5tB{+ Q0fiYnUHx3vIVCg!0BB+iu>b%7 literal 0 HcmV?d00001 diff --git a/app/android/app/src/main/res/drawable/launch_background.xml b/app/android/app/src/main/res/drawable/launch_background.xml index 31d178b5..3fe6b2e8 100644 --- a/app/android/app/src/main/res/drawable/launch_background.xml +++ b/app/android/app/src/main/res/drawable/launch_background.xml @@ -1,10 +1,9 @@ - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/android/app/src/main/res/values-v31/styles.xml b/app/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..1fef03a7 --- /dev/null +++ b/app/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/android/app/src/main/res/values/styles.xml b/app/android/app/src/main/res/values/styles.xml index d74aa35c..c8a5e609 100644 --- a/app/android/app/src/main/res/values/styles.xml +++ b/app/android/app/src/main/res/values/styles.xml @@ -5,6 +5,7 @@ @drawable/launch_background + false