Skip to content

Pro backend 0.2 + libsession 1.7 compatibility updates#710

Open
jagerman wants to merge 17 commits into
session-foundation:devfrom
jagerman:pro-wire-format-updates
Open

Pro backend 0.2 + libsession 1.7 compatibility updates#710
jagerman wants to merge 17 commits into
session-foundation:devfrom
jagerman:pro-wire-format-updates

Conversation

@jagerman

Copy link
Copy Markdown
Member

iOS counterpart of the Pro wire-format + display rework. Same wire contract; provider display becomes data-driven off the slug via a new SNUIKit hook.

This is required for iOS compatibility with session-foundation/session-pro-backend#2 and session-foundation/libsession-util#95.

Changes (see commits for more details)

  • ResponseHeader reshaped to status (ok/fail/ error) + errorCode + single error; consumers updated; already_redeemed special-case removed.
  • Dynamic provider display: Constants.PaymentProvider resolves pro_provider_<slug>_<suffix> via LocalizationHelper (falling back to the existing English so there's no regression before the translations land).
  • {pro_stores} store list: built from session_pro_backend_visible_platforms (own platform first, gated on the store translation) via a new SNUIKit.proVisiblePlatformStores() config hook (implemented by the app and the share extension); substituted into the "purchase via …" messages, replacing the two-store enumeration.
  • error_code → user-facing message: ResponseHeader.userFacingMessage maps error_code to a localized pro_error_<slug> string (falling back to the backend diagnostic).

Dependencies / follow-ups (Mac)

  • Bump the iOS libsession pin once the libsession PR is merged
  • Add SessionMessagingKit/SessionPro/ProErrorMessage.swift to project.pbxproj
    (and remove the deleted AddProPaymentResponseStatus.swift).
  • Get required crowdin translation updates

Comment thread SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift Outdated
Comment thread SessionMessagingKit/SessionPro/SessionProManager.swift Outdated
Comment thread SessionNetworkingKit/SessionPro/Types/BackendUserProStatus.swift
Comment thread SessionNetworkingKit/SessionPro/Types/PaymentProvider.swift Outdated
Comment thread SessionNetworkingKit/SessionPro/Types/PaymentStatus.swift
Comment on lines +22 to +24
static let oneMonthCode: String = "1m"
static let threeMonthsCode: String = "3m"
static let twelveMonthsCode: String = "1y"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ones are more of an issue. These aren't permanently fixed strings, but are rather string-encoded duration values, and I think it's libsession's job to interpret them rather than passing them through raw like this.

I'm looking into that change now.

Comment thread SessionNetworkingKit/SessionPro/Types/ProProof.swift Outdated
jagerman and others added 9 commits July 21, 2026 21:38
…d1e6)

Adapt the in-repo Swift C-API glue + Pro consumers to the redesigned
libsession pro-backend / proof C API. Not yet compiled (iOS builds need
macOS/Xcode; also requires libsession re-pinned to the 73e4d1e6-equivalent,
e.g. via the Session_CompileLibSession scheme against a local checkout).

Pro-backend API layer (SessionNetworkingKit/SessionPro):
- Provider & plan are now opaque wire slugs (PaymentProvider/Plan gain
  .other(String); drop the removed C enums). PaymentStatus REFUNDED->REVOKED.
- Requests drop the `version` field; timestamps ms->seconds (`ts`,
  `refund_requested_ts`); revocations `ticket` widened to Int64.
- add-payment/refund send provider_code + one opaque payment_id
  (UserTransaction: providerCode+paymentId; drop typed google/apple fields
  and order_id).
- Responses adapt to reshaped C structs: pro_proof_response_parse (covers
  add+generate), payment item purchased_ts/revoked_ts (fractional-second
  doubles) + string plan/provider + opaque payment_id (appleTransactionId is
  now a computed accessor over payment_id for App Store), revocations
  retry_in/retain_for + revocation_tag/effective_ts, refund response drops
  `version`.
