Skip to content

Add face verification for document issuance - #646

Merged
w-ensink merged 47 commits into
masterfrom
regula-face-matching
Aug 11, 2026
Merged

Add face verification for document issuance#646
w-ensink merged 47 commits into
masterfrom
regula-face-matching

Conversation

@DibranMulder

@DibranMulder DibranMulder commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Implements the Yivi client side of face verification per the go-passport-issuer face-verification design.

After the NFC chip read, the app runs a liveness session (camera) and passes the resulting liveness_transaction_id to the passport issuer, which matches the live face against the document chip portrait (DG2/DG6) server-side and gates issuance on the result. Applies to passport, ID card and driving licence.

Flow

The passport issuer decides per session whether face verification applies: its start-validation response carries a face verification announcement, and the announcement's presence is the signal. Without one the app skips the step entirely and goes straight to issuance, so the feature can be switched off remotely and old issuers keep working.

When it does apply: document readout → Yivi face-verification intro screen → liveness session (passive) → "preparing" loader → credential issuance.

On a matching face, issuance proceeds directly. When the issuer rejects the face match (HTTP 400), the app surfaces the generic issuance error screen (retry / cancel) with a dedicated failed-face illustration; retry re-runs the readout and liveness. A liveness session that produces no transaction id is failed in the app rather than sent on: the issuer announced the step, so a request without the id would leave the face check silently missing.

Architecture

Follows the repo's existing FOSS injection pattern (as used for OCR / QR), so the proprietary Regula SDK stays out of yivi_core and the F-Droid build:

  • yivi_core — mockable RegulaFaceService interface, RegulaLivenessResult, and a regulaFaceServiceProvider defaulting to null (disabled). withLivenessTransaction() attaches the transaction id to the issuance request via RawDocumentData.copyWith. faceVerificationConfigProvider holds the issuer's announcement for the current flow, which also names the Face API the session must target.
  • yivi_app — concrete RegulaFaceServiceImpl backed by flutter_face_api in web-service mode (the Regula license lives on the Face API server; no license file ships). Injected via runYiviApp(regulaFaceService: ...), built against the announced Face API URL, so no environment is pinned at compile time.
  • yivi_fdroidRegulaWebFaceService: the same liveness step through Regula's web Face SDK, running on a Yivi-hosted capture page that the issuer serves under /capture and that the app loads in an embedded webview_flutter WebView (F-Droid face verification via Regula's web Face SDK #665/feat(fdroid): FOSS liveness via Regula web Face SDK in a WebView #666). The FOSS build therefore has face verification with no proprietary native code in the APK: it ships the BSD WebView plugin and the page URL, and Regula's code executes remotely. Cancel, back and page errors throw, so the flow lands on the same issuance error screen as the native build.

Client UI

  • A Yivi-themed intro screen replaces Regula's built-in "Time for a selfie" onboarding (skipped). It shows a code-drawn face-match animation (document portrait + phone selfie, highlighted then matched), a short explanation, and left-aligned guidance tips (point the selfie camera, enough light, look straight, remove glasses/hats). The animation is marked decorative (ExcludeSemantics) so screen readers rely on the intro copy. Copy is localized through the app's own i18n (nl/de/en); the screen is titled "Gezichtsverificatie" / "Gesichtsverifizierung" / "Face verification".
  • On the native build, liveness defaults to passive and cannot be cancelled mid-verification: the Regula close button is hidden on both the camera screen (config) and the processing screen (blanked icon), and the camera toolbar's torch/light toggle is hidden. The watermark is removed, portrait is forced, and all Regula screens are themed with the Yivi palette. Regula's remaining screens (camera hints, retry, processing) are relabeled per the active language using the SDK's real string keys, and set to request the Yivi font.
  • Regula's success (checkmark) screen is skipped: liveness passing only means a live face was captured, not that it matches. The real match happens server-side during issuance, so a client-side success screen would be misleading.
  • On the F-Droid build the capture page runs inside the standard Yivi Scaffold/app bar, so there is no browser chrome and the WebView is granted the camera and nothing else.
  • After liveness, a loader ("preparing your data…") is shown and issuance replaces the readout route, so the user is not returned to the readout page.

