Skip to content

Pro: declare message features from the proof, not the plan's state - #2173

Merged
mpretty-cyro merged 1 commit into
session-foundation:devfrom
mpretty-cyro:fix/pro-overhang-feature-declaration
Aug 19, 2026
Merged

Pro: declare message features from the proof, not the plan's state#2173
mpretty-cyro merged 1 commit into
session-foundation:devfrom
mpretty-cyro:fix/pro-overhang-feature-declaration

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

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:

sender's row      9,800 characters
recipient's row   2,000 characters

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:

what the composer allows      currentUserHasProAccess()          ACCESS
what proof is attached        currentUserProProofForAccess()      ACCESS
what features are declared    proDataState.type is ProStatus.Active   ← DISPLAY

addProFeatures returned early on plan state, so an overhang message went out with a valid credential and
an 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

addProFeatures now gates on currentUserProProofForAccess() — 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_BADGE was skipped in
the 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 MessageSender already
refused 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:compilePlayDebugKotlin green. :app:testPlayDebugUnitTest 251 tests, 0 failures — none of which
covers this path, so that says the change is safe rather than that it works.

Verified on a device instead, with the session-appium pro_overhang spec: two devices, a real grant with a
short 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.

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.
@mpretty-cyro
mpretty-cyro merged commit c4e24eb into session-foundation:dev Aug 19, 2026
5 checks passed
@mpretty-cyro
mpretty-cyro deleted the fix/pro-overhang-feature-declaration branch August 19, 2026 06:06
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