Pro: declare message features from the proof, not the plan's state - #2173
Merged
mpretty-cyro merged 1 commit intoAug 19, 2026
Conversation
A message's Pro features are only honoured by a recipient that can verify the proof carried with it, so the declaration has to come from the same place the attachment does. It came from the plan's displayed state instead, and those two answers differ for exactly as long as a lapsed plan's proof stays valid. In that window the composer offered the Pro character limit, the send attached a valid proof, and the message claimed no feature — so the recipient applied the standard limit and truncated before storing. The sender kept the full text and the recipient kept 2000 characters, with nothing on either screen to say so. The same early return also skipped the profile features, so those messages claimed no Pro badge either. Gating on the proof accessor rather than the boolean access check keeps the declaration and the attachment reading one value: a mocked grant cannot produce a proof, and a message that claimed a feature it carried no credential for would be discarded by the recipient anyway.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An outgoing message could carry a valid Pro proof and no claim to the feature that proof authorises. The
recipient then correctly applied the standard limit, so the two ends of a conversation held different
messages and neither user was told.
Measured before it was diagnosed
A sender whose plan has lapsed while its proof is still valid — the "overhang", a designed state: the
backend keeps certifying proofs until coverage runs out, past the paid term. Sender composes 9,800
characters (Pro limit 10,000, standard 2,000). Both devices, same run:
Not a rendering artefact: the sender's bubble offers "Read more", the recipient's has nothing to reveal.
The two databases genuinely differ.
Cause
Three decisions are taken when sending, and only two read the proof:
addProFeaturesreturned early on plan state, so an overhang message went out with a valid credential andan empty feature bitset. The recipient reads features only when the proof validates, finds no
HIGHER_CHARACTER_LIMIT, applies 2,000, and truncates before persisting.The change
addProFeaturesnow gates oncurrentUserProProofForAccess()— the same accessor the attachment uses,rather than the boolean the composer uses. Those differ only on QA overrides, and using the same value as
the attachment is what makes the two structurally unable to diverge again: a declared feature is only
honoured by a recipient that can verify the proof carried alongside it, so the declaration and the
attachment have to come from one source.
One early return guarded both the message features and the profile features, so
PRO_BADGEwas skipped inthe overhang too. Both are restored.
A second bug fixed by the same line
A revoked proof while the plan still read Active: features were declared while
MessageSenderalreadyrefused to attach a revoked proof — the same inconsistency, mirrored. The new check is revocation-aware, so
neither direction happens.
Also now stricter, and consistent, for a proof that expired between renewals: nothing declared, nothing
attached.
Behaviour change worth knowing
A QA fixture that grants access without a real proof now declares nothing where it previously declared
features. Recipient-visible behaviour is unchanged — no proof was attached, so those features were already
discarded — but features are persisted on the sender's own row, so anything asserting the sender's local
message features under a mocked grant would now see an empty set. Nothing in session-appium does today;
this was checked, not assumed.
Testing
:app:compilePlayDebugKotlingreen.:app:testPlayDebugUnitTest251 tests, 0 failures — none of whichcovers this path, so that says the change is safe rather than that it works.
Verified on a device instead, with the session-appium
pro_overhangspec: two devices, a real grant with ashort plan so it lapses while the proof stays live, asserting both the sender's Pro limit and the
recipient's receipt of the full message. Fails without this change, passes with it. The artefact was
hash-verified and the app uninstalled first, since the versionCode is unchanged and Appium would otherwise
skip the reinstall.
The same spec already passed on iOS and Desktop, so this brings Android in line rather than establishing new
behaviour.