diff --git a/src/PendingBiometric.php b/src/PendingBiometric.php index 65537fbb..ef8cc33f 100644 --- a/src/PendingBiometric.php +++ b/src/PendingBiometric.php @@ -101,7 +101,14 @@ public function prompt(): bool if ($result) { $decoded = json_decode($result, true); - return isset($decoded['status']) && $decoded['status'] === 'success'; + // The bridge returns a function's data as-is on success + // (mobile-biometrics answers {"launched": true} on Android and + // {} on iOS) and only adds a status key for errors: + // {"status": "error", "code": ..., "message": ...}. + // The outcome itself arrives later as the Completed event. + $isError = isset($decoded['status']) && $decoded['status'] === 'error'; + + return ! $isError; } } diff --git a/tests/Unit/Biometrics/PendingBiometricTest.php b/tests/Unit/Biometrics/PendingBiometricTest.php new file mode 100644 index 00000000..a6b3555b --- /dev/null +++ b/tests/Unit/Biometrics/PendingBiometricTest.php @@ -0,0 +1,60 @@ + FakeBridge::enable()); +afterEach(fn () => FakeBridge::disable()); + +it('sends the id and event class to Biometric.Prompt', function () { + (new PendingBiometric)->id('unlock-1')->prompt(); + + FakeBridge::current()->assertCalled('Biometric.Prompt', fn ($params) => $params === [ + 'id' => 'unlock-1', + 'event' => Completed::class, + ]); +}); + +it('reports a started prompt for the payloads mobile-biometrics returns', function (string $response) { + FakeBridge::current()->respondTo('Biometric.Prompt', $response); + + expect((new PendingBiometric)->prompt())->toBeTrue(); +})->with([ + 'android' => '{"launched":true}', + 'ios' => '{}', + 'empty array' => '[]', +]); + +it('reports a bridge error as not started', function () { + FakeBridge::current()->respondTo('Biometric.Prompt', [ + 'status' => 'error', + 'code' => 'UNKNOWN_ERROR', + 'message' => 'Unexpected error', + 'data' => [], + ]); + + expect((new PendingBiometric)->prompt())->toBeFalse(); +}); + +it('reports a missing bridge function as not started', function () { + FakeBridge::current()->respondTo('Biometric.Prompt', null); + + expect((new PendingBiometric)->prompt())->toBeFalse(); +}); + +it('only prompts once', function () { + FakeBridge::current()->respondTo('Biometric.Prompt', '{}'); + + $pending = new PendingBiometric; + + expect($pending->prompt())->toBeTrue() + ->and($pending->prompt())->toBeFalse(); + + FakeBridge::current()->assertCalledTimes('Biometric.Prompt', 1); +});