Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ internal fun SdkFlowScreen(scope: UseSmileIDSampleStepScope, viewModel: SdkFlowV
}
return
}
// No form fixes this, so it exits like a mistyped product id — but says why first: a silent
// return to the product list is indistinguishable from a dead tap.
// No form fixes this. It lands where the SDK's own Failure does, on the unsubmitted job's
// details, so the reason is on screen: the builder's validate() returns build()'s verdict,
// so a flow the SDK would refuse stops here, and a silent return to the product list would
// read as a dead tap.
is FlowPreflight.Misconfigured -> {
val issues = preflight.issues
LaunchedEffect(Unit) {
Expand All @@ -94,7 +96,7 @@ internal fun SdkFlowScreen(scope: UseSmileIDSampleStepScope, viewModel: SdkFlowV
.ifBlank { "The flow did not validate" },
snapshot.environment,
)
engine.leaveToProducts()
engine.leaveRun(scope.entry, UseSmileIDSampleStep.VerificationDetails.wireName, arg = UNSUBMITTED_JOB_ID)
}
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.usesmileid.sampleapps.android

import com.usesmileid.core.exception.AgentModeWithEnhancedLivenessException
import com.usesmileid.core.exception.InvalidFieldValueException
import com.usesmileid.core.exception.UseSmileIDValidationException
import com.usesmileid.sampleapps.android.flow.BOUND_COUNTRY
Expand Down Expand Up @@ -29,8 +30,17 @@ import java.util.Base64
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

/** Narrower than "the SDK accepts this flow": `validate()` reports no error raised inside `screens { }`. */
/**
* Preflight's last gate is `validate()`, which returns the verdict the SDK's `build()` reaches, so a
* flow it passes is one the SDK accepts. Under Robolectric because the SDK reads the token's bindings
* with `android.util.Base64`, which a plain JVM test stubs out.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [36])
class SdkFlowPreflightTest {

@Test
Expand Down Expand Up @@ -185,8 +195,7 @@ class SdkFlowPreflightTest {
assertEquals("0000000", params.idNumber)
}

// The forbidden pair is absent because no host can catch it: `validate()` is `validateBuilder`,
// and the rule runs inside the SDK's `build()`. The mutex is tested in `sample-ui`.
// The UI never persists agent mode with enhanced liveness; the mutex is tested in `sample-ui`.
@Test
fun `every capture combination the UI can persist reaches the SDK`() {
val allowed = listOf(false to true, true to false, false to false)
Expand All @@ -205,9 +214,19 @@ class SdkFlowPreflightTest {
}
}

// The SDK refuses the Green Book on Enhanced Document Verification inside `screens { }`, which validate() cannot see.
@Test
fun `every document setting passes preflight`() {
fun `agent mode with enhanced liveness is refused before the SDK on every product that captures`() {
UseSmileIDSampleProduct.entries.filter { it.capture }.forEach { product ->
val verdict = preflight(snapshotFor(product).copy(allowAgentMode = true, enableEnhancedLiveness = true))
assertTrue(
"$product should be refused for the pair, got $verdict",
verdict is FlowPreflight.Misconfigured && verdict.issues.any { it is AgentModeWithEnhancedLivenessException },
)
}
}

@Test
fun `every document setting passes preflight, except the Green Book the SDK refuses on Enhanced Document Verification`() {
listOf(
UseSmileIDSampleProduct.DocumentVerification,
UseSmileIDSampleProduct.EnhancedDocumentVerification,
Expand All @@ -221,7 +240,13 @@ class SdkFlowPreflightTest {
allowSkipBack = !flag,
selfieFirst = flag,
)
assertEquals("$product $captureAs flag=$flag", FlowPreflight.Ready, preflight(snapshot))
val verdict = preflight(snapshot)
val refused = product == UseSmileIDSampleProduct.EnhancedDocumentVerification &&
captureAs == UseSmileIDSampleCaptureAs.GreenBook
assertTrue(
"$product $captureAs flag=$flag, got $verdict",
if (refused) verdict is FlowPreflight.Misconfigured else verdict == FlowPreflight.Ready,
)
}
}
}
Expand Down
16 changes: 8 additions & 8 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[versions]
# The SDK snapshot builds made against njia's pinned build; a bare -SNAPSHOT can resolve a build made against another njia.
usesmileid = "12.3.0-20261025.011035-260"
usesmileid-bom = "12.3.0-20261025.011035-261"
usesmileid-mlkit-face = "12.3.0-20261025.011035-257"
usesmileid-mlkit-document = "12.3.0-20261025.011035-258"
usesmileid-huawei-face = "12.3.0-20261025.011035-258"
usesmileid-huawei-document = "12.3.0-20261025.011035-259"
usesmileid-bridge = "12.3.0-20261025.011035-259"
usesmileid = "12.3.0-20261026.002955-291"
usesmileid-bom = "12.3.0-20261026.002955-292"
usesmileid-mlkit-face = "12.3.0-20261026.002955-288"
usesmileid-mlkit-document = "12.3.0-20261026.002955-289"
usesmileid-huawei-face = "12.3.0-20261026.002955-289"
usesmileid-huawei-document = "12.3.0-20261026.002955-290"
usesmileid-bridge = "12.3.0-20261026.002955-290"
# The four apps' navigation engine: one snapshot build, pinned, until its first release; the SDK's older build would win as a bare -SNAPSHOT.
njia = "0.0.1-20261025.005300-10"
njia = "0.0.1-20261025.232953-11"

android-gradle-plugin = "9.3.1"
kotlin = "2.4.21"
Expand Down
5 changes: 3 additions & 2 deletions android/maestro/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ appId: ${APP_ID}
checked: true

# --- the consent switch, and the validation failure it is meant to demonstrate -------------------
# No consent screen and no bound consent: the SDK's builder refuses the flow and delivers a typed
# Failure, and the card is where the message lands. The host's own pre-flight cannot see this one.
# No consent screen and no bound consent: the flow is refused, and the card is where the message
# lands. The host's pre-flight sees it, since the builder's validate() returns build()'s verdict, and
# lands on the same card as the SDK's typed Failure would.
- tapOn:
id: "sample_setting_consent_step"
- assertVisible:
Expand Down
4 changes: 2 additions & 2 deletions android/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# The definition of done for the Android app: tokens, notices, lint, unit tests, release assemble.
#
# Device flows need a device and are not run here:
# maestro --device <serial> test -e APP_ID=com.usesmileid.sampleapps.android.debug android/maestro
# maestro --device <serial> test -e APP_ID=com.usesmileid.sampleapps.android android/maestro
# maestro --device <serial> test -e APP_ID=com.usesmileid.sample.android.debug android/maestro
# maestro --device <serial> test -e APP_ID=com.usesmileid.sample.android android/maestro
set -euo pipefail

cd "$(dirname "$0")"
Expand Down
11 changes: 3 additions & 8 deletions docs/plan/capture-as-follows-the-document.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,9 @@ cases:
- Expo: `use-smile-id-sample-document-capture-mapping.test.ts`

A new test proves that Match never produces a flow the SDK refuses, for every row in the fixture on both
document products. The builder's `validate()` cannot prove it on any platform: it checks the builder's
own state and never runs the job-type rules, which is why `SdkFlowPreflightTest` as written passes
either way. The rules run in `build()`, which is public on Flutter and React Native and internal on
Android and iOS. On those two the public path is the dispatching validator,
`FlowValidator.validate(configuration, …)` on Android and
`FlowValidator.validate(configuration:mlAnalyzerRegistry:networkClient:)` on iOS, given a
`FlowConfiguration` built from the sample's own steps, which the iOS preflight already constructs. The
same call makes the Android and iOS preflights see job-type refusals, which they cannot today.
document products. The builder's `validate()` proves it on every platform: it returns the verdict
`build()` reaches, job-type rules included, so `SdkFlowPreflightTest` sees the Green Book refusal on
Enhanced Document Verification.
Comment on lines +231 to +233

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The doc now says validate() proves job-type rules on every platform. The iOS preflight in this same PR says the opposite: its validator.validate(configuration:) runs only screen and structural rules, and "the SDK's build also runs the job rules". So on iOS the Green Book refusal still escapes preflight. Scope the claim to the platforms where it holds and keep the iOS gap visible, ideally as a backlog item. [general, importance: 5]

Suggested change
document products. The builder's `validate()` proves it on every platform: it returns the verdict
`build()` reaches, job-type rules included, so `SdkFlowPreflightTest` sees the Green Book refusal on
Enhanced Document Verification.
document products. On Android, Flutter and React Native the builder's `validate()` proves it: it
returns the verdict `build()` reaches, job-type rules included, so `SdkFlowPreflightTest` sees the
Green Book refusal on Enhanced Document Verification. On iOS the preflight's
`FlowValidator.validate(configuration:)` runs only the screen and structural rules, so job-type
refusals still surface only from the SDK's build.


**Goldens.** Re-record the form (trigger in Match, override and Generic-sheet states, one-sided and
two-sided rows), the "Capture as" sheet, and the document picker on Enhanced Document Verification,
Expand Down
18 changes: 9 additions & 9 deletions expo/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
"dependencies": {
"@expo/ui": "~57.0.18",
"@react-native-async-storage/async-storage": "2.2.0",
"@smileid/njia": "0.0.1-SNAPSHOT.10",
"@smileid/njia": "0.0.1-SNAPSHOT.11",
"@smileid/sample-ui": "workspace:*",
"@smileid/usesmileid": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_huawei_document": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_huawei_face": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_mlkit_document": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_mlkit_face": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_vision_document": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid_vision_face": "12.3.1-SNAPSHOT.69",
"@smileid/usesmileid": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_huawei_document": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_huawei_face": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_mlkit_document": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_mlkit_face": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_vision_document": "12.3.1-SNAPSHOT.71",
"@smileid/usesmileid_vision_face": "12.3.1-SNAPSHOT.71",
"expo": "57.0.23",
"expo-brightness": "~57.0.2",
"expo-camera": "~57.0.5",
Expand Down Expand Up @@ -54,7 +54,7 @@
},
"devDependencies": {
"@react-native/jest-preset": "0.86.3",
"@smileid/njia-testing": "0.0.1-SNAPSHOT.10",
"@smileid/njia-testing": "0.0.1-SNAPSHOT.11",
"@testing-library/react-native": "14.0.1",
"@types/jest": "29.5.14",
"expo-doctor": "1.20.4",
Expand Down
2 changes: 1 addition & 1 deletion expo/app/test/use-smile-id-sample-flow-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe('what the SDK is handed', () => {
expect(built(snapshot()).userId).toBeUndefined();
});

// `validate()` is weaker than the build: a missing consent icon passes one and fails the other.
// The build itself, whose verdict `validate()` returns without building.
it('every product builds a configuration the SDK accepts', () => {
for (const product of smileIDSampleProducts) {
const result = built(
Expand Down
Loading
Loading