Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
b329dc3
fix(kyc): correct emailConfirmed contract docs, fail-loud date parse,…
TaprootFreak Jul 12, 2026
fd739bd
docs(screens): update the inventory tally for the confirm-email page
TaprootFreak Jul 12, 2026
9c0e0cb
test(goldens): support chat page state baselines (#816)
TaprootFreak Jul 12, 2026
fa33455
test(goldens): support create-ticket page state baselines (#816)
TaprootFreak Jul 12, 2026
dead128
test(goldens): support email-capture buy-flow variant baseline (#816)
TaprootFreak Jul 12, 2026
c7189ae
test(goldens): support tickets page state baselines (#816)
TaprootFreak Jul 12, 2026
b0d18f0
test(goldens): verify-pin app-lock, lockout and biometric-hint state …
TaprootFreak Jul 12, 2026
35c8069
test(goldens): setup-pin confirm-mismatch and store-failure baselines…
TaprootFreak Jul 12, 2026
e2a8017
test(goldens): create-wallet loading state baseline (#816)
TaprootFreak Jul 12, 2026
6c413ac
test(goldens): verify-seed error, verifying, verified and commit-fail…
TaprootFreak Jul 12, 2026
af95176
test(goldens): restore-wallet complete, in-flight, success and failur…
TaprootFreak Jul 12, 2026
4b0ea9c
fix(support): render message time in the message's local zone, not th…
TaprootFreak Jul 12, 2026
70cc484
Merge pull request #821 from RealUnitCH/test/goldens-pin-onboarding
TaprootFreak Jul 12, 2026
a50f7ae
Merge pull request #820 from RealUnitCH/test/goldens-support
TaprootFreak Jul 12, 2026
36a3b89
Merge pull request #819 from RealUnitCH/fix/kyc-confirm-email-review-…
TaprootFreak Jul 12, 2026
c1fc48e
test(goldens): settings screens state baselines (#816) (#822)
TaprootFreak Jul 12, 2026
97b18b6
test(kyc): cover registration form-widget interaction + confirm-email…
TaprootFreak Jul 12, 2026
165d5f1
fix(kyc): return to the in-flight route after resume/PIN re-lock inst…
TaprootFreak Jul 12, 2026
5640879
test(goldens): KYC email + financial-data state baselines (#816) (#830)
TaprootFreak Jul 12, 2026
9c147b1
test(goldens): KYC registration wizard state baselines (#816) (#829)
TaprootFreak Jul 12, 2026
c5c2425
test(goldens): KYC status/verification state baselines (#816) (#828)
TaprootFreak Jul 12, 2026
fc2e217
docs(store): add App Store promotional text (#826)
TaprootFreak Jul 12, 2026
3c09457
fix(kyc): clear stale country-field error on selection and neutralize…
TaprootFreak Jul 12, 2026
3e7011f
test(goldens): regenerate country-field state goldens for the neutral…
TaprootFreak Jul 12, 2026
766792a
feat(kyc): drive the legal-disclaimer gate from the API (#831)
TaprootFreak Jul 12, 2026
9054b55
test(kyc): restore 100% coverage for the legal-acceptance feature (#837)
TaprootFreak Jul 12, 2026
e596aed
test(goldens): buy flow state baselines (#816) (#834)
TaprootFreak Jul 12, 2026
5843f4b
test(goldens): dashboard / transaction-history / receive state baseli…
TaprootFreak Jul 12, 2026
cfadd40
test(goldens): connect-bitbox / debug-auth / legal-document state bas…
TaprootFreak Jul 12, 2026
5637315
feat(kyc): show a manual-review screen when the RealUnit registration…
TaprootFreak Jul 12, 2026
0b56954
chore: bump version floor to 1.2.0 (#839)
TaprootFreak Jul 12, 2026
010b0a6
Merge pull request #824 from RealUnitCH/staging
TaprootFreak Jul 12, 2026
a4dba40
fix(kyc): sign the server-provided registration date, not the device …
TaprootFreak Jul 13, 2026
577467b
docs(handbook): cover all 268 golden baselines (#843)
TaprootFreak Jul 13, 2026
e3a5354
fix(kyc): surface structured API rejections on the registration submi…
TaprootFreak Jul 13, 2026
ab786dd
fix(ci): give the cold-start welcome assert head-room for iOS 26 hier…
TaprootFreak Jul 13, 2026
ce48858
test: cover hidden activated-surface code and gate against blind spot…
TaprootFreak Jul 13, 2026
88cf78b
test: cover the boot-time security-flag migration (#846)
TaprootFreak Jul 13, 2026
8977699
feat(kyc): hard-wire tax residences to include the address country (#…
TaprootFreak Jul 14, 2026
3150edd
fix(bitbox): keep pairing CTAs tappable on small screens and large te…
TaprootFreak Jul 14, 2026
8610155
fix(kyc): hide dead remove button on the primary tax residence row (#…
TaprootFreak Jul 14, 2026
0f4544e
ci: disable Actions cache for flutter-action on the self-hosted runne…
TaprootFreak Jul 14, 2026
5c490b6
ci: lower self-hosted job timeout from 30 to 15 minutes (#853)
TaprootFreak Jul 15, 2026
b5d59fc
fix(responsive): keep CTAs tappable across 25 surfaces at any device …
TaprootFreak Jul 15, 2026
a6275ba
fix(kyc): seed the tax step from the stored declaration and bound it …
TaprootFreak Jul 15, 2026
765f2a1
ci(golden-regenerate): checkout live branch head instead of pinned di…
TaprootFreak Jul 15, 2026
047b6ac
Merge pull request #841 from RealUnitCH/staging
TaprootFreak Jul 15, 2026
4ce94fc
ci: dedupe staging-lane runs via shared concurrency group (#857)
TaprootFreak Jul 15, 2026
29c3b5d
fix(responsive): keep bottom-sheet CTAs tappable at large text scale …
TaprootFreak Jul 15, 2026
389a866
fix(buy): typed handling for primary-email confirm errors and unconfi…
TaprootFreak Jul 15, 2026
8e2ab79
fix(ci): route staging checks without cancelled required checks (#861)
TaprootFreak Jul 15, 2026
3d8ff8c
docs(handbook): stage the four no-registration web baselines (18 → 22…
TaprootFreak Jul 15, 2026
87f79b1
Merge pull request #859 from RealUnitCH/staging
TaprootFreak Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 30 additions & 0 deletions .coverage-visibility-allowlist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Coverage visibility allowlist — see scripts/check-coverage-visibility.sh.
#
# In-scope .dart files that legitimately produce NO coverable lines, so they
# never emit an `SF:` record in the scoped tracefile and would otherwise trip
# the visibility gate. Every entry here is a pure abstract interface / port, an
# enum, const-only data, or a Drift table schema — there is no executable body
# to cover.
#
# This is a ratchet: a NEW in-scope file that carries no coverage fails the
# build. Fix it by adding a test that exercises it (preferred). Only add a line
# here if the file genuinely has no coverable lines, and say why in a comment.
# Blank lines and `#` comments are ignored. Paths are repo-relative.

# Abstract interfaces / ports (implementations live elsewhere and are tested):
lib/packages/io/backup_exclusion_port.dart
lib/packages/io/documents_directory_port.dart
lib/packages/service/biometric/biometric_port.dart
lib/packages/service/price_service.dart
lib/screens/kyc/steps/ident/cubits/kyc_ident/sumsub_ident_port.dart

# Enums (no bodies):
lib/packages/service/biometric/biometric_auth_outcome.dart
lib/packages/service/dfx/models/payment/payment_info_error.dart
lib/screens/restore_wallet/cubit/validate_seed/validate_seed_state.dart

# Const-only data (covered as values by default_assets_test.dart, no lines):
lib/packages/utils/default_assets.dart

# Drift table schema (column getters carry coverage:ignore-line):
lib/packages/storage/node_storage.dart
33 changes: 28 additions & 5 deletions .github/workflows/golden-regenerate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,38 @@ jobs:
regenerate:
name: Regenerate golden baselines
runs-on: [self-hosted, macOS, ARM64, m3-ultra, realunit-app]
timeout-minutes: 30
# Same reasoning as `golden-tests` in `pull-request.yaml`: the old 30 was
# sized for the cache era (~18 minutes of stalled Actions-cache overhead).
# Regeneration is that same workload plus a commit+push, so 15 minutes
# still leaves room for a cold Flutter SDK download, and it frees this
# repo's single self-hosted slot far sooner when a run wedges. Keep the
# two jobs' timeouts in sync, like the rest of their setup.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
# Explicit token so the later `git push` is authenticated. Without
# this, checkout still works but the remote is configured with no
# credential helper and the push fails with HTTP 403.
token: ${{ secrets.GITHUB_TOKEN }}
# Single-slot runner can start minutes after dispatch. Default
# checkout pins github.sha (stale by then); render against that
# tree and the later push is rejected because the branch tip has
# moved. Checkout the live head instead (github.ref_name — the
# dispatched branch; workflow_dispatch always runs with --ref
# <branch>) so baselines match the tree they get committed to.
ref: ${{ github.ref_name }}
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
channel: "stable"
cache: true
# Self-hosted runner already keeps the Flutter SDK persistently
# under `_work/_tool` across runs. The Actions cache restore on
# this runner stalls at 0.5-0.8 MB/s, times out after the 10
# minute segment timeout, falls back to a cache miss anyway,
# then burns ~8 minutes on a cache-save — ~18 minutes of pure
# overhead that returns nothing useful here.
cache: false
- run: flutter pub get
- run: dart run tool/generate_localization.dart
- run: dart run tool/generate_release_info.dart
Expand All @@ -75,9 +94,13 @@ jobs:
exit 0
fi
git commit -m "test(goldens): regenerate baselines on the self-hosted runner"
# On protected branches (develop/main) this fails by design.
# The next step uploads the PNGs as an artifact so the user can
# still recover the regen output.
# Protected branches (develop/main): fails by design — no force-push,
# no bypass. Feature branches: after the ref: fix above this should
# normally succeed. If the branch still moves between checkout and
# push (now a seconds-wide window), fail loud; the fallback artifact
# step below still uploads the PNGs. Recovery: re-dispatch (cheap).
# Do NOT rebase or force-push — that would publish baselines that no
# longer match the tree head they were rendered against.
git push

# Runs only if the push step failed. Uploads the regenerated PNGs so
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ jobs:
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "61" ]; then
echo "expected 61 screenshots, got $count" >&2
if [ "$count" != "278" ]; then
echo "expected 278 screenshots, got $count" >&2
exit 1
fi

Expand Down Expand Up @@ -149,11 +149,11 @@ jobs:
exit 1
fi

# Screenshots dir must contain all 61 PNGs assembled from Goldens.
# Screenshots dir must contain all 278 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
# Mix of original 01-26 range + new 27-61 range so a regression
# in either half surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error; do
# Mix of the original 01-61 range and the 62-268 batch (every Golden
# baseline) so a regression in either half surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/screenshots/${name}.png")
# 200 (auth happens to match) or 401 (auth fails but file exists)
# both prove the file is on disk. 404 means it was not assembled.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/handbook-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:
branches: [staging, develop]
paths:
- "docs/handbook/**"
- "test/goldens/screens/**"
- "test/goldens/**"
- "scripts/assemble-handbook-screenshots.sh"
- "scripts/assemble-handbook-store-listing.py"
- "scripts/templates/store-listing.html.tmpl"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,13 @@ jobs:
- name: Stage web e2e baselines from web repo
env:
WEB_DIR: _web-checkout
# RealUnitCH/web commits 18 Playwright visual baselines under
# RealUnitCH/web commits 22 Playwright visual baselines under
# tests/__screenshots__/{desktop-chromium,tablet-chromium,mobile-safari}/.
# `-ne` below fails the build on ANY drift (add OR remove) so a change
# to the committed set upstream requires an explicit bump here AND a
# matching card edit in docs/handbook/de/index.html (#spec-web) in the
# SAME change — same guard rationale as the api steps above.
EXPECTED_WEB_BASELINE_COUNT: 18
EXPECTED_WEB_BASELINE_COUNT: 22
run: |
set -euo pipefail

Expand Down
87 changes: 58 additions & 29 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,25 @@ name: RealUnit Build
# below (saves macOS-runner minutes while work is still in progress).
# - `labeled` / `unlabeled` are intentionally omitted: realunit-app has
# no label-gated heavy job here (Tier 3 has its own workflow).
# - `push: develop` and `push: staging` keep post-merge verification
# as an authoritative source of truth, independent of any PR state.
# `staging` participates because feature → staging merges land an
# integration commit whose verification cannot be deferred to the
# auto-opened staging → develop PR (the gate must be authoritative
# at the moment the commit lands on staging). Other branches do not
# need a post-push check — the PR-event already covers them.
# - `push: develop` keeps post-merge verification as an authoritative
# source of truth. Staging fallback routing lives in the separate
# `staging-ci-fallback.yaml` workflow: it dispatches this workflow
# only when no ready staging → develop PR owns CI for the pushed SHA.
# - `workflow_dispatch` is kept as a manual override and is NOT
# draft-gated (see the job `if:` guard below).
on:
workflow_dispatch:
push:
branches: [develop, staging]
branches: [develop]
pull_request:
branches-ignore: [main]
types: [opened, synchronize, reopened, ready_for_review]

# Group by PR number so a new push to the same PR cancels the
# in-flight run for the outdated commit. macOS runners are scarce —
# letting an obsolete run finish wastes ~10 minutes per push. Grouping
# by SHA (the previous approach) put every commit in its own group,
# which meant no cancellation ever happened and back-to-back pushes
# queued sequentially.
# Falls back to `github.ref` for `push` and `workflow_dispatch` events
# (where there is no `pull_request.number`): `refs/heads/develop` for
# post-merge runs, the dispatched ref for manual triggers.
# Group PR runs by PR number, and push/dispatch runs by ref. A new SHA
# cancels work for the outdated SHA in the same lane. Staging push
# routing is isolated in `staging-ci-fallback.yaml`; it either relies on
# this PR workflow or dispatches this workflow as a fallback, but never
# shares a concurrency group with the PR run.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
Expand All @@ -60,7 +53,7 @@ jobs:
runs-on: macos-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
Expand Down Expand Up @@ -151,7 +144,7 @@ jobs:

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-lcov
path: coverage/lcov.info
Expand All @@ -168,7 +161,7 @@ jobs:
# fail-open (silent `exit 0`).
- name: Upload coverage summary
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: coverage-summary
path: coverage/lcov.summary
Expand Down Expand Up @@ -228,14 +221,20 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Download coverage summary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: coverage-summary
path: coverage

- name: Download scoped coverage tracefile
uses: actions/download-artifact@v7
with:
name: coverage-lcov
path: coverage

- name: Enforce coverage floor
run: |
set -euo pipefail
Expand Down Expand Up @@ -275,6 +274,18 @@ jobs:
fi
fi

# Closes the coverage blind spot the floor % alone cannot see: an in-scope
# file that no test ever loads is absent from lcov.info entirely, so it
# sits in neither the numerator nor the denominator and the scoped %
# stays 100 % while the file is wholly untested. This step enumerates the
# on-disk activated surface (same globs as the filter step above) and
# fails when a file carries no coverage and is not in the committed
# lineless allowlist. See scripts/check-coverage-visibility.sh.
- name: Enforce in-scope file visibility
run: |
set -euo pipefail
bash scripts/check-coverage-visibility.sh coverage/lcov.info .coverage-visibility-allowlist

# Visual regression tests on the self-hosted runner. Goldens live
# under `test/goldens/` and are validated pixel-for-pixel against committed
# baselines under `test/goldens/screens/**/goldens/macos/`. The Mac Studio
Expand All @@ -295,14 +306,29 @@ jobs:
name: Visual Regression
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: [self-hosted, macOS, ARM64, m3-ultra, realunit-app]
timeout-minutes: 30
# 30 was sized for the era when `subosito/flutter-action@v2` still pulled
# the SDK through the Actions cache and burned ~18 minutes on a stalled
# restore plus the trailing cache-save. Without that overhead the job
# finishes in ~1 minute, so 15 is generous headroom — it still covers the
# worst normal case, where the SDK is absent from the runner's persistent
# tool cache and has to be downloaded from scratch. The lower ceiling is
# what matters here: the self-hosted runner has exactly ONE slot for this
# repo, so a wedged job blocks every other PR until it is killed. 15 hands
# the slot back twice as fast as 30.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: subosito/flutter-action@v2
with:
flutter-version: "3.41.6"
channel: "stable"
cache: true
# Self-hosted runner already keeps the Flutter SDK persistently
# under `_work/_tool` across runs. The Actions cache restore on
# this runner stalls at 0.5-0.8 MB/s, times out after the 10
# minute segment timeout, falls back to a cache miss anyway,
# then burns ~8 minutes on a cache-save — ~18 minutes of pure
# overhead that returns nothing useful here.
cache: false
- run: flutter pub get
- run: dart run tool/generate_localization.dart
- run: dart run tool/generate_release_info.dart
Expand All @@ -311,7 +337,7 @@ jobs:
run: flutter test test/goldens
- name: Upload golden diff on failure
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: golden-diffs
path: test/goldens/**/failures/**
Expand All @@ -328,10 +354,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: "1.25.5"
# This repository has no Go module; caching would only emit a
# misleading "go.sum not found" warning for this audit-only job.
cache: false

- name: Install bitbox-audit
continue-on-error: true
Expand Down Expand Up @@ -359,7 +388,7 @@ jobs:

- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: bitbox-audit-report
path: bitbox-audit-report.md
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/staging-ci-fallback.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Staging CI Router

# A staging push normally produces a `synchronize` event on the open
# staging → develop promotion PR. That PR run owns the canonical required
# checks. If the PR is missing, draft, stale, or cannot be queried, this
# workflow dispatches the canonical RealUnit Build as a fail-safe.
on:
push:
branches: [staging]
workflow_dispatch:

permissions:
actions: write
contents: read
pull-requests: read

# Only the newest staging SHA needs routing. This group is deliberately
# isolated from RealUnit Build, so it can never cancel required PR checks.
concurrency:
group: staging-ci-router-${{ github.ref }}
cancel-in-progress: true

jobs:
route:
name: Route staging CI
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Use PR CI or dispatch fail-safe CI
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

SHOULD_DISPATCH=true
REASON="manual routing request"

if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
REASON="promotion PR lookup failed"
if PULLS_JSON="$(
gh api --method GET "/repos/${GITHUB_REPOSITORY}/pulls" \
-f state=open \
-f base=develop \
-f head="${GITHUB_REPOSITORY_OWNER}:staging" \
-f per_page=100
)"; then
if jq -e \
--arg repo "${GITHUB_REPOSITORY}" \
--arg sha "${GITHUB_SHA}" \
'any(.[]; .draft == false and .head.repo.full_name == $repo and .head.sha == $sha)' \
<<<"${PULLS_JSON}" >/dev/null; then
SHOULD_DISPATCH=false
REASON="ready promotion PR at this SHA owns canonical CI"
else
REASON="no ready promotion PR at this SHA"
fi
else
echo "::warning::Could not query the staging promotion PR; dispatching fail-safe CI"
fi
fi

if [ "${SHOULD_DISPATCH}" = "true" ]; then
gh workflow run pull-request.yaml \
--repo "${GITHUB_REPOSITORY}" \
--ref staging
echo "::notice::Dispatched RealUnit Build on staging (${REASON})"
else
echo "::notice::No fallback dispatch needed (${REASON})"
fi

{
echo "### Staging CI routing"
echo
echo "- Fallback dispatched: \`${SHOULD_DISPATCH}\`"
echo "- Reason: ${REASON}"
echo "- SHA: \`${GITHUB_SHA}\`"
} >> "${GITHUB_STEP_SUMMARY}"
Loading
Loading