Skip to content

feat(kotlin-sdk): DashPay invitations — create, claim, reclaim, persistence (DIP-13) - #4284

Open
shumkov wants to merge 10 commits into
v4.1-devfrom
feat/kotlin-dashpay-invitations
Open

feat(kotlin-sdk): DashPay invitations — create, claim, reclaim, persistence (DIP-13)#4284
shumkov wants to merge 10 commits into
v4.1-devfrom
feat/kotlin-dashpay-invitations

Conversation

@shumkov

@shumkov shumkov commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Issue being fixed or feature implemented

Ports the shipped iOS DashPay invitation feature (DIP-13 sub-feature 3', PR #4041) to the Kotlin SDK and KotlinExampleApp — the last DashPay gap left out of the K1–K3 migration. Android was deliberately fail-closed (on_persist_invitations_fn: None), refusing invitation creation rather than risk the voucher-key-reuse defect class (55937e1).

Spec + owner decisions + funded-e2e evidence: docs/dashpay/KOTLIN_INVITATIONS_SPEC.md.

What was done?

Zero shared-Rust changes — all invitation logic stays in rs-platform-wallet; the port is bindings + persistence + UI:

  • Persistence spine: InvitationEntity (field-exact with PersistentInvitation.swift, no secret column) + DAO; Room v7→v8 additive migration; per-row bridge slots + JNI trampoline; CAPABILITY_INVITATIONS attested in the same commit that wires the path. Hardening: the address-pool persist no longer silently skips a missing IdentityInvitation account row — it creates it or fails the round so Rust aborts before broadcasting (red→green demonstrated); JNI↔Kotlin descriptor lockstep is pinned by an instrumented test resolving every bridge slot up front.
  • Bindings + wrappers: parseInvitation / createInvitation / claimInvitation JNI exports + Kotlin wrappers (InvitationPreview, 6-key claim set); reclaim forwards consumeInvitationVoucher through the existing resume/top-up bindings (Rust core's authorized_invitation_reclaim remains the enforcement, matching iOS's trust model) with two new outpoint-taking reclaim wrappers — the only true-passing call sites.
  • App UI: InvitationsScreen / CreateInvitationSheet / ClaimInvitationSheet / ReclaimInvitationSheet, testTags = iOS accessibility identifiers verbatim; deep-link intent-filters with walletless parking; the verbatim reclaim-outcome classifier + reclaimInFlight marker discipline; bearer-secret hygiene (no logging, text-only share, sensitive + auto-cleared clipboard).
  • Independent review folded: a cross-model review produced 17 findings; all confirmed ones fixed (active-identity/wallet binding, deep-link navigation + one-shot intents, app-scope clipboard clear, busy-gated dismissal, fail-closed DAO marker writes, QR-scan claim path, a stale red Rust test). Deferrals with rationale in spec §9.
  • QA contract rows DP-12..DP-19 (KotlinExampleApp/TEST_PLAN.md), parity manifest (kotlin: supported/partial) + regenerated PARITY_SUMMARY.md.

How Has This Been Tested?

  • JVM suites green (handler round-trips incl. the round-failure red→green, classifier outcome matrix, wrapper seams, InvitationPreview contract).
  • cargo test -p rs-unified-sdk-jni --lib 37/37; clippy clean.
  • Instrumented on the CI-image emulator: 32/32 — Room MIGRATION_7_8 + full v1→v8 chain, the descriptor-resolution gate, FFI smoke.
  • Funded testnet e2e on an arm64 emulator (evidence in spec §7): create (0.03, InstantSend-locked, row persisted) → claim (new identity, 2.82B credits, no L1 on the claimer side) → already-consumed race classified live ("This invitation was already claimed.", row → Claimed) → an IS-timeout create still persisted + reclaimable → reclaim-as-top-up (+2.92B credits on-chain, row → Reclaimed) → malformed-link rejection. DP-14 (two-wallet contact bootstrap) and DP-18 (reclaim-as-register) remain manual funded gates, as on iOS.

Breaking Changes

None.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

🤖 Generated with Claude Code

shumkov and others added 10 commits July 22, 2026 20:15
Research + five-lens review (feasibility, security, scope, adversarial,
fact-check) folded; owner decisions recorded in §10 (reclaim JNI guard
drop, dual deep-link filters with walletless parking, one PR / five
slices, WAL synchronous=NORMAL residual accepted+documented).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bridge)

Slice 1 of the invitations port (docs/dashpay/KOTLIN_INVITATIONS_SPEC.md):

