diff --git a/lib/features/account/account_page.dart b/lib/features/account/account_page.dart index d5816e19..4112b0a4 100644 --- a/lib/features/account/account_page.dart +++ b/lib/features/account/account_page.dart @@ -20,7 +20,7 @@ class AccountPage extends HookConsumerWidget { final pfis = ref.watch(pfisProvider); final credentials = ref.watch(vcsProvider); final featureFlags = ref.watch(featureFlagsProvider); - const dap = '@username/didpay.me'; + final dap = Loc.of(context).placeholderDap; return Scaffold( body: SafeArea( @@ -28,7 +28,7 @@ class AccountPage extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProfile(context, dap), - const Center(child: Text(dap)), + Center(child: Text(dap)), const SizedBox(height: Grid.lg), Expanded( child: SingleChildScrollView( diff --git a/lib/features/dap/dap_form.dart b/lib/features/dap/dap_form.dart index dad6be37..b04950f2 100644 --- a/lib/features/dap/dap_form.dart +++ b/lib/features/dap/dap_form.dart @@ -1,5 +1,5 @@ import 'package:dap/dap.dart'; -import 'package:didpay/features/did/did_qr_tile.dart'; +import 'package:didpay/features/dap/dap_qr_tile.dart'; import 'package:didpay/l10n/app_localizations.dart'; import 'package:didpay/shared/next_button.dart'; import 'package:didpay/shared/theme/grid.dart'; @@ -9,15 +9,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class DapForm extends HookConsumerWidget { final String buttonTitle; - final void Function(String) onSubmit; + final ValueNotifier?> dap; + final void Function(List) onSubmit; - DapForm({required this.buttonTitle, required this.onSubmit, super.key}); + DapForm({ + required this.buttonTitle, + required this.dap, + required this.onSubmit, + super.key, + }); final _formKey = GlobalKey(); @override Widget build(BuildContext context, WidgetRef ref) { - final dap = useState(null); final errorText = useState(null); final focusNode = useFocusNode(); @@ -40,16 +45,14 @@ class DapForm extends HookConsumerWidget { focusNode: focusNode, controller: textController, onTap: () => errorText.value = null, - onTapOutside: (_) async => _updateErrorText( + onTapOutside: (_) async => _parseDap( textController.text, errorMessage, - dap, errorText, ).then((_) => focusNode.unfocus()), - onFieldSubmitted: (_) async => _updateErrorText( + onFieldSubmitted: (_) async => _parseDap( textController.text, errorMessage, - dap, errorText, ), enableSuggestions: false, @@ -66,21 +69,18 @@ class DapForm extends HookConsumerWidget { ), ), ), - DidQrTile( - title: Loc.of(context).dontKnowTheirDap, - didTextController: textController, + DapQrTile( + dapTextController: textController, errorText: errorText, ), NextButton( - onPressed: () async => _updateErrorText( + onPressed: () => _parseDap( textController.text, errorMessage, - dap, errorText, ).then( - (_) => errorText.value == null - ? onSubmit(textController.text) - : null, + (parsedDap) => + errorText.value == null ? _resolveDap(parsedDap) : null, ), title: buttonTitle, ), @@ -89,17 +89,34 @@ class DapForm extends HookConsumerWidget { ); } - Future _updateErrorText( + Future _parseDap( String inputText, String errorMessage, - ValueNotifier dap, ValueNotifier errorText, ) async { try { - dap.value = Dap.parse(inputText); + final parsedDap = Dap.parse(inputText); errorText.value = null; + return parsedDap; } on Exception { errorText.value = errorMessage; } + return null; + } + + Future _resolveDap(Dap? parsedDap) async { + if (parsedDap == null) return; + try { + dap.value = const AsyncValue.loading(); + final result = await DapResolver().resolve(parsedDap); + + await Future.delayed(const Duration(milliseconds: 500)); + + dap.value = AsyncValue.data(result.dap); + onSubmit(result.moneyAddresses); + } on Exception catch (e) { + dap.value = + AsyncError('${e.runtimeType}: Invalid DAP', StackTrace.current); + } } } diff --git a/lib/features/dap/dap_qr_tile.dart b/lib/features/dap/dap_qr_tile.dart new file mode 100644 index 00000000..2e7a370c --- /dev/null +++ b/lib/features/dap/dap_qr_tile.dart @@ -0,0 +1,94 @@ +import 'package:didpay/features/device/device_info_service.dart'; +import 'package:didpay/features/did/did_qr_tabs.dart'; +import 'package:didpay/l10n/app_localizations.dart'; +import 'package:didpay/shared/theme/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:web5/web5.dart'; + +class DapQrTile extends HookConsumerWidget { + final TextEditingController dapTextController; + final ValueNotifier? errorText; + + const DapQrTile({ + required this.dapTextController, + this.errorText, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPhysicalDevice = useState(true); + + useEffect( + () { + Future.microtask( + () async => isPhysicalDevice.value = + await ref.read(deviceInfoServiceProvider).isPhysicalDevice(), + ); + return null; + }, + [], + ); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: Grid.xxs), + child: ListTile( + leading: const Icon(Icons.qr_code), + title: Text( + Loc.of(context).dontKnowTheirDap, + style: Theme.of(context).textTheme.bodyMedium, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => isPhysicalDevice.value + ? _scanQrCode( + context, + dapTextController, + errorText, + Loc.of(context).noDapQrCodeFound, + ) + : _simulateScanQrCode( + context, + dapTextController, + errorText, + ), + ), + ); + } + + Future _scanQrCode( + BuildContext context, + TextEditingController dapTextController, + ValueNotifier? errorText, + String errorMessage, + ) async { + final qrValue = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => DidQrTabs(dap: Loc.of(context).placeholderDap), + ), + ); + + final isValid = qrValue != null && + await DidResolver.resolve(qrValue).then((result) => !result.hasError()); + dapTextController.text = isValid ? qrValue : ''; + errorText?.value = isValid ? null : errorMessage; + } + + Future _simulateScanQrCode( + BuildContext context, + TextEditingController didTextController, + ValueNotifier? errorText, + ) async { + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(Loc.of(context).simulatedQrCodeScan), + ), + ); + + didTextController.text = '@moegrammer/didpay.me'; + errorText?.value = null; + } +} diff --git a/lib/features/did/did_form.dart b/lib/features/did/did_form.dart index f6f4e03a..12ad06b8 100644 --- a/lib/features/did/did_form.dart +++ b/lib/features/did/did_form.dart @@ -64,7 +64,6 @@ class DidForm extends HookConsumerWidget { ), ), DidQrTile( - title: Loc.of(context).dontKnowTheirDid, didTextController: textController, errorText: errorText, ), diff --git a/lib/features/did/did_qr_tile.dart b/lib/features/did/did_qr_tile.dart index fbf81013..d6a87148 100644 --- a/lib/features/did/did_qr_tile.dart +++ b/lib/features/did/did_qr_tile.dart @@ -8,12 +8,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:web5/web5.dart'; class DidQrTile extends HookConsumerWidget { - final String title; final TextEditingController didTextController; final ValueNotifier? errorText; const DidQrTile({ - required this.title, required this.didTextController, this.errorText, super.key, @@ -39,7 +37,7 @@ class DidQrTile extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.qr_code), title: Text( - title, + Loc.of(context).dontKnowTheirDid, style: Theme.of(context).textTheme.bodyMedium, ), trailing: const Icon(Icons.chevron_right), @@ -67,7 +65,7 @@ class DidQrTile extends HookConsumerWidget { ) async { final qrValue = await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => const DidQrTabs(dap: 'username@didpay.me'), + builder: (context) => DidQrTabs(dap: Loc.of(context).placeholderDap), ), ); diff --git a/lib/features/payment/payment_details_page.dart b/lib/features/payment/payment_details_page.dart index 9816c9f2..63460919 100644 --- a/lib/features/payment/payment_details_page.dart +++ b/lib/features/payment/payment_details_page.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:didpay/features/did/did_provider.dart'; -import 'package:didpay/features/kcc/kcc_consent_page.dart'; +// import 'package:didpay/features/kcc/kcc_consent_page.dart'; import 'package:didpay/features/payment/payment_method_operations.dart'; import 'package:didpay/features/payment/payment_methods_page.dart'; import 'package:didpay/features/payment/payment_review_page.dart'; @@ -8,13 +8,13 @@ import 'package:didpay/features/payment/payment_state.dart'; import 'package:didpay/features/payment/payment_types_page.dart'; import 'package:didpay/features/tbdex/tbdex_service.dart'; import 'package:didpay/features/transaction/transaction.dart'; -import 'package:didpay/features/vcs/vcs_notifier.dart'; +// import 'package:didpay/features/vcs/vcs_notifier.dart'; import 'package:didpay/l10n/app_localizations.dart'; import 'package:didpay/shared/error_message.dart'; import 'package:didpay/shared/header.dart'; import 'package:didpay/shared/json_schema_form.dart'; import 'package:didpay/shared/loading_message.dart'; -import 'package:didpay/shared/modal/modal_flow.dart'; +// import 'package:didpay/shared/modal/modal_flow.dart'; import 'package:didpay/shared/theme/grid.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -60,9 +60,7 @@ class PaymentDetailsPage extends HookConsumerWidget { body: SafeArea( child: rfq.value != null ? rfq.value!.when( - data: (_) => LoadingMessage( - message: Loc.of(context).gettingYourQuote, - ), + data: (_) => Container(), loading: () => LoadingMessage( message: Loc.of(context).sendingYourRequest, ), @@ -111,22 +109,30 @@ class PaymentDetailsPage extends HookConsumerWidget { : selectedPaymentMethod.value as PayoutMethod?, ), onPaymentFormSubmit: (paymentState) async { - await _hasRequiredVc( + await _sendRfq( context, ref, paymentState, - offeringCredentials, - ).then( - (hasRequiredVc) async => !hasRequiredVc - ? null - : _sendRfq( - context, - ref, - paymentState, - rfq, - claims: offeringCredentials.value, - ), + rfq, + claims: offeringCredentials.value, ); + // TODO(ethan-tbd): uncomment below to initiate KCC flow + // await _hasRequiredVc( + // context, + // ref, + // paymentState, + // offeringCredentials, + // ).then( + // (hasRequiredVc) async => !hasRequiredVc + // ? null + // : _sendRfq( + // context, + // ref, + // paymentState, + // rfq, + // claims: offeringCredentials.value, + // ), + // ); }, ), ], @@ -294,60 +300,59 @@ class PaymentDetailsPage extends HookConsumerWidget { ref.read(didProvider), paymentState.copyWith(claims: claims), ) - .then((rfq) async { - state.value = AsyncData(rfq); - await Navigator.of(context) - .push( - MaterialPageRoute( - builder: (context) => PaymentReviewPage( - paymentState: paymentState.copyWith( - exchangeId: rfq.metadata.id, - claims: claims, + .then( + (rfq) async => Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => PaymentReviewPage( + paymentState: paymentState.copyWith( + exchangeId: rfq.metadata.id, + claims: claims, + ), + ), ), - ), - ), - ) - .then((_) { - if (context.mounted) state.value = null; - }); - }); + ) + .then((_) { + if (context.mounted) state.value = null; + }), + ); } on Exception catch (e) { state.value = AsyncError(e, StackTrace.current); } } - Future _hasRequiredVc( - BuildContext context, - WidgetRef ref, - PaymentState paymentState, - ValueNotifier?> offeringCredentials, - ) async { - final presentationDefinition = - paymentState.selectedOffering?.data.requiredClaims; - final credentials = - presentationDefinition?.selectCredentials(ref.read(vcsProvider)); + // Future _hasRequiredVc( + // BuildContext context, + // WidgetRef ref, + // PaymentState paymentState, + // ValueNotifier?> offeringCredentials, + // ) async { + // final presentationDefinition = + // paymentState.selectedOffering?.data.requiredClaims; + // final credentials = + // presentationDefinition?.selectCredentials(ref.read(vcsProvider)); - if (credentials == null && presentationDefinition == null) { - return true; - } + // if (credentials == null && presentationDefinition == null) { + // return true; + // } - if (credentials != null && credentials.isNotEmpty) { - offeringCredentials.value = credentials; - return true; - } + // if (credentials != null && credentials.isNotEmpty) { + // offeringCredentials.value = credentials; + // return true; + // } - final issuedCredential = await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ModalFlow( - initialWidget: KccConsentPage(pfi: paymentState.selectedPfi!), - ), - fullscreenDialog: true, - ), - ); + // final issuedCredential = await Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => ModalFlow( + // initialWidget: KccConsentPage(pfi: paymentState.selectedPfi!), + // ), + // fullscreenDialog: true, + // ), + // ); - if (issuedCredential == null) return false; + // if (issuedCredential == null) return false; - offeringCredentials.value = [issuedCredential as String]; - return true; - } + // offeringCredentials.value = [issuedCredential as String]; + // return true; + // } } diff --git a/lib/features/payment/payment_state.dart b/lib/features/payment/payment_state.dart index 0a9abb37..c7d46051 100644 --- a/lib/features/payment/payment_state.dart +++ b/lib/features/payment/payment_state.dart @@ -1,3 +1,4 @@ +import 'package:dap/dap.dart'; import 'package:decimal/decimal.dart'; import 'package:didpay/features/countries/countries.dart'; import 'package:didpay/features/pfis/pfi.dart'; @@ -14,6 +15,7 @@ class PaymentState { final Decimal? payinAmount; final Decimal? payoutAmount; final Decimal? exchangeRate; + final List? moneyAddresses; final Country? selectedCountry; final Pfi? selectedPfi; final Offering? selectedOffering; @@ -35,6 +37,7 @@ class PaymentState { this.payinAmount, this.payoutAmount, this.exchangeRate, + this.moneyAddresses, this.selectedCountry, this.selectedPfi, this.selectedOffering, @@ -57,6 +60,7 @@ class PaymentState { Decimal? payinAmount, Decimal? payoutAmount, Decimal? exchangeRate, + List? moneyAddresses, Country? selectedCountry, Pfi? selectedPfi, Offering? selectedOffering, @@ -78,6 +82,7 @@ class PaymentState { payinAmount: payinAmount ?? this.payinAmount, payoutAmount: payoutAmount ?? this.payoutAmount, exchangeRate: exchangeRate ?? this.exchangeRate, + moneyAddresses: moneyAddresses ?? this.moneyAddresses, selectedCountry: selectedCountry ?? this.selectedCountry, selectedPfi: selectedPfi ?? this.selectedPfi, selectedOffering: selectedOffering ?? this.selectedOffering, diff --git a/lib/features/send/send_page.dart b/lib/features/send/send_page.dart index 97430110..6c8c6f45 100644 --- a/lib/features/send/send_page.dart +++ b/lib/features/send/send_page.dart @@ -1,3 +1,4 @@ +import 'package:dap/dap.dart'; import 'package:didpay/features/countries/countries_page.dart'; import 'package:didpay/features/dap/dap_form.dart'; import 'package:didpay/features/feature_flags/feature_flag.dart'; @@ -7,9 +8,12 @@ import 'package:didpay/features/payment/payment_amount_page.dart'; import 'package:didpay/features/payment/payment_state.dart'; import 'package:didpay/features/transaction/transaction.dart'; import 'package:didpay/l10n/app_localizations.dart'; +import 'package:didpay/shared/error_message.dart'; import 'package:didpay/shared/header.dart'; +import 'package:didpay/shared/loading_message.dart'; import 'package:didpay/shared/theme/grid.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class SendPage extends HookConsumerWidget { @@ -19,33 +23,51 @@ class SendPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final featureFlags = ref.watch(featureFlagsProvider); + final dap = useState?>(null); + return Scaffold( appBar: _buildAppBar(context, featureFlags), body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Header( - title: Loc.of(context).whoDoYouWantToPay, - subtitle: Loc.of(context).enterADap, - ), - Expanded( - child: DapForm( - buttonTitle: Loc.of(context).next, - onSubmit: (did) => Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const PaymentAmountPage( - paymentState: PaymentState( - transactionType: TransactionType.send, - ), + child: dap.value != null + ? dap.value!.when( + data: (_) => Container(), + loading: () => + LoadingMessage(message: Loc.of(context).verifyingDap), + error: (error, _) => ErrorMessage( + message: error.toString(), + onRetry: () => dap.value = null, + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Header( + title: Loc.of(context).whoDoYouWantToPay, + subtitle: Loc.of(context).enterADap, + ), + Expanded( + child: DapForm( + buttonTitle: Loc.of(context).next, + dap: dap, + onSubmit: (moneyAddresses) => Navigator.of(context) + .push( + MaterialPageRoute( + builder: (_) => PaymentAmountPage( + paymentState: PaymentState( + transactionType: TransactionType.send, + moneyAddresses: moneyAddresses, + ), + ), + fullscreenDialog: true, + ), + ) + .then((_) { + if (context.mounted) dap.value = null; + }), ), - fullscreenDialog: true, ), - ), + ], ), - ), - ], - ), ), ); } diff --git a/lib/features/tbdex/tbdex_service.dart b/lib/features/tbdex/tbdex_service.dart index b9bfcd2e..b83d9893 100644 --- a/lib/features/tbdex/tbdex_service.dart +++ b/lib/features/tbdex/tbdex_service.dart @@ -28,7 +28,11 @@ class TbdexService { case TransactionType.send: filter = paymentState.selectedCountry != null ? GetOfferingsFilter(payoutCurrency: 'MXN') - : null; + : GetOfferingsFilter( + payoutCurrency: paymentState + .moneyAddresses?.firstOrNull?.currency + .toUpperCase(), + ); break; } @@ -48,6 +52,8 @@ class TbdexService { ); } + await Future.delayed(const Duration(milliseconds: 500)); + return offeringsMap; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c7174e95..3ee2ddde 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -60,6 +60,7 @@ "invalidDid": "Invalid DID", "invalidDap": "Invalid DAP", "noDidQrCodeFound": "No DID QR Code found", + "noDapQrCodeFound": "No DAP QR Code found", "myDap": "My DAP", "myVc": "My VC", "vcNotFound": "VC not found", @@ -162,5 +163,7 @@ } }, "ifYouExitNow": "If you exit now, you'll lose all your progress", - "enterADap": "Enter a Decentralized Agnostic Paytag (DAP)" + "enterADap": "Enter a Decentralized Agnostic Paytag (DAP)", + "verifyingDap": "Verifying DAP...", + "placeholderDap": "@username/didpay.me" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 032964f3..ee2ef374 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -355,6 +355,12 @@ abstract class Loc { /// **'No DID QR Code found'** String get noDidQrCodeFound; + /// No description provided for @noDapQrCodeFound. + /// + /// In en, this message translates to: + /// **'No DAP QR Code found'** + String get noDapQrCodeFound; + /// No description provided for @myDap. /// /// In en, this message translates to: @@ -840,6 +846,18 @@ abstract class Loc { /// In en, this message translates to: /// **'Enter a Decentralized Agnostic Paytag (DAP)'** String get enterADap; + + /// No description provided for @verifyingDap. + /// + /// In en, this message translates to: + /// **'Verifying DAP...'** + String get verifyingDap; + + /// No description provided for @placeholderDap. + /// + /// In en, this message translates to: + /// **'@username/didpay.me'** + String get placeholderDap; } class _LocDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 30836af9..a15f6209 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -140,6 +140,9 @@ class LocEn extends Loc { @override String get noDidQrCodeFound => 'No DID QR Code found'; + @override + String get noDapQrCodeFound => 'No DAP QR Code found'; + @override String get myDap => 'My DAP'; @@ -388,4 +391,10 @@ class LocEn extends Loc { @override String get enterADap => 'Enter a Decentralized Agnostic Paytag (DAP)'; + + @override + String get verifyingDap => 'Verifying DAP...'; + + @override + String get placeholderDap => '@username/didpay.me'; } diff --git a/pubspec.lock b/pubspec.lock index 79547d3c..238ad6d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -110,7 +110,7 @@ packages: description: path: "." ref: main - resolved-ref: dbdfd35d90dd0fc764b6dba4da35fa4755a8a845 + resolved-ref: f05f43d2151aae7e268b64869ea4925f2ca358c6 url: "https://github.com/TBD54566975/dap-dart.git" source: git version: "0.1.0"