- Provider URLs from session_pro_backend_get_provider_urls(code); provider
  display NAMES hardcoded English placeholders in Constants+LibSession
  (TODO: Crowdin) since libsession stopped shipping the metadata table.

Proof / revocation / config format (forced by the same re-pin):
- session_protocol_pro_proof: gen_index_hash->revocation_tag, expiry ms->sec
  at the C boundary (ProProof keeps its ms Swift API; conversion is boundary-
  only and round-trips losslessly).
- Protobuf proof expiry now travels as whole seconds (Message/VisibleMessage).
- convo_info_volatile config: pro_gen_index_hash->pro_revocation_tag,
  pro_expiry_unix_ts_ms->pro_expiry_ts (seconds).

ClientPlatform(provider) made total over the new cases (Google Play->android,
everything else incl. nil/rangeproof/unknown->iOS, with the nil-origin
limitation documented for the display phase).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up from a symbol sweep of the re-pinned libsession C API:

- user_profile pro access expiry: the `_ms` convenience variants were removed;
  call user_profile_get/set_pro_access_expiry (whole seconds) and convert at
  the boundary (LibSession+UserProfile).
- session_protocol_pro_proof_status / _is_active now take a whole-second `ts`
  (compared against the proof's second-precision expiry); pass seconds
  (SessionProManager).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…URL/PUBKEY)

Since 73e4d1e6, get-details status/user_status became opaque string codes (the
SESSION_PRO_BACKEND_PAYMENT_STATUS / _USER_PRO_STATUS enums were deleted), and
libsession now owns the backend URL + signing pubkey.

- BackendUserProStatus / PaymentStatus: reworked from C-enum bridges to string
  codes. user-status fails closed (unknown -> .neverBeenPro, never grants Pro);
  payment-status (informational only) passes unknowns through via .other.
- Parse the now-`char[64]` status fields from the get-details response +
  payment item (added CAccessible for the response struct).
- SessionPro.swift: read the backend base URL from SESSION_PRO_BACKEND_URL and
  the Ed pubkey from SESSION_PRO_BACKEND_PUBKEY (32 bytes -> hex) instead of the
  stale hard-coded pro-backend-dev / fc9477… copies; X25519 still derived on the
  fly from the Ed key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ble/JSON round-trip)

Previously each pro response was a Decodable type, so the networking layer
JSON-decoded the raw bytes (JSONDecoder), the init then fell through to the
AnyCodable branch and re-encoded back to JSON, and only then handed the bytes to
libsession's C parser: raw -> JSONDecoder -> AnyCodable -> JSONEncoder -> libsession.
That baked a "the wire is JSON" assumption into the client.

Now the pro API fetches the response as raw `Data` (responseType: Data.self — the
same first-class path used for attachment/display-picture downloads) and `.map`s it
through a plain `init(parsing: Data)` that runs libsession's C parser directly. No
Codable, no AnyCodable, no round-trip — the wire format is a libsession<->backend
contract the client never inspects. Callers still receive the typed response structs.