- InvitationEntity (field-exact port of PersistentInvitation.swift, no
  secret column) + InvitationDao; DashDatabase v7→v8 additive migration
  with exported 8.json; wallet-teardown pass.
- Per-row bridge slots onPersistInvitationUpsert/-Removal + handler impl:
  staged into the round buffer, upsert preserves the client-written
  statusRaw/reclaimInFlight columns (Rust emits only Created).
- JNI trampoline tramp_persist_invitations; on_persist_invitations_fn
  flipped from the fail-closed None to Some; the handler now attests
  CAPABILITY_INVITATIONS (0x02) in the same change that wires the path.
- Voucher-key-reuse hardening: the address-pool persist no longer
  silently skips a missing IdentityInvitation account row — it creates
  the row (healing installs that predate invitation support) or fails
  the round so Rust aborts before broadcasting the funding tx.
  Red→green: invitationPoolEntryCreatesTheMissingAccountRow and
  invitationPoolEntryWithoutWalletFailsTheRound fail against the
  pre-fix silent skip (verified ✖) and pass with the fix (✔).
- Descriptor lockstep is now tested, not eyeballed: a bridge-method
  table + nativeVerifyPersistenceBridgeDescriptors export resolve every
  (name, descriptor) pair up front; instrumented FfiSmokeTest pins it.

JVM suite green (55/55 handler tests incl. 7 new); cargo check clean.
Instrumented tiers (migration 7→8, descriptor smoke) run on the CI
emulator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 2 of the invitations port:

- Three JNI exports on DashpayNative (rs-unified-sdk-jni/src/dashpay.rs):
  parseInvitation (compact-JSON preview, malformed link =>
  structurallyValid:false, never an exception), createInvitation
  (returns the bearer link; outpoint out-param discarded as on iOS),
  claimInvitation (IdentityRegistrationNativeResult via the resume
  convention, handle transferred only after result construction).
  Section-level rule: the URI is the bearer credential and is never
  interpolated into exception messages or logs.
- Kotlin wrappers: Dashpay.parseInvitation/createInvitation (+ the
  InvitationPreview mirror of Swift's type with the
  gate-on-username-not-hasInviter contract), and
  IdentityRegistration.claimInvitation (6-key fresh-registration set,
  adopt-then-release managed handle, injectable native seam).
- ManagedIdentityHandleGuard promoted to pub(crate) for reuse.

Tests: claim wrapper seam (forwarding, handle freed on success and on
validation failure, wrong-slot key set rejected before JNI) +
InvitationPreview JSON contract. JVM suite green; cargo check + clippy
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…laim wrappers

Slice 3 of the invitations port:

- The resume/top-up JNI bindings now forward consumeInvitationVoucher
  verbatim instead of hardcoding false; the interim JNI-level
  generic_asset_lock_recovery_allowed guard is removed (it existed only
  until reclaim shipped). Authorization is Rust core's
  authorized_invitation_reclaim: invitation-typed locks are refused on
  every generic path regardless of the flag and consumable only into a
  register/top-up target with the flag set — the same single-gate trust
  model shipped on iOS.
- Two new outpoint-taking Kotlin wrappers, the ONLY true-passing call
  sites: IdentityCredits.reclaimInvitationAsTopUp (returns the new
  balance) and IdentityRegistration.reclaimInvitationAsNewIdentity
  (base 4-key set — a reclaim sends no contact request, matching iOS
  authKeyCount=4). Generic recovery wrappers and all existing call sites
  are unchanged (still false, still funding-type-gated).

Tests: reclaim seam coverage (flag forwarded, raw outpoint, handle
freed, wrong-slot key set rejected); the existing recovery tests keep
the generic paths' false discipline pinned. JVM suite green; cargo
check clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…im, sent list

