Pro redemption reflow (Android): implicit redemption + libsession-owned renewal - #2147
Open
jagerman wants to merge 8 commits into
Open
Pro redemption reflow (Android): implicit redemption + libsession-owned renewal#2147jagerman wants to merge 8 commits into
jagerman wants to merge 8 commits into
Conversation
Adapts the app to implicit redemption + libsession-owned rotation/renewal (pairs with the glue on branch pro-redemption-reflow / libsession 58bafc4a): - Delete add_pro_payment: remove AddProPaymentApi, ProStatusManager.addProPayment, the PurchaseEvent.Failed.ServerError retry UX + PaymentServer/NonRetryable exceptions. New ProStatusManager.onPurchaseInFlight() = setProPrepaid(now) + schedule proof worker. - ProProofGenerationWorker: rotating key via ED25519.proRotatingSeed(masterKey, now) (deterministic weekly seed) instead of random; runs for ACTIVE *or* pro_prepaid (redemption); 'not Pro yet' is retryable while prepaid (WorkManager exp-backoff = capped client poll; pro_prepaid 1-week gate terminates). - FetchProStatusWorker: renewal via UserProfile.getProRenewalTarget(now) (drops the autoRenewing/expiry bug); also drives the redemption poll when a (possibly synced) pro_prepaid marker is seen while not active. - PlayStore listener: only mark in-flight on PURCHASED (skip PENDING). - Refund 'in progress' now read from synced config (getRefundRequested) — the wire field is gone; threaded into ProDataMapper.toProStatus. Verified: glue builds green standalone; app-side inspection-clean (no dangling refs). Local app compile blocked by the pre-existing local-glue AGP/kotlin.android plugin mismatch. Remaining: purchase-pending UI (getProPrepaid) display. WIP; not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Count/truncate message codepoints natively (String.codePointCount / offsetByCodePoints) instead of the glue's utf16_count round-trip. - addProFeatures now feeds the native codepoint count to SessionProtocol.proFeaturesForMessage, letting libsession own the count -> higher-character-limit decision. - MAX_CHARACTER_PRO / MAX_CHARACTER_REGULAR are single-sourced from libsession (SessionProtocol.PRO_HIGHER/STANDARD_CHARACTER_LIMIT).
Clients must not care about or assert libsession's rotating-seed rotation window (it's libsession-owned and may change). Reword the rotating-key comments to just 'the seed for now' instead of 'weekly'/'rotation period'; no code logic relied on the window.
…e guards Align android's worker-based renewal with the reviewed design (agent-comms/pro-proof-renewal-loop-design.md). Android was already renewal_target-driven + backoff-bounded (WorkManager) + revocation-clearing; the remaining gaps: - Drop the random(10..60min) pre-expiry jitter -> deterministic 60min lead. Per-device jitter leaks device count via the landed-renewal order statistic; libsession owns timing, config resolution settles concurrent renewals. - ProProofGenerationWorker: upgrade guard (replace proof iff newer expiry) + write E<-account_expiry from the proof response, so the renewal path keeps E fresh without a get_pro_status. - FetchProStatusWorker: downgrade guard on the Never/Expired clear (never wipe a still-valid proof a fresh sync just landed).
…d change Coverage gap found while verifying the reconcile triggers: the status-refresh scheduler watched proAccessExpiry (E), the proof, status expiry, and startup — but nothing watched pro_prepaid (I). So a prepaid synced from another device's purchase never kicked this device's redemption poll, breaking the cross-device 'any device pulls the entitlement through' resilience (design 1.7). Watch (E, I) so a synced prepaid triggers a status refresh -> scheduleProofGenerationIfNeeded -> poll.
ProErrorCode is the generate_pro_proof error_code vocabulary; the backend emits subscription_expired there for a lapsed entitlement (base.ErrorCode), NOT expired. "expired" is a user_status value (never/active/expired) — a different wire field, already modelled correctly by ProUserStatus. The two vocabularies are deliberately disjoint so no token identifies two fields; ProErrorCode.EXPIRED="expired" borrowed the status token into the error_code enum, so it could never match a real slug and the actual lapse code was unmodelled.
…rties The composite-build hook (build the glue from source in one Gradle run with the app, for live-testing coordinated glue+app changes) previously required passing -Dsession.libsession_util.project.path on every invocation. Read the same glue-checkout path from local.properties too (gitignored, set once), so a plain ./gradlew builds glue-from-source for devs who opt in, while production/CI — with no such entry — keep using the published AAR. The -D system property still works and takes precedence for a one-off override. Requires the glue on AGP 9 (matched toolchain) so both share one Gradle run.
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.
See PRs session-foundation/session-pro-backend#4 and session-foundation/libsession-util#104
The Pro subscription client used to decide for itself when to renew a proof, when to
redeem a purchase, and how to schedule that work — logic that had drifted between
platforms and, worse, leaked how many devices were on an account (through per-device
renewal jitter). This PR hands all of those decisions to libsession and reduces the app
to a thin driver: it asks libsession whether and when to renew, and acts on the answer.
Redemption is now implicit. There's no separate "register this payment" step. When a
purchase is pending, the app simply calls
generate_pro_proof, which both redeems thepurchase and returns the proof. The old
AddProPaymentApiis removed.Renewal timing and the rotating signing key come from libsession. Every device derives
the same rotating key for the current window and renews on the same deterministic
schedule, so all of an account's devices converge instead of each rolling its own random
key and timing. The previous per-device jitter — which leaked device count — is gone.
Writes to the synced proof are now guarded. Because the proof lives in synced user
config, the app no longer blindly overwrites it: it replaces a proof only with one that
extends coverage, clears on an authoritative
expired/neveronly when it isn't alreadyholding a valid proof, and treats revocation as terminal. It also now reacts to a purchase
made on a different device (a synced
pro_prepaid) by polling to redeem on this device'sbehalf — previously nothing triggered that.
Smaller fixes. Message character limits are counted as Unicode codepoints via
libsession rather than a client-side copy of the rules, and a mis-set failure-code
constant was corrected (
subscription_expired, not theexpireduser-status token).Depends on the libsession-util-android glue re-pinned to libsession
078c611b(companion PR).Version: bumped to 1.34.0.
Dev convenience: you can now build the glue from source alongside the app in one Gradle
run by pointing
local.propertiesat a libsession-util-android checkout; production/CI,with no such entry, keep using the published AAR.