Skip to content
59 changes: 59 additions & 0 deletions include/session/config/user_profile.h
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,65 @@ LIBSESSION_EXPORT int64_t user_profile_get_pro_access_expiry(const config_object
LIBSESSION_EXPORT void user_profile_set_pro_access_expiry(
config_object* conf, int64_t access_expiry_ts);

/// API: user_profile/user_profile_get_pro_auto_renewing
///
/// Returns whether the account's current Session Pro subscription is auto-renewing. Backend-derived
/// (the `auto_renewing` field on /get_pro_status); set alongside the access expiry.
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
///
/// Outputs:
/// - `int` -- 1 if the subscription is known to be auto-renewing, otherwise 0 (terminal, unknown,
/// or not Pro).
LIBSESSION_EXPORT int user_profile_get_pro_auto_renewing(const config_object* conf);

/// API: user_profile/user_profile_set_pro_auto_renewing
///
/// Records whether the current Session Pro subscription is auto-renewing: nonzero stores the flag,
/// 0 clears it (which is also how it is cleared when the subscription lapses).
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
/// - `auto_renewing` -- [in] nonzero if auto-renewing, 0 to clear
///
/// Outputs:
/// - `void`
LIBSESSION_EXPORT void user_profile_set_pro_auto_renewing(config_object* conf, int auto_renewing);

/// API: user_profile/user_profile_get_pro_grace_period
///
/// Returns the account's grace period in seconds (`get_pro_status.grace_period_duration`), or 0 if
/// none is stored. Backend-derived and synced alongside the access expiry, so any linked device can
/// compute when coverage actually ends: `access_expiry + grace_period`. The access expiry is the
/// payment-due date -- the instant the term was paid through -- and `[E, E + G)` is the window
/// where the payment is overdue but service continues.
///
/// There is deliberately no companion presence check: the backend sends 0 whenever the
/// subscription is not auto-renewing, so "unset" and "zero" describe the same account and both give
/// `expiry - 0 == expiry`.
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
///
/// Outputs:
/// - `int64_t` -- the grace period in seconds, or 0 if unset.
LIBSESSION_EXPORT int64_t user_profile_get_pro_grace_period(const config_object* conf);

/// API: user_profile/user_profile_set_pro_grace_period
///
/// Sets the account's grace period, in seconds. Set alongside `user_profile_set_pro_access_expiry`
/// from each `get_pro_status` response; 0 (or negative) clears it.
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
/// - `grace_seconds` -- [in] the grace period in seconds, or 0 to clear
///
/// Outputs:
/// - `void`
LIBSESSION_EXPORT void user_profile_set_pro_grace_period(
config_object* conf, int64_t grace_seconds);

/// API: user_profile/user_profile_get_refund_requested
///
/// Retrieves the timestamp at which the user requested a refund of their current Session Pro
Expand Down
61 changes: 61 additions & 0 deletions include/session/config/user_profile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ using namespace std::literals;
/// flight"), so all the account's devices poll the backend to pull the entitlement through.
/// Inserted only when not already pro; cleared automatically when entitlement lands; values
/// more than a week in the past are ignored on read.
/// A - set to 1 when the current Session Pro subscription is auto-renewing; omitted when it is
/// terminal (will not renew), unknown, or the account isn't Pro. Backend-derived
/// (get_pro_status.auto_renewing) and synced across devices; the client sets it alongside `E`
/// and clears it (sets false) when the subscription lapses.
/// G - how much longer the account keeps being served past `E`, in seconds
/// (get_pro_status.grace_period_duration): the store's dunning window plus the backend's own
/// renewal-latency allowance. Coverage ends at `E + G`; `[E, E + G)` is the window where the
/// payment is overdue but service continues. Backend-derived and set alongside `E`. Omitted
/// when zero, which is also what the backend sends when the subscription is not auto-renewing
/// -- so an absent `G` and a zero `G` mean the same thing and coverage ends at `E`.
/// P - user profile url after re-uploading (should take precedence over `p` when `T > t`).
/// Q - user profile decryption key (binary) after re-uploading (should take precedence over `q`
/// when `T > t`).
Expand Down Expand Up @@ -339,6 +349,57 @@ class UserProfile : public ConfigBase {
/// will expire, or nullopt to remove the value.
void set_pro_access_expiry(std::optional<sys_seconds> access_expiry_ts);

/// API: user_profile/UserProfile::get_pro_auto_renewing
///
/// Returns whether the account's current Session Pro subscription is auto-renewing (true) or
/// terminal/unknown (false). Backend-derived (the `auto_renewing` field on /get_pro_status);
/// the client sets it alongside `set_pro_access_expiry`. Only a `true` value is stored, so an
/// account that isn't Pro, or whose renewal status has not been learned, reads as false.
///
/// Inputs: None
///
/// Outputs:
/// - `bool` -- true iff the subscription is known to be auto-renewing.
bool get_pro_auto_renewing() const;

/// API: user_profile/UserProfile::set_pro_auto_renewing
///
/// Records whether the current Session Pro subscription is auto-renewing. `true` stores the
/// flag; `false` erases it -- which is also how it is cleared when the subscription lapses.
///
/// Inputs:
/// - `auto_renewing` -- true if the subscription auto-renews; false to clear the flag.
void set_pro_auto_renewing(bool auto_renewing);

/// API: user_profile/UserProfile::get_pro_grace_period
///
/// Returns how much longer the account keeps being served past `E`
/// (`get_pro_status.grace_period_duration`), or zero if none is stored. Backend-derived and
/// synced alongside `E`, so any linked device can compute when coverage actually ends:
/// `get_pro_access_expiry() + get_pro_grace_period()`. `E` itself is the payment-due date --
/// the instant the term was paid through -- and `[E, E + G)` is the window where the payment is
/// overdue but service continues.
///
/// Note this deliberately returns a plain duration rather than an optional: the backend sends
/// zero when the subscription is not auto-renewing, so "no grace stored" and "a grace of zero"
/// describe the same account and both give `E + 0 == E`. There is no state a caller could act
/// on differently, so there is nothing for a presence check to disambiguate.
///
/// Inputs: None
///
/// Outputs:
/// - `std::chrono::seconds` -- the grace period, or `0s` if unset.
std::chrono::seconds get_pro_grace_period() const;

/// API: user_profile/UserProfile::set_pro_grace_period
///
/// Records the account's grace period, in seconds. Set alongside `set_pro_access_expiry` from
/// each `get_pro_status` response; a zero (or negative) value erases the key.
///
/// Inputs:
/// - `grace` -- the grace period; zero or negative clears it.
void set_pro_grace_period(std::chrono::seconds grace);

/// API: user_profile/UserProfile::get_refund_requested
///
/// Retrieves the timestamp at which the user requested a refund of their current Session Pro
Expand Down
40 changes: 36 additions & 4 deletions include/session/pro_backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,29 @@ typedef struct session_pro_backend_response_header {
typedef struct session_pro_backend_pro_proof_response {
session_pro_backend_response_header header;
session_protocol_pro_proof proof;
/// The account's true, grace-inclusive subscription entitlement end (unix seconds), or 0 if
/// this response carries no horizon. Advisory and unsigned (pro-wire-protocol.md §2.2): use for
/// display / refreshing the cached access expiry only -- NOT an entitlement authority and NOT
/// part of the proof signature. Distinct from `proof.expiry_ts` (the clamped <=30d
/// The end of the paid term (unix seconds) -- coverage runs to this plus the grace below -- or
/// 0 if this response carries no horizon. Advisory and unsigned (pro-wire-protocol.md §2.2):
/// use for display / refreshing the cached access expiry only -- NOT an entitlement authority
/// and NOT part of the proof signature. Distinct from `proof.expiry_ts` (the clamped <=30d
/// proof-validity window). Populated on a successful proof and on a `subscription_expired`
/// failure (a now-past value); 0 on `not_subscribed` / `revoked` / protocol errors.
int64_t account_expiry_ts;
/// How much longer (seconds) the account keeps being served past `account_expiry_ts`, so
/// coverage ends at `account_expiry_ts + account_grace_period_duration`. 0 when the
/// subscription is not auto-renewing.
///
/// ⚠️ MEANINGFUL ONLY WHEN `header.status` IS OK. Filled only on the success path, so every
/// non-OK outcome -- protocol error, `stale_request`, transport failure, where the account is
/// untouched -- yields a plain 0 that is indistinguishable from "no grace". Nothing in the
/// struct signals which you have. Read it inside the success branch or not at all.
int64_t account_grace_period_duration;
/// Whether the subscription behind `account_expiry_ts` renews itself.
///
/// ⚠️ MEANINGFUL ONLY WHEN `header.status` IS OK, and this one is the more dangerous of the two:
/// every non-OK outcome yields a plain false, and the config key it feeds is presence-only,
/// where writing false ERASES. Reading it after a failed request destroys a renewing flag
/// learned from `get_pro_status`, on a response that said nothing about the account.
bool account_auto_renewing;
} session_pro_backend_pro_proof_response;

/// API: session_pro_backend/pro_proof_response_free
Expand Down Expand Up @@ -201,7 +217,13 @@ typedef struct session_pro_backend_pro_payment_item {
/// Provider purchase time, fractional UNIX seconds. Millisecond-precise: the value passes
/// through a millisecond-resolution representation, so sub-millisecond digits are not retained.
double purchased_ts;
/// When THIS payment's term was paid through.
int64_t expiry_ts;
/// The dunning window this one payment's provider declared -- raw store data. NOT the same
/// quantity as the account-level `grace_period_duration` on the status response, which adds the
/// backend's renewal-latency allowance and is gated on the account renewing. This one is not
/// gated, so a cancelled subscription can still carry a stale non-zero value. For "when does
/// entitlement end", use the account-level field.
int64_t grace_period_duration;
int64_t platform_refund_expiry_ts;
/// Provider revocation instant, fractional UNIX seconds (millisecond-precise; 0 if not revoked)
Expand All @@ -218,7 +240,17 @@ typedef struct session_pro_backend_get_pro_status_response {
/// NUL-terminated; points into the response's `internal_`.
const char* status;
bool auto_renewing;
/// The account's PAYMENT-DUE date: when the current term was paid through. Does NOT include the
/// grace period -- entitlement runs to `expiry_ts + grace_period_duration`, and `status` is
/// judged against that sum. Do not subtract the grace from this.
int64_t expiry_ts;
/// How much longer (seconds) entitlement continues PAST `expiry_ts`: the provider's dunning
/// window plus the backend's renewal-latency allowance. Add it to `expiry_ts` to get the
/// instant entitlement ends; `[expiry_ts, expiry_ts + this)` is overdue-but-still-served. 0
/// when `auto_renewing` is false, so the sum stays correct there without a special case.
///
/// ACCOUNT-level. `latest_payment.grace_period_duration` shares the name and is a different
/// quantity -- see there.
int64_t grace_period_duration;
/// True if the account has at least one payment, in which case `latest_payment` is populated
/// with the most recent one; false means the account has no payments and `latest_payment` is
Expand Down
Loading