(The request-body build still round-trips libsession's JSON through AnyCodable; that's
the mirror-image cleanup, left for a follow-up.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
libsession's C request API is now a thin shim over the C++ `*_request()`: each
`session_pro_backend_<x>_request_build(...)` returns an owning
`session_pro_backend_request { endpoint, content_type, string8 data, … }`. So the
client stops re-implementing the request in Swift.

- New `ProRequest` wraps the shim: calls the builder, reads endpoint/content_type/
  raw body out of the owned C struct, frees it. One call per request.
- `SessionProAPI`: each function now just builds via the shim and POSTs — routing on
  `proRequest.endpoint` (libsession owns the route; no client endpoint copies), setting
  `Content-Type` from `proRequest.contentType` (no hardcoded format), and sending the
  raw `body` bytes. Response side unchanged (already parses raw Data via libsession).
- `Endpoint` is now a thin string-carrier (drops the hardcoded path enum).
- Deleted the five request structs, `Signatures`/`Signature`, and `UserTransaction`
  (signing + serialisation + the JSON round-trip all live in libsession now).

This closes the request-side "client never builds/knows the wire format" gap: raw body,
libsession-supplied content-type, libsession-supplied endpoint — all in one shim call.

NOTE (Mac/Xcode): pin the in-repo libsession to 418cdaba, and update the Xcode project —
remove the 8 deleted files' references and add ProRequest.swift (project.pbxproj isn't
edited here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ResponseHeader is now a closed status enum (.ok/.fail/.error) + optional
errorCode slug + optional diagnostic error, built from the reshaped C
session_pro_backend_response_header; isSuccess == .ok. Drops the errors[] array
and the already-redeemed special-case (needsRefreshProProof): an ok add-payment
always carries a proof now, so SessionProManager always updates the config on
success and derives failure text from error/errorCode. Delete the obsolete
AddProPaymentResponseStatus enum.

NOTE (Mac): remove AddProPaymentResponseStatus.swift from project.pbxproj and
bump the libsession build to a9e790a8 (Delta session-foundation#12); Delta session-foundation#13 (67087e93) needs no
client change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e messages

Resolve provider display from pro_provider_<slug>_<suffix> (LocalizationHelper, fallback to English);
build the {pro_stores} purchasable-store list from session_pro_backend_visible_platforms (app_store
first, gated on the store translation) via a new SNUIKit proVisiblePlatformStores() hook; map error_code
slugs to localized pro_error_<slug> strings (ResponseHeader.userFacingMessage).

NB: ProErrorMessage.swift still needs adding to project.pbxproj; iOS libsession pin must include
visible_platforms (c6e5a210).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The request-building collapse onto libsession's one-call C shim (f7d595e)
deleted 9 per-request/type source files but left their PBXFileReference /
PBXBuildFile / sources-phase entries in the SessionNetworkingKit target,
so the branch failed at input-file validation before any Swift compiled.
Also registers two on-disk files that were never added to a target.

Removed (dead refs, SessionNetworkingKit):
  Requests/{AppProPaymentRequest,GenerateProProofRequest,GetProDetailsRequest,
            GetProRevocationsRequest,SetPaymentRefundRequestedRequest}.swift
  Types/{AddProPaymentResponseStatus,Signature,Signatures,UserTransaction}.swift

Added:
  SessionNetworkingKit/SessionPro/Types/ProRequest.swift -> SessionNetworkingKit
  SessionMessagingKit/SessionPro/ProErrorMessage.swift   -> SessionMessagingKit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (generate-proof, refund, revocations)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jagerman
jagerman force-pushed the pro-wire-format-updates branch from f28e59c to b1a2f53 Compare July 22, 2026 00:39
jagerman and others added 8 commits July 21, 2026 22:02
Three fixes to build the Pro wire-format branch from source against
libsession pro-backend-updates (c6e5a210):

- CAccessible: add CChar64 typealias + get/set overloads. libsession's
  pro_payment_item now carries `status`/`plan`/`payment_provider` as
  opaque char[64] strings (with `_count`); the string-buffer helper only
  covered 8/65/67/101/128/224/254/268, so char[64] had no `get`.
- SessionPro+Convenience: handle the opaque `Plan.other(_)` case in the
  plan-title/subtitle switches (they were non-exhaustive). Unrecognised
  period codes fall back to the generic per-month framing rather than
  breaking — locale-aware formatting of arbitrary codes is deferred
  display work.
- Crypto+LibSession: session_protocol_decode_for_community's `ts` param
  is now int64_t; pass Int64 (a posted timestamp is always positive).

Session_CompileLibSession scheme now builds GREEN (Swift compiles + links).
Remaining: 5 non-fatal LintLocalizableStrings findings (1 dynamic-key
false positive in ProErrorMessage, 4 real {pro_stores} vs
{platform_store} token mismatches in the string catalog) — reported to
betwixt for the coordinated Crowdin/string work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aligns the iOS Swift domain with libsession's `revocation_tag` and with
android/desktop naming. Pro has never shipped, so no data migration — the
DB column + Codable keys rename straight across.

- Domain field: `ProProof.genIndexHash` -> `revocationTag`.
- DB column / Codable keys / state carriers: `proGenIndexHashHex` ->
  `proRevocationTagHex`, `genIndexHashHex` -> `revocationTagHex`
  (migration _048, Profile, LibSession config handling, Profile+Updating,
  ConversationDataHelper, ObservableKey, SessionProManager, settings VM).

LEFT UNCHANGED (per betwixt): the shared cross-platform protobuf field
`SessionProtos.proto: genIndexHash = 2` and its generated accessors
(SNProto/SessionProtos.pb.swift). At the two proto boundaries the domain
`revocationTag` maps to/from the proto's `.genIndexHash`/`setGenIndexHash`
(Message.swift, VisibleMessage.swift). `revocation_tag` C keypaths were
already correct.

Session_CompileLibSession builds GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oto revocationTag

Task 05 (Jason's three decisions):

1. Proto field genIndexHash -> revocationTag (iOS's hand-synced .proto +
   regenerated-equivalent edits to SNProto.swift/SessionProtos.pb.swift;
   field number 2 unchanged so wire/serialization is identical). Boundary
   call sites use the new setRevocationTag/.revocationTag accessors. iOS is
   now fully clean of genIndexHash (only the wire field name changed).

2. BackendUserProStatus is now opaque: added `.unknown(String)` carrying the
   raw wire value for display/telemetry. CRITICAL: every gating/display switch
   treats `.unknown` exactly like `.neverBeenPro` (fail closed — an unknown
   status NEVER grants Pro). Central gate `currentUserIsCurrentlyPro` is
   `status == .active` (fail-closed by construction). Kept CaseIterable/
   MockableFeature by supplying a manual `allCases` (the 3 known cases; the
   free-form `.unknown` is a real-backend value, not a dev-picker option).

3. PaymentProvider slugs now derive from libsession's
   SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* C constants (imported as CChar
   tuples, read via a small cString helper) so libsession is the single source
   of truth. Dropped the "keep in sync" literal comment.

(status/plan have no libsession constants to derive from — kept opaque +
unknown-safe as-is.) Session_CompileLibSession builds GREEN against c6e5a210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…trip)

The ProProof expiry travels on the wire (proto ProProof.expiryUnixTs) and in
libsession (session_protocol_pro_proof.expiry_ts) as whole unix SECONDS, and
iOS's own clock is seconds (Date.timeIntervalSince1970). The Swift domain was
needlessly carrying it as milliseconds, doing seconds->ms->seconds at every
boundary — exactly where a forgotten conversion becomes a 1000x proof-expiry
bug. Make the proof-expiry domain seconds-native:

- Rename ProProof.expiryUnixTimestampMs -> expiryUnixTimestampSeconds and the
  carriers/DB column that hold the PROOF expiry (proExpiryUnixTimestampMs ->
  proExpiryUnixTimestampSeconds; ProProofMetadata, Profile.ProState,
  ObservableKey). No migration (Pro never shipped) — column semantics change
  from ms to seconds; all readers/writers agree on seconds.
- Boundaries are now identity (no *1000 / /1000):
  - proto: Message.setExpiryUnixTs(...Seconds) / VisibleMessage reads expiryUnixTs raw
  - libsession: ProProof.expiry_ts <-> ...Seconds direct; convo pro_expiry_ts direct
  - clock: SessionProManager entitlement check compares timeIntervalSince1970 to
    the raw seconds value.
- refreshProProofIfNeeded now computes in seconds; the still-ms account
  access-expiry (accessExpiryTimestampMs) + network clock are bridged to seconds
  locally (their seconds migration is a deliberate follow-up — it cascades into
  the GetProDetailsResponse/PaymentItem ms domain, bigger blast radius).
- Fixed the stale "milliseconds" proto comment on expiryUnixTs (whole seconds).

Wire is unchanged (still seconds; field number 2/4 unchanged).
Session_CompileLibSession builds GREEN against c6e5a210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in-place)

Session Pro ships behind a feature flag, so migration _048_SessionProChanges
already ran on live databases — the earlier commits renamed its `profile`
columns IN PLACE, which silently broke every already-migrated DB (the app hung
at splash: `column not found: "proExpiryUnixTimestampSeconds"` because the live
schema still had `proExpiryUnixTimestampMs`/`proGenIndexHashHex`).

Fix:
- Restore _048 to its released form (proExpiryUnixTimestampMs, proGenIndexHashHex).
- Add _052_RenameProColumnsForWireFormat: ALTER TABLE profile RENAME COLUMN
  proGenIndexHashHex -> proRevocationTagHex and proExpiryUnixTimestampMs ->
  proExpiryUnixTimestampSeconds, then convert existing ms values to whole
  seconds (/1000). Fresh installs (_048 then _052) and upgraded installs
  converge on the same schema.
- Register _052 in SNMessagingKit.migrations + pbxproj (SessionMessagingKit).

Verified: existing simulator DB upgrades cleanly, app launches past splash with
account data intact. Session_CompileLibSession builds GREEN against c6e5a210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the seconds-native pass (task07 did the message-proof expiry). All
Pro timestamps/durations are now whole unix seconds (matching libsession, the
wire, and iOS's clock), represented as integer UInt64 to stay consistent with
the app's integer-timestamp convention (not Date/TimeInterval). Eliminates the
seconds->ms->seconds round-trips at the libsession/response boundaries.

- PaymentItem: expiry/redeemed/platformRefundExpiry/refundRequested +
  gracePeriodDuration -> `...Seconds` (UInt64), assigned directly from the
  int64 libsession values. purchased/revoked are `double` seconds carrying
  only millisecond precision (sys_ms-backed), so they're kept as integer
  MILLISECONDS (`...TimestampMs`, ×1000+truncate) to retain that precision —
  the one deliberate ms exception, clearly labelled.
- RevocationItem.effective, GetProDetailsResponse (expiry/gracePeriod/
  nextAutoRenewing), SessionPro.State (accessExpiry/nextAutoRenewing/
  displayTimestamp) -> `...Seconds`.
- libSession boundaries are now identity: user_profile pro_access_expiry
  read/write drop ×1000//1000; refund-request hands over whole seconds.
- Removed the temporary ms bridge in refreshProProofIfNeeded (both sides are
  seconds now); the only remaining /1000s are legit conversions of the app's
  millisecond network clock to seconds for comparison/signing.
- Fixed the dev-mock: mockCurrentUserAccessExpiryTimestamp (whole seconds) now
  lands in a seconds field (was assigned into an ms field -> Jan 1970).

Wire stays whole seconds. Session_CompileLibSession builds GREEN vs c6e5a210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x compute)

The app hung on the Session splash on every launch. Root cause: computing a
`MockableFeature<BuildVariant>`'s integer raw index eventually resolved a
*localized* string, and that ran inside feature-store init while the SNUIKit
config lock was held — deadlocking a cluster of threads around that lock and the
`Constants.buildVariants`/`PaymentProvider.appStore` static-init dispatch_once.

Chain: `MockableFeatureValue.rawValue` matches `self` to `allCases` via
`String(reflecting:)`, which for `BuildVariant` invoked its `CustomStringConvertible`
`description` -> `buildVariantStringProvider()` -> `Constants.buildVariants` ->
`PaymentProvider.appStore` -> `providerString` -> `LocalizationHelper.localized()`
-> `SNUIKit.shouldShowStringKeys()` (configLock).

Fix: give `BuildVariant` a plain, non-localised `debugDescription`, which
`String(reflecting:)` prefers over `description` — so raw-index bookkeeping no
longer triggers localization. `rawValue` keeps using `String(reflecting:)`
(NOT `==`: some conformers, e.g. `SessionPro.LoadingState`, implement `==` via
`rawValue`, so matching by `==` there recurses infinitely — added a comment
documenting both constraints).

Pre-existing branch bug (betwixt's dynamic-provider-display work), exposed once
the branch could actually run. Verified: the app now launches past the splash to
the home screen. Session_CompileLibSession builds GREEN vs c6e5a210.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants