Pro redemption reflow (iOS): implicit redemption, libsession renewal/rotation - #723
Open
jagerman wants to merge 6 commits into
Open
Pro redemption reflow (iOS): implicit redemption, libsession renewal/rotation#723jagerman wants to merge 6 commits into
jagerman wants to merge 6 commits into
Conversation
…l/rotation, config-synced refund/prepaid Aligns iOS with the 5-repo Session Pro "redemption reflow" (against libsession 58bafc4a). 1. /add_pro_payment is gone — redemption is implicit. Deleted addProPayment (and the purchase retry loop + the Transaction.updates add-payment call); after a purchase the client just requests a proof via generate_pro_proof and the backend binds the account's unbound payments on any master-signed request. Removed the setPaymentRefundRequested and add_pro_payment request builders and the now-dead SetPaymentRefundRequestedResponse; the AddProPaymentOrGenerateProProofResponse type collapses to GenerateProProofResponse. 2. Rotating seed + renewal owned by libsession. Rotating key is now the deterministic weekly seed via session_protocol_pro_rotating_seed (new sessionProRotatingKeyPair generator), replacing the ad-hoc self.rotatingKeyPair ?? ed25519KeyPair(). Renewal timing polls user_profile_get_pro_renewal_target(now) (<= now ⇒ renew) instead of the buggy autoRenewing-gated needsNewProof logic. 3. Refund state is config-synced. requestRefund records it via user_profile_set_refund_requested after Apple's beginRefundRequest (no backend call); the pending flag reads from user_profile_get_refund_requested. Dropped refund_requested_ts from PaymentItem / GetProStatusResponse and carry it on SessionPro.State (drives refundingStatus). 4. pro_prepaid "purchase in flight" marker set in purchasePro's .success branch (user_profile_set_pro_prepaid) so other devices poll the entitlement through. 5. Client-side prepaid poll (startPrepaidPoll): while pro_prepaid is set and not-yet-Pro, poll with a capped exponential backoff; stops when the marker clears (entitlement landed, already Pro, or libsession's one-week staleness gate). 6. §4 C-API: dropped the removed sent_timestamp_ms arg from session_protocol_encode_for_community_inbox. (iOS never calls session_encrypt_for_group / session_decrypt_group_message, and encode_for_group is unchanged, so nothing else to do.) Adds the refund/prepaid/renewal accessors to LibSessionCacheType (+ no-op defaults + mock). Built green (app + tests) via Session_CompileLibSession. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
libsession's session_protocol_pro_features_for_utf8/_utf16 are gone (as of a4809f7c), replaced by session_protocol_pro_features_for_message(codepoint_count) — it no longer inspects the text; the caller passes the codepoint count. - messageFeatures(for:): pass message.unicodeScalars.count (the Unicode codepoint count for an always-valid Swift String; surrogate pair = 1, matching libsession's simdutf path) instead of round-tripping the UTF-8 bytes through libsession to count. - numberOfCharactersLeft(for:): collapse to characterLimit - content.unicodeScalars.count (no messageFeatures round-trip). - FeatureStatus: drop .utfDecodingError (and its UTF_DECODING_ERROR mappings) — there's no text for libsession to fail decoding on; only .success / .exceedsCharacterLimit remain (unknown defaults to .success). - FeaturesForMessage: drop codePointCount (output field removed from the C struct — it only echoed the input) and the now-unreachable .invalidString. - ConversationViewModel: drop the .utfDecodingError case and its .utf16.count fallback. NOTE: iOS verification uses Session_CompileLibSession (source at a4809f7c), so this is NOT gated on the SPM re-publish (task session-foundation#24) — that only affects the default scheme / CI. Built green (app + tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Session/Conversations/Input View/InputView.swift was tracked in git but had no fileRef in Session.xcodeproj, so it was never compiled — a leftover from promoting the input view into SessionUIKit as a public class. It also referenced LibSession.numberOfCharactersLeft(for:isSessionPro:), which exists nowhere in the tree (only invisible because the file never compiled). The live input view is SessionUIKit/Components/Input View/InputView.swift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n window The client passes raw `now` into session_protocol_pro_rotating_seed and does no window/period arithmetic — but two of my comments characterized the schedule as 'weekly' / 'rotation period'. The rotation schedule is libsession-owned and may change; reword to 'the seed for now, libsession owns the schedule'. Comment-only; no code change. (Mirrors betwixt's android/desktop wording pass.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the single-idle-device renewal gap (valid subscription, proof near/at expiry, nothing was triggering a renewal) using the settled cross-client reconcile contract. Renewal (scheduleNextProofRenewal): deterministic reconcile driven entirely by libsession's pro_renewal_target(now) — no client-side window or jitter. Due (<= now) → refreshProState (re-confirms the backend subscription status, since a lapsed local proof flips inferred status to .expired while the subscription is still active — then regenerates the proof); future → Task.sleep to the target; retry-on-failure keyed on proof validity (dark 15s / covered 60s, no backoff). Loop state is ephemeral (re-derived from config each pass), so suspension / process death recover on the next reconcile; refreshProState's single-flight guard prevents two generate_pro_proof in flight. Triggers: willEnterForeground (the robust one — iOS suspends the process so a Task.sleep across a background period is untrustworthy; the foreground re-run catches anything that lapsed while suspended, before the user can send), plus init and updateWithLatestFromUserConfig (cross-device proof updates). The in-foreground Task.sleep is the secondary path for the app-stays-open case. Send rule 1: attachProInfoIfNeeded now attaches a currently-valid proof or NONE (proProofIsActive guard) — never a known-expired one. Sending is never blocked by a lapsed proof; the message just goes out without Pro metadata. startPrepaidPoll still owns the purchase-in-flight (dark/no-proof) case; it folds into this loop once libsession's renewal_target entitlement/prepaid gate lands (deferred per betwixt). Built green (app + tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…epaid folded, §6 revocation) Folds the iOS renewal loop into the agreed cross-client design-of-record (Rev 2), against libsession 078c611b (gated pro_renewal_target + account_expiry_ts on the proof response). Reconcile loop (§2): reconcileProofRenewal is now the event-driven RECONCILE — reads pro_renewal_target(now) (NONE→dormant / future→one wake / <=now→due), with ephemeral lastRequestAt + darkAttempt spacing. Covered spaces flat 60s; dark uses the linear backoff min(15s×darkAttempt, 900s) (covered resets darkAttempt). Single-flight is best-effort via the spacing check — overlap accepted (§1.9; the §4 monotonic merge no-ops a byte-identical late duplicate). The advisory in-foreground wake re-runs reconcile; the guarantee is the trigger set (willEnterForeground / config-sync / init / purchase / Transaction.updates). No unarmed-exit (the gate returns NONE for non-entitled), no durable state. Renewal action (§1.3): pure generate_pro_proof — refreshProState/get_pro_status is display-only now (auto-renew/grace/refund fields) and no longer mints or clears the proof. Preserves the ensureNetworkConnection wait so a connectivity outage recovers promptly rather than backoff-lagged. ON_COMPLETE (§4), reads live config inside one atomic mutation: success → replace proof iff new.expiry > current.expiry + write E←account_expiry (ungated, H4); subscription_expired / not_subscribed → clear iff no unexpired proof (leaves pro_prepaid, §7.3); revoked → terminal clear, no E; transient / unrecognized → nothing (opaque-value fail-closed). Prepaid poll folded in: removed startPrepaidPoll — a pending purchase surfaces as renewal_target<=now (dark path) via the gate. purchasePro / Transaction.updates now markPurchaseInFlight + reconcile. Send (§6): attach requires unexpired AND unrevoked (currentUserProofIsValid); the revocation-list job clears our own credential when our own proof is revoked (clearOwnCredentialIfRevoked). GenerateProProofResponse gains accountExpiryTimestampSeconds + the typed Outcome (success/subscriptionExpired/notSubscribed/revoked/transient) from header.status + error_code slug. Built green (app + tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mpretty-cyro
approved these changes
Aug 2, 2026
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
Brings the iOS Pro client onto the redemption-reflow model, matching the other platforms.
Payment flow. Redemption is implicit via generate_pro_proof — the old add-payment call
and its response type are replaced by
GenerateProProofResponse— andset_refund_requestedis gone, with the refund-pending flag now living in user config.
Renewal.
SessionProManagergains the same reconcile behaviour as the other clients:libsession owns renewal timing and the deterministic rotating key, the stored proof is
replaced only when coverage extends, the cached account expiry is refreshed from the proof
response, a lapse clears the proof only when no valid one is held, and revocation is
terminal. Self-revocation clears our own credential, and a proof is attached to outgoing
messages only when it is both unexpired and unrevoked.
Version: bumped to 2.16.0 (
MARKETING_VERSION2.15.4 → 2.16.0).Note: needs libsession-util-spm re-published at the merged dev tip and the iOS pin
bumped before it will build in CI.