diff --git a/lib/dashboard/dashboard.dart b/lib/dashboard/dashboard.dart index 07de65519..837604caa 100644 --- a/lib/dashboard/dashboard.dart +++ b/lib/dashboard/dashboard.dart @@ -14,4 +14,5 @@ export 'search/search.dart'; export 'select_network_fee_bottom_sheet/select_network_fee_bottom_sheet.dart'; export 'self_issued_credential_button/self_issued_credential_button.dart'; export 'src/src.dart'; +export 'user_pin/user_pin.dart'; export 'wert/wert.dart'; diff --git a/lib/dashboard/home/tab_bar/credentials/oid4c4vc_pick/oid4c4vc_credential_pick/view/oid4c4vc_credential_pick_page.dart b/lib/dashboard/home/tab_bar/credentials/oid4c4vc_pick/oid4c4vc_credential_pick/view/oid4c4vc_credential_pick_page.dart index 8fd18b534..6f02e2c8b 100644 --- a/lib/dashboard/home/tab_bar/credentials/oid4c4vc_pick/oid4c4vc_credential_pick/view/oid4c4vc_credential_pick_page.dart +++ b/lib/dashboard/home/tab_bar/credentials/oid4c4vc_pick/oid4c4vc_credential_pick/view/oid4c4vc_credential_pick_page.dart @@ -8,16 +8,20 @@ class Oidc4vcCredentialPickPage extends StatelessWidget { const Oidc4vcCredentialPickPage({ super.key, required this.credentials, + required this.userPin, }); final List credentials; + final String? userPin; static Route route({ required List credentials, + required String? userPin, }) => MaterialPageRoute( builder: (context) => Oidc4vcCredentialPickPage( credentials: credentials, + userPin: userPin, ), settings: const RouteSettings(name: '/Oidc4vcCredentialPickPage'), ); @@ -26,7 +30,10 @@ class Oidc4vcCredentialPickPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => Oidc4vcCredentialPickCubit(), - child: Oidc4vcCredentialPickView(credentials: credentials), + child: Oidc4vcCredentialPickView( + credentials: credentials, + userPin: userPin, + ), ); } } @@ -35,9 +42,11 @@ class Oidc4vcCredentialPickView extends StatelessWidget { const Oidc4vcCredentialPickView({ super.key, required this.credentials, + required this.userPin, }); final List credentials; + final String? userPin; @override Widget build(BuildContext context) { @@ -73,9 +82,10 @@ class Oidc4vcCredentialPickView extends StatelessWidget { creds.add(credentials[i]); } - context - .read() - .addCredentialsInLoop(creds); + context.read().addCredentialsInLoop( + credentials: creds, + userPin: userPin, + ); }, text: l10n.proceed, ), diff --git a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart index 334aaaef6..2c8d4d21c 100644 --- a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart +++ b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart @@ -242,6 +242,8 @@ class QRCodeScanCubit extends Cubit { return; } } + } else { + emit(state.acceptHost(isRequestVerified: true)); } } catch (e) { log.e(e); @@ -281,19 +283,17 @@ class QRCodeScanCubit extends Cubit { if (oidc4vcType.isEnabled && state.uri.toString().startsWith(oidc4vcType.offerPrefix)) { currentOIIDC4VCType = oidc4vcType; + break; } } if (currentOIIDC4VCType != null) { /// issuer side (oidc4VCI) - await initiateOIDC4VCCredentialIssuance( + await startOIDC4VCCredentialIssuance( scannedResponse: state.uri.toString(), - credentialsCubit: credentialsCubit, - oidc4vcType: currentOIIDC4VCType, - didKitProvider: didKitProvider, + currentOIIDC4VCType: currentOIIDC4VCType, qrCodeScanCubit: qrCodeScanCubit, - secureStorageProvider: getSecureStorage, dioClient: dioClient, ); return; @@ -499,6 +499,77 @@ class QRCodeScanCubit extends Cubit { } } + Future startOIDC4VCCredentialIssuance({ + required String scannedResponse, + required OIDC4VCType currentOIIDC4VCType, + required QRCodeScanCubit qrCodeScanCubit, + required DioClient dioClient, + }) async { + switch (currentOIIDC4VCType) { + case OIDC4VCType.DEFAULT: + case OIDC4VCType.HEDERA: + final dynamic credentialOfferJson = await getCredentialOfferJson( + scannedResponse: scannedResponse, + dioClient: dioClient, + ); + if (credentialOfferJson == null) break; + + final dynamic userPinRequired = credentialOfferJson['grants'] + ['urn:ietf:params:oauth:grant-type:pre-authorized_code'] + ['user_pin_required']; + + if (userPinRequired == null) break; + + if (userPinRequired is bool && userPinRequired) { + emit( + state.copyWith( + qrScanStatus: QrScanStatus.success, + route: UserPinPage.route( + onCancel: () { + goBack(); + }, + onProceed: (String userPin) async { + await initiateOIDC4VCCredentialIssuance( + scannedResponse: scannedResponse, + credentialsCubit: credentialsCubit, + oidc4vcType: currentOIIDC4VCType, + didKitProvider: didKitProvider, + qrCodeScanCubit: qrCodeScanCubit, + secureStorageProvider: getSecureStorage, + dioClient: dioClient, + userPin: userPin, + ); + }, + ), + ), + ); + } else { + break; + } + + return; + + case OIDC4VCType.GAIAX: + case OIDC4VCType.EBSIV2: + break; + + case OIDC4VCType.EBSIV3: + case OIDC4VCType.JWTVC: + throw Exception(); + } + + await initiateOIDC4VCCredentialIssuance( + scannedResponse: scannedResponse, + credentialsCubit: credentialsCubit, + oidc4vcType: currentOIIDC4VCType, + didKitProvider: didKitProvider, + qrCodeScanCubit: qrCodeScanCubit, + secureStorageProvider: getSecureStorage, + dioClient: dioClient, + userPin: null, + ); + } + Future isVCPresentable( PresentationDefinition? presentationDefinition, ) async { @@ -527,11 +598,17 @@ class QRCodeScanCubit extends Cubit { return true; } - void navigateToOidc4vcCredentialPickPage(List credentials) { + void navigateToOidc4vcCredentialPickPage({ + required List credentials, + required String? userPin, + }) { emit( state.copyWith( qrScanStatus: QrScanStatus.success, - route: Oidc4vcCredentialPickPage.route(credentials: credentials), + route: Oidc4vcCredentialPickPage.route( + credentials: credentials, + userPin: userPin, + ), ), ); } @@ -727,13 +804,17 @@ class QRCodeScanCubit extends Cubit { } } - Future addCredentialsInLoop(List credentials) async { + Future addCredentialsInLoop({ + required List credentials, + required String? userPin, + }) async { try { OIDC4VCType? currentOIIDC4VCType; for (final oidc4vcType in OIDC4VCType.values) { if (oidc4vcType.isEnabled && state.uri.toString().startsWith(oidc4vcType.offerPrefix)) { currentOIIDC4VCType = oidc4vcType; + break; } } @@ -753,6 +834,7 @@ class QRCodeScanCubit extends Cubit { credentialType: credentialType.toString(), isLastCall: i + 1 == credentials.length, dioClient: DioClient('', Dio()), + userPin: userPin, ); } oidc4vc.resetNonceAndAccessToken(); diff --git a/lib/dashboard/user_pin/user_pin.dart b/lib/dashboard/user_pin/user_pin.dart new file mode 100644 index 000000000..c0cf45bf2 --- /dev/null +++ b/lib/dashboard/user_pin/user_pin.dart @@ -0,0 +1 @@ +export 'view/user_pin_page.dart'; diff --git a/lib/dashboard/user_pin/view/user_pin_page.dart b/lib/dashboard/user_pin/view/user_pin_page.dart new file mode 100644 index 000000000..3ef9341ae --- /dev/null +++ b/lib/dashboard/user_pin/view/user_pin_page.dart @@ -0,0 +1,229 @@ +import 'dart:io'; + +import 'package:altme/app/app.dart'; +import 'package:altme/dashboard/dashboard.dart'; +import 'package:altme/l10n/l10n.dart'; +import 'package:altme/theme/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class UserPinPage extends StatelessWidget { + const UserPinPage({ + super.key, + required this.onProceed, + required this.onCancel, + }); + + final void Function(String pincode) onProceed; + final Function onCancel; + + static Route route({ + required void Function(String pincode) onProceed, + required Function onCancel, + }) { + return MaterialPageRoute( + builder: (_) => UserPinPage( + onProceed: onProceed, + onCancel: onCancel, + ), + settings: const RouteSettings(name: '/UserPinPage'), + ); + } + + @override + Widget build(BuildContext context) { + return UserPinView( + onCancel: onCancel, + onProceed: onProceed, + ); + } +} + +class UserPinView extends StatefulWidget { + const UserPinView({ + super.key, + required this.onProceed, + required this.onCancel, + }); + + final void Function(String pincode) onProceed; + final Function onCancel; + + @override + State createState() => _UserPinViewState(); +} + +class _UserPinViewState extends State { + final TextEditingController pinController = TextEditingController(); + + late final _selectionControls = Platform.isIOS + ? AppCupertinoTextSelectionControls(onPaste: _onPaste) + : AppMaterialTextSelectionControls(onPaste: _onPaste); + + Future _onPaste(TextSelectionDelegate value) async { + final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + final text = data?.text ?? ''; + if (text.isEmpty) { + return; + } else { + _setPinControllerText(text); + } + } + + void _insertKey(String key) { + final text = pinController.text; + _setPinControllerText(text + key); + } + + void _setPinControllerText(String text) { + pinController.text = text; + pinController.selection = TextSelection.fromPosition( + TextPosition(offset: pinController.text.length), + ); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return WillPopScope( + onWillPop: () async { + Navigator.of(context).pop(); + widget.onCancel.call(); + return true; + }, + child: BasePage( + scrollView: false, + titleLeading: BackLeadingButton( + onPressed: () { + Navigator.of(context).pop(); + widget.onCancel.call(); + }, + ), + body: BackgroundCard( + height: double.infinity, + width: double.infinity, + padding: const EdgeInsets.all(Sizes.spaceSmall), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + const SizedBox(height: Sizes.space2XLarge), + Text( + "Please insert your pin.????", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: Sizes.space2XLarge), + Form( + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + selectionControls: _selectionControls, + controller: pinController, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + cursorWidth: 4, + autofocus: false, + keyboardType: TextInputType.none, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + cursorRadius: const Radius.circular(4), + textAlign: TextAlign.start, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(Sizes.smallRadius), + ), + ), + hintText: '000000', + contentPadding: const EdgeInsets.symmetric( + vertical: 5, + horizontal: 10, + ), + suffixStyle: + Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w900, + ), + ), + ), + ), + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: SingleChildScrollView( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.width * 0.7, + child: LayoutBuilder( + builder: (_, constraint) { + return NumericKeyboard( + allowAction: true, + keyboardUIConfig: KeyboardUIConfig( + digitShape: BoxShape.rectangle, + spacing: 40, + digitInnerMargin: EdgeInsets.zero, + keyboardRowMargin: EdgeInsets.zero, + digitBorderWidth: 0, + digitTextStyle: Theme.of(context) + .textTheme + .calculatorKeyboardDigitTextStyle, + keyboardSize: constraint.biggest, + ), + trailingButton: KeyboardButton( + digitShape: BoxShape.rectangle, + digitBorderWidth: 0, + semanticsLabel: 'delete', + icon: Image.asset( + IconStrings.keyboardDelete, + width: Sizes.icon2x, + color: Colors.white, + ), + allowAction: true, + onLongPress: (_) {}, + onTap: (_) { + final text = pinController.text; + if (text.isNotEmpty) { + String pincode = ''; + if (text.length > 1) { + pincode = + text.substring(0, text.length - 1); + } + _setPinControllerText(pincode); + } + }, + ), + onKeyboardTap: _insertKey, + ); + }, + ), + ), + ), + ), + ), + ], + ), + ), + navigation: SafeArea( + child: Padding( + padding: const EdgeInsets.only( + left: Sizes.spaceSmall, + right: Sizes.spaceSmall, + bottom: Sizes.spaceSmall, + ), + child: MyElevatedButton( + borderRadius: Sizes.normalRadius, + text: l10n.next, + onPressed: () => widget.onProceed.call(pinController.text), + ), + ), + ), + ), + ); + } +} diff --git a/lib/oidc4vc/initiate_oidv4vc_credential_issuance.dart b/lib/oidc4vc/initiate_oidv4vc_credential_issuance.dart index 5fe9f17fe..449d6b33a 100644 --- a/lib/oidc4vc/initiate_oidv4vc_credential_issuance.dart +++ b/lib/oidc4vc/initiate_oidv4vc_credential_issuance.dart @@ -17,6 +17,7 @@ Future initiateOIDC4VCCredentialIssuance({ required CredentialsCubit credentialsCubit, required SecureStorageProvider secureStorageProvider, required DioClient dioClient, + required String? userPin, }) async { final Uri uriFromScannedResponse = Uri.parse(scannedResponse); @@ -44,7 +45,10 @@ Future initiateOIDC4VCCredentialIssuance({ } if (credentialType is List) { - qrCodeScanCubit.navigateToOidc4vcCredentialPickPage(credentialType); + qrCodeScanCubit.navigateToOidc4vcCredentialPickPage( + credentials: credentialType, + userPin: userPin, + ); } else { final OIDC4VC oidc4vc = oidc4vcType.getOIDC4VC; await getAndAddCredential( @@ -57,6 +61,7 @@ Future initiateOIDC4VCCredentialIssuance({ secureStorageProvider: secureStorageProvider, isLastCall: true, dioClient: dioClient, + userPin: userPin, ); oidc4vc.resetNonceAndAccessToken(); qrCodeScanCubit.goBack(); @@ -73,6 +78,7 @@ Future getAndAddCredential({ required SecureStorageProvider secureStorageProvider, required bool isLastCall, required DioClient dioClient, + required String? userPin, }) async { final Uri uriFromScannedResponse = Uri.parse(scannedResponse); @@ -130,6 +136,7 @@ Future getAndAddCredential({ privateKey: privateKey, credentialSupportedTypes: oidc4vcType.credentialSupported, indexValue: oidc4vcType.indexValue, + userPin: userPin, ); await addOIDC4VCCredential( diff --git a/lib/splash/view/splash_page.dart b/lib/splash/view/splash_page.dart index 03a50b7a1..f44949710 100644 --- a/lib/splash/view/splash_page.dart +++ b/lib/splash/view/splash_page.dart @@ -154,20 +154,18 @@ class _SplashViewState extends State { if (oidc4vcType.isEnabled && uri.toString().startsWith(oidc4vcType.offerPrefix)) { currentOIIDC4VCType = oidc4vcType; + break; } } if (currentOIIDC4VCType != null) { // ignore: require_trailing_commas - await initiateOIDC4VCCredentialIssuance( - scannedResponse: uri.toString(), - credentialsCubit: context.read(), - oidc4vcType: currentOIIDC4VCType, - didKitProvider: DIDKitProvider(), - qrCodeScanCubit: context.read(), - secureStorageProvider: secure_storage.getSecureStorage, - dioClient: DioClient('', Dio()), - ); + await context.read().startOIDC4VCCredentialIssuance( + scannedResponse: uri.toString(), + currentOIIDC4VCType: currentOIIDC4VCType, + qrCodeScanCubit: context.read(), + dioClient: DioClient('', Dio()), + ); } } }); diff --git a/packages/oidc4vc/lib/src/oidc4vc.dart b/packages/oidc4vc/lib/src/oidc4vc.dart index 6f7c4d2e4..db8c67831 100644 --- a/packages/oidc4vc/lib/src/oidc4vc.dart +++ b/packages/oidc4vc/lib/src/oidc4vc.dart @@ -197,6 +197,7 @@ class OIDC4VC { String? preAuthorizedCode, String? mnemonic, String? privateKey, + String? userPin, }) async { final kIssuer = getIssuer( preAuthorizedCode: preAuthorizedCode, @@ -204,7 +205,11 @@ class OIDC4VC { credentialRequestUri: credentialRequestUri, ); - final tokenData = buildTokenData(preAuthorizedCode, credentialRequestUri); + final tokenData = buildTokenData( + preAuthorizedCode: preAuthorizedCode, + credentialRequestUri: credentialRequestUri, + userPin: userPin, + ); final openidConfigurationUrl = '$kIssuer/.well-known/openid-configuration'; @@ -268,10 +273,11 @@ class OIDC4VC { accessToken = null; } - Map buildTokenData( + Map buildTokenData({ + required Uri credentialRequestUri, String? preAuthorizedCode, - Uri credentialRequestUri, - ) { + String? userPin, + }) { late Map tokenData; if (preAuthorizedCode != null) { @@ -288,6 +294,11 @@ class OIDC4VC { 'grant_type': 'authorization_code', }; } + + if (userPin != null) { + tokenData['user_pin'] = userPin; + } + return tokenData; }