diff --git a/app/lib/helpers/globals.dart b/app/lib/helpers/globals.dart index ffbc2256..bae45b37 100644 --- a/app/lib/helpers/globals.dart +++ b/app/lib/helpers/globals.dart @@ -24,7 +24,6 @@ class Globals { ValueNotifier emailVerified = ValueNotifier(false); ValueNotifier phoneVerified = ValueNotifier(false); - ValueNotifier identityVerified = ValueNotifier(false); final JRouter router = JRouter(); diff --git a/app/lib/helpers/kyc_helpers.dart b/app/lib/helpers/kyc_helpers.dart index 2d578971..40d54cee 100644 --- a/app/lib/helpers/kyc_helpers.dart +++ b/app/lib/helpers/kyc_helpers.dart @@ -1,11 +1,8 @@ import 'dart:convert'; import 'package:flutter_pkid/flutter_pkid.dart'; -import 'package:threebotlogin/models/idenfy.dart'; -import 'package:threebotlogin/services/idenfy_service.dart'; import 'package:threebotlogin/services/migration_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; -import 'package:threebotlogin/services/tfchain_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; @@ -31,16 +28,9 @@ Future fetchPKidData() async { Future handleKYCData( Map emailData, Map phoneData) async { - final address = await getMyAddress(); - final identityVerificationStatus = - await getVerificationStatus(address: address); - - await saveCorrectVerificationStates( - emailData, phoneData, identityVerificationStatus); 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) { @@ -64,39 +54,8 @@ Future handleKYCData( Globals().phoneVerified.value = true; await savePhoneInCorrectFormatPKid(phoneData); } - - if (isIdentityVerified == true) { - Globals().identityVerified.value = true; - final data = await getVerificationData(); - final firstName = utf8.decode(latin1.encode(data.orgFirstName!)); - final lastName = utf8.decode(latin1.encode(data.orgLastName!)); - await saveIdentity('$lastName $firstName', data.docIssuingCountry, - data.docDob, data.docSex, data.idenfyRef); - } } -Future saveCorrectVerificationStates( - Map emailData, - Map phoneData, - VerificationStatus identityVerificationStatus) async { - if (identityVerificationStatus.status == VerificationState.VERIFIED) { - await setIsIdentityVerified(true); - } else { - await setIsIdentityVerified(false); - } - - if (phoneData.containsKey('spi')) { - await setIsPhoneVerified(true); - } else { - await setIsPhoneVerified(false); - } - - if (emailData.containsKey('sei')) { - await setIsEmailVerified(true); - } else { - await setIsEmailVerified(false); - } -} bool checkEmail(String email) { String? emailValue = diff --git a/app/lib/helpers/login_helpers.dart b/app/lib/helpers/login_helpers.dart index 5845fde0..3719f434 100644 --- a/app/lib/helpers/login_helpers.dart +++ b/app/lib/helpers/login_helpers.dart @@ -49,80 +49,6 @@ Future?> readScopeAsObject( 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'] = { diff --git a/app/lib/main.dart b/app/lib/main.dart index 787ed6f2..89ba0ff6 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -49,12 +49,10 @@ Future main() async { Future setGlobalValues() async { Map email = await getEmail(); Map phone = await getPhone(); - Map identity = await getIdentity(); Globals().emailVerified.value = (email['sei'] != null); Globals().phoneVerified.value = (phone['spi'] != null); - Globals().identityVerified.value = - (identity['signedIdentityNameIdentifier'] != null); + } class MyApp extends ConsumerWidget { diff --git a/app/lib/models/wallet.dart b/app/lib/models/wallet.dart index f55a1915..eb75c05b 100644 --- a/app/lib/models/wallet.dart +++ b/app/lib/models/wallet.dart @@ -14,6 +14,7 @@ class Wallet { required this.tfchainAddress, required this.tfchainBalance, required this.type, + required this.verificationStatus, }); String name; final String stellarSecret; @@ -23,6 +24,7 @@ class Wallet { String stellarBalance; String tfchainBalance; final WalletType type; + String verificationStatus; } class PkidWallet { diff --git a/app/lib/providers/wallets_provider.dart b/app/lib/providers/wallets_provider.dart index 0ca1a2c3..1685db70 100644 --- a/app/lib/providers/wallets_provider.dart +++ b/app/lib/providers/wallets_provider.dart @@ -1,7 +1,9 @@ import 'package:mutex/mutex.dart'; import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:threebotlogin/services/idenfy_service.dart'; import 'package:threebotlogin/services/wallet_service.dart'; import 'package:threebotlogin/services/stellar_service.dart' as StellarService; @@ -48,6 +50,25 @@ class WalletsNotifier extends StateNotifier> { }); } +Future verifyWallet(String walletName) async { + final idenfyServiceUrl = Globals().idenfyServiceUrl; + await _mutex.protect(() async { + final wallet = state.where((w) => w.name == walletName).firstOrNull; + if (wallet != null) { + try { + final updatedVerificationStatus = await getVerificationStatus( + address: wallet.tfchainAddress, + idenfyServiceUrl: idenfyServiceUrl, + ); + wallet.verificationStatus = updatedVerificationStatus.status.name; + state = [...state]; + } catch (e) { + logger.e('[verifyWallet] Error during verification: $e'); + } + } + }); +} + void reloadBalances() async { if (!_reload) return await TFChainService.disconnect(); if (!_loading) { diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index cf214ab6..23965a7e 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -1,34 +1,23 @@ import 'dart:async'; import 'dart:convert'; import 'dart:core'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_pkid/flutter_pkid.dart'; import 'package:http/http.dart'; -import 'package:idenfy_sdk_flutter/idenfy_sdk_flutter.dart'; -import 'package:idenfy_sdk_flutter/models/auto_identification_status.dart'; -import 'package:idenfy_sdk_flutter/models/idenfy_identification_status.dart'; -import 'package:threebotlogin/events/events.dart'; -import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/kyc_helpers.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/main.dart'; -import 'package:threebotlogin/models/idenfy.dart'; -import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/screens/authentication_screen.dart'; -import 'package:threebotlogin/screens/wizard/web_view.dart'; import 'package:threebotlogin/services/gridproxy_service.dart'; -import 'package:threebotlogin/services/idenfy_service.dart'; import 'package:threebotlogin/services/identity_service.dart'; import 'package:threebotlogin/services/open_kyc_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; -import 'package:threebotlogin/services/tfchain_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/services/wallet_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; +import 'package:threebotlogin/widgets/kyc_widget.dart'; import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:threebotlogin/widgets/phone_widget.dart'; @@ -47,15 +36,11 @@ class _IdentityVerificationScreenState String email = ''; String phone = ''; - String kycLogs = ''; - String reference = ''; bool emailVerified = false; bool phoneVerified = false; - bool identityVerified = false; - bool isInIdentityProcess = false; bool isLoading = false; bool hidePhoneVerifyButton = false; @@ -128,30 +113,12 @@ class _IdentityVerificationScreenState } } - setIdentityVerified() { - if (mounted) { - setState(() { - identityVerified = Globals().identityVerified.value; - }); - } - } - - setHidePhoneVerify() { - if (mounted) { - setState(() { - hidePhoneVerifyButton = Globals().identityVerified.value; - }); - } - } - @override void initState() { super.initState(); Globals().emailVerified.addListener(setEmailVerified); Globals().phoneVerified.addListener(setPhoneVerified); - Globals().identityVerified.addListener(setIdentityVerified); - Globals().hidePhoneButton.addListener(setHidePhoneVerify); checkPhoneStatus(); getUserValues(); startOrResumeEmailCountdown(); @@ -201,17 +168,6 @@ class _IdentityVerificationScreenState } }); }); - getIdentity().then((verificationDate) { - setState(() { - if (verificationDate['identityName'] != null) { - identityVerified = true; - setIsIdentityVerified(true); - } else { - identityVerified = false; - setIsIdentityVerified(false); - } - }); - }); getSpending(); } @@ -228,17 +184,18 @@ class _IdentityVerificationScreenState ), ); } - + @override Widget build(BuildContext context) { return LayoutDrawer( titleText: 'Identity', - content: FutureBuilder( + content: + FutureBuilder( future: getEmail(), builder: (ctx, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (isLoading) { - return _pleaseWait(); + return pleaseWait(context); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), @@ -250,7 +207,6 @@ class _IdentityVerificationScreenState animation: Listenable.merge([ Globals().emailVerified, Globals().phoneVerified, - Globals().identityVerified ]), builder: (BuildContext context, _) { return Column( @@ -287,289 +243,49 @@ class _IdentityVerificationScreenState ), customDivider(context: context), - // Step one: verify email - _fillCard( - getCorrectState(1, emailVerified, phoneVerified, - identityVerified), - 1, - email, - Icons.email), - customDivider(context: context), - - // Step two: verify phone - (Globals().phoneVerification == true || - (Globals().spendingLimit > 0 && - spending > Globals().spendingLimit)) - ? _fillCard( - getCorrectState(2, emailVerified, - phoneVerified, identityVerified), - 2, - phone, - Icons.phone) - : Container(), - customDivider(context: context), - - // Step three: verify identity - (Globals().isOpenKYCEnabled || - (Globals().spendingLimit > 0 && - spending > Globals().spendingLimit)) - ? _fillCard( - getCorrectState(3, emailVerified, - phoneVerified, identityVerified), - 3, - extract3Bot(doubleName), - Icons.perm_identity) - : Container(), - - Globals().redoIdentityVerification && - identityVerified == true - ? ElevatedButton( - onPressed: () async { - await verifyIdentityProcess(); - }, - child: const 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\n'; - kycLogs += - 'Phone verified: $isPhoneVerified\n'; - kycLogs += - 'Identity verified: $isIdentityVerified\n'; - - setState(() {}); - }, - child: const Text('KYC Status')) - : Container(), - Text(kycLogs), - ], - ); - }, - ), - ], - ), - ), - ); - } - return _pleaseWait(); - }, - ), - ); - } - - termsAndConditionsDialog() { - bool isAccepted = false; - - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext customContext) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return CustomDialog( - title: 'Terms and Conditions', - type: DialogType.Info, - image: Icons.info, - widgetDescription: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: MediaQuery.of(context).size.height * 0.01), - RichText( - text: TextSpan( - text: - "As part of the verification process, we utilize iDenfy to verify your identity. Please ensure you review iDenfy's ", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - children: [ - TextSpan( - text: 'Security and Compliance', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - fontWeight: FontWeight.bold, - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - TextSpan( - text: ', which include their ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - TextSpan( - text: 'Terms & Conditions, Privacy Policy,', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - fontWeight: FontWeight.bold, - color: - Theme.of(context).colorScheme.onSurface, - ), - ), - TextSpan( - text: ' and other relevant documents.', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - Theme.of(context).colorScheme.onSurface, - ), - ) - // - ], - ), - ), - Row( - children: [ - Checkbox( - value: isAccepted, - onChanged: (bool? value) { - setState(() { - isAccepted = value ?? false; - }); - }, - ), - Expanded( - child: RichText( - text: TextSpan( - text: 'I have read and agreed to ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: - Theme.of(context).colorScheme.onSurface, + // Step one: verify email + _fillCard( + getCorrectState(1, emailVerified, + phoneVerified), + 1, + email, + Icons.email), + customDivider(context: context), + + // Step two: verify phone + (Globals().phoneVerification == true || + (Globals().spendingLimit > 0 && + spending > Globals().spendingLimit)) + ? _fillCard( + getCorrectState(2, emailVerified, + phoneVerified), + 2, + phone, + Icons.phone) + : Container(), + customDivider(context: context), + const ListTile( + leading: Icon(Icons.info), + title: Text( + 'KYC Verification has been moved to wallet page.' ), - children: [ - TextSpan( - text: 'iDenfy Terms and Conditions.', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const WebView( - url: - 'https://www.idenfy.com/security/', - title: - 'iDenfy Terms and Conditions', - ), - ), - ); - }, - ), - TextSpan( - text: '.', - style: Theme.of(context).textTheme.bodyMedium, - ), + ), + ], - ), - ), - ), - ], - ) - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(customContext); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: isAccepted - ? () async { - Navigator.pop(customContext); - await verifyIdentityProcess(); - } - : null, - child: Text( - 'Continue', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: isAccepted - ? Theme.of(context).colorScheme.primary - : Theme.of(context).disabledColor, - ), + ); + }) + ], ), - ), - ], + ) + ); - }, - ); - }, - ); - } - - showAreYouSureToExitDialog() { - return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext customContext) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Are you sure', - description: 'Are you sure you want to exit the verification process', - actions: [ - TextButton( - child: const Text('No'), - onPressed: () async { - Navigator.pop(customContext); - }, - ), - TextButton( - child: Text( - 'Yes', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.warning), - ), - onPressed: () async { - Navigator.pop(customContext); - setState(() { - isInIdentityProcess = false; - }); - }, - ), - ], + } + return pleaseWait(context); + }, ), ); } - + Future copySeedPhrase() async { Clipboard.setData(ClipboardData(text: (await getPhrase()).toString())); @@ -616,185 +332,6 @@ class _IdentityVerificationScreenState } } - Future initIdenfySdk(String token) async { - IdenfyIdentificationResult? idenfySDKresult; - try { - idenfySDKresult = await IdenfySdkFlutter.start(token); - } catch (e) { - logger.e(e); - if (context.mounted) { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) => CustomDialog( - type: DialogType.Error, - image: Icons.close, - title: 'Error', - description: - 'Something went wrong. Please contact support if this issue persists.', - actions: [ - TextButton( - onPressed: () { - Navigator.pop(dialogContext); - }, - child: const Text('Close')) - ], - ), - ); - } - } - await Future.delayed(const Duration(seconds: 5)); - if (idenfySDKresult != null && - idenfySDKresult.autoIdentificationStatus != - AutoIdentificationStatus.UNVERIFIED) { - await handleIdenfyResponse(); - } - } - - Future handleIdenfyResponse() async { - VerificationStatus verificationStatus; - try { - final address = await getMyAddress(); - verificationStatus = await getVerificationStatus(address: address); - } catch (e) { - setState(() { - isLoading = false; - }); - logger.e(e); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Error, - image: Icons.error, - title: 'Error', - description: - 'Failed to get the verification status. \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } - if (verificationStatus.status == VerificationState.VERIFIED) { - identityVerified = true; - setIsIdentityVerified(true); - Globals().identityVerified.value = true; - try { - final data = await getVerificationData(); - final firstName = utf8.decode(latin1.encode(data.orgFirstName!)); - final lastName = utf8.decode(latin1.encode(data.orgLastName!)); - await saveIdentity('$lastName $firstName', data.docIssuingCountry, - data.docDob, data.docSex, data.idenfyRef); - Events().emit(IdentityCallbackEvent(type: 'success')); - } on BadRequest catch (e) { - setState(() { - isLoading = false; - }); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Bad Request', - description: - '$e \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on Unauthorized catch (e) { - setState(() { - isLoading = false; - }); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Unauthorized', - description: - '$e \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } catch (e) { - setState(() { - isLoading = false; - }); - logger.e(e); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Error, - image: Icons.error, - title: 'Error', - description: - 'Failed to process the verification details. \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } - } else { - identityVerified = false; - setIsIdentityVerified(false); - Globals().identityVerified.value = false; - Events().emit(IdentityCallbackEvent(type: 'failed')); - } - setState(() {}); - } - - Widget _pleaseWait() { - return Dialog( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 10, - ), - CircularProgressIndicator( - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox( - height: 10, - ), - Text( - 'One moment please', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.onSurface), - ), - const SizedBox( - height: 10, - ), - ], - ), - ); - } - Future _loadingDialog() { return showDialog( barrierDismissible: false, @@ -985,9 +522,7 @@ class _IdentityVerificationScreenState children: [ Expanded( child: Text( - step == 3 - ? 'Identity' - : (text.isEmpty + (text.isEmpty ? 'Unknown' : text), ), @@ -1075,27 +610,20 @@ class _IdentityVerificationScreenState } break; - // Verify phone - case 2: - { - await verifyPhone(); - } - break; - - // Verify identity - case 3: - { - await verifyIdentityProcess(); - } - break; - default: - {} - break; - } - }, - child: const Text('Verify')), - ); - }) + // Verify phone + case 2: + { + await verifyPhone(); + } + break; + default: + {} + break; + } + }, + child: const Text('Verify'))); + } + ) ], ), )) @@ -1134,12 +662,6 @@ class _IdentityVerificationScreenState } return; } - // Only make this section clickable if it is Identity Verification + Current Phase - if (step != 3) { - return; - } - - return showIdentityDetails(); }, child: Column(children: [ Padding( @@ -1213,18 +735,6 @@ class _IdentityVerificationScreenState ), ]) : const Column(), - step == 3 - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(left: 15), - child: Icon( - Icons.chevron_right, - ), - ), - ]) - : const Column() ], ), ], @@ -1233,393 +743,6 @@ class _IdentityVerificationScreenState ])); } - Future verifyIdentityProcess() async { - setState(() { - isLoading = true; - }); - - Token token; - try { - token = await getToken(); - - setState(() { - isLoading = false; - isInIdentityProcess = true; - }); - } on BadRequest catch (e) { - setState(() { - isLoading = false; - }); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Bad Request', - description: - '$e \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on Unauthorized catch (e) { - setState(() { - isLoading = false; - }); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Unauthorized', - description: - '$e \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on TooManyRequests catch (_) { - setState(() { - isLoading = false; - }); - final maxRetries = Globals().maximumKYCRetries; - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Maximum Requests Reached', - description: - 'You already had $maxRetries requests in last 24 hours.\nPlease try again in 24 hours.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on NotEnoughBalance catch (_) { - final wallets = (await getPkidWallets()) - .where((w) => w.type == WalletType.NATIVE) - .toList(); - setState(() { - isLoading = false; - }); - final minimumBalance = Globals().minimumTFChainBalanceForKYC; - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: 'Not enough balance', - description: wallets.isEmpty - ? 'Please initialize a wallet and fund it with at least $minimumBalance TFTs.' - : 'Please fund your ${wallets.first.name} TFChain wallet with at least $minimumBalance TFTs.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on NoTwinId catch (_) { - setState(() { - isLoading = false; - }); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Warning, - image: Icons.warning, - title: "Account doesn't exist", - description: - 'Your account is not activated.\nPlease go to wallet section and initialize your wallet.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - )); - } on AlreadyVerified catch (_) { - setState(() { - isLoading = false; - }); - return await handleIdenfyResponse(); - } catch (e) { - setState(() { - isLoading = false; - }); - logger.e(e); - return showDialog( - context: context, - builder: (BuildContext context) => CustomDialog( - type: DialogType.Error, - image: Icons.error, - title: 'Failed to setup process', - description: - 'Something went wrong. \nIf this issue persist, please contact support.', - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } - await initIdenfySdk(token.authToken); - } - - Future showIdentityDetails() { - return showDialog( - context: context, - builder: (BuildContext context) => Dialog( - child: FutureBuilder( - future: getIdentity(), - builder: (BuildContext customContext, - AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return _pleaseWait(); - } - String name = snapshot.data['identityName']; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 10, - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 10), - child: Column( - children: [ - Text( - 'ID CARD', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer), - textAlign: TextAlign.center, - ), - const SizedBox(height: 5), - Row(children: [ - Text( - 'Your own personal KYC ID CARD', - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer), - ), - ]), - ], - )), - Container( - color: Theme.of(context).colorScheme.secondaryContainer, - padding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 20), - child: Column( - children: [ - Row( - children: [ - Text( - 'Full name', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ) - ], - ), - Row( - children: [ - Text( - name, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ) - ], - ) - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 20), - child: Column( - children: [ - Row( - children: [ - Text( - 'Birthday', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ), - ], - ), - Row( - children: [ - Text( - snapshot.data['identityDOB'] != 'None' - ? snapshot.data['identityDOB'] - : 'Unknown', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ) - ], - ) - ], - ), - ), - Container( - color: Theme.of(context).colorScheme.secondaryContainer, - padding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 20), - child: Column( - children: [ - Row( - children: [ - Text( - 'Country', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ) - ], - ), - Row( - children: [ - Text( - snapshot.data['identityCountry'] != 'None' - ? snapshot.data['identityCountry'] - : 'Unknown', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ) - ], - ) - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 20), - child: Column( - children: [ - Row( - children: [ - Text( - 'Gender', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary, - ), - ) - ], - ), - Row( - children: [ - Text( - snapshot.data['identityGender'] != 'None' - ? snapshot.data['identityGender'] - : 'Unknown', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ) - ], - ) - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () { - Navigator.pop(customContext); - }, - child: const Text('OK')), - const SizedBox( - height: 10, - ), - ], - ) - ], - ); - }, - ), - )); - } - Future resendEmailDialog(context) { return showDialog( context: context, diff --git a/app/lib/screens/recover_screen.dart b/app/lib/screens/recover_screen.dart index 488bf49b..0fcb4e54 100644 --- a/app/lib/screens/recover_screen.dart +++ b/app/lib/screens/recover_screen.dart @@ -95,7 +95,6 @@ class _RecoverScreenState extends State { await savePhrase(seedPhrase); await saveFingerprint(false); await saveDoubleName(doubleName); - await handleKYCData(dataMap[0], dataMap[1]); await fixPkidMigration(); diff --git a/app/lib/screens/wallets/wallet_info.dart b/app/lib/screens/wallets/wallet_info.dart index 738d5c80..7f9b89ed 100644 --- a/app/lib/screens/wallets/wallet_info.dart +++ b/app/lib/screens/wallets/wallet_info.dart @@ -1,10 +1,15 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/kyc_widget.dart'; import 'package:threebotlogin/widgets/wallets/warning_dialog.dart'; class WalletDetailsWidget extends ConsumerStatefulWidget { @@ -114,6 +119,8 @@ class _WalletDetailsWidgetState extends ConsumerState { tfchainAddressController.text = widget.wallet.tfchainAddress; walletNameController.text = widget.wallet.name; walletName = widget.wallet.name; + final wallet = + ref.watch(walletsNotifier).firstWhere((w) => w.name == walletName); return SingleChildScrollView( child: Padding( @@ -270,7 +277,70 @@ class _WalletDetailsWidgetState extends ConsumerState { ), ), ), - ) + ), + const SizedBox(height: 40), + Text( + 'KYC Verification', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Center( + child: SizedBox( + width: MediaQuery.of(context).size.width - 40, + child: ElevatedButton( + onPressed: () async { + if (wallet.verificationStatus != 'VERIFIED') { + await termsAndConditionsDialog( + context: context, wallet: wallet); + + final completer = Completer(); + StreamSubscription? subscription; + try { + subscription = + Events().onEvent(IdentityCallbackEvent, (event) { + if (event is IdentityCallbackEvent && + event.type == 'success') { + logger.i( + '[Event Listener] IdentityCallbackEvent with success received.'); + completer.complete(); + } + }); + + await completer.future; + await walletsRef.verifyWallet(wallet.name); + } catch (e) { + logger.e( + '[Event Listener] Error while waiting for event: $e'); + } finally { + if (subscription != null) { + await subscription.cancel(); + } + } + } else { + showIdentityDetails(context, wallet.tfchainSecret); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: wallet.verificationStatus != 'VERIFIED' + ? Theme.of(context).colorScheme.errorContainer + : Theme.of(context).colorScheme.primaryContainer, + ), + child: Text( + wallet.verificationStatus != 'VERIFIED' + ? 'Verify your Identity' + : 'Show Verified Data', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: wallet.verificationStatus != 'VERIFIED' + ? Theme.of(context).colorScheme.onErrorContainer + : Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), + ), + ) ], ), ), diff --git a/app/lib/services/idenfy_service.dart b/app/lib/services/idenfy_service.dart index 47beeb1c..3d851e68 100644 --- a/app/lib/services/idenfy_service.dart +++ b/app/lib/services/idenfy_service.dart @@ -6,10 +6,9 @@ import 'package:threebotlogin/services/tfchain_service.dart'; import 'package:convert/convert.dart'; import 'package:threebotlogin/models/idenfy.dart'; -Future> _prepareRequestHeaders() async { +Future> _prepareRequestHeaders({required String walletSecretSeed} ) async { final idenfyServiceUrl = Globals().idenfyServiceUrl; - final signer = await getMySigner(); - final address = signer.keypair!.address; + final signer = await getSignerFromSeed(walletSecretSeed); final now = DateTime.now().millisecondsSinceEpoch; final content = '$idenfyServiceUrl:$now'; final contentHex = hex.encode(content.codeUnits); @@ -17,16 +16,16 @@ Future> _prepareRequestHeaders() async { final headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'X-Client-ID': address, + 'X-Client-ID': signer.keypair!.address, 'X-Challenge': contentHex, 'X-Signature': signedContent, }; return headers; } -Future getToken() async { +Future getToken(String seed) async { final idenfyServiceUrl = Globals().idenfyServiceUrl; - final headers = await _prepareRequestHeaders(); + final headers = await _prepareRequestHeaders(walletSecretSeed: seed); final response = await http.post( Uri.https(idenfyServiceUrl, '/api/v1/token'), @@ -50,8 +49,7 @@ Future getToken() async { } Future getVerificationStatus( - {required String address}) async { - final idenfyServiceUrl = Globals().idenfyServiceUrl; + {required String address, required String idenfyServiceUrl}) async { final response = await http.get( Uri.https(idenfyServiceUrl, '/api/v1/status', {'client_id': address}), @@ -70,9 +68,9 @@ Future getVerificationStatus( } } -Future getVerificationData() async { +Future getVerificationData(String seed) async { final idenfyServiceUrl = Globals().idenfyServiceUrl; - final headers = await _prepareRequestHeaders(); + final headers = await _prepareRequestHeaders(walletSecretSeed: seed); final response = await http.get( Uri.https(idenfyServiceUrl, '/api/v1/data'), diff --git a/app/lib/services/identity_service.dart b/app/lib/services/identity_service.dart index 7c4a4b51..c3e2d093 100644 --- a/app/lib/services/identity_service.dart +++ b/app/lib/services/identity_service.dart @@ -8,7 +8,7 @@ String getFullNameOfObject(Map identityName) { } String getCorrectState( - int step, emailVerified, phoneVerified, identityVerified) { + int step, emailVerified, phoneVerified) { if (step == 1) { if (!emailVerified) { return 'CurrentPhase'; @@ -27,23 +27,5 @@ String getCorrectState( return 'Verified'; } - if (step == 3) { - if (identityVerified) { - return 'Verified'; - } - - if (!emailVerified) { - return 'Unverified'; - } - - if (!phoneVerified) { - return 'Unverified'; - } - - if (emailVerified && phoneVerified && !identityVerified) { - return 'CurrentPhase'; - } - } - return ''; } diff --git a/app/lib/services/shared_preference_service.dart b/app/lib/services/shared_preference_service.dart index 702886a5..3be150c2 100644 --- a/app/lib/services/shared_preference_service.dart +++ b/app/lib/services/shared_preference_service.dart @@ -230,48 +230,6 @@ Future savePhone(String phone, String? signedPhoneIdentifier) async { 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'), - 'identityCountry': prefs.getString('identityCountry'), - 'identityDOB': prefs.getString('identityDOB'), - 'identityGender': prefs.getString('identityGender'), - }; -} - -Future saveIdentity(String? identityName, String? identityCountry, - String? identityDOB, String? identityGender, String? referenceId) async { - final SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('identityName'); - prefs.remove('identityCountry'); - prefs.remove('identityDOB'); - prefs.remove('identityGender'); - - prefs.setString('identityName', identityName!); - prefs.setString('identityCountry', identityCountry!); - prefs.setString('identityDOB', identityDOB!); - prefs.setString('identityGender', identityGender!); - - updateUserData('identity_reference', referenceId!); - Globals().identityVerified.value = true; -} /// /// diff --git a/app/lib/services/tfchain_service.dart b/app/lib/services/tfchain_service.dart index 88a160f0..267376a5 100644 --- a/app/lib/services/tfchain_service.dart +++ b/app/lib/services/tfchain_service.dart @@ -2,6 +2,8 @@ import 'dart:convert'; +import 'package:bip39/bip39.dart'; +import 'package:convert/convert.dart'; import 'package:tfchain_client/generated/dev/types/pallet_collective/votes.dart'; import 'package:tfchain_client/generated/dev/types/tfchain_support/types/farm.dart'; import 'package:tfchain_client/models/council.dart'; @@ -15,6 +17,7 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:hashlib/hashlib.dart' as hashlib; import 'package:signer/signer.dart'; +import 'package:substrate_bip39/substrate_bip39.dart'; Future getMySeed() async { final derivedSeed = await getDerivedSeed(WalletConfig().appId()); @@ -38,6 +41,19 @@ Future getMySigner() async { return signer; } +Future getSignerFromSeed(String walletSeed) async { + final signer = Signer(); + if (walletSeed.startsWith('0x')) { + signer.fromHexSeed(walletSeed, KPType.sr25519); + } else { + final entropy = mnemonicToEntropy(walletSeed); + final seed = await CryptoScheme.seedFromEntropy(hex.decode(entropy)); + final hexSeed = '0x${hex.encode(seed).substring(0, 64)}'; + signer.fromHexSeed(hexSeed, KPType.sr25519); + } + return signer; +} + Future getMyAddress() async { final signer = await getMySigner(); return signer.keypair!.address; diff --git a/app/lib/services/wallet_service.dart b/app/lib/services/wallet_service.dart index 8f1f1c71..7ee8745d 100644 --- a/app/lib/services/wallet_service.dart +++ b/app/lib/services/wallet_service.dart @@ -6,6 +6,7 @@ import 'package:gridproxy_client/models/farms.dart'; import 'package:threebotlogin/apps/wallet/wallet_config.dart'; import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/services/idenfy_service.dart'; import 'package:threebotlogin/services/gridproxy_service.dart'; import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; @@ -45,10 +46,11 @@ Future> getPkidWallets() async { Future> listWallets() async { List pkidWallets = await getPkidWallets(); final String chainUrl = Globals().chainUrl; + final idenfyServiceUrl = Globals().idenfyServiceUrl; final List wallets = await compute((void _) async { final List> walletFutures = []; for (final w in pkidWallets) { - final walletFuture = loadWallet(w.name, w.seed, w.type, chainUrl); + final walletFuture = loadWallet(w.name, w.seed, w.type, chainUrl, idenfyServiceUrl); walletFutures.add(walletFuture); } return await Future.wait(walletFutures); @@ -95,7 +97,7 @@ Future<(Stellar.Client, TFChain.Client)> loadWalletClients(String walletName, } Future loadWallet(String walletName, String walletSeed, - WalletType walletType, String chainUrl) async { + WalletType walletType, String chainUrl, String idenfyServiceUrl) async { final (stellarClient, tfchainClient) = await loadWalletClients(walletName, walletSeed, walletType, chainUrl); final balances = await Future.wait([ @@ -105,6 +107,8 @@ Future loadWallet(String walletName, String walletSeed, final stellarBalance = balances.first.toString(); final tfchainBalance = balances.last.toString() == '0.0' ? '0' : balances.last.toString(); + final kycVerified = + await getVerificationStatus(address: tfchainClient.keypair!.address,idenfyServiceUrl: idenfyServiceUrl ); final wallet = Wallet( name: walletName, stellarSecret: stellarClient.secretSeed, @@ -114,6 +118,7 @@ Future loadWallet(String walletName, String walletSeed, stellarBalance: stellarBalance, tfchainBalance: tfchainBalance, type: walletType, + verificationStatus: kycVerified.status.name, ); return wallet; } diff --git a/app/lib/widgets/add_farm.dart b/app/lib/widgets/add_farm.dart index 88d43cff..6a630034 100644 --- a/app/lib/widgets/add_farm.dart +++ b/app/lib/widgets/add_farm.dart @@ -1,5 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/idenfy.dart'; @@ -74,8 +75,9 @@ class _NewFarmState extends State { _add(String farmName) async { Farm? farm; try { + final idenfyServiceUrl = Globals().idenfyServiceUrl; final kycVerified = - await getVerificationStatus(address: _selectedWallet!.tfchainAddress); + await getVerificationStatus(address: _selectedWallet!.tfchainAddress, idenfyServiceUrl: idenfyServiceUrl); if (kycVerified.status == VerificationState.VERIFIED) { final f = await createFarm(farmName, _selectedWallet!.tfchainSecret, _selectedWallet!.stellarAddress); diff --git a/app/lib/widgets/farm_item.dart b/app/lib/widgets/farm_item.dart index 5e258ad4..5081fb17 100644 --- a/app/lib/widgets/farm_item.dart +++ b/app/lib/widgets/farm_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/idenfy.dart'; @@ -237,8 +238,9 @@ class _FarmItemWidgetState extends State { children: [ IconButton( onPressed: () async { + final idenfyServiceUrl = Globals().idenfyServiceUrl; final kycVerified = await getVerificationStatus( - address: tfchainAddress!); + address: tfchainAddress!, idenfyServiceUrl: idenfyServiceUrl); if (kycVerified.status != VerificationState.VERIFIED) { showDialog( diff --git a/app/lib/widgets/kyc_widget.dart b/app/lib/widgets/kyc_widget.dart new file mode 100644 index 00000000..77437b3f --- /dev/null +++ b/app/lib/widgets/kyc_widget.dart @@ -0,0 +1,648 @@ +import 'dart:convert'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:idenfy_sdk_flutter/idenfy_sdk_flutter.dart'; +import 'package:idenfy_sdk_flutter/models/idenfy_identification_status.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/identity_callback_event.dart'; +import 'package:threebotlogin/models/idenfy.dart'; +import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/screens/wizard/web_view.dart'; +import 'package:threebotlogin/services/idenfy_service.dart'; +import 'package:threebotlogin/helpers/globals.dart'; +import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/services/wallet_service.dart'; +import 'package:threebotlogin/widgets/custom_dialog.dart'; +import 'package:idenfy_sdk_flutter/models/auto_identification_status.dart'; + +termsAndConditionsDialog( + {required BuildContext context, required Wallet wallet}) { + bool isAccepted = false; + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext customContext) { + return StatefulBuilder( + builder: (BuildContext childContext, StateSetter setState) { + return CustomDialog( + title: 'Terms and Conditions', + type: DialogType.Info, + image: Icons.info, + widgetDescription: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: MediaQuery.of(childContext).size.height * 0.01), + RichText( + text: TextSpan( + text: + "As part of the verification process, we utilize iDenfy to verify your identity. Please ensure you review iDenfy's ", + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(childContext).colorScheme.onSurface, + ), + children: [ + TextSpan( + text: 'Security and Compliance', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(childContext) + .colorScheme + .onSurface, + ), + ), + TextSpan( + text: ', which include their ', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(childContext) + .colorScheme + .onSurface, + ), + ), + TextSpan( + text: 'Terms & Conditions, Privacy Policy,', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(childContext) + .colorScheme + .onSurface, + ), + ), + TextSpan( + text: ' and other relevant documents.', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(childContext) + .colorScheme + .onSurface, + ), + ) + // + ], + ), + ), + Row( + children: [ + Checkbox( + value: isAccepted, + onChanged: (bool? value) { + setState(() { + isAccepted = value ?? false; + }); + }, + ), + Expanded( + child: RichText( + text: TextSpan( + text: 'I have read and agreed to ', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(childContext) + .colorScheme + .onSurface, + ), + children: [ + TextSpan( + text: 'iDenfy Terms and Conditions.', + style: Theme.of(childContext) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(childContext) + .colorScheme + .primary, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Navigator.push( + childContext, + MaterialPageRoute( + builder: (context) => const WebView( + url: + 'https://www.idenfy.com/security/', + title: 'iDenfy Terms and Conditions', + ), + ), + ); + }, + ), + TextSpan( + text: '.', + style: + Theme.of(childContext).textTheme.bodyMedium, + ), + ], + ), + ), + ), + ], + ) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(customContext); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: isAccepted + ? () async { + Navigator.pop(customContext); + await verifyIdentityProcess( + context: context, wallet: wallet); + } + : null, + child: Text( + 'Continue', + style: Theme.of(childContext).textTheme.bodyMedium!.copyWith( + color: isAccepted + ? Theme.of(childContext).colorScheme.primary + : Theme.of(childContext).disabledColor, + ), + ), + ), + ], + ); + }, + ); + }, + ); +} + +Future verifyIdentityProcess({ + required BuildContext context, + required Wallet wallet, +}) async { + Token token; + try { + token = await getToken(wallet.tfchainSecret); + } on BadRequest catch (e) { + showWarningDialog( + context: context, + title: 'Bad Request', + description: '$e \nIf this issue persist, please contact support.', + ); + return; + } on Unauthorized catch (e) { + showWarningDialog( + context: context, + title: 'Unauthorized', + description: '$e \nIf this issue persist, please contact support.', + ); + return; + } on TooManyRequests catch (_) { + final maxRetries = Globals().maximumKYCRetries; + showWarningDialog( + context: context, + title: 'Maximum Requests Reached', + description: + 'You already had $maxRetries requests in last 24 hours.\nPlease try again in 24 hours.', + ); + return; + } on NotEnoughBalance catch (_) { + final wallets = (await getPkidWallets()) + .where((w) => w.seed == wallet.tfchainSecret) + .toList(); + final minimumBalance = Globals().minimumTFChainBalanceForKYC; + showWarningDialog( + context: context, + title: 'Not enough balance', + description: wallets.isEmpty + ? 'Please initialize a wallet and fund it with at least $minimumBalance TFTs.' + : 'Please fund your ${wallets.first.name} TFChain wallet with at least $minimumBalance TFTs.'); + return; + } on NoTwinId catch (_) { + showWarningDialog( + context: context, + title: "Account doesn't exist", + description: + 'Your account is not activated.\nPlease go to wallet section and initialize your wallet.'); + return; + } on AlreadyVerified catch (_) { + return await handleIdenfyResponse( + context: context, walletAddress: wallet.tfchainAddress); + } catch (e) { + logger.e(e); + showErrorDialog( + context: context, + title: 'Failed to setup process', + description: + 'Something went wrong. \nIf this issue persist, please contact support.', + ); + return; + } + await initIdenfySdk(token.authToken, + context: context, walletAddress: wallet.tfchainAddress); +} + +Future handleIdenfyResponse({ + required BuildContext context, + required String walletAddress, + AutoIdentificationStatus? idenfyState, +}) async { + VerificationStatus verificationStatus; + try { + final idenfyServiceUrl = Globals().idenfyServiceUrl; + verificationStatus = await getVerificationStatus( + address: walletAddress, + idenfyServiceUrl: idenfyServiceUrl, + ); + } catch (e) { + Events().emit(IdentityCallbackEvent(type: 'failed')); + logger.e(e); + showErrorDialog( + context: context, + title: 'Error', + description: + 'Failed to get the verification status. \nIf this issue persist, please contact support.', + ); + return; + } + if (verificationStatus.status == VerificationState.VERIFIED) { + Events().emit(IdentityCallbackEvent(type: 'success')); + } else { + Events().emit(IdentityCallbackEvent(type: 'failed')); + } +} + +Future initIdenfySdk(String token, + {required BuildContext context, required String walletAddress}) async { + IdenfyIdentificationResult? idenfySDKresult; + try { + idenfySDKresult = await IdenfySdkFlutter.start(token); + } catch (e) { + logger.e(e); + if (context.mounted) { + showErrorDialog( + context: context, + title: 'Error', + description: + 'Something went wrong. Please contact support if this issue persists.'); + } + } + await Future.delayed(const Duration(seconds: 10)); + if (idenfySDKresult != null) { + await handleIdenfyResponse( + context: context, + walletAddress: walletAddress, + idenfyState: idenfySDKresult.autoIdentificationStatus); + } +} + +void showWarningDialog({ + required BuildContext context, + required String title, + required String description, +}) { + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + type: DialogType.Warning, + image: Icons.warning, + title: title, + description: description, + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); +} + +void showErrorDialog({ + required BuildContext context, + required String title, + required String description, +}) { + showDialog( + context: context, + builder: (BuildContext context) => CustomDialog( + type: DialogType.Error, + image: Icons.error, + title: title, + description: description, + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); +} + +Widget pleaseWait(BuildContext context) { + return Dialog( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 10, + ), + CircularProgressIndicator( + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + height: 10, + ), + Text( + 'One moment please', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.onSurface), + ), + const SizedBox( + height: 10, + ), + ], + ), + ); +} + +Future showIdentityDetails(BuildContext context, String walletSeed) { + return showDialog( + context: context, + builder: (BuildContext context) => Dialog( + child: FutureBuilder( + future: getVerificationData(walletSeed), + builder: (BuildContext customContext, + AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return pleaseWait(context); + } else if (snapshot.connectionState == ConnectionState.done && + snapshot.data == null) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + Text( + 'No data available for the provided wallet address.', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.pop(customContext); + }, + child: const Text('OK')), + const SizedBox( + height: 10, + ), + ], + ) + ], + ); + } + final data = snapshot.data; + final firstName = + utf8.decode(latin1.encode(data.orgFirstName!)); + final lastName = utf8.decode(latin1.encode(data.orgLastName!)); + final fullName = '$lastName $firstName'; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 10, + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 10), + child: Column( + children: [ + Text( + 'ID CARD', + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Row(children: [ + Text( + 'Your own personal KYC ID CARD', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer), + ), + ]), + ], + )), + Container( + color: Theme.of(context).colorScheme.secondaryContainer, + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 20), + child: Column( + children: [ + Row( + children: [ + Text( + 'Full name', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + Row( + children: [ + Text( + fullName, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ) + ], + ) + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 20), + child: Column( + children: [ + Row( + children: [ + Text( + 'Birthday', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + Row( + children: [ + Text( + data.docDob != 'None' ? data.docDob : 'Unknown', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ) + ], + ) + ], + ), + ), + Container( + color: Theme.of(context).colorScheme.secondaryContainer, + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 20), + child: Column( + children: [ + Row( + children: [ + Text( + 'Country', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + Row( + children: [ + Text( + data.docIssuingCountry != 'None' + ? data.docIssuingCountry + : 'Unknown', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ) + ], + ) + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 20), + child: Column( + children: [ + Row( + children: [ + Text( + 'Gender', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ) + ], + ), + Row( + children: [ + Text( + data.docSex != 'None' ? data.docSex : 'Unknown', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ) + ], + ) + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.pop(customContext); + }, + child: const Text('OK')), + const SizedBox( + height: 10, + ), + ], + ) + ], + ); + }, + ), + )); +} diff --git a/app/lib/widgets/preference_dialog.dart b/app/lib/widgets/preference_dialog.dart index 0988649d..87f68f7a 100644 --- a/app/lib/widgets/preference_dialog.dart +++ b/app/lib/widgets/preference_dialog.dart @@ -369,232 +369,6 @@ class _PreferenceDialogState extends State { } }, ); - - case 'identityName': - return FutureBuilder( - future: getIdentity(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).colorScheme.onSurface, - width: 0.5, - )), - ), - child: CheckboxListTile( - value: - (previousSelectedScope[scopeItem] == null) - ? mandatory - : previousSelectedScope[scopeItem], - onChanged: ((mandatory == true) - ? null - : (value) { - toggleScope(scopeItem, value); - }), - title: Text( - 'NAME (IDENTITY)${(mandatory ? " *" : "")}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox(width: 0, height: 0); - } - }, - ); - - case 'identityDOB': - return FutureBuilder( - future: getIdentity(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).colorScheme.onSurface, - width: 0.5, - )), - ), - child: CheckboxListTile( - value: - (previousSelectedScope[scopeItem] == null) - ? mandatory - : previousSelectedScope[scopeItem], - onChanged: ((mandatory == true) - ? null - : (value) { - toggleScope(scopeItem, value); - }), - title: Text( - 'DATE OF BIRTH (IDENTITY)${(mandatory ? " *" : "")}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox(width: 0, height: 0); - } - }, - ); - - case 'identityGender': - return FutureBuilder( - future: getIdentity(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).colorScheme.onSurface, - width: 0.5, - )), - ), - child: CheckboxListTile( - value: - (previousSelectedScope[scopeItem] == null) - ? mandatory - : previousSelectedScope[scopeItem], - onChanged: ((mandatory == true) - ? null - : (value) { - toggleScope(scopeItem, value); - }), - title: Text( - 'GENDER (IDENTITY)${(mandatory ? " *" : "")}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox(width: 0, height: 0); - } - }, - ); - - case 'identityDocumentMeta': - return FutureBuilder( - future: getIdentity(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).colorScheme.onSurface, - width: 0.5, - )), - ), - child: CheckboxListTile( - value: - (previousSelectedScope[scopeItem] == null) - ? mandatory - : previousSelectedScope[scopeItem], - onChanged: ((mandatory == true) - ? null - : (value) { - toggleScope(scopeItem, value); - }), - title: Text( - 'DOCUMENT META DATA (IDENTITY)${(mandatory ? " *" : "")}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox(width: 0, height: 0); - } - }, - ); - - case 'identityCountry': - return FutureBuilder( - future: getIdentity(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: - Theme.of(context).colorScheme.onSurface, - width: 0.5, - )), - ), - child: CheckboxListTile( - value: - (previousSelectedScope[scopeItem] == null) - ? mandatory - : previousSelectedScope[scopeItem], - onChanged: ((mandatory == true) - ? null - : (value) { - toggleScope(scopeItem, value); - }), - title: Text( - 'COUNTRY (IDENTITY)${(mandatory ? " *" : "")}', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .onSurface, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox(width: 0, height: 0); - } - }, - ); - case 'walletAddress': return FutureBuilder( future: getWallets(), diff --git a/app/lib/widgets/wallets/add_wallet.dart b/app/lib/widgets/wallets/add_wallet.dart index 115e24cf..cf79d1b6 100644 --- a/app/lib/widgets/wallets/add_wallet.dart +++ b/app/lib/widgets/wallets/add_wallet.dart @@ -296,8 +296,9 @@ class _NewWalletState extends ConsumerState { Future loadAddedWallet(String walletName, String walletSecret, {WalletType type = WalletType.IMPORTED}) async { final chainUrl = Globals().chainUrl; + final idenfyServiceUrl = Globals().idenfyServiceUrl; final Wallet wallet = await compute((void _) async { - final wallet = await loadWallet(walletName, walletSecret, type, chainUrl); + final wallet = await loadWallet(walletName, walletSecret, type, chainUrl, idenfyServiceUrl); return wallet; }, null); return wallet; diff --git a/app/lib/widgets/wallets/wallet_card.dart b/app/lib/widgets/wallets/wallet_card.dart index 17d3c57f..5fe56e83 100644 --- a/app/lib/widgets/wallets/wallet_card.dart +++ b/app/lib/widgets/wallets/wallet_card.dart @@ -62,6 +62,7 @@ class _WalletCardWidgetState extends ConsumerState { Widget build(BuildContext context) { List cardContent = []; wallets = ref.watch(walletsNotifier); + final wallet = wallets.where((w) => w.name == widget.wallet.name).firstOrNull; if (widget.wallet.type == WalletType.NATIVE && widget.wallet.stellarBalance == '-1') { cardContent = [ @@ -161,11 +162,31 @@ class _WalletCardWidgetState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.wallet.name, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), + Row( + children: [ + Text( + widget.wallet.name, + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + const Spacer(), + Text( + wallet!.verificationStatus, + style: wallet.verificationStatus == 'VERIFIED' + ? Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ) + : TextStyle( + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], ), const SizedBox(height: 10), ...cardContent,