Slice 4 of the invitations port (mirrors Views/DashPay/*Invitation*.swift;
testTags reuse the iOS accessibility identifiers verbatim):

- InvitationsScreen (dashpay.invitations.{list,create,reclaim}): Room-Flow
  driven sent-invitations list, filtered to loaded wallets, per-row
  wallet-scoped reclaim, status badges; new DashPayInvitations route +
  DashPayTabScreen toolbar entries (dashpay.openSentInvitations,
  dashpay.claimInvitation — enabled without an active identity).
- CreateInvitationSheet (dashpay.invite.create.*): default 0.03 DASH,
  contact-request-back toggle gated on a DPNS username, QR from an
  in-memory bitmap, TEXT-only share, clipboard copy flagged sensitive on
  API 33+ and compare-and-cleared after ~60s (Android has no local-only /
  expiring clipboard; documented platform gap vs iOS).
- ClaimInvitationSheet (dashpay.invite.claim.*): paste or deep-link seed,
  off-chain preview (amount '—'), claim wallet = active identity's wallet
  else first loaded (fresh invitee works), 6-key set pre-persisted, post-
  claim 'Add <username>?' prompt -> DPNS resolve -> contact request.
- ReclaimInvitationSheet (dashpay.invite.reclaim.*): top-up vs register
  targets (register = base 4-key set), in-memory isReclaiming single-
  flight gating submit AND dismissal, reclaimInFlight marker persisted
  only immediately before the consume (write must succeed), terminal
  status+marker saved atomically, failures resolved through the pure
  InvitationReclaimLogic classifier (typed tombstone -> Reclaimed;
  exact 'already completely used' phrase split by prior marker into
  Claimed vs explicitly-ambiguous; 'is not tracked' + marker -> leave
  state; stale-marker clearing rule) — iOS copy carried verbatim.
- Deep links: dashpay://invite + legacy invitations.dashpay.io/applink
  intent-filters (unverified until #4096 serves assetlinks.json);
  MainActivity parks the URI in AppUiState until a wallet exists —
  deliberate deviation from iOS, which drops a walletless link (flagged
  upstream); mid-claim second links stay parked (invitationClaimInFlight).
- All three network flows run in the application scope with
  NonCancellable around marker->consume->status (dismissal-safe).

Tests: InvitationReclaimLogicTest ports the full classifier outcome
matrix incl. exact-phrase false-positive safety, marker-clear rule,
outpoint split, next-unused-index. :app:assembleDebug +
:app:testDebugUnitTest green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice 5 bookkeeping: PARITY.md gains the four invitation view rows
(InvitationsView / CreateInvitationSheet / ClaimInvitationSheet /
ReclaimInvitationSheet — all ported, with the documented Android
adaptations); KOTLIN_INVITATIONS_SPEC.md status notes slices 1-5
implemented with instrumented + funded-testnet QA as the remaining
environment-bound gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ounts

formatDuffs already appends the unit; funded-e2e screenshots showed
'0.03 DASH DASH' on the sent-invitations row and reclaim sheet. Display
copy only — no behavior delta, so no regression test (per the docs/typo
carve-out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- KotlinExampleApp TEST_PLAN.md gains DP-12..DP-19 (shared IDs with the
  iOS plan) with the 2026-07-23 funded-run evidence inline; DP-14
  (two-wallet bootstrap — needs a DPNS-named inviter) and DP-18
  (reclaim-as-register) stay open as manual funded gates.
- sdk-parity-manifest: dashpay.invitations kotlin host unsupported →
  sdk=supported / example_app=partial, with kotlin verification entries
  (JVM unit, device migration + descriptor tests, manual DP-19); the
  retired JNI-guard test reference is replaced by the wrapper-seam pin.
- PARITY_SUMMARY regenerated via check_sdk_parity_manifest --write-summary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Independent review (17 findings) triaged; the confirmed ones fixed:

- Stale JNI vtable test still asserted on_persist_invitations_fn is None
  — cargo test -p rs-unified-sdk-jni --lib was red; now 37/37 green.
- Deep links now navigate to the DashPay tab (MainScreen effect) instead
  of parking invisibly, and invite intents are consumed one-shot so an
  Activity recreation can't re-park a handled link.
- Create and claim now bind to the DashPay tab's ACTIVE identity/wallet
  (route param + preferredWalletIdHex) instead of an arbitrary first row.
- Clipboard auto-clear moved to the application scope (a composition
  scope died with the sheet, leaving the bearer link forever) and keyed
  by a per-copy label nonce so it can never wipe a newer clip.
- Host-level sheet dismissal (scrim/back) is now gated while create or
  reclaim is busy — a mid-create dismissal could discard the only bearer
  link; a reopened reclaim could double-submit.
- InvitationDao marker/status updates return the affected-row count and
  the reclaim marker write fails closed on zero rows (concurrent wallet
  deletion race).
- Claim sheet gains the QR-scan path (routes through the shared scanner
  and the pending-invite parking mechanism); sent rows show the advisory
  expiry (with an expired state) alongside creation time.
- Unused InvitationDao.observeByWallet dropped; manifest comment made
  timeless; reclaim/claim KDocs cite the Swift source path.

Deferred with rationale (recorded in the spec §9): typed funded-failure
recovery outpoint (needs a shared-Rust change; tracked-lock diagnostics
cover recovery), driving the Rust pre-broadcast abort from a JVM test,
and the test-naming nit (sibling-file convention wins).

JVM suites + :app:assembleDebug green; cargo lib tests 37/37.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d70145a-ac21-4845-aa68-c17d8a2ef02f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

⛔ Blockers found — Sonnet deferred (commit f474ab1)
Canonical validated blockers: 3

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The JNI ownership, persistence callbacks, reclaim authorization forwarding, and fail-closed storage path are sound. Three blocking issues remain: the claim can submit a URI different from its displayed preview, QR scans pass the bearer credential through restorable saved state, and invitation registrations can reuse identity-key slots held by registrations already in flight; two additional security and timestamp-width improvements are also warranted.

Source: reviewers codex/general=gpt-5.6-sol(completed); codex/security-auditor=gpt-5.6-sol(completed); codex/ffi-engineer=gpt-5.6-sol(completed); verifier=codex/verifier=gpt-5.6-sol(completed); coordinator=openclaw-agent/cliproxy/gpt-5.6-sol(orchestration-only).

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — security-auditor (completed), gpt-5.6-sol — ffi-engineer (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 3 blocking | 🟡 2 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ClaimInvitationSheet.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ClaimInvitationSheet.kt:107-113: Submit only the URI associated with the displayed preview
  Changing `uriText` does not immediately invalidate `preview`; the replacement is parsed later by `LaunchedEffect`. During that interval, `canClaim` remains true based on the previous valid preview, but `claim()` snapshots the newly entered URI. The user can therefore approve one displayed inviter and irreversibly claim a different credential, and the post-claim contact prompt can also use metadata from the stale preview. Store the trimmed URI with each completed preview, require that pair to match before enabling Claim, and snapshot both values together when submission starts.
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ClaimInvitationSheet.kt:120-124: Reserve identity HD slots across every registration flow
  The next identity index is derived only from identities already committed to Room. It excludes slots held by the application-scoped `RegistrationCoordinator` in `PreparingKeys`, `InFlight`, or `Unconfirmed`, and the chosen index is not atomically reserved before keys are derived. A concurrent normal registration, claim, or reclaim-register can therefore use the same wallet/index and produce identical ECDSA public-key hashes for different identities. Platform's identity-create state validation treats those hashes as duplicates and applies a `PartiallyUseAssetLockAction` charging the unique-key penalty plus processing fees, so the invitation voucher can be partially consumed without creating the identity; retries continue selecting the absent Room slot. The same allocation occurs in `ReclaimInvitationSheet.kt:143-145`. Introduce an application-scoped per-wallet slot allocator shared by all registration paths and reserve the index before key derivation.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt`:
- [BLOCKING] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt:140-147: Keep scanned invitation URIs out of SavedStateHandle
  The generic scanner writes its raw result into `SavedStateHandle`, and this invitation path consumes the plaintext bearer URI from that channel. Values in `SavedStateHandle` participate in saved-instance-state restoration, so a state snapshot taken after delivery and before the collector clears the key can retain the voucher WIF across activity or process recreation. This violates the feature's normative rule that invitation URIs remain in memory and are never persisted. Deliver invitation scan results through a transient, non-saveable one-shot registry or application-scoped event instead.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/CreateInvitationSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/CreateInvitationSheet.kt:203-214: Protect bearer invitation screens with FLAG_SECURE
  The result screen renders a QR containing the plaintext voucher WIF without activating the app's existing `LocalSecureScreen` protection. Android can consequently expose the credential through screenshots, screen recordings, MediaProjection, or task snapshots, allowing anyone with the image to claim the voucher. Hold `FLAG_SECURE` while the created-link QR is visible and while `ClaimInvitationSheet` contains a nonblank invitation URI, then release it on disposal, following the seed and key-reveal screen pattern.

In `packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/tokens/Dashpay.kt:434: Carry u32 invitation timestamps without signed narrowing
  The Kotlin-to-JNI path narrows Unix seconds to a signed 32-bit `Int`, while the shared C API intentionally accepts `u32`. At Unix second 2147483648, the default becomes negative and JNI rejects every invitation creation even though the C API remains valid through 2106. The persistence callback independently casts `expiry_unix` and `created_at_secs` from `u32` to `jint`, and `InvitationEntity` stores both as `Int`; the roughly 24-hour expiry begins overflowing shortly before the creation-time cutoff, producing negative values and incorrect display and ordering. Use Kotlin `Long` and JNI `jlong` throughout, validate the range `1..u32::MAX` before Rust casts, update the callback descriptor and descriptor-resolution test, and store both Room fields as `Long`.

Comment on lines +107 to +113
val canClaim = !isClaiming && preview.structurallyValid && claimWallet != null

fun claim() {
if (!canClaim || isClaiming) return
val wallet = claimWallet ?: return
val mgr = manager ?: return
val uri = uriText.trim()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Submit only the URI associated with the displayed preview

Changing uriText does not immediately invalidate preview; the replacement is parsed later by LaunchedEffect. During that interval, canClaim remains true based on the previous valid preview, but claim() snapshots the newly entered URI. The user can therefore approve one displayed inviter and irreversibly claim a different credential, and the post-claim contact prompt can also use metadata from the stale preview. Store the trimmed URI with each completed preview, require that pair to match before enabling Claim, and snapshot both values together when submission starts.

source: ['codex']

Comment on lines +140 to +147
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
LaunchedEffect(savedStateHandle) {
savedStateHandle
?.getStateFlow<String?>(org.dashfoundation.example.navigation.QrScanner.RESULT_KEY, null)
?.collect { scanned ->
if (!scanned.isNullOrBlank()) {
savedStateHandle[org.dashfoundation.example.navigation.QrScanner.RESULT_KEY] = null
appUiState.pendingInviteUri.value = scanned

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Keep scanned invitation URIs out of SavedStateHandle

The generic scanner writes its raw result into SavedStateHandle, and this invitation path consumes the plaintext bearer URI from that channel. Values in SavedStateHandle participate in saved-instance-state restoration, so a state snapshot taken after delivery and before the collector clears the key can retain the voucher WIF across activity or process recreation. This violates the feature's normative rule that invitation URIs remain in memory and are never persisted. Deliver invitation scan results through a transient, non-saveable one-shot registry or application-scoped event instead.

source: ['codex']

Comment on lines +120 to +124
val identityIndex = InvitationReclaimLogic.nextUnusedIdentityIndex(
walletOwned
.filter { it.walletId?.contentEquals(wallet.walletId) == true }
.map { it.identityIndex },
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Reserve identity HD slots across every registration flow

The next identity index is derived only from identities already committed to Room. It excludes slots held by the application-scoped RegistrationCoordinator in PreparingKeys, InFlight, or Unconfirmed, and the chosen index is not atomically reserved before keys are derived. A concurrent normal registration, claim, or reclaim-register can therefore use the same wallet/index and produce identical ECDSA public-key hashes for different identities. Platform's identity-create state validation treats those hashes as duplicates and applies a PartiallyUseAssetLockAction charging the unique-key penalty plus processing fees, so the invitation voucher can be partially consumed without creating the identity; retries continue selecting the absent Room slot. The same allocation occurs in ReclaimInvitationSheet.kt:143-145. Introduce an application-scoped per-wallet slot allocator shared by all registration paths and reserve the index before key derivation.

source: ['codex']

Comment on lines +203 to +214
Text("Invitation ready", style = MaterialTheme.typography.titleLarge)
Text(
"Share this link with your friend. It funds their new identity — " +
"treat it like cash.",
style = MaterialTheme.typography.bodyMedium,
)
remember(link) { generateQrBitmap(link) }?.let { qr ->
Image(
bitmap = qr.asImageBitmap(),
contentDescription = "Invitation QR",
modifier = Modifier.size(220.dp).align(Alignment.CenterHorizontally),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Protect bearer invitation screens with FLAG_SECURE

The result screen renders a QR containing the plaintext voucher WIF without activating the app's existing LocalSecureScreen protection. Android can consequently expose the credential through screenshots, screen recordings, MediaProjection, or task snapshots, allowing anyone with the image to claim the voucher. Hold FLAG_SECURE while the created-link QR is visible and while ClaimInvitationSheet contains a nonblank invitation URI, then release it on disposal, following the seed and key-reveal screen pattern.

source: ['codex']

inviterIdentityId: ByteArray?,
inviterUsername: String?,
coreSignerHandle: Long,
nowUnix: Int = (System.currentTimeMillis() / 1000L).toInt(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Carry u32 invitation timestamps without signed narrowing

The Kotlin-to-JNI path narrows Unix seconds to a signed 32-bit Int, while the shared C API intentionally accepts u32. At Unix second 2147483648, the default becomes negative and JNI rejects every invitation creation even though the C API remains valid through 2106. The persistence callback independently casts expiry_unix and created_at_secs from u32 to jint, and InvitationEntity stores both as Int; the roughly 24-hour expiry begins overflowing shortly before the creation-time cutoff, producing negative values and incorrect display and ordering. Use Kotlin Long and JNI jlong throughout, validate the range 1..u32::MAX before Rust casts, update the callback descriptor and descriptor-resolution test, and store both Room fields as Long.

source: ['codex']

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.

2 participants