user_profile: add the pro grace period, and distinguish an unset auto-renewing flag - #124
Draft
mpretty-cyro wants to merge 2 commits into
Draft
Conversation
Clients sometimes need to know whether a Pro subscription is terminal or auto-renewing (e.g. "renews on X" vs "expires on X"). Store the backend's `auto_renewing` (from get_pro_status) as a presence-only config flag `A`: 1 when auto-renewing, absent otherwise (terminal / unknown / not Pro). Deliberately not tri-state: unlike blinded_msgreqs `M`, this is backend- derived fact, not a defaulted client preference, so there's no upgrade- default edge case that a distinct "unset" would guard. And no t/T bump -- it's synced pro state like E/I/R, not a user-initiated profile edit. Exposes get_/set_pro_auto_renewing (C++ bool; C 0/1) with unit + C-API coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-renewing flag Completes the config side of session-foundation#121 against the refresh spec, which asks for both `auto_renewing` and `grace` to be synced alongside `E`. - New key `G`: the account's grace period in seconds. The backend folds grace into the stored expiry for auto-renewing subscriptions, so `E` is the end of coverage rather than the date a renewal is due; `E - G` recovers the paid-through instant on any device. Clearing `E` clears `G` with it, since a grace that outlived its expiry would pair with whatever wrote `E` next. - `get_pro_auto_renewing_opt()` / `user_profile_has_pro_auto_renewing()`: the flag is presence-only, so the existing getter answers 0 both for "not auto-renewing" and for "never learned". A client deciding at startup whether to fetch `/get_pro_status` needs those apart, since an account entitled before the field existed reads as false forever otherwise. A separate predicate rather than a -1 sentinel on the getter: a negative int cast to an unsigned boolean is non-zero, so the natural JNI binding would silently read "unknown" as "auto-renewing".
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.
user_profile: add the pro grace period, and distinguish an unset auto-renewing flag
Base:
jagerman/pro-auto-renewing-config@8e5634b8— the head of #121, not the-pfsvariant (identical commit subject, different sha, sits on the PFS track).Branch:
feature/pro-auto-renewing-tristate· Commit:086a420e· 4 files, +214 −1Completes the config side of #121 against the Pro status-refresh spec, which asks for both
auto_renewingandgraceto be synced alongsideE. #121 as it stands shipsauto_renewingonly.
1. New key
G— the account's grace period, in secondsWhy it is needed rather than merely nice. The backend folds the grace period into the stored
expiry for auto-renewing subscriptions —
payment_expiry_at = expiry_at + grace if auto_renewing,written to
users.expiry_atand sent verbatim asget_pro_status.expiry_ts. SoEis the end ofcoverage, not the date a renewal is due, and
user_statusis judged against that same value. Thebackend states it plainly at
base.py:158: "Grace periods added to a subscription's paid-throughinstant."
With
Gsynced, any device recovers the paid-through instant asE − G. Without it, aconfig-only consumer — notably each client's cold-start status-fetch gate — cannot compute it at all.
Not optional, deliberately, unlike
A. The backend sends0whenever the subscription is notauto-renewing, so "unset" and "zero" describe the same account and both give
E − 0 == E. There is nostate a caller could act on differently, so there is nothing for a presence check to disambiguate.
Clearing
EclearsG. A grace that outlived its expiry would pair with whatever wroteEnext— usually a proof outcome, which carries no grace of its own to correct it with.
set_pro_access_expiryalready clears
IandRas side effects, so this follows the existing shape rather than introducingone. (Insurance rather than the load-bearing fix — see §4.)
2.
auto_renewing: telling "unset" apart from "false"The flag is presence-only (
set_nonzero_int), sofalseerases the key and the existing getteranswers
0for both "not auto-renewing" and "never learned". A client deciding at startup whetherto call
/get_pro_statusneeds those apart: an account entitled before this field existed reads asfalseforever, and the fetch that would populate it is the one being skipped.get_pro_auto_renewing()andset_pro_auto_renewing()are unchanged, so every existing callerkeeps the collapsing behaviour, which is right for anything that only asks "is it renewing".
Why a separate predicate and not a
-1sentinel on the getterA sentinel was the first shape; the three clients killed it with platform evidence:
jbooleanisuint8_t. The natural binding, mirroring the accessor directly aboveit, does
static_cast<jboolean>(-1)= 255 =JNI_TRUE. "Never stored" would reach Kotlin as"auto-renewing", silently, with no diagnostic — and that is the reading which makes a startup
gate decline the fetch, so the state never corrects.
number;if (getProAutoRenewingOpt())reads unknown astrue in JS, and TypeScript would not catch it.
!= 0.Two of three exposed, and the one where it is silent is also the one where it fails toward the
dangerous answer.
3. Testing
All tests passed (25755870 assertions in 128 test cases), and specifically:G→ …870 with the clear-pairingtest), which is this repo's own guard against a stale
testAllbinary reporting a previous build'sresult.
user_profile_get_pro_access_expiry, not inferred from a green compile.clang-format— a green run taken before the format pass would havebeen a stale-binary claim.
CLANG_FORMAT_DESIRED_VERSION=19 ./utils/format.sh; the diff touches nothing unrelated.Tests pin the behaviour that is easy to get wrong: that setting the flag
falsereturns it to unsetrather than to a stored false, and that clearing
Eprevents a laterEwrite from inheriting a stalegrace.
4. 🔴 Known limitations — please read before merging
(a) This is unknown vs true, not a genuine tri-state.
set_pro_auto_renewing(false)erases thekey, so a stored false and an absent one remain the same state. It answers "has anyone ever written
this?" and not "is this user explicitly not auto-renewing?". Making it a real tri-state means
storing
0instead of erasing — a wire-encoding change, and a different decision.(b)
E − Gis only coherent ifEandGare written from the same response. Thegenerate_pro_proofresponse carriesaccount_expiry_tsbut no grace, so a proof outcome writesEand leaves
Guntouched. A companion backend change addsaccount_grace_period_durationto thatresponse (
Session-Pro-Backend,feature/proof-response-carries-grace). Without it,Gis a strictimprovement over having nothing but is not reliable across a proof-then-grace-transition sequence.
The clear-pairing in §1 does not close this on its own — it is insurance; the backend field is the fix.
(c) The API shape is yours. The predicate-vs-sentinel choice was made on client evidence rather
than on preference, and it is worth confirming. If you would rather supply the change yourself, this
branch is disposable.
Note: This is based on #121