Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions library/src/main/cpp/pro_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ JavaLocalRef<jobject> serialize_revocation_item(JNIEnv* env, const pb::ProRevoca
jobject serialize_pro_proof_response(JNIEnv* env, const pb::GenerateProProofResponse& resp) {
static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProProofResponse",
"(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;"
"Lnetwork/loki/messenger/libsession_util/pro/ProProof;J)V");
"Lnetwork/loki/messenger/libsession_util/pro/ProProof;JJZ)V");
auto header = serialize_response_header(env, resp);
JavaLocalRef<jobject> proof(env, nullptr);
if (resp) // ResponseBase::operator bool: true iff status == Ok (proof populated on success)
Expand All @@ -114,8 +114,16 @@ jobject serialize_pro_proof_response(JNIEnv* env, const pb::GenerateProProofResp
resp.account_expiry
? static_cast<jlong>(resp.account_expiry->time_since_epoch().count())
: 0;
// Both advisory fields are REQUIRED on a successful proof (json_require in core), so there is no
// absent case and they cross as plain values. Zero/false on the failure outcomes that carry no
// proof, which is the truthful value there. Deliberately not optional: a defaulted false is not
// inert, because writing false to the presence-only config key ERASES it β€” so a malformed
// response now fails the parse and the client keeps what it has, rather than persisting a default.
jlong grace_s = static_cast<jlong>(resp.account_grace_period.count());
jboolean auto_renewing = resp.account_auto_renewing;
return env->NewObject(
cls.java_class, cls.constructor, header.get(), proof.get(), account_expiry_s);
cls.java_class, cls.constructor, header.get(), proof.get(), account_expiry_s,
grace_s, auto_renewing);
}

jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& resp) {
Expand Down
34 changes: 34 additions & 0 deletions library/src/main/cpp/user_profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,40 @@ Java_network_loki_messenger_libsession_1util_UserProfile_removeProAccessExpiry(J
ptrToProfile(env, thiz)->set_pro_access_expiry(std::nullopt);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_getProAutoRenewing(JNIEnv *env,
jobject thiz) {
return static_cast<jboolean>(ptrToProfile(env, thiz)->get_pro_auto_renewing());
}

extern "C"
JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_setProAutoRenewing(JNIEnv *env,
jobject thiz,
jboolean auto_renewing) {
// Presence-only upstream: set_pro_auto_renewing uses set_nonzero_int, so writing false ERASES
// the key rather than storing it. Absent therefore means "terminal/unknown", and a caller
// cannot distinguish it from an explicit false through this binding β€” which is the whole of
// libsession PR #121's tri-state limitation, not something introduced here.
ptrToProfile(env, thiz)->set_pro_auto_renewing(auto_renewing);
}

extern "C"
JNIEXPORT jlong JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_getProGracePeriodSeconds(JNIEnv *env,
jobject thiz) {
return static_cast<jlong>(ptrToProfile(env, thiz)->get_pro_grace_period().count());
}

extern "C"
JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_setProGracePeriodSeconds(JNIEnv *env,
jobject thiz,
jlong grace_seconds) {
ptrToProfile(env, thiz)->set_pro_grace_period(std::chrono::seconds{grace_seconds});
}

extern "C"
JNIEXPORT jlong JNICALL
Java_network_loki_messenger_libsession_1util_UserProfile_getProFeaturesRaw(JNIEnv *env,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package network.loki.messenger.libsession_util

import network.loki.messenger.libsession_util.pro.ProConfig
import java.time.Duration
import network.loki.messenger.libsession_util.protocol.ProProfileFeatures
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.BlindedContact
Expand Down Expand Up @@ -87,6 +88,28 @@ interface ReadableUserProfile: ReadableConfig {
fun getProConfig(): ProConfig?
fun getProAccessExpiry(): Long?

/**
* Whether the subscription is auto-renewing, from synced config (key `A`, libsession #121).
*
* **Presence-only.** `setProAutoRenewing(false)` ERASES the key, so `false` here means either
* "not auto-renewing" or "never written" β€” absent reads as terminal/unknown. Callers that need
* to tell those apart cannot, through this API.
*/
fun getProAutoRenewing(): Boolean

/**
* The account's grace period, from synced config (key `G`).
*
* How much longer the account is served PAST [getProAccessExpiry], so coverage ends at
* `proAccessExpiry + proGracePeriod`. The expiry is the payment-due date and needs no adjustment
* to be displayed β€” do not subtract this from it.
*
* Zero when unset, and zero is also what the backend sends when the subscription is not
* auto-renewing β€” the two describe the same account and both give `E + 0 == E`, so there is
* nothing for a presence check to disambiguate.
*/
fun getProGracePeriod(): Duration

/** When a refund was requested (unix seconds), or null if none (values >1 week old read as null). */
fun getRefundRequested(): Long?

Expand Down Expand Up @@ -123,6 +146,12 @@ interface MutableUserProfile : ReadableUserProfile, MutableConfig {
fun setProAccessExpiry(epochSeconds: Long)
fun removeProAccessExpiry()

/** See [getProAutoRenewing] β€” writing `false` erases the key rather than storing it. */
fun setProAutoRenewing(autoRenewing: Boolean)

/** See [getProGracePeriod]. Write it from the SAME response that supplied the access expiry. */
fun setProGracePeriod(grace: Duration)

/** Record (epochSeconds) or clear (null) the "refund requested" flag; synced across devices. */
fun setRefundRequested(epochSeconds: Long?)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package network.loki.messenger.libsession_util

import java.time.Duration

import network.loki.messenger.libsession_util.pro.ProConfig
import network.loki.messenger.libsession_util.pro.ProProof
import network.loki.messenger.libsession_util.protocol.ProProfileFeatures
Expand Down Expand Up @@ -47,6 +49,12 @@ class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), Muta
external override fun setAnimatedAvatar(animatedAvatar: Boolean)
external override fun setProAccessExpiry(epochSeconds: Long)
external override fun removeProAccessExpiry()
external override fun getProAutoRenewing(): Boolean
external override fun setProAutoRenewing(autoRenewing: Boolean)
private external fun getProGracePeriodSeconds(): Long
override fun getProGracePeriod(): Duration = Duration.ofSeconds(getProGracePeriodSeconds())
private external fun setProGracePeriodSeconds(seconds: Long)
override fun setProGracePeriod(grace: Duration) = setProGracePeriodSeconds(grace.seconds)
private external fun getProFeaturesRaw(): Long
override fun getProFeatures(): ProProfileFeatures = ProProfileFeatures(getProFeaturesRaw())
external override fun getProConfig(): ProConfig?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,62 @@ data class ProProofResponse(
override val header: ProResponseHeader,
val proof: ProProof?,
/**
* Advisory account (subscription) expiry β€” grace-inclusive true entitlement end; set on a
* successful proof and on a `subscription_expired` failure (a past value), null otherwise.
* Advisory account (subscription) expiry β€” the payment-due date, with coverage running
* [accountGracePeriod] past it; set on a successful proof and on a `subscription_expired` failure
* (a past value), null otherwise.
* Distinct from [proof]'s own clamped expiry; use for the "Pro until X" display and to refresh
* the cached access expiry, never for entitlement gating.
*/
val accountExpiry: Instant?,
/**
* How much longer the account is served past [accountExpiry], so coverage ends at
* `accountExpiry + accountGracePeriod`. Do NOT subtract it from the expiry.
*
* ⚠️ **Only meaningful on a SUCCESSFUL proof.** Core's parser returns on the failure path before
* filling this, so on every non-OK outcome it holds a struct default of zero β€” and nothing here
* can tell you that: there is no presence flag and the type is not nullable. **Read it inside a
* success branch or not at all.**
*
* Not nullable because core no longer models the absent case, so a nullable type would be a lie
* that invites `?: Duration.ZERO` β€” and zero is not inert, since writing it can clear the config
* key. The protection moved from the type to the call site's placement; it did not disappear.
*/
val accountGracePeriod: Duration,
/**
* Whether the subscription behind [accountExpiry] renews itself.
*
* ⚠️ **Only meaningful on a SUCCESSFUL proof** β€” same as [accountGracePeriod], and worse here,
* because the default is `false` and writing `false` into the presence-only config key **ERASES**
* it. On `subscription_expired`/`not_subscribed`/`revoked` that erasure is truthful; on a protocol
* error or transport failure it would wipe a flag `get_pro_status` had correctly learned, from a
* response that said nothing about the account.
*
* Not nullable because core no longer models absence β€” but absence did not go away, it collapsed
* into an indistinguishable value. **Read it inside a success branch or not at all.**
*/
val accountAutoRenewing: Boolean,
) : ProResponse {
/** Raw-epoch constructor used by the JNI layer (see the file header). */
/**
* Raw-epoch constructor used by the JNI layer (see the file header).
*
* [accountExpirySeconds] keeps the 0-means-absent sentinel because that field genuinely is absent
* on some outcomes; the other two are always populated on success, so they cross as plain values.
* That asymmetry is real rather than an oversight β€” see each property.
*/
@Keep
constructor(
header: ProResponseHeader,
proof: ProProof?,
accountExpirySeconds: Long,
) : this(header, proof, accountExpirySeconds.secondsToInstantOrNull())
accountGracePeriodSeconds: Long,
accountAutoRenewing: Boolean,
) : this(
header = header,
proof = proof,
accountExpiry = accountExpirySeconds.secondsToInstantOrNull(),
accountGracePeriod = Duration.ofSeconds(accountGracePeriodSeconds),
accountAutoRenewing = accountAutoRenewing,
)
}

/** One payment/subscription record from get-pro-status. */
Expand All @@ -138,7 +180,10 @@ data class ProPaymentItem(
@Serializable(with = InstantAsEpochMillisSerializer::class)
val expiry: Instant?, // access expiry for this payment; null if not activated
@Serializable(with = DurationAsSecondsSerializer::class)
val gracePeriod: Duration, // grace beyond [expiry] before access is really lost
// PAYMENT-level grace: what the store declared about THIS transaction. NOT the same quantity as
// GetProStatusResponse.gracePeriod, and notably not gated on auto-renewing β€” a cancelled
// subscriber can keep a multi-day value here. For coverage questions use the account-level field.
val gracePeriod: Duration,
@Serializable(with = InstantAsEpochMillisSerializer::class)
val platformRefundExpiry: Instant?, // deadline for a platform ("quick") refund; null if n/a
@Serializable(with = InstantAsEpochMillisSerializer::class)
Expand Down Expand Up @@ -187,9 +232,12 @@ data class GetProStatusResponse(
val latestPayment: ProPaymentItem?, // the single most-recent payment, or null when none
val autoRenewing: Boolean,
@Serializable(with = InstantAsEpochMillisSerializer::class)
val expiry: Instant?, // account access expiry (incl. grace); null if never subscribed
val expiry: Instant?, // the payment-due date; null if never subscribed
// ACCOUNT-level grace: how much longer we are served past [expiry], so coverage ends at
// `expiry + gracePeriod`. This is the field a client wants for coverage questions β€” see the
// warning on ProPaymentItem.gracePeriod, which shares the name and answers a different question.
@Serializable(with = DurationAsSecondsSerializer::class)
val gracePeriod: Duration, // grace included in [expiry]
val gracePeriod: Duration,
) : ProResponse {
/** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */
@Keep
Expand Down
Loading