Dependencies

  • vcmrtd (yivi_core + yivi_app) is pinned to ref: feat/face-verification-announcement (+ path: vcmrtd, since the package lives in a subdirectory): v4.0.0 plus the commit that adds the issuer's face verification announcement to StartValidationResult, alongside RawDocumentData.livenessTransactionId / copyWith. This branch has to be merged and tagged in vcmrtd, and both pubspecs repinned to that tag, before this PR merges: a branch ref can be rebased or deleted under us.
  • Adds MIT-licensed flutter_face_api + flutter_face_core_basic to yivi_app only, and webview_flutter + webview_flutter_android to yivi_fdroid only.

Tests

  • yivi_core: unit tests for the service/provider/threading and language forwarding, including that a session without a transaction id fails; unit tests for the capture URL the FOSS flow derives from the issuer; widget tests for the intro screen and for where a readout failure is surfaced when the screen was torn down.
  • yivi_fdroid: unit tests for the capture-page message parsing and for RegulaWebFaceService.
  • yivi_app: integration test taps through the intro and asserts the transaction id + active language reach the issuer, and that no id is sent when the issuer announces nothing.

Notes for reviewers / follow-ups

  • On-device verification still needed: the Regula screens' exact look, that the hidden close/torch buttons and skipped success screen behave as intended across SDK versions, the WebView capture page on a real F-Droid build, and the post-liveness loader → issuance transition.
  • The rejected-match error reuses the generic issuance error screen (dedicated illustration only); its title/tip text is still the generic issuance-error copy. Detection keys off the issuer's Store failed: 400 during a face-verification issuance.
  • Regula font: the Dart side requests Open Sans, but Flutter's bundled font is not visible to Regula's native UI — the ttf must be registered with the native platform (Android res/font/assets; iOS UIAppFonts + bundled ttf) for it to actually render. Not yet wired.
  • The capture page is served without Cache-Control, so the WebView clears its cache before each load; the durable fix is Cache-Control: no-store on the served page.

Run a Regula liveness session after the NFC chip read and pass the
resulting liveness transaction id to the passport issuer, which matches
the live face against the chip portrait.

- Add mockable RegulaFaceService interface + null-defaulting provider in
  yivi_core; concrete flutter_face_api implementation in yivi_app,
  injected via runYiviApp so the FOSS yivi_fdroid build has no Regula
  dependency (face verification disabled there).
- Thread liveness_transaction_id into the issuance request via
  RawDocumentData.copyWith (withLivenessTransaction helper).
- Bump vcmrtd to the branch that adds livenessTransactionId / FaceMatch.
- Unit tests for the service/provider/threading and integration tests
  asserting the transaction id reaches the issuer.
@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29250157693

Dibran Mulder added 2 commits July 13, 2026 15:15
- Add a Yivi-themed face verification intro screen shown after the
  successful document readout, replacing Regula's onboarding. It carries
  the heading, guidance and a privacy statement (processed only by Yivi
  on EU-sovereign servers, not stored, never shared), localized via the
  app's own i18n (nl/de/en).
- Default to passive liveness, hide the Regula watermark, force portrait,
  and theme every Regula screen with the Yivi palette.
- Localize Regula's remaining screens (camera hints, retry, processing,
  success) per the app's active language using the SDK's real string keys.
- Navigate to issuance through the root navigator so issuance opens even
  if the NFC screen is torn down while the native liveness UI is in front.
- Tests: intro screen widget tests, language forwarding, and an
  integration test tapping through the intro to the issuer.
- Remove the heading from the intro body (it duplicated the app bar title).
- Add a "remove facial accessories, hats, etc." guidance tip.
- Fix the Dutch privacy wording ("gezichtsafbeeldingen") and align the
  English/German copy to "face images" for consistency.
@github-actions

Copy link
Copy Markdown

🎬 Recorded 0
0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29253072636

Dibran Mulder added 2 commits July 13, 2026 15:46
- Add a code-drawn face-verification animation (document portrait + phone
  selfie, highlighted then matched with a check) on the intro screen,
  replacing the static icon. Follows the sibling animations' TickerMode
  convention so tests don't hang on the loop.
- After face verification, show a loader page that leads into issuance
  instead of falling back to the document readout page: the screen behind
  the intro switches to a loader, and issuance replaces the readout route
  (pushReplacement) so the user is not returned to it.
