diff --git a/.github/workflows/build_dev.yaml b/.github/workflows/build_dev.yaml index 5fa2e52..53d5d3a 100644 --- a/.github/workflows/build_dev.yaml +++ b/.github/workflows/build_dev.yaml @@ -6,62 +6,13 @@ on: - dev jobs: - version: - name: Version & Build + validate: + name: Validate Package runs-on: macos-latest - environment: release - permissions: - contents: write - - outputs: - version: ${{ steps.get_new_version.outputs.result}} steps: - name: Checkout code uses: actions/checkout@v4 - with: - token: ${{secrets.ELEVATED_TOKEN}} - - - name: 📇 Configure git - run: | - git fetch --prune --unshallow - git config --global user.name "GitHub Actions" - git config --global user.email "gh-actions@merckgroup.com" - shell: bash - - # Retrieve the new version - - name: 🔂 Run standard-version - uses: actions/github-script@v7 - with: - script: | - const {execSync} = require('child_process'); - execSync('npx standard-version --skip.tag --prerelease', {stdio: 'inherit'}); - - # Retrieve the new version - - name: ⏎ Get new version - uses: actions/github-script@v7 - id: get_new_version - with: - result-encoding: string - script: | - const fs = require('fs'); - const package = JSON.parse(fs.readFileSync('package.json', 'utf8')); - return package.version; - - - name: Print new version - run: echo ${{ steps.get_new_version.outputs.result}} - - # Bump the pubspec.yaml file - - name: ⬆️ Bump pubspec.yaml - uses: emdgroup/mtrust-urp/.github/shared_actions/update-pubspec@dev - with: - version: ${{ steps.get_new_version.outputs.result }} - directory: . - - - name: 📝 Update version in readme - uses: emdgroup/mtrust-urp/.github/shared_actions/update-pubspec-readme-version@dev - with: - directory: . - name: Setup Dart uses: dart-lang/setup-dart@v1 @@ -82,17 +33,11 @@ jobs: with: directory: "." - # We first commit with proper message and add an empty commit to keep the files history clean - - name: Update repo versions - run: | - git add . - git commit -m "chore(release): ${{ steps.get_new_version.outputs.result }}" - git commit --allow-empty -m "chore(release): ${{ steps.get_new_version.outputs.result }} [skip ci]" - git push origin dev - - # For this part it is important to not push a commit with [skip ci] before the tag release - - name: Push tag for pub.dev - run: | - git commit --allow-empty -m "chore(release): ${{ steps.get_new_version.outputs.result }}" - git tag -a v${{ steps.get_new_version.outputs.result }} -m "Pub.dev version ${{ steps.get_new_version.outputs.result }}" - git push origin v${{ steps.get_new_version.outputs.result }} + api-guard: + needs: validate + uses: emdgroup/mtrust-api-guard/.github/workflows/publish_workflow.yaml@main + with: + git_push_branch: dev + git_push: true + pre_release: true + secrets: inherit diff --git a/.github/workflows/build_main.yaml b/.github/workflows/build_main.yaml index b646a76..e319d75 100644 --- a/.github/workflows/build_main.yaml +++ b/.github/workflows/build_main.yaml @@ -6,62 +6,13 @@ on: - main jobs: - version: - name: Version & Build + validate: + name: Validate Package runs-on: macos-latest - environment: release - permissions: - contents: write - - outputs: - version: ${{ steps.get_new_version.outputs.result}} steps: - name: Checkout code uses: actions/checkout@v4 - with: - token: ${{secrets.ELEVATED_TOKEN}} - - - name: 📇 Configure git - run: | - git fetch --prune --unshallow - git config --global user.name "GitHub Actions" - git config --global user.email "gh-actions@merckgroup.com" - shell: bash - - # Retrieve the new version - - name: 🔂 Run standard-version - uses: actions/github-script@v7 - with: - script: | - const {execSync} = require('child_process'); - execSync('npx standard-version --skip.tag', {stdio: 'inherit'}); - - # Retrieve the new version - - name: ⏎ Get new version - uses: actions/github-script@v7 - id: get_new_version - with: - result-encoding: string - script: | - const fs = require('fs'); - const package = JSON.parse(fs.readFileSync('package.json', 'utf8')); - return package.version; - - - name: Print new version - run: echo ${{ steps.get_new_version.outputs.result}} - - # Bump the pubspec.yaml file - - name: ⬆️ Bump pubspec.yaml - uses: emdgroup/mtrust-urp/.github/shared_actions/update-pubspec@main - with: - version: ${{ steps.get_new_version.outputs.result }} - directory: . - - - name: 📝 Update version in readme - uses: emdgroup/mtrust-urp/.github/shared_actions/update-pubspec-readme-version@main - with: - directory: . - name: Setup Dart uses: dart-lang/setup-dart@v1 @@ -82,24 +33,17 @@ jobs: with: directory: "." - # We first commit with proper message and add an empty commit to keep the files history clean - - name: Update repo versions - run: | - git add . - git commit -m "chore(release): ${{ steps.get_new_version.outputs.result }}" - git commit --allow-empty -m "chore(release): ${{ steps.get_new_version.outputs.result }} [skip ci]" - git push origin main - - # For this part it is important to not push a commit with [skip ci] before the tag release - - name: Push tag for pub.dev - run: | - git commit --allow-empty -m "chore(release): ${{ steps.get_new_version.outputs.result }}" - git tag -a v${{ steps.get_new_version.outputs.result }} -m "Pub.dev version ${{ steps.get_new_version.outputs.result }}" - git push origin v${{ steps.get_new_version.outputs.result }} + api-guard: + needs: validate + uses: emdgroup/mtrust-api-guard/.github/workflows/publish_workflow.yaml@main + with: + git_push_branch: main + git_push: true + secrets: inherit rebase_dev: name: Write changes to dev branch - needs: [version] + needs: [api-guard] runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/pr_dev.yaml b/.github/workflows/pr_dev.yaml index d075208..1a78037 100644 --- a/.github/workflows/pr_dev.yaml +++ b/.github/workflows/pr_dev.yaml @@ -66,3 +66,8 @@ jobs: uses: emdgroup/mtrust-urp/.github/shared_actions/check-dart-licenses@dev with: directory: . + + api-guard: + uses: emdgroup/mtrust-api-guard/.github/workflows/pr_workflow.yaml@main + with: + pre_release: true \ No newline at end of file diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml index c991045..1a5cdd7 100644 --- a/.github/workflows/pr_main.yaml +++ b/.github/workflows/pr_main.yaml @@ -66,3 +66,6 @@ jobs: uses: emdgroup/mtrust-urp/.github/shared_actions/check-dart-licenses@main with: directory: . + + api-guard: + uses: emdgroup/mtrust-api-guard/.github/workflows/pr_workflow.yaml@main diff --git a/.gitignore b/.gitignore index 1a8796f..da3b032 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,4 @@ test/failures **/GeneratedPluginRegistrant.swift **/generated_plugin_registrant.cc **/generated_plugin_registrant.h -**/generated_plugins.cmake \ No newline at end of file +**/generated_plugins.cmake diff --git a/README.md b/README.md index 1ef320c..4437601 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,9 @@ To display the SEC Modal, utilize the `SecModalBuilder` widget. It requires a co strategy: _connectionStrategy, payload: // Payload, onVerificationDone: (mesurement) {}, - onVerificationFailed: () {}, + onVerificationFailed: (exception) { + // Handle verification failure. The exception provides details about the cause. + }, onDismiss: (){ } // Optionally canDismiss: true, // Define whether the user can dismiss the modal builder: (context, openModal) { diff --git a/analysis_options.yaml b/analysis_options.yaml index 2822833..a671adc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,4 +6,8 @@ analyzer: exclude: - "lib/src/**/*.g.dart" - "lib/src/**/*.freezed.dart" - - "lib/src/ui/l10n/*.dart" \ No newline at end of file + - "lib/src/ui/l10n/*.dart" + +api_guard: + exclude: + - "example/**" \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index ba543b6..033cdf4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,8 @@ import 'package:example/virtual_strategy.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:mtrust_sec_kit/mtrust_sec_kit.dart'; - import 'package:liquid_flutter/liquid_flutter.dart'; +import 'package:mtrust_sec_kit/mtrust_sec_kit.dart'; import 'package:mtrust_urp_ble_strategy/mtrust_urp_ble_strategy.dart'; void main() { @@ -102,8 +101,8 @@ class _MainAppState extends State { onVerificationDone: (measurement) { debugPrint("Verification done ${measurement.measurement}"); }, - onVerificationFailed: () { - debugPrint("Verification failed"); + onVerificationFailed: (exception) { + debugPrint("Verification failed: $exception"); }, builder: (context, openModal) { return LdButton( diff --git a/lib/src/sec_reader.dart b/lib/src/sec_reader.dart index f3e10d2..5830436 100644 --- a/lib/src/sec_reader.dart +++ b/lib/src/sec_reader.dart @@ -61,7 +61,10 @@ class SECReader extends CmdWrapper { ); if (!connceted) { - throw SecReaderException(message: 'Failed to connect to reader'); + throw SecReaderException( + message: 'Failed to connect to reader', + type: SecReaderExceptionType.connectionFailed, + ); } return SECReader(connectionStrategy: connectionStrategy); @@ -91,6 +94,7 @@ class SECReader extends CmdWrapper { if (!connected) { throw SecReaderException( message: 'Failed to connect to found reader $reader', + type: SecReaderExceptionType.connectionFailed, ); } @@ -111,6 +115,11 @@ class SECReader extends CmdWrapper { ); } + @override + Future addCoreCmdToQueue(UrpCoreCommand coreCommand) async { + return _addCommandToQueue(coreCommand: coreCommand); + } + /// Pings the device. @override Future ping() async { @@ -129,7 +138,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to get info'); + throw SecReaderException( + message: 'Failed to get info', + type: SecReaderExceptionType.commandFailed, + ); } return UrpDeviceInfo.fromBuffer(res.payload); } @@ -143,7 +155,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to get power state'); + throw SecReaderException( + message: 'Failed to get power state', + type: SecReaderExceptionType.commandFailed, + ); } return UrpPowerState.fromBuffer(res.payload); } @@ -167,20 +182,14 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to get name'); + throw SecReaderException( + message: 'Failed to get name', + type: SecReaderExceptionType.commandFailed, + ); } return UrpDeviceName.fromBuffer(res.payload); } - /// Pair the device. - @override - Future pair() async { - final cmd = UrpCoreCommand( - command: UrpCommand.urpPair, - ); - await _addCommandToQueue(coreCommand: cmd); - } - /// Unpair the device. @override Future unpair() async { @@ -253,7 +262,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to get public key'); + throw SecReaderException( + message: 'Failed to get public key', + type: SecReaderExceptionType.commandFailed, + ); } return UrpPublicKey.fromBuffer(res.payload); } @@ -267,7 +279,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to get public key'); + throw SecReaderException( + message: 'Failed to get device id', + type: SecReaderExceptionType.commandFailed, + ); } return UrpDeviceId.fromBuffer(res.payload); } @@ -291,7 +306,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to connect to AP'); + throw SecReaderException( + message: 'Failed to connect to AP', + type: SecReaderExceptionType.commandFailed, + ); } return UrpWifiState.fromBuffer(res.payload); } @@ -315,7 +333,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(coreCommand: cmd); if (!res.hasPayload()) { - throw SecReaderException(message: 'Failed to start AP'); + throw SecReaderException( + message: 'Failed to start AP', + type: SecReaderExceptionType.commandFailed, + ); } return UrpWifiState.fromBuffer(res.payload); } @@ -330,7 +351,7 @@ class SECReader extends CmdWrapper { } /// Prepares (primes) a measurement for the given [payload]. - Future prime(String payload) async { + Future prime(String payload) async { final cmd = UrpSecDeviceCommand( command: UrpSecCommand.urpSecPrime, primeParameters: UrpSecPrimeParameters(payload: payload), @@ -340,7 +361,7 @@ class SECReader extends CmdWrapper { return UrpSecPrimeResponse.fromBuffer(res.payload); } catch (e) { if (e is DeviceError) { - if (e.errorCode != 4) { + if (e.errorCode != UrpErrorCode.urpLeaseError) { rethrow; } final publicKey = await getPublicKey(); @@ -360,7 +381,7 @@ class SECReader extends CmdWrapper { await setToken(newToken); return prime(payload); } - return null; + rethrow; } } @@ -461,7 +482,10 @@ class SECReader extends CmdWrapper { final res = await _addCommandToQueue(deviceCommand: cmd); if (!res.hasPayload()) { - throw Exception('Failed to get model info'); + throw SecReaderException( + message: 'Failed to get model info', + type: SecReaderExceptionType.commandFailed, + ); } final urpSecModels = UrpSecModels.fromBuffer(res.payload); return urpSecModels.models; diff --git a/lib/src/sec_reader_exception.dart b/lib/src/sec_reader_exception.dart index f8ede29..9574d07 100644 --- a/lib/src/sec_reader_exception.dart +++ b/lib/src/sec_reader_exception.dart @@ -9,15 +9,23 @@ enum SecReaderExceptionType { /// Failed to get or install new token tokenFailed, + /// Failed to connect to a reader + connectionFailed, + + /// A command sent to the reader failed or returned an unexpected response + commandFailed, + /// Unspecified error unspecified, } -/// Exception thrown to indicate errors related to the SEC reader. +/// Exception thrown to indicate errors from the SEC reader. /// -/// This exception extends the [Error] class and is designed -/// to be used specifically for handling errors in the context of SEC -/// reading. +/// All errors thrown by `SECReader` methods use this type, with a +/// [SecReaderExceptionType] to categorize the failure. Inside the widget UI, +/// the exception mapper translates these into localized exceptions for +/// display, preserving the original [SecReaderException] so that +/// `onVerificationFailed` callbacks can access the typed failure cause. class SecReaderException implements Exception { /// Creates a new instance of [SecReaderException]. /// @@ -30,6 +38,31 @@ class SecReaderException implements Exception { this.type = SecReaderExceptionType.unspecified, }); + /// Creates a [SecReaderException] from an arbitrary exception. + /// + /// If [exception] is already a [SecReaderException], it is returned as-is, + /// preserving its [type]. Otherwise, a new instance is created with + /// [SecReaderExceptionType.unspecified]. + /// + /// The [fallbackMessage] is preferred over `exception.toString()` when + /// creating a new instance. This is typically the localized error message + /// produced by the exception mapper. + /// + /// Used by the widget layer to extract a [SecReaderException] from a + /// mapped exception for the `onVerificationFailed` callback. + factory SecReaderException.from( + dynamic exception, { + String? fallbackMessage, + }) { + if (exception is SecReaderException) { + return exception; + } + + return SecReaderException( + message: fallbackMessage ?? exception?.toString() ?? 'Unknown error', + ); + } + /// A message providing additional details about the SEC reader error. /// /// If not specified during the exception creation, a default message @@ -40,9 +73,3 @@ class SecReaderException implements Exception { /// The default value is [SecReaderExceptionType.unspecified]. final SecReaderExceptionType type; } - -/// Exception thrown when a SEC reader is not found. -class SecConnectionFailedException extends SecReaderException { - /// Creates a new instance of [SecConnectionFailedException]. - SecConnectionFailedException() : super(message: 'SEC reader not found'); -} diff --git a/lib/src/ui/scanning_view.dart b/lib/src/ui/scanning_view.dart index 3c6c541..270cad1 100644 --- a/lib/src/ui/scanning_view.dart +++ b/lib/src/ui/scanning_view.dart @@ -30,8 +30,15 @@ class ScanningView extends StatelessWidget { UrpSecSecureMeasurement measurement, ) onVerificationDone; - /// Function to call when the verification fails. - final Future Function() onVerificationFailed; + /// Called when the verification fails. + /// + /// The exception is extracted from the mapped exception produced by + /// the exception mapper. If the original exception was already a + /// [SecReaderException], it is passed through directly. Otherwise, a new + /// [SecReaderException] is created using the mapper's localized message. + final Future Function( + SecReaderException exception, + ) onVerificationFailed; @override Widget build(BuildContext context) { @@ -159,17 +166,8 @@ class ScanningView extends StatelessWidget { ], ); case (LdSubmitStateType.error): - var message = + final message = measurementController.state.error?.message ?? SecLocalizations.of(context).verificationFailedMessage; - if (measurementController.state.error?.exception.runtimeType == - SecReaderExceptionType) { - final error = measurementController.state.error?.exception - as SecReaderException; - if (error.type == - SecReaderExceptionType.incompatibleFirmware) { - message = SecLocalizations.of(context).incompatibleFirmware; - } - } return LdAutoSpace( key: const Key('failed-scanning-view'), animate: true, @@ -193,7 +191,15 @@ class ScanningView extends StatelessWidget { width: double.infinity, borderRadius: LdTheme.of(context).radius(LdSize.l), size: LdSize.l, - onPressed: onVerificationFailed, + onPressed: () { + return onVerificationFailed( + SecReaderException.from( + measurementController.state.error?.exception, + fallbackMessage: + measurementController.state.error?.message, + ), + ); + }, loadingText: SecLocalizations.of(context).disconnecting, context: context, child: Text( diff --git a/lib/src/ui/sec_modal.dart b/lib/src/ui/sec_modal.dart index 924c732..d97ba35 100644 --- a/lib/src/ui/sec_modal.dart +++ b/lib/src/ui/sec_modal.dart @@ -42,8 +42,15 @@ class SecModalBuilder extends StatelessWidget { /// Will be called if a verification was successful. final void Function(UrpSecSecureMeasurement measurement) onVerificationDone; - /// Will be called if a verification failed. - final void Function() onVerificationFailed; + /// Called when the verification fails. + /// + /// Receives a [SecReaderException] describing the failure cause. + /// Use [SecReaderException.type] to distinguish between failure types + /// (e.g. [SecReaderExceptionType.tokenFailed], + /// [SecReaderExceptionType.measurementFailed]). + final void Function( + SecReaderException exception, + ) onVerificationFailed; /// Called when the user dismisses the sheet. final void Function()? onDismiss; @@ -89,7 +96,7 @@ class SecModalBuilder extends StatelessWidget { return builder(context, () async { final result = await openModal(); if (result is SecResultFailed) { - onVerificationFailed(); + onVerificationFailed(result.exception); } else if (result is SecResultSuccess) { onVerificationDone(result.measurement); } else { @@ -125,7 +132,13 @@ class SecResultSuccess extends SecResult { class SecResultDismissed extends SecResult {} /// Returned in case of a failed SEC verification (e.g. a timeout) -class SecResultFailed extends SecResult {} +class SecResultFailed extends SecResult { + /// Creates a new instance of [SecResultFailed] + SecResultFailed(this.exception); + + /// The exception that caused the failure. + final SecReaderException exception; +} /// Build a modal using [SecWidget], pops the result of the SEC verification. /// The result is either [SecResultSuccess], [SecResultFailed] @@ -178,8 +191,8 @@ LdModal secModal({ onVerificationDone: (UrpSecSecureMeasurement measurement) async { Navigator.of(context).pop(SecResultSuccess(measurement)); }, - onVerificationFailed: () async { - Navigator.of(context).pop(SecResultFailed()); + onVerificationFailed: (exception) async { + Navigator.of(context).pop(SecResultFailed(exception)); }, tokenAmount: tokenAmount, ), diff --git a/lib/src/ui/sec_widget.dart b/lib/src/ui/sec_widget.dart index 2f9bd41..6c9e1fb 100644 --- a/lib/src/ui/sec_widget.dart +++ b/lib/src/ui/sec_widget.dart @@ -34,8 +34,20 @@ class SecWidget extends StatelessWidget { UrpSecSecureMeasurement measurement, ) onVerificationDone; - /// Will be called if a verification failed. - final Future Function() onVerificationFailed; + /// Called when verification fails. + /// + /// The exception is extracted from the mapped exception produced by + /// the exception mapper. If the original exception was a + /// [SecReaderException], it is returned as-is (preserving its [SecReaderExceptionType]). + /// Otherwise, a new [SecReaderException] is created with the mapper's + /// localized message and [SecReaderExceptionType.unspecified]. + /// + /// Use [SecReaderException.type] to distinguish failure causes + /// (e.g. [SecReaderExceptionType.tokenFailed], + /// [SecReaderExceptionType.incompatibleFirmware]). + final Future Function( + SecReaderException exception, + ) onVerificationFailed; /// Amount of token to be requested on token refresh. final int? tokenAmount; @@ -52,8 +64,8 @@ class SecWidget extends StatelessWidget { connectionStrategy: strategy, storageAdapter: storageAdapter, connectedBuilder: (BuildContext context) { - return LdSubmit( - config: LdSubmitConfig( + return LdSubmit( + config: LdSubmitConfig( loadingText: locale.primingTitle, autoTrigger: true, action: () async { @@ -67,16 +79,11 @@ class SecWidget extends StatelessWidget { return reader.prime(payload); }, ), - builder: LdSubmitCustomBuilder( + builder: LdSubmitCustomBuilder( builder: (context, controller, stateType) { if (stateType == LdSubmitStateType.error) { - var message = - controller.state.error?.message ?? 'Unknown error'; - - if (controller.state.error?.exception.runtimeType - is ApiException) { - message = locale.tokenFailed; - } + final message = + controller.state.error?.message ?? locale.primeFailed; return LdAutoSpace( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -104,7 +111,15 @@ class SecWidget extends StatelessWidget { ) else LdButtonWarning( - onPressed: onVerificationFailed, + onPressed: () { + return onVerificationFailed( + SecReaderException.from( + controller.state.error?.exception, + fallbackMessage: + controller.state.error?.message, + ), + ); + }, context: context, child: Text( locale.done, @@ -138,9 +153,9 @@ class SecWidget extends StatelessWidget { controller.reset(); await onVerificationDone(measurement); }, - onVerificationFailed: () async { + onVerificationFailed: (e) async { controller.reset(); - await onVerificationFailed(); + await onVerificationFailed(e); }, remainingScans: controller.state.result?.gsa, ); @@ -154,6 +169,16 @@ class SecWidget extends StatelessWidget { } } +/// Maps exceptions thrown during the SEC workflow into localized +/// [LdException]s for display by Liquid's error UI. +/// +/// Handles three categories of exceptions: +/// - [SecReaderException]: Mapped to localized messages based on +/// [SecReaderExceptionType]. The original exception is preserved so it +/// can be extracted by `onVerificationFailed` callbacks. +/// - [DeviceError]: Raw BLE device errors that propagated through [SECReader] +/// without being wrapped. Presented as a generic retriable error. +/// - All other exceptions: Delegated to the base [LdExceptionMapper]. class _SecExceptionMapper extends LdExceptionMapper { _SecExceptionMapper({ required this.secLocalizations, @@ -168,6 +193,8 @@ class _SecExceptionMapper extends LdExceptionMapper { final retriable = { SecReaderExceptionType.tokenFailed, SecReaderExceptionType.measurementFailed, + SecReaderExceptionType.connectionFailed, + SecReaderExceptionType.commandFailed, SecReaderExceptionType.unspecified, }; return LdException( @@ -177,9 +204,19 @@ class _SecExceptionMapper extends LdExceptionMapper { secLocalizations.incompatibleFirmware, SecReaderExceptionType.measurementFailed => secLocalizations.verificationFailedMessage, + SecReaderExceptionType.connectionFailed => localizations.unknownError, + SecReaderExceptionType.commandFailed => localizations.unknownError, SecReaderExceptionType.unspecified => localizations.unknownError, }, canRetry: retriable.contains(e.type), + exception: e, + ); + } + + if (e is DeviceError) { + return LdException( + message: localizations.unknownError, + exception: e, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 5df7faf..67436e2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,9 @@ dependencies: sdk: flutter intl: ^0.20.2 liquid_flutter: ^22.0.4 - mtrust_urp_core: 9.1.0-12 - mtrust_urp_types: ^6.2.0 - mtrust_urp_ui: 9.1.0-12 + mtrust_urp_core: ^9.1.0-13 + mtrust_urp_types: ^6.2.1 + mtrust_urp_ui: ^9.1.0-13 dev_dependencies: flutter_test: sdk: flutter diff --git a/test/goldens/SecWidget/Idle/l-dark.png b/test/goldens/SecWidget/Idle/l-dark.png index a2a70ef..027b090 100644 Binary files a/test/goldens/SecWidget/Idle/l-dark.png and b/test/goldens/SecWidget/Idle/l-dark.png differ diff --git a/test/goldens/SecWidget/Idle/l-light.png b/test/goldens/SecWidget/Idle/l-light.png index 6b69e9a..fae1042 100644 Binary files a/test/goldens/SecWidget/Idle/l-light.png and b/test/goldens/SecWidget/Idle/l-light.png differ diff --git a/test/goldens/SecWidget/Idle/m-dark.png b/test/goldens/SecWidget/Idle/m-dark.png index 71660ae..8267eba 100644 Binary files a/test/goldens/SecWidget/Idle/m-dark.png and b/test/goldens/SecWidget/Idle/m-dark.png differ diff --git a/test/goldens/SecWidget/Idle/m-light.png b/test/goldens/SecWidget/Idle/m-light.png index c9ab33f..0536929 100644 Binary files a/test/goldens/SecWidget/Idle/m-light.png and b/test/goldens/SecWidget/Idle/m-light.png differ diff --git a/test/goldens/SecWidget/Idle/s-dark.png b/test/goldens/SecWidget/Idle/s-dark.png index 7732a05..5fbfa6b 100644 Binary files a/test/goldens/SecWidget/Idle/s-dark.png and b/test/goldens/SecWidget/Idle/s-dark.png differ diff --git a/test/goldens/SecWidget/Idle/s-light.png b/test/goldens/SecWidget/Idle/s-light.png index ecfed43..272e015 100644 Binary files a/test/goldens/SecWidget/Idle/s-light.png and b/test/goldens/SecWidget/Idle/s-light.png differ diff --git a/test/goldens/SecWidget/Measure Complete/l-dark.png b/test/goldens/SecWidget/Measure Complete/l-dark.png index 5617b83..8d7a4a8 100644 Binary files a/test/goldens/SecWidget/Measure Complete/l-dark.png and b/test/goldens/SecWidget/Measure Complete/l-dark.png differ diff --git a/test/goldens/SecWidget/Measure Complete/l-light.png b/test/goldens/SecWidget/Measure Complete/l-light.png index ea9e8be..65fcc78 100644 Binary files a/test/goldens/SecWidget/Measure Complete/l-light.png and b/test/goldens/SecWidget/Measure Complete/l-light.png differ diff --git a/test/goldens/SecWidget/Measure Complete/m-dark.png b/test/goldens/SecWidget/Measure Complete/m-dark.png index 5659563..8a4f8d4 100644 Binary files a/test/goldens/SecWidget/Measure Complete/m-dark.png and b/test/goldens/SecWidget/Measure Complete/m-dark.png differ diff --git a/test/goldens/SecWidget/Measure Complete/m-light.png b/test/goldens/SecWidget/Measure Complete/m-light.png index d23fffd..bdf344d 100644 Binary files a/test/goldens/SecWidget/Measure Complete/m-light.png and b/test/goldens/SecWidget/Measure Complete/m-light.png differ diff --git a/test/goldens/SecWidget/Measure Complete/s-dark.png b/test/goldens/SecWidget/Measure Complete/s-dark.png index e1f426c..cff57be 100644 Binary files a/test/goldens/SecWidget/Measure Complete/s-dark.png and b/test/goldens/SecWidget/Measure Complete/s-dark.png differ diff --git a/test/goldens/SecWidget/Measure Complete/s-light.png b/test/goldens/SecWidget/Measure Complete/s-light.png index 02b16ff..b5d044c 100644 Binary files a/test/goldens/SecWidget/Measure Complete/s-light.png and b/test/goldens/SecWidget/Measure Complete/s-light.png differ diff --git a/test/goldens/SecWidget/Measure Fail/l-dark.png b/test/goldens/SecWidget/Measure Fail/l-dark.png index 14d8f4e..f29c60a 100644 Binary files a/test/goldens/SecWidget/Measure Fail/l-dark.png and b/test/goldens/SecWidget/Measure Fail/l-dark.png differ diff --git a/test/goldens/SecWidget/Measure Fail/l-light.png b/test/goldens/SecWidget/Measure Fail/l-light.png index ea3a852..f0a643f 100644 Binary files a/test/goldens/SecWidget/Measure Fail/l-light.png and b/test/goldens/SecWidget/Measure Fail/l-light.png differ diff --git a/test/goldens/SecWidget/Measure Fail/m-dark.png b/test/goldens/SecWidget/Measure Fail/m-dark.png index 8dd79d7..a434df9 100644 Binary files a/test/goldens/SecWidget/Measure Fail/m-dark.png and b/test/goldens/SecWidget/Measure Fail/m-dark.png differ diff --git a/test/goldens/SecWidget/Measure Fail/m-light.png b/test/goldens/SecWidget/Measure Fail/m-light.png index d0cc821..b42491b 100644 Binary files a/test/goldens/SecWidget/Measure Fail/m-light.png and b/test/goldens/SecWidget/Measure Fail/m-light.png differ diff --git a/test/goldens/SecWidget/Measure Fail/s-dark.png b/test/goldens/SecWidget/Measure Fail/s-dark.png index 42af985..bb713ed 100644 Binary files a/test/goldens/SecWidget/Measure Fail/s-dark.png and b/test/goldens/SecWidget/Measure Fail/s-dark.png differ diff --git a/test/goldens/SecWidget/Measure Fail/s-light.png b/test/goldens/SecWidget/Measure Fail/s-light.png index 9193450..674c1ba 100644 Binary files a/test/goldens/SecWidget/Measure Fail/s-light.png and b/test/goldens/SecWidget/Measure Fail/s-light.png differ diff --git a/test/goldens/SecWidget/Measuring/l-dark.png b/test/goldens/SecWidget/Measuring/l-dark.png index 3bafbfd..8419644 100644 Binary files a/test/goldens/SecWidget/Measuring/l-dark.png and b/test/goldens/SecWidget/Measuring/l-dark.png differ diff --git a/test/goldens/SecWidget/Measuring/l-light.png b/test/goldens/SecWidget/Measuring/l-light.png index 7b90db5..328060b 100644 Binary files a/test/goldens/SecWidget/Measuring/l-light.png and b/test/goldens/SecWidget/Measuring/l-light.png differ diff --git a/test/goldens/SecWidget/Measuring/m-dark.png b/test/goldens/SecWidget/Measuring/m-dark.png index bd82ceb..1afa9d1 100644 Binary files a/test/goldens/SecWidget/Measuring/m-dark.png and b/test/goldens/SecWidget/Measuring/m-dark.png differ diff --git a/test/goldens/SecWidget/Measuring/m-light.png b/test/goldens/SecWidget/Measuring/m-light.png index eb12aa1..7cb4ca7 100644 Binary files a/test/goldens/SecWidget/Measuring/m-light.png and b/test/goldens/SecWidget/Measuring/m-light.png differ diff --git a/test/goldens/SecWidget/Measuring/s-dark.png b/test/goldens/SecWidget/Measuring/s-dark.png index 4d5af99..8706a1d 100644 Binary files a/test/goldens/SecWidget/Measuring/s-dark.png and b/test/goldens/SecWidget/Measuring/s-dark.png differ diff --git a/test/goldens/SecWidget/Measuring/s-light.png b/test/goldens/SecWidget/Measuring/s-light.png index e74bc7a..a15d6bb 100644 Binary files a/test/goldens/SecWidget/Measuring/s-light.png and b/test/goldens/SecWidget/Measuring/s-light.png differ diff --git a/test/goldens/SecWidget/Priming/l-dark.png b/test/goldens/SecWidget/Priming/l-dark.png index a7cfb4a..8c3dd95 100644 Binary files a/test/goldens/SecWidget/Priming/l-dark.png and b/test/goldens/SecWidget/Priming/l-dark.png differ diff --git a/test/goldens/SecWidget/Priming/l-light.png b/test/goldens/SecWidget/Priming/l-light.png index 2eaea54..33a218b 100644 Binary files a/test/goldens/SecWidget/Priming/l-light.png and b/test/goldens/SecWidget/Priming/l-light.png differ diff --git a/test/goldens/SecWidget/Priming/m-dark.png b/test/goldens/SecWidget/Priming/m-dark.png index 2ed272c..5e5e1fc 100644 Binary files a/test/goldens/SecWidget/Priming/m-dark.png and b/test/goldens/SecWidget/Priming/m-dark.png differ diff --git a/test/goldens/SecWidget/Priming/m-light.png b/test/goldens/SecWidget/Priming/m-light.png index dd44394..ff2c092 100644 Binary files a/test/goldens/SecWidget/Priming/m-light.png and b/test/goldens/SecWidget/Priming/m-light.png differ diff --git a/test/goldens/SecWidget/Priming/s-dark.png b/test/goldens/SecWidget/Priming/s-dark.png index 83f0584..d9feb06 100644 Binary files a/test/goldens/SecWidget/Priming/s-dark.png and b/test/goldens/SecWidget/Priming/s-dark.png differ diff --git a/test/goldens/SecWidget/Priming/s-light.png b/test/goldens/SecWidget/Priming/s-light.png index 63b2d33..c9eae90 100644 Binary files a/test/goldens/SecWidget/Priming/s-light.png and b/test/goldens/SecWidget/Priming/s-light.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/l-dark.png b/test/goldens/SecWidget/Waiting for measurement/l-dark.png index 63ed3b1..6303f79 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/l-dark.png and b/test/goldens/SecWidget/Waiting for measurement/l-dark.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/l-light.png b/test/goldens/SecWidget/Waiting for measurement/l-light.png index ba9abb9..67ac125 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/l-light.png and b/test/goldens/SecWidget/Waiting for measurement/l-light.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/m-dark.png b/test/goldens/SecWidget/Waiting for measurement/m-dark.png index d85572f..2d9fa55 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/m-dark.png and b/test/goldens/SecWidget/Waiting for measurement/m-dark.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/m-light.png b/test/goldens/SecWidget/Waiting for measurement/m-light.png index 95fce76..c8ea728 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/m-light.png and b/test/goldens/SecWidget/Waiting for measurement/m-light.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/s-dark.png b/test/goldens/SecWidget/Waiting for measurement/s-dark.png index fbd59fc..ca2e077 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/s-dark.png and b/test/goldens/SecWidget/Waiting for measurement/s-dark.png differ diff --git a/test/goldens/SecWidget/Waiting for measurement/s-light.png b/test/goldens/SecWidget/Waiting for measurement/s-light.png index 5d35478..870ef54 100644 Binary files a/test/goldens/SecWidget/Waiting for measurement/s-light.png and b/test/goldens/SecWidget/Waiting for measurement/s-light.png differ diff --git a/test/sec_widget_test.dart b/test/sec_widget_test.dart index c78f63d..abeb05f 100644 --- a/test/sec_widget_test.dart +++ b/test/sec_widget_test.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:liquid_flutter/liquid_flutter.dart'; import 'package:mtrust_sec_kit/mtrust_sec_kit.dart'; +import 'package:mtrust_sec_kit/src/ui/l10n/sec_locale_en.dart'; import 'golden_utils.dart'; import 'test_utils.dart'; @@ -27,7 +30,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -48,7 +51,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -74,7 +77,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -106,7 +109,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -142,7 +145,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -179,7 +182,7 @@ void main() { storageAdapter: storageAdapter, payload: '', onVerificationDone: (_) async {}, - onVerificationFailed: () async {}, + onVerificationFailed: (_) async {}, ), ), ); @@ -210,4 +213,105 @@ void main() { width: 500, ); }); + + testWidgets('SecWidget calls onVerificationFailed on error', + (WidgetTester tester) async { + urpUiDisableAnimations = true; + ldDisableAnimations = true; + + // Create a strategy that simulates a measurement failure + final strategy = CompleterStrategy( + withReaders: true, + // Return a response with an empty payload to simulate a measurement failure + measurementResponse: UrpResponse(), + ); + + final storageAdapter = MockStorageAdapter(); + + SecReaderException? capturedError; + var failureCallbackCalled = false; + + final theme = LdTheme(); + + await tester.pumpWidget( + LdThemeProvider( + theme: theme, + autoSize: false, + brightnessMode: LdThemeBrightnessMode.light, + child: MaterialApp( + localizationsDelegates: const [ + GlobalWidgetsLocalizations.delegate, + LiquidLocalizations.delegate, + UrpUiLocalizations.delegate, + SecLocalizations.delegate, + ], + supportedLocales: const [Locale('en')], + home: Scaffold( + body: AspectRatio( + aspectRatio: 1, + child: SecWidget( + strategy: strategy.strategy, + storageAdapter: storageAdapter, + payload: 'test-payload', + onVerificationDone: (_) async {}, + onVerificationFailed: (exception) async { + failureCallbackCalled = true; + capturedError = exception; + }, + ), + ), + ), + ), + ), + ); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Verify connect button is available + expect(find.byKey(const Key('connect_button')), findsOneWidget); + await tester.tap(find.byKey(const Key('connect_button'))); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Complete the prime operation to transition to "Waiting for measurement" state + strategy.primeCompleter.complete(); + + await tester.pumpAndSettle(); + await tester.tap(find.text('Start scan')); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Complete the measurement completer - strategy will throw the error + strategy.startMeasurementCompleter.complete(); + + // Wait for error to propagate + await tester.pump(const Duration(seconds: 3)); + await tester.pumpAndSettle(); + + // Check whether the 'verification failed' message is displayed in the UI + expect( + find.textContaining(SecLocalizationsEn().verificationFailed), + findsAny, + ); + + // Tap the 'Done' button in order to trigger the onVerificationFailed callback + await tester.tap(find.text(SecLocalizationsEn().done)); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Verify the callback was called with the correct exception + expect(failureCallbackCalled, isTrue); + expect(capturedError, isNotNull); + + // The exception type should be measurementFailed as set in the strategy + expect(capturedError?.type, SecReaderExceptionType.measurementFailed); + + // Pump through the remaining 10-second timer in LdSubmitController to ensure that there are no pending timers that + // could cause issues in subsequent tests + await tester.pumpAndSettle(const Duration(seconds: 10)); + }); } diff --git a/test/test_utils.dart b/test/test_utils.dart index 54a6ad3..2edb68f 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -1,8 +1,6 @@ import 'dart:async'; -import 'package:mtrust_urp_core/mtrust_urp_core.dart'; -import 'package:mtrust_urp_types/sec.pb.dart'; -import 'package:mtrust_urp_ui/src/storage_adapter.dart'; +import 'package:mtrust_sec_kit/mtrust_sec_kit.dart'; import 'package:mtrust_urp_virtual_strategy/mtrust_urp_virtual_strategy.dart'; final reader1 = FoundDevice( @@ -24,7 +22,11 @@ final reader3 = FoundDevice( ); class CompleterStrategy { - CompleterStrategy({bool withReaders = false, this.useDelays = false}) { + CompleterStrategy({ + bool withReaders = false, + this.useDelays = false, + this.measurementResponse, + }) { strategy = UrpVirtualStrategy((UrpRequest request) async { final payload = UrpSecCommandWrapper.fromBuffer(request.payload); switch (payload.deviceCommand.command) { @@ -59,20 +61,21 @@ class CompleterStrategy { await startMeasurementCompleter.future; } - return UrpResponse( - payload: UrpSecSecureMeasurement( - signature: [0, 0, 0, 0], - measurement: UrpSecMeasurement( - result: [ - UrpSecMeasurementResult( - modelId: '123', - scoreDistance: 0.5, - orthogonalDistance: 0.6, + return measurementResponse ?? + UrpResponse( + payload: UrpSecSecureMeasurement( + signature: [0, 0, 0, 0], + measurement: UrpSecMeasurement( + result: [ + UrpSecMeasurementResult( + modelId: '123', + scoreDistance: 0.5, + orthogonalDistance: 0.6, + ), + ], ), - ], - ), - ).writeToBuffer(), - ); + ).writeToBuffer(), + ); // ignore: no_default_cases default: return UrpResponse(); @@ -98,6 +101,7 @@ class CompleterStrategy { Completer primeCompleter = Completer(); Completer startMeasurementCompleter = Completer(); late UrpVirtualStrategy strategy; + UrpResponse? measurementResponse; } class MockStorageAdapter extends StorageAdapter { diff --git a/version_badge.svg b/version_badge.svg new file mode 100644 index 0000000..303c8f2 --- /dev/null +++ b/version_badge.svg @@ -0,0 +1 @@ +version: 3.0.0version3.0.0 \ No newline at end of file diff --git a/version_output.json b/version_output.json new file mode 100644 index 0000000..5e094aa --- /dev/null +++ b/version_output.json @@ -0,0 +1 @@ +{"version":"3.0.0","changelog":"## 3.0.0\nReleased on: 2/16/2026, changelog automatically generated.\n\n","badge":""} \ No newline at end of file