Skip to content

Pairing survives a project whose rules are older than the app - #41

Merged
jsconu merged 1 commit into
mainfrom
fix/pairing-claim-refused
Sep 21, 2026
Merged

jsconu merged 1 commit into
mainfrom
fix/pairing-claim-refused

Conversation

@jsconu

@jsconu jsconu commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Fixes the "That code isn't active ... the code was found, but linking this phone was refused" failure that made pairing impossible on a Firebase project whose firestore.rules hadn't been re-published since #39.

What was wrong

Claiming a code wrote three documents in one transaction: the child profile, the code doc, and the kid device's linkedDevices doc. That third write only became legal in the rules in the same commit that added it — so if the deployed rules were older than the app, Firestore refused the entire transaction. The code doc itself stayed readable, which is why the error said the code was found.

Reproduced in the Firestore emulator

Running the repo's real rules, with and without the linkedDevices block:

rules without linkedDevices current rules
claim with the link write inside the transaction (before) permission-denied pairs
claim with the link write after it (after) pairs, link refused and ignored pairs, links

Changes

  • PairingRepository — the transaction holds only the two writes that are the pairing. The linkedDevices doc is written straight after, best effort. The Residual security and reliability items from the #38 review #39 security binding is unchanged: getAfter still proves this device claimed that parent's code.
  • firestore.rules — resource.data.get('deviceUid', null), because reading a field that isn't there is an evaluation error, not null, and denied the claim outright.
  • ChildProfile.swift — assigning nil to a Swift dictionary subscript deletes the key, so every child created in the iOS parent app was written without deviceUid and could never be paired. Writes NSNull() now, matching the rest of that file.
  • The claim's error path no longer marks the code used while diagnosing which step was refused.
  • Regression test for a child doc with no deviceUid field, and a README note to re-publish the rules whenever that file changes.

Verification

:shared:testDebugUnitTest and :shared:compileDebugAndroidTestKotlin pass locally; the emulator matrix above is the substantive check. Confirmed working on a real phone against the live project after re-publishing the rules.

🤖 Generated with Claude Code

Claiming a pairing code wrote three documents in one transaction: the child
profile, the code itself, and the kid device's linkedDevices doc (#39). That
third write is the one a deployed firestore.rules only started permitting in
the same commit that added it, so on any project whose rules hadn't been
re-published since, Firestore refused the whole transaction and every claim
failed with "the code was found, but linking this phone was refused" - a live
code, an unpairable phone, and nothing a parent could do about it.

Only the two writes that ARE the pairing stay in the transaction. The
linkedDevices doc is written straight after, best effort: if it's refused the
phone is paired and works, it just can't read the parent's self-tracked stats.
The #39 binding is unchanged - the rule's getAfter still proves this device
claimed that parent's code, which holds just as well outside the transaction.

Two more ways a claim could be refused, both invisible from the kid's side:

- A missing field is an evaluation error in the security rules, not null, so
  `resource.data.deviceUid == null` denied any child written without the key
  at all. Now get('deviceUid', null).
- The iOS parent app wrote exactly that shape: assigning nil to a Swift
  dictionary subscript removes the key, so every child created there was born
  unpairable. It writes NSNull now, like the rest of that file already did.

The claim error also no longer marks the code used while working out which
step was refused - diagnosing a failed attempt shouldn't burn a good code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jsconu
jsconu merged commit 4fea68e into main Sep 21, 2026
3 checks passed
@jsconu
jsconu deleted the fix/pairing-claim-refused branch September 21, 2026 18:17
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.

1 participant