- Expand the intro explanation to describe the upcoming face check and
  lead into the tips.
- Rename the start button to "Start verification".
- Hide the close button on the Regula liveness screen (the user has
  already confirmed on the intro screen).
- Restyle Regula's camera hint labels as a light chip with dark text, to
  echo the intro tips.
@github-actions

Copy link
Copy Markdown

🎬 Recorded 0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29255111443

- Center-align the intro explanation and guidance tips.
- Move the privacy statement into an expandable "Where does my data go?"
  section (reusing Collapsible) instead of an always-visible card.
- Reword the privacy text to "servers hosted in the Netherlands".
@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29255345586

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29256190916

Dibran Mulder added 2 commits July 13, 2026 16:18
- Left-align the intro text and drop the collapsible; the privacy
  statement is shown inline as a plain hint.
- Promote "point the selfie camera at your face" to a guidance tip and
  shorten the explanation accordingly.
- Reword the preparing loader to use "gegevens" (the app's term) instead
  of "kaart".
- Request the Yivi font (Open Sans) on the Regula screens.
Drop the "where does my data go" privacy statement from the intro screen
to avoid over-informing users; remove the widget and the i18n key.
@github-actions

Copy link
Copy Markdown

🎬 Recorded 0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29257374477

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29257783666

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29273715635

@DibranMulder
DibranMulder marked this pull request as ready for review July 13, 2026 18:27
flutter_face_api pins FaceSDK 8.2.4530, which is not on the CocoaPods CDN
trunk (it only has up to 8.2.4516) but is hosted in Regula's own podspecs
repo. Add that source (plus the default CDN, which provides FaceCoreBasic
8.2.2127) so `pod install` can resolve the Regula pods on iOS.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dobby consolidated review — face verification (Yivi client side)

Solid, well-tested implementation that keeps the Regula SDK out of yivi_core and the F-Droid build via the existing FOSS-injection pattern. Rule sweep (Dart list-equality, locale en/nl/de parity, bloc-transformer, tests-on-features, design-slop, WCAG) is clean except one low-severity a11y note. Requesting changes on two functional bugs below before merge.

Blocking

  • 🐞 Production build defaults to the staging Face API URL (regula_face_service.dart).
  • 🐞 Error path in _startIssuance guards on the wrong mounted and can silently swallow issuance failures (nfc_reading_screen.dart).

Non-blocking

  • Lockfiles regenerated on a non-CI Flutter SDK (spurious dep downgrades).
  • vcmrtd pinned to an unmerged branch commit (author already flagged this).
  • Face-match animation lacks a Semantics label (WCAG 1.1.1 A).

Rule checks that passed: Dart List/Uint8List == trap (none), locale completeness (en/nl/de all at key parity), bloc sequential-transformer (N/A — Riverpod), tests-required (service/provider/widget + integration coverage present), design AI-slop tells (none), WCAG AA copy/layout.

Tests: yivi_core unit/widget suite green locally; yivi_app integration tests are Firebase Test Lab-only (not run locally).

Comment thread yivi_app/lib/regula_face_service.dart Outdated
Comment thread yivi_core/pubspec.lock Outdated
Comment thread yivi_core/pubspec.yaml Outdated
@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29274824794

Dibran Mulder added 5 commits July 14, 2026 13:55
- Rename the screen title to Gezichtsverificatie (nl) / Gesichtsverifizierung
  (de); reword the accessories tip to remove glasses/hats.
- Mark the intro face-match animation decorative (ExcludeSemantics) so screen
  readers skip it; the intro copy already conveys the flow.
- Hide the Regula camera toolbar's torch/light toggle and blank the
  processing-screen close button (a transparent icon) so liveness cannot be
  cancelled mid-verification.
- Skip Regula's success (checkmark) screen: the real match is server-side, so
  a client-side success screen would be misleading.
- Show the dedicated failed_face_verification illustration on the generic
  error screen when the issuer rejects the face match (HTTP 400).
@github-actions

Copy link
Copy Markdown

🎬 Recorded 0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/29334903346

The _startIssuance catch guarded on the NfcReadingScreen's own mounted
state, but the native Regula liveness UI can tear this screen down on some
devices. When that happened an issuance failure was silently swallowed: no
error was shown and _preparingIssuance was never reset. Fall back to the
root navigator's error screen (as the success path and handlePointer do)
when the screen is gone.
@DibranMulder

DibranMulder commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Processed the review feedback:

  • _startIssuance mounted guard — Fixed. The catch guarded on the NfcReadingScreen State's mounted, so an issuance failure was silently swallowed when the native liveness UI tore the screen down. It now falls back to the root navigator's error screen (pushErrorScreen/pushReplacementErrorScreen, matching the success path and handlePointer) when the screen is gone; the in-screen error (with the dedicated failed-face illustration + retry) is still used when it's still mounted. setState stays guarded by mounted since it can't run on a disposed State.
  • Production defaults to staging Face API URL — Intentionally left as https://faceapi.staging.yivi.app for now; the Face API must be deployed to production first, after which the default will be switched. The URL is already overridable via the serviceUrl constructor param.
  • vcmrtd pinned to unmerged commit — Fixed. Now pinned to the released v3.9.0 tag in both packages.
  • Face-match animation a11y (WCAG 1.1.1) — Addressed. The animation is wrapped in ExcludeSemantics (decorative; the intro copy conveys the flow).
  • Lockfiles regenerated on non-CI SDK — The vcmrtd bump commit's lock diff is limited to the vcmrtd ref/resolved-ref only (no dep downgrades). The stale sdks block predates these commits; regenerating it needs the CI-pinned Flutter 3.44.4, which isn't available in my local env — leaving that for a CI-SDK regeneration.

@w-ensink

Copy link
Copy Markdown
Member

/dobby good job. Now review again and approve if it's good enough. We will upgrade the vcmrtd package later, so don't worry about that.

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31371137722

dobby-coder Bot added 2 commits August 10, 2026 09:37
The fallback added in 5c2d171 guards the whole `_startScanning` try, but
only the liveness session can tear this screen down. Everything before it
(`startSessionAtPassportIssuer`, the config write, `readDocument`) runs for
every issuer, including the ones that announce no face verification, and an
unmounted State there means the user left the route themselves. Their
failure was then pushed as a full-screen error over wherever they had
navigated to, which is a behaviour change for the non-face flow.

Gate the fallback on a `livenessStarted` flag set just before
`withLivenessTransaction`, and replace the route instead of pushing over it,
matching what `_startIssuance` already does for this same flow so dismissing
the error cannot return the user to the readout page they finished.

Both branches are pinned by tests, verified failing before / passing after.
382bcc5 narrowed the rejected-match check from a bare "400" anywhere in the
issuer's message to the `Store failed: 400` prefix vcmrtd actually throws, but
nothing tested which illustration the error screen ends up with. Cover both
directions: a rejected match gets the failed-face illustration, and an
unrelated failure whose body merely happens to carry those digits does not.

The second case fails against the pre-narrowing check.
@github-actions

Copy link
Copy Markdown

🎬 Recorded 0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31375463618

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rules Dobby 2, cycle 6. Verdict approve.

Re-swept at 3adf5164. The two scope nits from the last review are fixed, and I pushed the fixes to this branch rather than sending it back: they were small and specific, and the round trip has not been paying for itself on this PR.

What I changed

c6e05f39 scopes the readout error fallback to the liveness step. The fallback added in 5c2d171 guarded the whole _startScanning try, but only the liveness session can tear this screen down. startSessionAtPassportIssuer, the faceVerificationConfigProvider write and readDocument all run before any liveness UI exists, and they run for issuers that announce no face verification at all. An unmounted State there means the user left the route themselves, so their failure was being pushed as a full-screen error over wherever they had navigated to. That was a behaviour change for the non-face flow this PR otherwise does not touch. The fallback is now gated on a livenessStarted flag set just before withLivenessTransaction, and it replaces the route instead of pushing over it, which is what _startIssuance already does for this flow.

3adf5164 adds the test the Store failed: 400 narrowing in 382bcc5 never had. It covers both directions: a rejected match gets the failed-face illustration, and an unrelated failure whose body happens to carry those digits does not.

Both commits' tests were checked failing-before and passing-after by reverting only the lib/ change. yivi_core is 243/243 green, flutter analyze reports no issues, dart format is clean.

Rule sweep

Clean. I checked Dart List/Uint8List reference equality, json_serializable one-directional fields, sealed-subtype exports, lifecycle-guard promotion, the yivi_app/yivi_fdroid build-config pairing (no build config is touched by these commits), closed clientmodels enums, and the house prose rules plus the German register in the one changed locale string, which is now consistently formal Sie across that block. The only thing the sweep turned up was the missing test for the narrowed error match, which is the second commit above.

Before this merges

vcmrtd is still pinned to ref: feat/face-verification-announcement in yivi_core/pubspec.yaml:77 and yivi_app/pubspec.yaml:59. That branch still exists at 6650c960 and there is no tag above v4.0.0 carrying the announcement work, so the pin can be rebased or deleted out from under this branch. It needs the vcmrtd branch merged and tagged and both pubspecs repinned, which is the precondition you already wrote into the description. Nothing in the fix loop can do that part, so it stays with you.

Two things carried over from earlier reviews, both non-blocking: the description is stale in the three places I listed last time, and the Regula font is requested on the Dart side but not registered with the native platform.

My earlier CHANGES_REQUESTED reviews on this PR no longer describe anything open. Every finding in them is resolved, so that is stale review state rather than open work. Dismiss them when convenient and this is good to go once vcmrtd is tagged.

@dobby-coder

dobby-coder Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Done — I swept the 3 new fix commits against the relevant rules (all clean), then fixed the two remaining nits myself and pushed them straight to regula-face-matching:

  • Scoped the readout error fallback to the liveness step only, so a stray State disposal earlier in _startScanning (before any liveness UI exists) no longer shoves a full-screen error over wherever the user actually navigated to.
  • Added the regression test the Store failed: 400 narrowing was missing (covers both a real rejected match and an unrelated failure that happens to share those digits).

Both were verified failing-before/passing-after, and yivi_core is 243/243 green with flutter analyze/dart format clean and CI passing at the new head. Posted the consolidated review here: #646 (review) (verdict: approve — no bugs left, so no loop back this time).

One thing I can't fix myself: vcmrtd is still pinned to the feat/face-verification-announcement branch ref in both pubspecs, and that branch needs to be merged/tagged with the pins repinned to a real tag before this can merge — that's the precondition already called out in the PR description. Also still open but non-blocking: the PR description is stale in a few spots, and the Regula font isn't registered on the native side yet.

And a housekeeping note: my earlier CHANGES_REQUESTED reviews on this PR are now stale (everything in them is resolved) — that's likely what's showing the PR as blocked, so worth dismissing those when convenient.

PR: #646

@w-ensink
w-ensink dismissed dobby-coder[bot]’s stale review August 10, 2026 09:54

Stale according to dobby

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31375687204

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31382427688

@w-ensink

Copy link
Copy Markdown
Member

/dobby merge master into this branch and fix merge conflicts

@dobby-coder

dobby-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

On it — I'll merge master into this branch and resolve the merge conflicts, then update the PR.

Conflict in nfc_reading_screen.dart, where master's privacy-screen work and
this branch's face verification both changed the NFC read call:

- Imports: keep both `navigation.dart` (branch) and `privacy_screen.dart`
  (master).
- The read: keep master's `PrivacyScreen.suspendDuring` wrapper, with the
  branch's `startValidation.nonceAndSessionId` inside it. The issuer's
  start-validation now returns a `StartValidationResult` rather than a bare
  `NonceAndSessionId`, so master's destructured `nonce`/`sessionId` no longer
  exist.

Two test files needed the same reconciliation:

- `nfc_reading_privacy_screen_test.dart` (new on master) stubs the issuer, so
  its stub returns a `StartValidationResult` with no face verification
  announcement — the face step stays out of a test about the NFC read.
- `nfc_reading_error_surfacing_test.dart` now mocks the `privacy_screen`
  channel. The read runs inside `suspendDuring`, and an unmocked channel
  replies with a null envelope, which `MethodChannel` turns into a
  `MissingPluginException` — the flow then never reached the face verification
  or issuance errors the tests assert on.
@dobby-coder

dobby-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Merged master (through 4314be6, the privacy-screen PR #702) into regula-face-matching in 1dbf726. The branch is mergeable again.

One conflict, in nfc_reading_screen.dart, where both sides changed the same NFC read call.

Both imports are kept: util/navigation.dart from this branch and util/privacy_screen.dart from master.

The read call is the substantive half. Master wrapped readDocument in PrivacyScreen.suspendDuring so the iOS reader sheet does not put the blur over the scanning animation. This branch had changed what the read gets its Active Authentication params from: startSessionAtPassportIssuer() now returns a StartValidationResult (it also carries the face verification announcement), so master's destructured nonce/sessionId no longer exist. Resolution keeps master's wrapper with this branch's accessor inside it:

final result = await PrivacyScreen.suspendDuring(
  () => _getDocumentReader().readDocument(
    iosNfcMessages: _createIosNfcMessageMapper(),
    activeAuthenticationParams: startValidation.nonceAndSessionId,
  ),
);

The suspension covers only the chip read. The face verification intro and the liveness session run after it returns, outside the suspension, because those are in-app screens rather than system UI that resigns the app active.

Two test files needed the same reconciliation. Neither is a behaviour change, but both are silent breakages a plain textual merge leaves behind:

  • nfc_reading_privacy_screen_test.dart (new on master) stubs the passport issuer, and its stub still returned a bare NonceAndSessionId, so it no longer compiled. It now returns a StartValidationResult with no face verification announcement, keeping the face step out of a test about the NFC read.
  • nfc_reading_error_surfacing_test.dart (this branch) had three tests fail after the merge. The read now runs inside suspendDuring, and an unmocked method channel replies with a null envelope, which MethodChannel turns into a MissingPluginException. The read therefore failed before the flow reached the face verification or issuance errors those tests assert on. The test now mocks the privacy_screen channel, the same way master's own privacy-screen test does.

Verified that the resolution dropped nothing from either side: the branch's contribution patch before the merge and after it are identical apart from the two hunks above (and the two-space reindent from the suspendDuring wrapper).

Ran locally on Flutter 3.44.9:

  • yivi_core: flutter test, 253 passed
  • yivi_fdroid: flutter test, 13 passed
  • dart format --set-exit-if-changed and flutter analyze --no-fatal-infos clean in yivi_core, yivi_app and yivi_fdroid

The blocker named in the PR description is untouched by this merge and still stands: vcmrtd is pinned to ref: feat/face-verification-announcement in both pubspecs, and that branch needs merging and tagging before this PR can go in.

Requested by @w-ensink.

@dobby-coder

dobby-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Turns out there's nothing to merge — master is already fully merged into this branch. That happened earlier today in 1dbf726b, which brought in master through #702, and I wrote up how that was resolved in this comment. git merge origin/master now just says "Already up to date," so there's no conflict left for me to fix.

The PR shows as blocked, but that's not a merge conflict — mergeable is MERGEABLE, four required checks are still running, and nothing has requested changes. The real remaining blocker is the vcmrtd dependency pin already noted in the PR description: both pubspecs point at ref: feat/face-verification-announcement, which needs to be merged and tagged in vcmrtd and repinned before this can land.

No commits made, nothing pushed.

@github-actions

Copy link
Copy Markdown

🎬 Recorded 0
0 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31478417197

@github-actions

Copy link
Copy Markdown

🎬 Recorded 1 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31480046024

@w-ensink

Copy link
Copy Markdown
Member

/dobby review again and approve if it's good enough.

@dobby-coder

dobby-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

On it — I'll re-review PR #646 and, if everything checks out against our rules, go ahead and approve it. I'll post my review shortly.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rules Dobby 2 — cycle 7. Verdict approve.

Re-swept at 4ef47c62. Three commits since the last review: a master merge, set vcmrtd to v4.1.0, and fix document reading integration tests.

The dependency blocker is cleared

Cycle 1 flagged the vcmrtd pin as merge-blocking because it pointed at a branch ref (feat/face-verification-announcement), which can be rebased or deleted under us. It is now ref: v4.1.0, and that holds up:

  • the tag exists and resolves to fdc4e83a32de3199c6faeb6d1363c041eb88f926
  • all three lockfiles (yivi_core, yivi_app, yivi_fdroid) carry that exact resolved-ref
  • the four commits between v4.0.0 and v4.1.0 are exactly vcmrtd#192, the announcement this PR reads — no unrelated work rode along, so folding the bump into this PR was the right call

The cross-repo half is merged, tagged and wired up. That was the last thing actually blocking.

What I ran

  • yivi_core: 253/253 pass
  • yivi_fdroid: 13/13 pass
  • flutter analyze: yivi_app and yivi_fdroid clean, yivi_core reports one info • unnecessary_import (inline below, the only analyze finding in the PR)
  • CI at this head: lint, unit-test, test-fdroid-build-script and both irmagobridge builds green. The Android/iOS app builds were still running when I looked — worth a glance before you hit merge, though nothing in the last three commits touches build config.

Rule sweep

Eight candidates, six refuted:

  • Five "pending timer at teardown" flags on nfc_reading_error_surfacing_test.dart. The scanning animations do loop on a Future.delayed that dispose() cannot cancel, and none of these tests do the SizedBox.shrink() + pump-past-holdDuration teardown that usually needs. But the file passes 5/5, so the animation is not mounted on the paths these tests drive. Refuted by running it, not by reading it.
  • FaceVerificationConfig missing from the yivi_core barrel. It is declared in vcmrtd, not yivi_core, so any consumer importing package:vcmrtd/vcmrtd.dart can already name it — which is what the integration-test helper does. Nothing in this hierarchy is sealed either.
  • Hardcoded size: 20 on the tip icon. IrmaTheme has no icon-size token and five other call sites in yivi_core/lib already pass a literal. House pattern, not a breach.

Locale parity holds: face_verification.* carries the same eight keys in nl, de and en, and every key the Dart side reads is defined. The changelog entry sits under ### Added and both entry points really do inject a concrete service, so it is not describing dead code. The yivi_app / yivi_fdroid dependency divergence is the deliberate FOSS split, and the version: lines still match.

What is left

Five non-blocking items inline: two style, two nits, one a11y note. None of them should hold the merge, and the two pubspec ones are single-line if you want them.

The on-device list in your own PR body is still the real remaining risk and not something I can check from here — the Regula screens' look, the hidden close/torch buttons, the skipped success screen across SDK versions, the WebView capture page on a real F-Droid build, and the Regula font that still needs registering with the native platform.

Comment thread yivi_core/pubspec.yaml
Comment thread yivi_app/pubspec.yaml
Comment thread yivi_fdroid/lib/face_capture_webview.dart
Comment thread yivi_core/test/nfc_reading_error_surfacing_test.dart
Comment thread yivi_app/integration_test/helpers/document_reading_helpers.dart
@dobby-coder

dobby-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Reviewed and approved ✅ — review 4906109948, with 6 non-blocking inline comments.

The thing that was actually risky checked out fine: vcmrtd now pins the v4.1.0 tag (not a movable branch ref), and that tag resolves cleanly to the four commits from vcmrtd#192 that this PR is meant to pull in — nothing extra rode along. Tests are green (yivi_core 253/253, yivi_fdroid 13/13), analyze is clean, and CI is green except the Android/iOS app builds, which were still running — worth a glance before merging.

Left inline, none blocking: the repin dropped some useful pubspec comments from #695 (why we avoid mrz_capture for F-Droid), the F-Droid WebView has no onNavigationRequest guard so the JS channel/camera grant follow redirects, an unused dart:typed_data import, a dead FakeRegulaFaceService.error, and the looping intro animation adding to an existing repo-wide WCAG 2.2.2 gap (better fixed once across all four animations, not here).

The on-device checks called out in the PR body itself (Regula screen look, hidden close/torch buttons, F-Droid capture page, native font registration) are still worth a manual pass since I can't verify those from here.

Comment thread yivi_app/lib/regula_face_service.dart
Comment thread yivi_app/lib/regula_face_service.dart
@github-actions

Copy link
Copy Markdown

🎬 Recorded 3 changed integration test(s) on iOS simulator (iPhone 16 Pro or nearest available):

  • yivi_app/integration_test/embedded_issuance_flows/driving_licence_test.dart
  • yivi_app/integration_test/embedded_issuance_flows/id_card_test.dart
  • yivi_app/integration_test/embedded_issuance_flows/passport_test.dart

Download the videos from the Artifacts section of the run: https://github.com/privacybydesign/irmamobile/actions/runs/31488302550

@w-ensink
w-ensink merged commit ddb2f67 into master Aug 11, 2026
16 of 17 checks passed
@w-ensink
w-ensink deleted the regula-face-matching branch August 11, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants