From b2f143039fdd52a569aa164f08f8c6861f98bc7c Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Fri, 17 Jul 2026 22:46:23 -0300 Subject: [PATCH 01/14] Pro wire-format update: revocation_tag, seconds, provider slugs, payment_id Adapt the Android glue to libsession-util pro-backend-updates (71d32b8a): - gen_index_hash -> revocation_tag (ProProof, Conversation.ProProofInfo, JNI, and protobuf regen: ProProof.genIndexHash -> revocationTag). - Pro timestamps milliseconds -> integer seconds (proof expiry, convo pro-expiry, generate-proof / get-details request ts, user-profile pro-access-expiry); dropped the misleading _ms name suffixes. - payment_provider / plan: integer enums -> opaque wire code strings. Provider slug constants (google_play / app_store / rangeproof). PaymentProviderMetadata table + getPaymentProviderMetadata removed (the apps now own store names / URLs). - Per-provider payment fields collapsed to a single opaque payment_id (add-payment drops order_id; provider passed as a code string). - decode_for_community now takes seconds (proof-validity check time). Bumps the libsession-util submodule to the pro-backend-updates tip. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitmodules | 2 +- .../protocol/BackendRequestsTest.kt | 17 ------ library/src/main/cpp/conversation.cpp | 28 ++++----- library/src/main/cpp/pro_backend.cpp | 59 +++++-------------- library/src/main/cpp/pro_proof_util.cpp | 20 +++---- library/src/main/cpp/protocol.cpp | 5 +- library/src/main/cpp/user_profile.cpp | 14 ++--- .../loki/messenger/libsession_util/Config.kt | 4 +- .../messenger/libsession_util/UserProfile.kt | 6 +- .../libsession_util/pro/BackendRequests.kt | 19 +++--- .../messenger/libsession_util/pro/ProProof.kt | 18 +++--- .../protocol/PaymentProviderMetadata.kt | 15 ----- .../libsession_util/util/Conversation.kt | 16 ++--- libsession-util | 2 +- 14 files changed, 81 insertions(+), 144 deletions(-) delete mode 100644 library/src/androidTest/kotlin/network/loki/messenger/libsession_util/protocol/BackendRequestsTest.kt delete mode 100644 library/src/main/java/network/loki/messenger/libsession_util/protocol/PaymentProviderMetadata.kt diff --git a/.gitmodules b/.gitmodules index d6ca3b1..6df0cbe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libsession-util"] path = libsession-util - url = https://github.com/session-foundation/libsession-util.git + url = https://github.com/jagerman/libsession-util.git diff --git a/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/protocol/BackendRequestsTest.kt b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/protocol/BackendRequestsTest.kt deleted file mode 100644 index 03233ca..0000000 --- a/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/protocol/BackendRequestsTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package network.loki.messenger.libsession_util.protocol - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import network.loki.messenger.libsession_util.pro.BackendRequests -import org.junit.Assert.assertNotNull -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class BackendRequestsTest { - - @Test - fun getProviderMetadataWorks() { - val metadata = BackendRequests.getPaymentProviderMetadata(BackendRequests.PAYMENT_PROVIDER_GOOGLE_PLAY) - assertNotNull(metadata) - } -} \ No newline at end of file diff --git a/library/src/main/cpp/conversation.cpp b/library/src/main/cpp/conversation.cpp index 29f464b..1ed3ce3 100644 --- a/library/src/main/cpp/conversation.cpp +++ b/library/src/main/cpp/conversation.cpp @@ -13,9 +13,9 @@ static auto ptrToConvoInfo(JNIEnv *env, jobject obj) { } JavaLocalRef serialize_pro_proof_info(JNIEnv *env, - std::optional> gen_index_hash, - const std::chrono::sys_time & expiry) { - if (!gen_index_hash) { + std::optional> revocation_tag, + const std::chrono::sys_seconds & expiry) { + if (!revocation_tag) { return {env, nullptr}; } @@ -28,7 +28,7 @@ JavaLocalRef serialize_pro_proof_info(JNIEnv *env, return {env, env->NewObject( class_info.java_class, class_info.constructor, - util::bytes_from_span(env, *gen_index_hash).get(), + util::bytes_from_span(env, *revocation_tag).get(), static_cast(expiry.time_since_epoch().count()) )}; } @@ -45,7 +45,7 @@ JavaLocalRef serialize_one_to_one(JNIEnv *env, const session::config::c JavaLocalRef(env, env->NewStringUTF(one_to_one.session_id.data())).get(), (jlong) one_to_one.last_read, (jboolean) one_to_one.unread, - serialize_pro_proof_info(env, one_to_one.pro_gen_index_hash, + serialize_pro_proof_info(env, one_to_one.pro_revocation_tag, one_to_one.pro_expiry_unix_ts).get())}; } @@ -65,12 +65,12 @@ struct WithProProofInfoClassInfo : public JavaClassInfo { }; struct ProProofInfoClassInfo : public JavaClassInfo { - jmethodID genIndexHash_getter; + jmethodID revocationTag_getter; jmethodID expiryUnixTs_getter; ProProofInfoClassInfo(JNIEnv *env, jobject obj) : JavaClassInfo(env, obj) - , genIndexHash_getter(env->GetMethodID(java_class, "getGenIndexHashBytes", "()[B")) + , revocationTag_getter(env->GetMethodID(java_class, "getRevocationTagBytes", "()[B")) , expiryUnixTs_getter(env->GetMethodID(java_class, "getExpiryUnixTs", "()J")) {} @@ -79,7 +79,7 @@ struct ProProofInfoClassInfo : public JavaClassInfo { return instance; } - static void read_gen_index_hash(std::optional &out, JNIEnv *env, jobject obj) { + static void read_revocation_tag(std::optional &out, JNIEnv *env, jobject obj) { if (!obj) { out = std::nullopt; return; @@ -89,7 +89,7 @@ struct ProProofInfoClassInfo : public JavaClassInfo { env, (jbyteArray) env->CallObjectMethod( obj, - get(env, obj).genIndexHash_getter)); + get(env, obj).revocationTag_getter)); JavaByteArrayRef bytes_ref(env, hash_bytes.get()); @@ -98,11 +98,11 @@ struct ProProofInfoClassInfo : public JavaClassInfo { std::min(env->GetArrayLength(hash_bytes.get()), 32), out->begin()); } - static std::chrono::sys_time read_pro_expiry(JNIEnv *env, jobject obj) { + static std::chrono::sys_seconds read_pro_expiry(JNIEnv *env, jobject obj) { if (!obj) return {}; jlong expiry_ts = env->CallLongMethod(obj, get(env, obj).expiryUnixTs_getter); - return std::chrono::sys_time{std::chrono::milliseconds{expiry_ts}}; + return std::chrono::sys_seconds{std::chrono::seconds{expiry_ts}}; } }; @@ -135,7 +135,7 @@ session::config::convo::one_to_one deserialize_one_to_one(JNIEnv *env, jobject i JavaLocalRef pro_proof(env, env->CallObjectMethod( info, WithProProofInfoClassInfo::get(env).proProofInfo_getter)); - ProProofInfoClassInfo::read_gen_index_hash(r.pro_gen_index_hash, env, pro_proof.get()); + ProProofInfoClassInfo::read_revocation_tag(r.pro_revocation_tag, env, pro_proof.get()); r.pro_expiry_unix_ts = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); return r; @@ -274,7 +274,7 @@ JavaLocalRef serialize_blinded_one_to_one(JNIEnv *env, const session::c JavaLocalRef(env, env->NewStringUTF(blinded_one_to_one.blinded_session_id.data())).get(), (jlong) blinded_one_to_one.last_read, (jboolean) blinded_one_to_one.unread, - serialize_pro_proof_info(env, blinded_one_to_one.pro_gen_index_hash, blinded_one_to_one.pro_expiry_unix_ts).get() + serialize_pro_proof_info(env, blinded_one_to_one.pro_revocation_tag, blinded_one_to_one.pro_expiry_unix_ts).get() )}; } @@ -302,7 +302,7 @@ session::config::convo::blinded_one_to_one deserialize_blinded_one_to_one(JNIEnv JavaLocalRef pro_proof(env, env->CallObjectMethod( info, WithProProofInfoClassInfo::get(env).proProofInfo_getter)); - ProProofInfoClassInfo::read_gen_index_hash(r.pro_gen_index_hash, env, pro_proof.get()); + ProProofInfoClassInfo::read_revocation_tag(r.pro_revocation_tag, env, pro_proof.get()); r.pro_expiry_unix_ts = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index d40a82f..a34bd3e 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -10,16 +10,20 @@ extern "C" JNIEXPORT jstring JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildAddProPaymentRequestJson( JNIEnv *env, jobject thiz, jint version, jbyteArray master_private_key, - jbyteArray rotating_private_key, jint payment_provider, jstring payment_id, - jstring order_id) { + jbyteArray rotating_private_key, jstring provider_code, jstring payment_id) { return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef provider_code_ref(env, provider_code); + JavaStringRef payment_id_ref(env, payment_id); + auto payment_id_view = payment_id_ref.view(); + auto json = session::pro_backend::AddProPaymentRequest::build_to_json( version, JavaByteArrayRef(env, master_private_key).get(), JavaByteArrayRef(env, rotating_private_key).get(), - static_cast(payment_provider), - JavaStringRef(env, payment_id).get_raw(), - JavaStringRef(env, order_id).get_raw()); + provider_code_ref.view(), + std::span( + reinterpret_cast(payment_id_view.data()), + payment_id_view.size())); return jni_utils::jstring_from_optional(env, json).release(); }); @@ -29,14 +33,14 @@ extern "C" JNIEXPORT jstring JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGenerateProProofRequestJson( JNIEnv *env, jobject thiz, jint version, jbyteArray master_private_key, - jbyteArray rotating_private_key, jlong now_ms) { + jbyteArray rotating_private_key, jlong now_seconds) { return run_catching_cxx_exception_or_throws(env, [=]() { auto json = session::pro_backend::GenerateProProofRequest::build_to_json( version, JavaByteArrayRef(env, master_private_key).get(), JavaByteArrayRef(env, rotating_private_key).get(), - std::chrono::sys_time { - std::chrono::milliseconds(now_ms) + std::chrono::sys_seconds { + std::chrono::seconds(now_seconds) } ); @@ -47,14 +51,14 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGeneratePr extern "C" JNIEXPORT jstring JNICALL Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProDetailsRequestJson( - JNIEnv *env, jobject thiz, jint version, jbyteArray pro_master_private_key, jlong now_ms, + JNIEnv *env, jobject thiz, jint version, jbyteArray pro_master_private_key, jlong now_seconds, jint count) { return run_catching_cxx_exception_or_throws(env, [=]() { auto json = session::pro_backend::GetProDetailsRequest::build_to_json( version, JavaByteArrayRef(env, pro_master_private_key).get(), - std::chrono::sys_time { - std::chrono::milliseconds(now_ms) + std::chrono::sys_seconds { + std::chrono::seconds(now_seconds) }, static_cast(count) ); @@ -62,36 +66,3 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProDeta return jni_utils::jstring_from_optional(env, json).release(); }); } - -extern "C" -JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_getPaymentProviderMetadata( - JNIEnv *env, jobject thiz, jint payment_provider) { - return run_catching_cxx_exception_or_throws(env, [=]() -> jobject { - if (payment_provider >= SESSION_PRO_BACKEND_PAYMENT_PROVIDER_COUNT || payment_provider < 0) { - return nullptr; - } - - const auto & metadata = SESSION_PRO_BACKEND_PAYMENT_PROVIDER_METADATA[payment_provider]; - - static BasicJavaClassInfo class_info( - env, - "network/loki/messenger/libsession_util/protocol/PaymentProviderMetadata", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); - - - return env->NewObject( - class_info.java_class, - class_info.constructor, - jni_utils::jstring_from_optional(env, std::string_view(metadata.device.data, metadata.device.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.store.data, metadata.store.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.platform.data, metadata.platform.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.platform_account.data, metadata.platform_account.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.refund_platform_url.data, metadata.refund_platform_url.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.refund_support_url.data, metadata.refund_support_url.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.refund_status_url.data, metadata.refund_status_url.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.update_subscription_url.data, metadata.update_subscription_url.size)).get(), - jni_utils::jstring_from_optional(env, std::string_view(metadata.cancel_subscription_url.data, metadata.cancel_subscription_url.size)).get() - ); - }); -} \ No newline at end of file diff --git a/library/src/main/cpp/pro_proof_util.cpp b/library/src/main/cpp/pro_proof_util.cpp index 48e2fcb..8e3b70b 100644 --- a/library/src/main/cpp/pro_proof_util.cpp +++ b/library/src/main/cpp/pro_proof_util.cpp @@ -20,33 +20,33 @@ static std::array from_hex(std::span input) { session::ProProof java_to_cpp_proof(JNIEnv *env, jobject proof) { struct ProProofMethods : public JavaClassInfo { jmethodID get_version; - jmethodID get_gen_index_hash; + jmethodID get_revocation_tag; jmethodID get_rotating_pub_key; - jmethodID get_expiry_ms; + jmethodID get_expiry_seconds; jmethodID get_signature; ProProofMethods(JNIEnv *env, jobject obj) : JavaClassInfo(env, obj) , get_version(env->GetMethodID(java_class, "getVersion", "()I")) - , get_gen_index_hash(env->GetMethodID(java_class, "getGenIndexHashHex", "()Ljava/lang/String;")) + , get_revocation_tag(env->GetMethodID(java_class, "getRevocationTagHex", "()Ljava/lang/String;")) , get_rotating_pub_key(env->GetMethodID(java_class, "getRotatingPubKeyHex", "()Ljava/lang/String;")) - , get_expiry_ms(env->GetMethodID(java_class, "getExpiryMs", "()J")) + , get_expiry_seconds(env->GetMethodID(java_class, "getExpirySeconds", "()J")) , get_signature(env->GetMethodID(java_class, "getSignatureHex", "()Ljava/lang/String;")) {} }; // Cache method IDs static ProProofMethods methods(env, proof); - jni_utils::JavaLocalRef gen_index_hash(env, (jstring) env->CallObjectMethod(proof, methods.get_gen_index_hash)); + jni_utils::JavaLocalRef revocation_tag(env, (jstring) env->CallObjectMethod(proof, methods.get_revocation_tag)); jni_utils::JavaLocalRef rotating_pub_key(env, (jstring) env->CallObjectMethod(proof, methods.get_rotating_pub_key)); jni_utils::JavaLocalRef signature(env, (jstring) env->CallObjectMethod(proof, methods.get_signature)); return { .version = static_cast(env->CallIntMethod(proof, methods.get_version)), - .gen_index_hash = from_hex<32>(jni_utils::JavaStringRef(env, gen_index_hash.get()).get()), + .revocation_tag = from_hex<32>(jni_utils::JavaStringRef(env, revocation_tag.get()).get()), .rotating_pubkey = from_hex<32>(jni_utils::JavaStringRef(env, rotating_pub_key.get()).get()), - .expiry_unix_ts = std::chrono::sys_time( - std::chrono::milliseconds(env->CallLongMethod(proof, methods.get_expiry_ms))), + .expiry_unix_ts = std::chrono::sys_seconds( + std::chrono::seconds(env->CallLongMethod(proof, methods.get_expiry_seconds))), .sig = from_hex<64>(jni_utils::JavaStringRef(env, signature.get()).get()), }; } @@ -60,7 +60,7 @@ JavaLocalRef cpp_to_java_proof(JNIEnv *env, const session::ProProof &pr class_info.java_class, class_info.constructor, static_cast(proof.version), - util::bytes_from_span(env, proof.gen_index_hash).get(), + util::bytes_from_span(env, proof.revocation_tag).get(), util::bytes_from_span(env, proof.rotating_pubkey).get(), static_cast(proof.expiry_unix_ts.time_since_epoch().count()), util::bytes_from_span(env, proof.sig).get() @@ -88,7 +88,7 @@ Java_network_loki_messenger_libsession_1util_pro_ProProof_nativeStatus(JNIEnv *e return static_cast(java_to_cpp_proof(env, thiz).status( JavaByteArrayRef(env, verify_pub_key).get(), - std::chrono::sys_time{std::chrono::milliseconds(now_unix_ts)}, + std::chrono::sys_seconds{std::chrono::seconds(now_unix_ts)}, signed_msg )); }); diff --git a/library/src/main/cpp/protocol.cpp b/library/src/main/cpp/protocol.cpp index 4b431aa..b762f32 100644 --- a/library/src/main/cpp/protocol.cpp +++ b/library/src/main/cpp/protocol.cpp @@ -149,8 +149,9 @@ Java_network_loki_messenger_libsession_1util_protocol_SessionProtocol_decodeForC auto decoded = session::decode_for_community( payload_ref.get(), - std::chrono::sys_time{ - std::chrono::milliseconds{timestamp_ms}}, + std::chrono::sys_seconds{ + std::chrono::duration_cast( + std::chrono::milliseconds{timestamp_ms})}, *java_to_cpp_array<32>(env, pro_backend_pub_key) ); diff --git a/library/src/main/cpp/user_profile.cpp b/library/src/main/cpp/user_profile.cpp index ceeaba4..3ff0dab 100644 --- a/library/src/main/cpp/user_profile.cpp +++ b/library/src/main/cpp/user_profile.cpp @@ -165,11 +165,11 @@ Java_network_loki_messenger_libsession_1util_UserProfile_setAnimatedAvatar(JNIEn extern "C" JNIEXPORT void JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_setProAccessExpiryMs(JNIEnv *env, - jobject thiz, - jlong epoch_mills) { - ptrToProfile(env, thiz)->set_pro_access_expiry(std::chrono::sys_time{ - std::chrono::milliseconds{epoch_mills} +Java_network_loki_messenger_libsession_1util_UserProfile_setProAccessExpiry(JNIEnv *env, + jobject thiz, + jlong epoch_seconds) { + ptrToProfile(env, thiz)->set_pro_access_expiry(std::chrono::sys_seconds{ + std::chrono::seconds{epoch_seconds} }); } @@ -210,8 +210,8 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getProConfig(JNIEnv *en extern "C" JNIEXPORT jlong JNICALL -Java_network_loki_messenger_libsession_1util_UserProfile_getProAccessExpiryMsOrZero(JNIEnv *env, - jobject thiz) { +Java_network_loki_messenger_libsession_1util_UserProfile_getProAccessExpiryOrZero(JNIEnv *env, + jobject thiz) { auto expiry = ptrToProfile(env, thiz)->get_pro_access_expiry(); return expiry ? expiry->time_since_epoch().count() : 0; } \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt index 8f8eef5..3b7d553 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -85,7 +85,7 @@ interface ReadableUserProfile: ReadableConfig { fun getProFeatures(): ProProfileFeatures fun getProConfig(): ProConfig? - fun getProAccessExpiryMs(): Long? + fun getProAccessExpiry(): Long? } interface MutableUserProfile : ReadableUserProfile, MutableConfig { @@ -108,7 +108,7 @@ interface MutableUserProfile : ReadableUserProfile, MutableConfig { fun setProConfig(proConfig: ProConfig) fun setProBadge(proBadge: Boolean) fun setAnimatedAvatar(animatedAvatar: Boolean) - fun setProAccessExpiryMs(epochMills: Long) + fun setProAccessExpiry(epochSeconds: Long) fun removeProAccessExpiry() } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt b/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt index 7762f60..c864b9c 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/UserProfile.kt @@ -45,12 +45,12 @@ class UserProfile private constructor(pointer: Long) : ConfigBase(pointer), Muta external override fun setProBadge(proBadge: Boolean) external override fun setAnimatedAvatar(animatedAvatar: Boolean) - external override fun setProAccessExpiryMs(epochMills: Long) + external override fun setProAccessExpiry(epochSeconds: Long) external override fun removeProAccessExpiry() private external fun getProFeaturesRaw(): Long override fun getProFeatures(): ProProfileFeatures = ProProfileFeatures(getProFeaturesRaw()) external override fun getProConfig(): ProConfig? - private external fun getProAccessExpiryMsOrZero(): Long - override fun getProAccessExpiryMs(): Long? = getProAccessExpiryMsOrZero().takeIf { it != 0L } + private external fun getProAccessExpiryOrZero(): Long + override fun getProAccessExpiry(): Long? = getProAccessExpiryOrZero().takeIf { it != 0L } } \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index a76e9d0..901b1b8 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -1,36 +1,33 @@ package network.loki.messenger.libsession_util.pro import network.loki.messenger.libsession_util.LibSessionUtilCApi -import network.loki.messenger.libsession_util.protocol.PaymentProviderMetadata - -typealias PaymentProvider = Int object BackendRequests : LibSessionUtilCApi() { - const val PAYMENT_PROVIDER_GOOGLE_PLAY: PaymentProvider = 1 - const val PAYMENT_PROVIDER_APP_STORE: PaymentProvider = 2 + // Canonical provider code slugs; must match libsession's + // SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constants (opaque wire codes). + const val PAYMENT_PROVIDER_GOOGLE_PLAY: String = "google_play" + const val PAYMENT_PROVIDER_APP_STORE: String = "app_store" + const val PAYMENT_PROVIDER_RANGEPROOF: String = "rangeproof" external fun buildAddProPaymentRequestJson( version: Int, masterPrivateKey: ByteArray, rotatingPrivateKey: ByteArray, - paymentProvider: PaymentProvider, + providerCode: String, paymentId: String, - orderId: String, ): String external fun buildGenerateProProofRequestJson( version: Int, masterPrivateKey: ByteArray, rotatingPrivateKey: ByteArray, - nowMs: Long, + nowSeconds: Long, ): String external fun buildGetProDetailsRequestJson( version: Int, proMasterPrivateKey: ByteArray, - nowMs: Long, + nowSeconds: Long, count: Int, ): String - - external fun getPaymentProviderMetadata(paymentProvider: PaymentProvider): PaymentProviderMetadata? } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProProof.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProProof.kt index 996bee0..eeba248 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProProof.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProProof.kt @@ -15,14 +15,14 @@ typealias ProProofStatus = Int data class ProProof( val version: Int, - @SerialName("gen_index_hash") - val genIndexHashHex: String, + @SerialName("revocation_tag") + val revocationTagHex: String, @SerialName("rotating_pkey") val rotatingPubKeyHex: String, - @SerialName("expiry_unix_ts_ms") - val expiryMs: Long, + @SerialName("expiry_ts") + val expirySeconds: Long, @SerialName("sig") val signatureHex: String @@ -30,15 +30,15 @@ data class ProProof( @Keep constructor( version: Int, - genIndexHash: ByteArray, + revocationTag: ByteArray, rotatingPubKey: ByteArray, - expiryMs: Long, + expirySeconds: Long, signature: ByteArray ): this( version = version, - genIndexHashHex = genIndexHash.toHexString(), + revocationTagHex = revocationTag.toHexString(), rotatingPubKeyHex = rotatingPubKey.toHexString(), - expiryMs = expiryMs, + expirySeconds = expirySeconds, signatureHex = signature.toHexString() ) @@ -73,7 +73,7 @@ data class ProProof( val signedMessageData = signedMessage?.data val signedMessageSignature = signedMessage?.signature return nativeStatus( - nowUnixTs = now.toEpochMilli(), + nowUnixTs = now.epochSecond, verifyPubKey = senderED25519PubKey, signedMessageData = signedMessageData, signedMessageSignature = signedMessageSignature diff --git a/library/src/main/java/network/loki/messenger/libsession_util/protocol/PaymentProviderMetadata.kt b/library/src/main/java/network/loki/messenger/libsession_util/protocol/PaymentProviderMetadata.kt deleted file mode 100644 index c7b7149..0000000 --- a/library/src/main/java/network/loki/messenger/libsession_util/protocol/PaymentProviderMetadata.kt +++ /dev/null @@ -1,15 +0,0 @@ -package network.loki.messenger.libsession_util.protocol - -import androidx.annotation.Keep - -data class PaymentProviderMetadata @Keep constructor( - val device: String, - val store: String, - val platform: String, - val platformAccount: String, - val refundPlatformUrl: String, - val refundSupportUrl: String, - val refundStatusUrl: String, - val updateSubscriptionUrl: String, - val cancelSubscriptionUrl: String, -) \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt b/library/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt index 19b0c12..44b80d4 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/util/Conversation.kt @@ -13,25 +13,25 @@ sealed interface Conversation { * This is not a full ProProof, it contains just enough for the clients to check it it's revoked. */ data class ProProofInfo( - val genIndexHash: Bytes, + val revocationTag: Bytes, val expiry: Instant, ) { @Keep constructor( - genIndexHash: ByteArray, - expiryMs: Long, + revocationTag: ByteArray, + expirySeconds: Long, ) : this( - genIndexHash = Bytes(genIndexHash), - expiry = Instant.ofEpochMilli(expiryMs) + revocationTag = Bytes(revocationTag), + expiry = Instant.ofEpochSecond(expirySeconds) ) @get:Keep - val genIndexHashBytes: ByteArray - get() = genIndexHash.data + val revocationTagBytes: ByteArray + get() = revocationTag.data @get:Keep val expiryUnixTs: Long - get() = expiry.toEpochMilli() + get() = expiry.epochSecond } /** diff --git a/libsession-util b/libsession-util index bb4edf8..71d32b8 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit bb4edf8d694bfcec75c348cffde013a5350fb1b3 +Subproject commit 71d32b8af1effba63aa1fb417d4268761552bfb8 From db7618eceab17b107ecb9d99837d1ac0cd23e398 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 21:09:37 -0300 Subject: [PATCH 02/14] Glue rework: symmetric request builders + response parsers (re-pin 73e4d1e6) Adapt to the redesigned session::pro_backend C++ API (free functions; request structs + X::parse deleted; version param removed; provider URLs re-added). libsession now owns both request-build and response-parse. - Re-pin libsession-util submodule 71d32b8a -> 73e4d1e6. - BackendRequests: build{AddProPayment,GenerateProProof,GetProDetails,Refund, Revocations}Request -> ProRequest{endpoint,body}; parse{AddPayment,ProProof, PaymentDetails,Refund,Revocations}Response -> typed structs; providerUrls(). Drop the old build*RequestJson (used the deleted structs) + version param. - New Kotlin mirrors (ProBackendResponses.kt): ProRequest, ProviderUrls, ProResponseHeader, ProProofResponse, ProPaymentItem/GetProDetailsResponse, ProRevocationItem/GetProRevocationsResponse, SetPaymentRefundRequestedResponse. - pro_backend.cpp: JNI marshalling of the C++ structs into those data classes; purchased/revoked timestamps are ms (sys_ms), the rest whole seconds. Kotlin + native (arm64) build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 299 +++++++++++++++--- .../libsession_util/pro/BackendRequests.kt | 47 ++- .../pro/ProBackendResponses.kt | 104 ++++++ libsession-util | 2 +- 4 files changed, 395 insertions(+), 57 deletions(-) create mode 100644 library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index a34bd3e..c024e0f 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -1,68 +1,275 @@ #include -#include "jni_utils.h" -#include "util.h" +#include +#include +#include #include +#include "jni_utils.h" +#include "util.h" +#include "pro_proof_util.h" + using namespace jni_utils; +namespace { + +namespace pb = session::pro_backend; + +constexpr const char* PKG = "network/loki/messenger/libsession_util/pro/"; + +std::span string_to_span(std::string_view s) { + return {reinterpret_cast(s.data()), s.size()}; +} + +// --- struct -> Kotlin marshalling helpers --- + +JavaLocalRef serialize_response_header(JNIEnv* env, const pb::Response& r) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProResponseHeader", + "(ILjava/util/List;)V"); + JavaLocalRef errors(env, jstring_list_from_collection(env, r.errors)); + return {env, env->NewObject(cls.java_class, cls.constructor, + static_cast(r.status), errors.get())}; +} + +jobject serialize_pro_request(JNIEnv* env, const pb::ProRequest& r) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProRequest", + "(Ljava/lang/String;Ljava/lang/String;)V"); + return env->NewObject(cls.java_class, cls.constructor, + jstring_from_optional(env, r.endpoint).get(), + jstring_from_optional(env, std::string_view(r.body)).get()); +} + +JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderUrls& u) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProviderUrls", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + return {env, env->NewObject(cls.java_class, cls.constructor, + jstring_from_optional(env, u.refund_platform_url).get(), + jstring_from_optional(env, u.refund_support_url).get(), + jstring_from_optional(env, u.refund_status_url).get(), + jstring_from_optional(env, u.update_subscription_url).get(), + jstring_from_optional(env, u.cancel_subscription_url).get())}; +} + +JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentItem& it) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProPaymentItem", + "(ILjava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); + return {env, env->NewObject(cls.java_class, cls.constructor, + static_cast(it.status), + jstring_from_optional(env, std::string_view(it.plan)).get(), + jstring_from_optional(env, std::string_view(it.payment_provider)).get(), + static_cast(it.auto_renewing), + static_cast(it.purchased_unix_ts.time_since_epoch().count()), // ms + static_cast(it.redeemed_unix_ts.time_since_epoch().count()), // s + static_cast(it.expiry_unix_ts.time_since_epoch().count()), // s + static_cast(it.grace_period_duration.count()), // s + static_cast(it.platform_refund_expiry_unix_ts.time_since_epoch().count()), // s + static_cast(it.revoked_unix_ts.time_since_epoch().count()), // ms + static_cast(it.refund_requested_unix_ts.time_since_epoch().count()), // s + jstring_from_optional(env, std::string_view(it.payment_id)).get())}; +} + +JavaLocalRef serialize_revocation_item(JNIEnv* env, const pb::ProRevocationItem& it) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProRevocationItem", + "(Ljava/lang/String;J)V"); + auto hex = oxenc::to_hex(it.revocation_tag.begin(), it.revocation_tag.end()); + return {env, env->NewObject(cls.java_class, cls.constructor, + jstring_from_optional(env, std::string_view(hex)).get(), + static_cast(it.effective_unix_ts.time_since_epoch().count()))}; +} + +jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& 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;)V"); + auto header = serialize_response_header(env, resp); + JavaLocalRef proof(env, nullptr); + if (resp.errors.empty()) + proof = cpp_to_java_proof(env, resp.proof); + return env->NewObject(cls.java_class, cls.constructor, header.get(), proof.get()); +} + +jobject serialize_get_details_response(JNIEnv* env, const pb::GetProDetailsResponse& resp) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProDetailsResponse", + "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/util/List;IIZJJJI)V"); + auto header = serialize_response_header(env, resp); + JavaLocalRef items(env, jlist_from_collection(env, resp.items, + [](JNIEnv* env, const pb::ProPaymentItem& it) -> std::optional> { + return serialize_payment_item(env, it); + })); + return env->NewObject(cls.java_class, cls.constructor, + header.get(), items.get(), + static_cast(resp.user_status), + static_cast(resp.error_report), + static_cast(resp.auto_renewing), + static_cast(resp.expiry_unix_ts.time_since_epoch().count()), + static_cast(resp.grace_period_duration.count()), + static_cast(resp.refund_requested_unix_ts.time_since_epoch().count()), + static_cast(resp.payments_total)); +} + +jobject serialize_revocations_response(JNIEnv* env, const pb::GetProRevocationsResponse& resp) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProRevocationsResponse", + "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;JJJLjava/util/List;)V"); + auto header = serialize_response_header(env, resp); + JavaLocalRef items(env, jlist_from_collection(env, resp.items, + [](JNIEnv* env, const pb::ProRevocationItem& it) -> std::optional> { + return serialize_revocation_item(env, it); + })); + return env->NewObject(cls.java_class, cls.constructor, + header.get(), + static_cast(resp.ticket), + static_cast(resp.retry_in.count()), + static_cast(resp.retain_for.count()), + items.get()); +} + +jobject serialize_refund_response(JNIEnv* env, const pb::SetPaymentRefundRequestedResponse& resp) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/SetPaymentRefundRequestedResponse", + "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Z)V"); + auto header = serialize_response_header(env, resp); + return env->NewObject(cls.java_class, cls.constructor, header.get(), + static_cast(resp.updated)); +} + +} // namespace + +// --- Request builders (return ProRequest{endpoint, body}) --- + extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildAddProPaymentRequestJson( - JNIEnv *env, jobject thiz, jint version, jbyteArray master_private_key, - jbyteArray rotating_private_key, jstring provider_code, jstring payment_id) { - return run_catching_cxx_exception_or_throws(env, [=]() { - JavaStringRef provider_code_ref(env, provider_code); - JavaStringRef payment_id_ref(env, payment_id); - auto payment_id_view = payment_id_ref.view(); - - auto json = session::pro_backend::AddProPaymentRequest::build_to_json( - version, +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildAddProPaymentRequest( + JNIEnv* env, jobject, jbyteArray master_private_key, jbyteArray rotating_private_key, + jstring provider_code, jstring payment_id) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef provider_ref(env, provider_code); + JavaStringRef payment_ref(env, payment_id); + auto req = pb::add_payment_request( JavaByteArrayRef(env, master_private_key).get(), JavaByteArrayRef(env, rotating_private_key).get(), - provider_code_ref.view(), - std::span( - reinterpret_cast(payment_id_view.data()), - payment_id_view.size())); - - return jni_utils::jstring_from_optional(env, json).release(); + provider_ref.view(), + string_to_span(payment_ref.view())); + return serialize_pro_request(env, req); }); } extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGenerateProProofRequestJson( - JNIEnv *env, jobject thiz, jint version, jbyteArray master_private_key, - jbyteArray rotating_private_key, jlong now_seconds) { - return run_catching_cxx_exception_or_throws(env, [=]() { - auto json = session::pro_backend::GenerateProProofRequest::build_to_json( - version, +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGenerateProProofRequest( + JNIEnv* env, jobject, jbyteArray master_private_key, jbyteArray rotating_private_key, + jlong now_seconds) { + return run_catching_cxx_exception_or_throws(env, [=]() { + auto req = pb::pro_proof_request( JavaByteArrayRef(env, master_private_key).get(), JavaByteArrayRef(env, rotating_private_key).get(), - std::chrono::sys_seconds { - std::chrono::seconds(now_seconds) - } - ); + std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}); + return serialize_pro_request(env, req); + }); +} - return jni_utils::jstring_from_optional(env, json).release(); +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProDetailsRequest( + JNIEnv* env, jobject, jbyteArray master_private_key, jlong now_seconds, jint count) { + return run_catching_cxx_exception_or_throws(env, [=]() { + auto req = pb::payment_details_request( + JavaByteArrayRef(env, master_private_key).get(), + std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}, + static_cast(count)); + return serialize_pro_request(env, req); }); } extern "C" -JNIEXPORT jstring JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProDetailsRequestJson( - JNIEnv *env, jobject thiz, jint version, jbyteArray pro_master_private_key, jlong now_seconds, - jint count) { - return run_catching_cxx_exception_or_throws(env, [=]() { - auto json = session::pro_backend::GetProDetailsRequest::build_to_json( - version, - JavaByteArrayRef(env, pro_master_private_key).get(), - std::chrono::sys_seconds { - std::chrono::seconds(now_seconds) - }, - static_cast(count) - ); - - return jni_utils::jstring_from_optional(env, json).release(); +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildRefundRequest( + JNIEnv* env, jobject, jbyteArray master_private_key, jlong now_seconds, + jlong refund_requested_seconds, jstring provider_code, jstring payment_id) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef provider_ref(env, provider_code); + JavaStringRef payment_ref(env, payment_id); + auto req = pb::refund_request( + JavaByteArrayRef(env, master_private_key).get(), + std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}, + std::chrono::sys_seconds{std::chrono::seconds(refund_requested_seconds)}, + provider_ref.view(), + string_to_span(payment_ref.view())); + return serialize_pro_request(env, req); + }); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildRevocationsRequest( + JNIEnv* env, jobject, jlong ticket) { + return run_catching_cxx_exception_or_throws(env, [=]() { + auto req = pb::revocations_request(static_cast(ticket)); + return serialize_pro_request(env, req); + }); +} + +// --- Response parsers (return typed structs) --- + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseAddPaymentResponse( + JNIEnv* env, jobject, jstring json) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef json_ref(env, json); + return serialize_pro_proof_response(env, pb::parse_add_payment(json_ref.view())); + }); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseProProofResponse( + JNIEnv* env, jobject, jstring json) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef json_ref(env, json); + return serialize_pro_proof_response(env, pb::parse_pro_proof(json_ref.view())); + }); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parsePaymentDetailsResponse( + JNIEnv* env, jobject, jstring json) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef json_ref(env, json); + return serialize_get_details_response(env, pb::parse_payment_details(json_ref.view())); + }); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseRefundResponse( + JNIEnv* env, jobject, jstring json) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef json_ref(env, json); + return serialize_refund_response(env, pb::parse_refund(json_ref.view())); + }); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseRevocationsResponse( + JNIEnv* env, jobject, jstring json) { + return run_catching_cxx_exception_or_throws(env, [=]() { + JavaStringRef json_ref(env, json); + return serialize_revocations_response(env, pb::parse_revocations(json_ref.view())); + }); +} + +// --- Provider URLs (source of truth in libsession; null if none) --- + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_providerUrls( + JNIEnv* env, jobject, jstring provider_code) { + return run_catching_cxx_exception_or_throws(env, [=]() -> jobject { + JavaStringRef code_ref(env, provider_code); + auto urls = pb::provider_urls(code_ref.view()); + if (!urls) + return nullptr; + return serialize_provider_urls(env, *urls).release(); }); } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index 901b1b8..027e1a4 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -2,6 +2,12 @@ package network.loki.messenger.libsession_util.pro import network.loki.messenger.libsession_util.LibSessionUtilCApi +/** + * Session Pro backend request builders + response parsers. libsession-util owns the wire format on + * both sides — the single source of truth — so the app never builds or parses the JSON itself: each + * `build*Request` signs internally and returns a [ProRequest] (endpoint + body) to POST, and each + * `parse*Response` turns the raw reply JSON into a typed struct (check `header.errors` first). + */ object BackendRequests : LibSessionUtilCApi() { // Canonical provider code slugs; must match libsession's // SESSION_PRO_BACKEND_PAYMENT_PROVIDER_CODE_* constants (opaque wire codes). @@ -9,25 +15,46 @@ object BackendRequests : LibSessionUtilCApi() { const val PAYMENT_PROVIDER_APP_STORE: String = "app_store" const val PAYMENT_PROVIDER_RANGEPROOF: String = "rangeproof" - external fun buildAddProPaymentRequestJson( - version: Int, + // --- Request builders (sign internally; return endpoint + JSON body) --- + + external fun buildAddProPaymentRequest( masterPrivateKey: ByteArray, rotatingPrivateKey: ByteArray, providerCode: String, paymentId: String, - ): String + ): ProRequest - external fun buildGenerateProProofRequestJson( - version: Int, + external fun buildGenerateProProofRequest( masterPrivateKey: ByteArray, rotatingPrivateKey: ByteArray, nowSeconds: Long, - ): String + ): ProRequest - external fun buildGetProDetailsRequestJson( - version: Int, - proMasterPrivateKey: ByteArray, + external fun buildGetProDetailsRequest( + masterPrivateKey: ByteArray, nowSeconds: Long, count: Int, - ): String + ): ProRequest + + external fun buildRefundRequest( + masterPrivateKey: ByteArray, + nowSeconds: Long, + refundRequestedSeconds: Long, + providerCode: String, + paymentId: String, + ): ProRequest + + external fun buildRevocationsRequest(ticket: Long): ProRequest + + // --- Response parsers (typed structs; check header.errors first) --- + + external fun parseAddPaymentResponse(json: String): ProProofResponse + external fun parseProProofResponse(json: String): ProProofResponse + external fun parsePaymentDetailsResponse(json: String): GetProDetailsResponse + external fun parseRefundResponse(json: String): SetPaymentRefundRequestedResponse + external fun parseRevocationsResponse(json: String): GetProRevocationsResponse + + // --- Provider URLs (libsession is the source of truth; null if none, e.g. rangeproof/unknown) --- + + external fun providerUrls(providerCode: String): ProviderUrls? } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt new file mode 100644 index 0000000..cb96934 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -0,0 +1,104 @@ +package network.loki.messenger.libsession_util.pro + +import androidx.annotation.Keep + +/** + * Kotlin mirrors of the `session::pro_backend` request/response structs. libsession-util owns both + * request construction and response parsing (the single source of truth for the wire shape); these + * types are what the JNI layer hands back so the app never re-parses the JSON itself. + * + * Timestamp conventions match the C++ structs: fields named `...UnixTs` are whole **unix seconds**; + * `...UnixTsMs` are **unix milliseconds** (the provider's sub-second precision, kept as ms); duration + * fields are **seconds**. A `0` timestamp means "not set". + */ + +/** Route + body for a request to POST to the Pro backend (from the `*Request` builders). */ +@Keep +data class ProRequest( + val endpoint: String, + val body: String, +) + +/** Per-provider support/management URLs — libsession is the source of truth for these. */ +@Keep +data class ProviderUrls( + val refundPlatformUrl: String, + val refundSupportUrl: String, + val refundStatusUrl: String, + val updateSubscriptionUrl: String, + val cancelSubscriptionUrl: String, +) + +/** + * Common response header. `status` is 0 for success (or a request-specific status enum, e.g. + * add-payment); a non-empty [errors] means the parse failed or was partial — always check first. + */ +@Keep +data class ProResponseHeader( + val status: Int, + val errors: List, +) { + val isSuccess: Boolean get() = errors.isEmpty() +} + +/** Response to add-payment / generate-proof: carries the freshly-issued proof (null on error). */ +@Keep +data class ProProofResponse( + val header: ProResponseHeader, + val proof: ProProof?, +) + +/** One payment/subscription record from get-details. */ +@Keep +data class ProPaymentItem( + val status: Int, // SESSION_PRO_BACKEND_PAYMENT_STATUS + val plan: String, // period code, e.g. "1m"/"3m"/"1y"; opaque + val paymentProvider: String, // provider slug, e.g. "google_play"; opaque + val autoRenewing: Boolean, + val purchasedUnixTsMs: Long, // sys_ms (provider purchase instant); always set + val redeemedUnixTs: Long, // seconds; 0 if not activated + val expiryUnixTs: Long, // seconds; 0 if not activated + val gracePeriodDurationSeconds: Long, + val platformRefundExpiryUnixTs: Long, // seconds + val revokedUnixTsMs: Long, // sys_ms; 0 if not revoked + val refundRequestedUnixTs: Long, // seconds; 0 if none + val paymentId: String, // opaque; confidential +) + +/** Response to get-details. */ +@Keep +data class GetProDetailsResponse( + val header: ProResponseHeader, + val items: List, + val userStatus: Int, // SESSION_PRO_BACKEND_USER_PRO_STATUS + val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT + val autoRenewing: Boolean, + val expiryUnixTs: Long, // seconds; includes grace period; may be in the past + val gracePeriodDurationSeconds: Long, + val refundRequestedUnixTs: Long, // seconds; 0 if none + val paymentsTotal: Int, +) + +/** One revocation-list entry. */ +@Keep +data class ProRevocationItem( + val revocationTagHex: String, // 32-byte opaque tag, hex + val effectiveUnixTs: Long, // seconds; revoked only once client clock >= this +) + +/** Response to get-revocations. */ +@Keep +data class GetProRevocationsResponse( + val header: ProResponseHeader, + val ticket: Long, + val retryInSeconds: Long, + val retainForSeconds: Long, + val items: List, +) + +/** Response to set-refund-requested. */ +@Keep +data class SetPaymentRefundRequestedResponse( + val header: ProResponseHeader, + val updated: Boolean, +) diff --git a/libsession-util b/libsession-util index 71d32b8..73e4d1e 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 71d32b8af1effba63aa1fb417d4268761552bfb8 +Subproject commit 73e4d1e65db1bfeb5949034bf3ca52724e002ab3 From 86927016884c43fff307126b8a18ccf6b9630171 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sat, 18 Jul 2026 21:16:45 -0300 Subject: [PATCH 03/14] Glue: add ProResponse interface (common header accessor for the app) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pro/ProBackendResponses.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index cb96934..94fa8e5 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -41,12 +41,17 @@ data class ProResponseHeader( val isSuccess: Boolean get() = errors.isEmpty() } +/** Common interface for the parsed Pro backend responses; check [header] before reading the data. */ +interface ProResponse { + val header: ProResponseHeader +} + /** Response to add-payment / generate-proof: carries the freshly-issued proof (null on error). */ @Keep data class ProProofResponse( - val header: ProResponseHeader, + override val header: ProResponseHeader, val proof: ProProof?, -) +) : ProResponse /** One payment/subscription record from get-details. */ @Keep @@ -68,7 +73,7 @@ data class ProPaymentItem( /** Response to get-details. */ @Keep data class GetProDetailsResponse( - val header: ProResponseHeader, + override val header: ProResponseHeader, val items: List, val userStatus: Int, // SESSION_PRO_BACKEND_USER_PRO_STATUS val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT @@ -77,7 +82,7 @@ data class GetProDetailsResponse( val gracePeriodDurationSeconds: Long, val refundRequestedUnixTs: Long, // seconds; 0 if none val paymentsTotal: Int, -) +) : ProResponse /** One revocation-list entry. */ @Keep @@ -89,16 +94,16 @@ data class ProRevocationItem( /** Response to get-revocations. */ @Keep data class GetProRevocationsResponse( - val header: ProResponseHeader, + override val header: ProResponseHeader, val ticket: Long, val retryInSeconds: Long, val retainForSeconds: Long, val items: List, -) +) : ProResponse /** Response to set-refund-requested. */ @Keep data class SetPaymentRefundRequestedResponse( - val header: ProResponseHeader, + override val header: ProResponseHeader, val updated: Boolean, -) +) : ProResponse From b52177c84a4e44f6e6d6786ed21ce3710500403f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 19 Jul 2026 14:53:27 -0300 Subject: [PATCH 04/14] =?UTF-8?q?WIP:=20android=20glue=20=E2=80=94=20re-pi?= =?UTF-8?q?n=20to=20FINAL=20PIN=20857783e6=20(status=20strings=20+=20URL/P?= =?UTF-8?q?UBKEY)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the vendored libsession submodule 73e4d1e6 -> 857783e6 and adapt to the two changes since 73e4d1e6: - get-details status / user_status are now opaque string codes (the SESSION_PRO_BACKEND_PAYMENT_STATUS / _USER_PRO_STATUS enums were deleted; C++ fields are std::string). serialize_payment_item / serialize_get_details_ response now marshal them as jstring; ProPaymentItem.status / GetProDetailsResponse.userStatus retyped Int -> String. - Expose the libsession-owned backend identity so the app stops carrying its own copy: BackendRequests.proBackendUrl() (session::pro_backend::URL) and proBackendPubKeyHex() (hex of session::pro_backend::PUBKEY). Single source of truth — a future URL/pubkey change happens in libsession only. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 29 ++++++++++++++++--- .../libsession_util/pro/BackendRequests.kt | 8 +++++ .../pro/ProBackendResponses.kt | 4 +-- libsession-util | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index c024e0f..60921d1 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -52,9 +52,9 @@ JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderUrl JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentItem& it) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProPaymentItem", - "(ILjava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); return {env, env->NewObject(cls.java_class, cls.constructor, - static_cast(it.status), + jstring_from_optional(env, std::string_view(it.status)).get(), // opaque status code string jstring_from_optional(env, std::string_view(it.plan)).get(), jstring_from_optional(env, std::string_view(it.payment_provider)).get(), static_cast(it.auto_renewing), @@ -90,7 +90,7 @@ jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& re jobject serialize_get_details_response(JNIEnv* env, const pb::GetProDetailsResponse& resp) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProDetailsResponse", - "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/util/List;IIZJJJI)V"); + "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/util/List;Ljava/lang/String;IZJJJI)V"); auto header = serialize_response_header(env, resp); JavaLocalRef items(env, jlist_from_collection(env, resp.items, [](JNIEnv* env, const pb::ProPaymentItem& it) -> std::optional> { @@ -98,7 +98,7 @@ jobject serialize_get_details_response(JNIEnv* env, const pb::GetProDetailsRespo })); return env->NewObject(cls.java_class, cls.constructor, header.get(), items.get(), - static_cast(resp.user_status), + jstring_from_optional(env, std::string_view(resp.user_status)).get(), // opaque status code string static_cast(resp.error_report), static_cast(resp.auto_renewing), static_cast(resp.expiry_unix_ts.time_since_epoch().count()), @@ -273,3 +273,24 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_providerUrls( return serialize_provider_urls(env, *urls).release(); }); } + +// --- Backend identity constants (single source of truth in libsession) --- + +extern "C" +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_proBackendUrl( + JNIEnv* env, jobject) { + return run_catching_cxx_exception_or_throws(env, [=]() { + return env->NewStringUTF(std::string(pb::URL).c_str()); + }); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_proBackendPubKeyHex( + JNIEnv* env, jobject) { + return run_catching_cxx_exception_or_throws(env, [=]() { + auto hex = oxenc::to_hex(pb::PUBKEY.begin(), pb::PUBKEY.end()); + return env->NewStringUTF(hex.c_str()); + }); +} diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index 027e1a4..e0d9c95 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -57,4 +57,12 @@ object BackendRequests : LibSessionUtilCApi() { // --- Provider URLs (libsession is the source of truth; null if none, e.g. rangeproof/unknown) --- external fun providerUrls(providerCode: String): ProviderUrls? + + // --- Backend identity (single source of truth in libsession; read these, don't hard-code) --- + + /** The Session Pro backend base URL (overridable prod/default), e.g. "https://pro.session.codes". */ + external fun proBackendUrl(): String + + /** The Session Pro backend Ed25519 signing pubkey, hex (64 chars). */ + external fun proBackendPubKeyHex(): String } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 94fa8e5..2f88015 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -56,7 +56,7 @@ data class ProProofResponse( /** One payment/subscription record from get-details. */ @Keep data class ProPaymentItem( - val status: Int, // SESSION_PRO_BACKEND_PAYMENT_STATUS + val status: String, // opaque status code: unredeemed/redeemed/expired/revoked val plan: String, // period code, e.g. "1m"/"3m"/"1y"; opaque val paymentProvider: String, // provider slug, e.g. "google_play"; opaque val autoRenewing: Boolean, @@ -75,7 +75,7 @@ data class ProPaymentItem( data class GetProDetailsResponse( override val header: ProResponseHeader, val items: List, - val userStatus: Int, // SESSION_PRO_BACKEND_USER_PRO_STATUS + val userStatus: String, // opaque status code: never/active/expired val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT val autoRenewing: Boolean, val expiryUnixTs: Long, // seconds; includes grace period; may be in the past diff --git a/libsession-util b/libsession-util index 73e4d1e..857783e 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 73e4d1e65db1bfeb5949034bf3ca52724e002ab3 +Subproject commit 857783e6dbe4b17ba8d27e1c0396905467cb34fb From 05592a849f03f057b9dce23399a248299339295f Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 09:54:14 -0300 Subject: [PATCH 05/14] android glue: re-pin to 418cdaba (*_at renames + ProRequest content_type) Bump vendored libsession 857783e6 -> 418cdaba and adapt the JNI marshalling: - C++ *_unix_ts -> *_at member renames (no wire change): serialize_payment_item / serialize_get_details_response / serialize_revocation_item (purchased_at, redeemed_at, expiry_at, platform_refund_expiry_at, revoked_at, refund_requested_at, effective_at), conversation.cpp pro_expiry_at, and pro_proof_util.cpp ProProof.expiry_at. (Kotlin field names/units unchanged.) - ProRequest now carries content_type: serialize_pro_request emits it (3-arg ctor) and ProRequest.kt gains `contentType`. C++ payload member is `data` (was `body`). Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/conversation.cpp | 8 +++---- library/src/main/cpp/pro_backend.cpp | 23 ++++++++++--------- library/src/main/cpp/pro_proof_util.cpp | 4 ++-- .../pro/ProBackendResponses.kt | 3 ++- libsession-util | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/library/src/main/cpp/conversation.cpp b/library/src/main/cpp/conversation.cpp index 1ed3ce3..48059a7 100644 --- a/library/src/main/cpp/conversation.cpp +++ b/library/src/main/cpp/conversation.cpp @@ -46,7 +46,7 @@ JavaLocalRef serialize_one_to_one(JNIEnv *env, const session::config::c (jlong) one_to_one.last_read, (jboolean) one_to_one.unread, serialize_pro_proof_info(env, one_to_one.pro_revocation_tag, - one_to_one.pro_expiry_unix_ts).get())}; + one_to_one.pro_expiry_at).get())}; } @@ -136,7 +136,7 @@ session::config::convo::one_to_one deserialize_one_to_one(JNIEnv *env, jobject i info, WithProProofInfoClassInfo::get(env).proProofInfo_getter)); ProProofInfoClassInfo::read_revocation_tag(r.pro_revocation_tag, env, pro_proof.get()); - r.pro_expiry_unix_ts = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); + r.pro_expiry_at = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); return r; } @@ -274,7 +274,7 @@ JavaLocalRef serialize_blinded_one_to_one(JNIEnv *env, const session::c JavaLocalRef(env, env->NewStringUTF(blinded_one_to_one.blinded_session_id.data())).get(), (jlong) blinded_one_to_one.last_read, (jboolean) blinded_one_to_one.unread, - serialize_pro_proof_info(env, blinded_one_to_one.pro_revocation_tag, blinded_one_to_one.pro_expiry_unix_ts).get() + serialize_pro_proof_info(env, blinded_one_to_one.pro_revocation_tag, blinded_one_to_one.pro_expiry_at).get() )}; } @@ -303,7 +303,7 @@ session::config::convo::blinded_one_to_one deserialize_blinded_one_to_one(JNIEnv info, WithProProofInfoClassInfo::get(env).proProofInfo_getter)); ProProofInfoClassInfo::read_revocation_tag(r.pro_revocation_tag, env, pro_proof.get()); - r.pro_expiry_unix_ts = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); + r.pro_expiry_at = ProProofInfoClassInfo::read_pro_expiry(env, pro_proof.get()); return r; diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 60921d1..5aa0925 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -33,10 +33,11 @@ JavaLocalRef serialize_response_header(JNIEnv* env, const pb::Response& jobject serialize_pro_request(JNIEnv* env, const pb::ProRequest& r) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProRequest", - "(Ljava/lang/String;Ljava/lang/String;)V"); + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); return env->NewObject(cls.java_class, cls.constructor, jstring_from_optional(env, r.endpoint).get(), - jstring_from_optional(env, std::string_view(r.body)).get()); + jstring_from_optional(env, r.content_type).get(), + jstring_from_optional(env, std::string_view(r.data)).get()); } JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderUrls& u) { @@ -58,13 +59,13 @@ JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentIt jstring_from_optional(env, std::string_view(it.plan)).get(), jstring_from_optional(env, std::string_view(it.payment_provider)).get(), static_cast(it.auto_renewing), - static_cast(it.purchased_unix_ts.time_since_epoch().count()), // ms - static_cast(it.redeemed_unix_ts.time_since_epoch().count()), // s - static_cast(it.expiry_unix_ts.time_since_epoch().count()), // s + static_cast(it.purchased_at.time_since_epoch().count()), // ms + static_cast(it.redeemed_at.time_since_epoch().count()), // s + static_cast(it.expiry_at.time_since_epoch().count()), // s static_cast(it.grace_period_duration.count()), // s - static_cast(it.platform_refund_expiry_unix_ts.time_since_epoch().count()), // s - static_cast(it.revoked_unix_ts.time_since_epoch().count()), // ms - static_cast(it.refund_requested_unix_ts.time_since_epoch().count()), // s + static_cast(it.platform_refund_expiry_at.time_since_epoch().count()), // s + static_cast(it.revoked_at.time_since_epoch().count()), // ms + static_cast(it.refund_requested_at.time_since_epoch().count()), // s jstring_from_optional(env, std::string_view(it.payment_id)).get())}; } @@ -74,7 +75,7 @@ JavaLocalRef serialize_revocation_item(JNIEnv* env, const pb::ProRevoca auto hex = oxenc::to_hex(it.revocation_tag.begin(), it.revocation_tag.end()); return {env, env->NewObject(cls.java_class, cls.constructor, jstring_from_optional(env, std::string_view(hex)).get(), - static_cast(it.effective_unix_ts.time_since_epoch().count()))}; + static_cast(it.effective_at.time_since_epoch().count()))}; } jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& resp) { @@ -101,9 +102,9 @@ jobject serialize_get_details_response(JNIEnv* env, const pb::GetProDetailsRespo jstring_from_optional(env, std::string_view(resp.user_status)).get(), // opaque status code string static_cast(resp.error_report), static_cast(resp.auto_renewing), - static_cast(resp.expiry_unix_ts.time_since_epoch().count()), + static_cast(resp.expiry_at.time_since_epoch().count()), static_cast(resp.grace_period_duration.count()), - static_cast(resp.refund_requested_unix_ts.time_since_epoch().count()), + static_cast(resp.refund_requested_at.time_since_epoch().count()), static_cast(resp.payments_total)); } diff --git a/library/src/main/cpp/pro_proof_util.cpp b/library/src/main/cpp/pro_proof_util.cpp index 8e3b70b..e640168 100644 --- a/library/src/main/cpp/pro_proof_util.cpp +++ b/library/src/main/cpp/pro_proof_util.cpp @@ -45,7 +45,7 @@ session::ProProof java_to_cpp_proof(JNIEnv *env, jobject proof) { .version = static_cast(env->CallIntMethod(proof, methods.get_version)), .revocation_tag = from_hex<32>(jni_utils::JavaStringRef(env, revocation_tag.get()).get()), .rotating_pubkey = from_hex<32>(jni_utils::JavaStringRef(env, rotating_pub_key.get()).get()), - .expiry_unix_ts = std::chrono::sys_seconds( + .expiry_at = std::chrono::sys_seconds( std::chrono::seconds(env->CallLongMethod(proof, methods.get_expiry_seconds))), .sig = from_hex<64>(jni_utils::JavaStringRef(env, signature.get()).get()), }; @@ -62,7 +62,7 @@ JavaLocalRef cpp_to_java_proof(JNIEnv *env, const session::ProProof &pr static_cast(proof.version), util::bytes_from_span(env, proof.revocation_tag).get(), util::bytes_from_span(env, proof.rotating_pubkey).get(), - static_cast(proof.expiry_unix_ts.time_since_epoch().count()), + static_cast(proof.expiry_at.time_since_epoch().count()), util::bytes_from_span(env, proof.sig).get() )}; } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 2f88015..0334bd4 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -12,10 +12,11 @@ import androidx.annotation.Keep * fields are **seconds**. A `0` timestamp means "not set". */ -/** Route + body for a request to POST to the Pro backend (from the `*Request` builders). */ +/** Route + content-type + body for a request to POST to the Pro backend (from the `*Request` builders). */ @Keep data class ProRequest( val endpoint: String, + val contentType: String, // relay verbatim as the Content-Type header; don't assume a format val body: String, ) diff --git a/libsession-util b/libsession-util index 857783e..418cdab 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 857783e6dbe4b17ba8d27e1c0396905467cb34fb +Subproject commit 418cdabad62c1a11ab22298f5682b7dd2956952e From 97f72302a60b78eaff61a7fee390553b700dd204 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 10:43:44 -0300 Subject: [PATCH 06/14] pro glue: expose Instant/Duration accessors on parsed response structs The JNI boundary still hands back raw epoch longs (efficient, primitive-only marshalling), but the app shouldn't juggle mixed-unit longs and magic-zero sentinels. Add typed Instant/Duration accessors on ProPaymentItem, GetProDetailsResponse and ProRevocationItem that convert once at the Kotlin glue boundary (seconds/ms per field) and map the "unset" 0 to null. Consumers should read these instead of the raw *UnixTs(Ms) fields. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pro/ProBackendResponses.kt | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 0334bd4..53765d7 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -1,17 +1,29 @@ package network.loki.messenger.libsession_util.pro import androidx.annotation.Keep +import java.time.Duration +import java.time.Instant /** * Kotlin mirrors of the `session::pro_backend` request/response structs. libsession-util owns both * request construction and response parsing (the single source of truth for the wire shape); these * types are what the JNI layer hands back so the app never re-parses the JSON itself. * - * Timestamp conventions match the C++ structs: fields named `...UnixTs` are whole **unix seconds**; - * `...UnixTsMs` are **unix milliseconds** (the provider's sub-second precision, kept as ms); duration - * fields are **seconds**. A `0` timestamp means "not set". + * The JNI boundary can only carry primitives efficiently, so the constructor fields are raw epoch + * integers (`...UnixTs` = whole **unix seconds**, `...UnixTsMs` = **unix milliseconds**; duration + * fields are **seconds**; `0` means "not set"). Consumers should prefer the typed `Instant`/`Duration` + * accessors below instead of the raw longs — the epoch→datetime conversion (and the `0 → null` "unset" + * mapping) happens once, here at the glue boundary, rather than being repeated at every call site. */ +/** Epoch **seconds** as an [Instant], or null when unset (`0`). */ +private fun Long.secondsToInstantOrNull(): Instant? = + if (this == 0L) null else Instant.ofEpochSecond(this) + +/** Epoch **milliseconds** as an [Instant], or null when unset (`0`). */ +private fun Long.msToInstantOrNull(): Instant? = + if (this == 0L) null else Instant.ofEpochMilli(this) + /** Route + content-type + body for a request to POST to the Pro backend (from the `*Request` builders). */ @Keep data class ProRequest( @@ -69,7 +81,22 @@ data class ProPaymentItem( val revokedUnixTsMs: Long, // sys_ms; 0 if not revoked val refundRequestedUnixTs: Long, // seconds; 0 if none val paymentId: String, // opaque; confidential -) +) { + /** Provider purchase instant (always set). */ + val purchased: Instant get() = Instant.ofEpochMilli(purchasedUnixTsMs) + /** When the payment was activated, or null if not yet activated. */ + val redeemed: Instant? get() = redeemedUnixTs.secondsToInstantOrNull() + /** Access expiry for this payment, or null if not activated. */ + val expiry: Instant? get() = expiryUnixTs.secondsToInstantOrNull() + /** Grace period beyond [expiry] before access is really lost. */ + val gracePeriodDuration: Duration get() = Duration.ofSeconds(gracePeriodDurationSeconds) + /** Deadline for a platform ("quick") refund, or null if not applicable. */ + val platformRefundExpiry: Instant? get() = platformRefundExpiryUnixTs.secondsToInstantOrNull() + /** When the payment was revoked, or null if not revoked. */ + val revoked: Instant? get() = revokedUnixTsMs.msToInstantOrNull() + /** When a refund was requested, or null if none. */ + val refundRequested: Instant? get() = refundRequestedUnixTs.secondsToInstantOrNull() +} /** Response to get-details. */ @Keep @@ -83,14 +110,24 @@ data class GetProDetailsResponse( val gracePeriodDurationSeconds: Long, val refundRequestedUnixTs: Long, // seconds; 0 if none val paymentsTotal: Int, -) : ProResponse +) : ProResponse { + /** Account-level access expiry (includes grace period), or null if never subscribed. */ + val expiry: Instant? get() = expiryUnixTs.secondsToInstantOrNull() + /** Grace period included in [expiry]. */ + val gracePeriodDuration: Duration get() = Duration.ofSeconds(gracePeriodDurationSeconds) + /** When a refund was requested, or null if none. */ + val refundRequested: Instant? get() = refundRequestedUnixTs.secondsToInstantOrNull() +} /** One revocation-list entry. */ @Keep data class ProRevocationItem( val revocationTagHex: String, // 32-byte opaque tag, hex val effectiveUnixTs: Long, // seconds; revoked only once client clock >= this -) +) { + /** The revocation only takes effect once the client clock reaches this instant. */ + val effective: Instant get() = Instant.ofEpochSecond(effectiveUnixTs) +} /** Response to get-revocations. */ @Keep From 2198644a6e1cde28a0249742074ff134837e8426 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 12:30:13 -0300 Subject: [PATCH 07/14] pro glue: get-details response is a clean, app-shaped @Serializable type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersedes the Instant/Duration *accessors* on the raw-long classes: the get-details fields themselves are now the app-facing types — Instant dates, Duration intervals, String slugs (plan/provider/status), no magic-int constants. Following the ProProof pattern, each class keeps a @Keep secondary constructor with the raw epoch-integer signature the C++ builds via NewObject (so the .cpp is untouched; verified BasicJavaClassInfo resolves the ctor by explicit signature), which converts once here at the glue boundary and maps the 0 "unset" sentinel to null. Types are @Serializable (epoch-millis Instant / seconds Duration) so the app can persist them directly. ProResponseHeader is now @Serializable too; its raw numeric `status` is kept but documented as redundant-with-errors / pending upstream removal — prefer isSuccess/errors. Revocations/proof/refund shapes are unchanged for now. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pro/ProBackendResponses.kt | 159 +++++++++++++----- 1 file changed, 115 insertions(+), 44 deletions(-) diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 53765d7..1442e59 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -1,19 +1,26 @@ package network.loki.messenger.libsession_util.pro import androidx.annotation.Keep +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.time.Duration import java.time.Instant /** - * Kotlin mirrors of the `session::pro_backend` request/response structs. libsession-util owns both - * request construction and response parsing (the single source of truth for the wire shape); these - * types are what the JNI layer hands back so the app never re-parses the JSON itself. + * Kotlin mirrors of the `session::pro_backend` response structs. libsession-util owns request + * construction and response parsing (the single source of truth for the wire shape); these types are + * what the glue hands the app so it never re-parses the wire itself. * - * The JNI boundary can only carry primitives efficiently, so the constructor fields are raw epoch - * integers (`...UnixTs` = whole **unix seconds**, `...UnixTsMs` = **unix milliseconds**; duration - * fields are **seconds**; `0` means "not set"). Consumers should prefer the typed `Instant`/`Duration` - * accessors below instead of the raw longs — the epoch→datetime conversion (and the `0 → null` "unset" - * mapping) happens once, here at the glue boundary, rather than being repeated at every call site. + * These are the app-facing shapes: dates are [Instant], intervals are [Duration], and opaque codes + * (plan/provider/status) stay [String] slugs — no magic-int constants. Because the JNI boundary can + * only marshal primitives, each type keeps a `@Keep` secondary constructor taking the raw epoch + * integers (the shape the C++ builds via `NewObject`); it converts once — here at the glue boundary — + * into the typed primary constructor, mapping the `0` "unset" sentinel to null. The types are + * `@Serializable` so the app can persist them directly (e.g. its SQLite pro-details cache). */ /** Epoch **seconds** as an [Instant], or null when unset (`0`). */ @@ -24,6 +31,20 @@ private fun Long.secondsToInstantOrNull(): Instant? = private fun Long.msToInstantOrNull(): Instant? = if (this == 0L) null else Instant.ofEpochMilli(this) +/** Serializes an [Instant] as epoch-millis, for the app's local persistence of these types. */ +private object InstantAsEpochMillisSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.toEpochMilli()) + override fun deserialize(decoder: Decoder): Instant = Instant.ofEpochMilli(decoder.decodeLong()) +} + +/** Serializes a [Duration] as whole seconds. */ +private object DurationAsSecondsSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Duration", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: Duration) = encoder.encodeLong(value.seconds) + override fun deserialize(decoder: Decoder): Duration = Duration.ofSeconds(decoder.decodeLong()) +} + /** Route + content-type + body for a request to POST to the Pro backend (from the `*Request` builders). */ @Keep data class ProRequest( @@ -43,9 +64,15 @@ data class ProviderUrls( ) /** - * Common response header. `status` is 0 for success (or a request-specific status enum, e.g. - * add-payment); a non-empty [errors] means the parse failed or was partial — always check first. + * Common response header. A non-empty [errors] means the parse failed or was partial — always check + * [isSuccess] first. + * + * NOTE: [status] is libsession's raw numeric status code, whose meaning varies by endpoint (a generic + * success/error flag for most responses, an add-payment outcome enum for add-payment). It is redundant + * with [errors] for everything except add-payment and is pending removal upstream — prefer [isSuccess] + * / [errors]; don't build new logic on the raw int. */ +@Serializable @Keep data class ProResponseHeader( val status: Int, @@ -67,56 +94,100 @@ data class ProProofResponse( ) : ProResponse /** One payment/subscription record from get-details. */ +@Serializable @Keep data class ProPaymentItem( - val status: String, // opaque status code: unredeemed/redeemed/expired/revoked - val plan: String, // period code, e.g. "1m"/"3m"/"1y"; opaque - val paymentProvider: String, // provider slug, e.g. "google_play"; opaque + val status: String, // opaque per-payment status slug: unredeemed/redeemed/expired/revoked + val plan: String, // period-code slug, e.g. "1m"/"3m"/"1y" + val paymentProvider: String, // provider slug, e.g. "google_play" val autoRenewing: Boolean, - val purchasedUnixTsMs: Long, // sys_ms (provider purchase instant); always set - val redeemedUnixTs: Long, // seconds; 0 if not activated - val expiryUnixTs: Long, // seconds; 0 if not activated - val gracePeriodDurationSeconds: Long, - val platformRefundExpiryUnixTs: Long, // seconds - val revokedUnixTsMs: Long, // sys_ms; 0 if not revoked - val refundRequestedUnixTs: Long, // seconds; 0 if none + @Serializable(with = InstantAsEpochMillisSerializer::class) + val purchased: Instant, // provider purchase instant; always set + @Serializable(with = InstantAsEpochMillisSerializer::class) + val redeemed: Instant?, // when activated; null if not activated + @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 + @Serializable(with = InstantAsEpochMillisSerializer::class) + val platformRefundExpiry: Instant?, // deadline for a platform ("quick") refund; null if n/a + @Serializable(with = InstantAsEpochMillisSerializer::class) + val revoked: Instant?, // when revoked; null if not revoked + @Serializable(with = InstantAsEpochMillisSerializer::class) + val refundRequested: Instant?, // when a refund was requested; null if none val paymentId: String, // opaque; confidential ) { - /** Provider purchase instant (always set). */ - val purchased: Instant get() = Instant.ofEpochMilli(purchasedUnixTsMs) - /** When the payment was activated, or null if not yet activated. */ - val redeemed: Instant? get() = redeemedUnixTs.secondsToInstantOrNull() - /** Access expiry for this payment, or null if not activated. */ - val expiry: Instant? get() = expiryUnixTs.secondsToInstantOrNull() - /** Grace period beyond [expiry] before access is really lost. */ - val gracePeriodDuration: Duration get() = Duration.ofSeconds(gracePeriodDurationSeconds) - /** Deadline for a platform ("quick") refund, or null if not applicable. */ - val platformRefundExpiry: Instant? get() = platformRefundExpiryUnixTs.secondsToInstantOrNull() - /** When the payment was revoked, or null if not revoked. */ - val revoked: Instant? get() = revokedUnixTsMs.msToInstantOrNull() - /** When a refund was requested, or null if none. */ - val refundRequested: Instant? get() = refundRequestedUnixTs.secondsToInstantOrNull() + /** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */ + @Keep + constructor( + status: String, + plan: String, + paymentProvider: String, + autoRenewing: Boolean, + purchasedUnixTsMs: Long, + redeemedUnixTs: Long, + expiryUnixTs: Long, + gracePeriodDurationSeconds: Long, + platformRefundExpiryUnixTs: Long, + revokedUnixTsMs: Long, + refundRequestedUnixTs: Long, + paymentId: String, + ) : this( + status = status, + plan = plan, + paymentProvider = paymentProvider, + autoRenewing = autoRenewing, + purchased = Instant.ofEpochMilli(purchasedUnixTsMs), + redeemed = redeemedUnixTs.secondsToInstantOrNull(), + expiry = expiryUnixTs.secondsToInstantOrNull(), + gracePeriod = Duration.ofSeconds(gracePeriodDurationSeconds), + platformRefundExpiry = platformRefundExpiryUnixTs.secondsToInstantOrNull(), + revoked = revokedUnixTsMs.msToInstantOrNull(), + refundRequested = refundRequestedUnixTs.secondsToInstantOrNull(), + paymentId = paymentId, + ) } /** Response to get-details. */ +@Serializable @Keep data class GetProDetailsResponse( override val header: ProResponseHeader, val items: List, - val userStatus: String, // opaque status code: never/active/expired + val userStatus: String, // opaque account-status slug: never/active/expired val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT val autoRenewing: Boolean, - val expiryUnixTs: Long, // seconds; includes grace period; may be in the past - val gracePeriodDurationSeconds: Long, - val refundRequestedUnixTs: Long, // seconds; 0 if none + @Serializable(with = InstantAsEpochMillisSerializer::class) + val expiry: Instant?, // account access expiry (incl. grace); null if never subscribed + @Serializable(with = DurationAsSecondsSerializer::class) + val gracePeriod: Duration, // grace included in [expiry] + @Serializable(with = InstantAsEpochMillisSerializer::class) + val refundRequested: Instant?, // when a refund was requested; null if none val paymentsTotal: Int, ) : ProResponse { - /** Account-level access expiry (includes grace period), or null if never subscribed. */ - val expiry: Instant? get() = expiryUnixTs.secondsToInstantOrNull() - /** Grace period included in [expiry]. */ - val gracePeriodDuration: Duration get() = Duration.ofSeconds(gracePeriodDurationSeconds) - /** When a refund was requested, or null if none. */ - val refundRequested: Instant? get() = refundRequestedUnixTs.secondsToInstantOrNull() + /** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */ + @Keep + constructor( + header: ProResponseHeader, + items: List, + userStatus: String, + errorReport: Int, + autoRenewing: Boolean, + expiryUnixTs: Long, + gracePeriodDurationSeconds: Long, + refundRequestedUnixTs: Long, + paymentsTotal: Int, + ) : this( + header = header, + items = items, + userStatus = userStatus, + errorReport = errorReport, + autoRenewing = autoRenewing, + expiry = expiryUnixTs.secondsToInstantOrNull(), + gracePeriod = Duration.ofSeconds(gracePeriodDurationSeconds), + refundRequested = refundRequestedUnixTs.secondsToInstantOrNull(), + paymentsTotal = paymentsTotal, + ) } /** One revocation-list entry. */ From 8593314f383a80884a2aa35f24846733c09b066b Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 16:16:57 -0300 Subject: [PATCH 08/14] pro glue: re-pin a9e790a8 + Delta #12 response envelope Re-pin libsession submodule 418cdaba -> a9e790a8 (Delta #12: response envelope is now a status enum + optional error_code slug + optional single diagnostic error, replacing the int status + errors[] array). - serialize_response_header reads the renamed C++ ResponseBase: status enum (marshalled as its ordinal), error_code / error (std::optional -> nullable jstring); no more errors[] iteration. - ProResponseHeader: status is the closed ProResponseStatus enum (Ok/Fail/ Error, ordinals matching the C enum) + nullable errorCode/error; isSuccess == status Ok. @Keep secondary ctor maps the JNI ordinal to the enum. Response-side app consumers (ProApi error handling, already-redeemed removal, error_code->Crowdin mapping) follow in the app repo. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 17 +++++++--- .../pro/ProBackendResponses.kt | 34 ++++++++++++++----- libsession-util | 2 +- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 5aa0925..e78aa98 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -23,12 +23,21 @@ std::span string_to_span(std::string_view s) { // --- struct -> Kotlin marshalling helpers --- -JavaLocalRef serialize_response_header(JNIEnv* env, const pb::Response& r) { +JavaLocalRef serialize_response_header(JNIEnv* env, const pb::ResponseBase& r) { + // Delta #12: status is now an enum (Ok/Fail/Error), plus an optional machine slug (error_code) + // and an optional English diagnostic (error); no more errors[] array. We marshal status as its + // ordinal (int) — the Kotlin @Keep ctor maps it to the ProResponseStatus enum. static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProResponseHeader", - "(ILjava/util/List;)V"); - JavaLocalRef errors(env, jstring_list_from_collection(env, r.errors)); + "(ILjava/lang/String;Ljava/lang/String;)V"); + auto sv = [](const std::optional& o) -> std::optional { + if (o) + return std::string_view{*o}; + return std::nullopt; + }; return {env, env->NewObject(cls.java_class, cls.constructor, - static_cast(r.status), errors.get())}; + static_cast(r.status), + jstring_from_optional(env, sv(r.error_code)).get(), + jstring_from_optional(env, sv(r.error)).get())}; } jobject serialize_pro_request(JNIEnv* env, const pb::ProRequest& r) { diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 1442e59..1547e3e 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -64,21 +64,37 @@ data class ProviderUrls( ) /** - * Common response header. A non-empty [errors] means the parse failed or was partial — always check - * [isSuccess] first. + * Outcome category for a Pro backend response (Delta #12). CLOSED set: [Ok] = success (payload fields + * set); [Fail] = rejected on client input / a precondition; [Error] = backend fault (the same request + * may succeed later, i.e. retryable). Ordinals match the C `SESSION_PRO_BACKEND_RESPONSE_STATUS` enum. + */ +@Serializable +enum class ProResponseStatus { Ok, Fail, Error } + +/** + * Common response header (Delta #12). Check [isSuccess] first. * - * NOTE: [status] is libsession's raw numeric status code, whose meaning varies by endpoint (a generic - * success/error flag for most responses, an add-payment outcome enum for add-payment). It is redundant - * with [errors] for everything except add-payment and is pending removal upstream — prefer [isSuccess] - * / [errors]; don't build new logic on the raw int. + * On non-[ProResponseStatus.Ok]: [errorCode] is a stable machine-readable slug (spec §5.1) — map known + * ones to a localized (Crowdin) string; an unknown slug is forward-compatible (falls through). [error] is + * an English diagnostic — NOT user-facing text; show it only when the slug has no i18n entry at all, and + * it's always safe to log. Both are null on success. */ @Serializable @Keep data class ProResponseHeader( - val status: Int, - val errors: List, + val status: ProResponseStatus, + val errorCode: String?, + val error: String?, ) { - val isSuccess: Boolean get() = errors.isEmpty() + val isSuccess: Boolean get() = status == ProResponseStatus.Ok + + /** JNI-facing constructor: status as the C enum ordinal, nullable slug + diagnostic. */ + @Keep + constructor(statusOrdinal: Int, errorCode: String?, error: String?) : this( + status = ProResponseStatus.values().getOrElse(statusOrdinal) { ProResponseStatus.Error }, + errorCode = errorCode, + error = error, + ) } /** Common interface for the parsed Pro backend responses; check [header] before reading the data. */ diff --git a/libsession-util b/libsession-util index 418cdab..a9e790a 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 418cdabad62c1a11ab22298f5682b7dd2956952e +Subproject commit a9e790a8049484b844e8bd3104e53980e9679120 From b5c03ed348f834679108e6575dde538cfbacb463 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 20 Jul 2026 23:47:04 -0300 Subject: [PATCH 09/14] Pro: wrap visible_platforms() (JNI + Kotlin); re-pin libsession to c6e5a210 Add BackendRequests.visiblePlatforms() returning the purchasable provider slugs (JNI over session::pro_backend::visible_platforms()). Bumps the libsession-util submodule a9e790a8 -> c6e5a210 (adds visible_platforms; includes Delta #13, client no-op). Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 22 +++++++++++++++++++ .../libsession_util/pro/BackendRequests.kt | 7 ++++++ libsession-util | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index e78aa98..3f29d03 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -304,3 +304,25 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_proBackendPubKe return env->NewStringUTF(hex.c_str()); }); } + +// --- Visible (purchasable) payment-provider slugs (single source of truth in libsession) --- + +extern "C" +JNIEXPORT jobjectArray JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_visiblePlatforms( + JNIEnv* env, jobject) { + return run_catching_cxx_exception_or_throws(env, [=]() -> jobjectArray { + auto platforms = pb::visible_platforms(); // std::span + jclass string_cls = env->FindClass("java/lang/String"); + jobjectArray arr = env->NewObjectArray( + static_cast(platforms.size()), string_cls, nullptr); + jsize i = 0; + for (auto slug : platforms) { + std::string s(slug); + jstring js = env->NewStringUTF(s.c_str()); + env->SetObjectArrayElement(arr, i++, js); + env->DeleteLocalRef(js); + } + return arr; + }); +} diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index e0d9c95..5ba53b3 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -58,6 +58,13 @@ object BackendRequests : LibSessionUtilCApi() { external fun providerUrls(providerCode: String): ProviderUrls? + /** + * The purchasable payment-provider slugs to surface to users (single source of truth in libsession; + * excludes non-purchasable providers like `rangeproof`). Order is not significant — the client + * applies its own ordering and skips slugs it has no display translation for. + */ + external fun visiblePlatforms(): Array + // --- Backend identity (single source of truth in libsession; read these, don't hard-code) --- /** The Session Pro backend base URL (overridable prod/default), e.g. "https://pro.session.codes". */ diff --git a/libsession-util b/libsession-util index a9e790a..c6e5a21 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit a9e790a8049484b844e8bd3104e53980e9679120 +Subproject commit c6e5a2107b6c8adc6fba9afc6bb92f2e77101249 From f9dee956c1907624fcb08bd208c18eb422963913 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Wed, 22 Jul 2026 22:21:49 -0300 Subject: [PATCH 10/14] Absorb get_pro_details -> get_pro_status split (libsession Delta #15) Re-pin libsession-util c6e5a21 -> 3f8aace0 and absorb the endpoint split: get_pro_details is replaced by get_pro_status (the light "am I Pro?" call) and a separate library-only get_payment_details history endpoint (not wired). - JNI (pro_backend.cpp): build the get_pro_status request via pro_status_request (drops the count param) and parse via parse_pro_status; the response now emits a single optional latest_payment (has-flag + nullable item) instead of an items[] list + payments_total. ProviderUrls -> ProviderURLs (C++ rename; fields now optional). plan is now a parsed ProPlanPeriod, rendered back to the canonical ""/"lifetime" slug the app still consumes as a String. Fixed a latent ResponseBase.errors reference (Delta #12 removed it) to use operator bool. - Kotlin: GetProDetailsResponse -> GetProStatusResponse (single latestPayment + hasLatestPayment flag, no items/paymentsTotal); buildGetProDetailsRequest -> buildGetProStatusRequest (no count); parsePaymentDetailsResponse -> parseProStatusResponse. Verified by recompiling the glue pro_backend.cpp translation unit against the new headers (cmake/ninja, arm64-v8a): builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 58 ++++++++++++------- .../libsession_util/pro/BackendRequests.kt | 5 +- .../pro/ProBackendResponses.kt | 24 ++++---- libsession-util | 2 +- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 3f29d03..87d900a 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -49,7 +49,7 @@ jobject serialize_pro_request(JNIEnv* env, const pb::ProRequest& r) { jstring_from_optional(env, std::string_view(r.data)).get()); } -JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderUrls& u) { +JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderURLs& u) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProviderUrls", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); return {env, env->NewObject(cls.java_class, cls.constructor, @@ -60,12 +60,28 @@ JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderUrl jstring_from_optional(env, u.cancel_subscription_url).get())}; } +// Delta #14/#15: `plan` is now a parsed ProPlanPeriod (count + unit) rather than a raw slug. Render it +// back to the canonical "" slug (or "lifetime") the Kotlin/app layer still consumes as a +// String, so the app-facing shape is unchanged. +std::string plan_to_string(const pb::ProPlanPeriod& plan) { + switch (plan.unit) { + case pb::ProPlanUnit::second: return std::to_string(plan.count) + "s"; + case pb::ProPlanUnit::day: return std::to_string(plan.count) + "d"; + case pb::ProPlanUnit::week: return std::to_string(plan.count) + "w"; + case pb::ProPlanUnit::month: return std::to_string(plan.count) + "m"; + case pb::ProPlanUnit::year: return std::to_string(plan.count) + "y"; + case pb::ProPlanUnit::lifetime: return "lifetime"; + } + return ""; +} + JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentItem& it) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProPaymentItem", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); + auto plan = plan_to_string(it.plan); return {env, env->NewObject(cls.java_class, cls.constructor, jstring_from_optional(env, std::string_view(it.status)).get(), // opaque status code string - jstring_from_optional(env, std::string_view(it.plan)).get(), + jstring_from_optional(env, std::string_view(plan)).get(), jstring_from_optional(env, std::string_view(it.payment_provider)).get(), static_cast(it.auto_renewing), static_cast(it.purchased_at.time_since_epoch().count()), // ms @@ -93,28 +109,31 @@ jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& re "Lnetwork/loki/messenger/libsession_util/pro/ProProof;)V"); auto header = serialize_response_header(env, resp); JavaLocalRef proof(env, nullptr); - if (resp.errors.empty()) + if (resp) // ResponseBase::operator bool: true iff status == Ok (proof populated on success) proof = cpp_to_java_proof(env, resp.proof); return env->NewObject(cls.java_class, cls.constructor, header.get(), proof.get()); } -jobject serialize_get_details_response(JNIEnv* env, const pb::GetProDetailsResponse& resp) { - static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProDetailsResponse", - "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/util/List;Ljava/lang/String;IZJJJI)V"); +jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& resp) { + // Delta #15: get_pro_details split into get_pro_status; the response now carries a single optional + // latest_payment (has-flag + nullable item) instead of an items[] list + payments_total. + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProStatusResponse", + "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/lang/String;Z" + "Lnetwork/loki/messenger/libsession_util/pro/ProPaymentItem;IZJJJ)V"); auto header = serialize_response_header(env, resp); - JavaLocalRef items(env, jlist_from_collection(env, resp.items, - [](JNIEnv* env, const pb::ProPaymentItem& it) -> std::optional> { - return serialize_payment_item(env, it); - })); + JavaLocalRef latest_payment(env, nullptr); + if (resp.latest_payment) + latest_payment = serialize_payment_item(env, *resp.latest_payment); return env->NewObject(cls.java_class, cls.constructor, - header.get(), items.get(), + header.get(), jstring_from_optional(env, std::string_view(resp.user_status)).get(), // opaque status code string + static_cast(resp.latest_payment.has_value()), + latest_payment.get(), static_cast(resp.error_report), static_cast(resp.auto_renewing), static_cast(resp.expiry_at.time_since_epoch().count()), static_cast(resp.grace_period_duration.count()), - static_cast(resp.refund_requested_at.time_since_epoch().count()), - static_cast(resp.payments_total)); + static_cast(resp.refund_requested_at.time_since_epoch().count())); } jobject serialize_revocations_response(JNIEnv* env, const pb::GetProRevocationsResponse& resp) { @@ -178,13 +197,12 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGeneratePr extern "C" JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProDetailsRequest( - JNIEnv* env, jobject, jbyteArray master_private_key, jlong now_seconds, jint count) { +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_buildGetProStatusRequest( + JNIEnv* env, jobject, jbyteArray master_private_key, jlong now_seconds) { return run_catching_cxx_exception_or_throws(env, [=]() { - auto req = pb::payment_details_request( + auto req = pb::pro_status_request( JavaByteArrayRef(env, master_private_key).get(), - std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}, - static_cast(count)); + std::chrono::sys_seconds{std::chrono::seconds(now_seconds)}); return serialize_pro_request(env, req); }); } @@ -241,11 +259,11 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseProProofRe extern "C" JNIEXPORT jobject JNICALL -Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parsePaymentDetailsResponse( +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_parseProStatusResponse( JNIEnv* env, jobject, jstring json) { return run_catching_cxx_exception_or_throws(env, [=]() { JavaStringRef json_ref(env, json); - return serialize_get_details_response(env, pb::parse_payment_details(json_ref.view())); + return serialize_pro_status_response(env, pb::parse_pro_status(json_ref.view())); }); } diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt index 5ba53b3..38703e0 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/BackendRequests.kt @@ -30,10 +30,9 @@ object BackendRequests : LibSessionUtilCApi() { nowSeconds: Long, ): ProRequest - external fun buildGetProDetailsRequest( + external fun buildGetProStatusRequest( masterPrivateKey: ByteArray, nowSeconds: Long, - count: Int, ): ProRequest external fun buildRefundRequest( @@ -50,7 +49,7 @@ object BackendRequests : LibSessionUtilCApi() { external fun parseAddPaymentResponse(json: String): ProProofResponse external fun parseProProofResponse(json: String): ProProofResponse - external fun parsePaymentDetailsResponse(json: String): GetProDetailsResponse + external fun parseProStatusResponse(json: String): GetProStatusResponse external fun parseRefundResponse(json: String): SetPaymentRefundRequestedResponse external fun parseRevocationsResponse(json: String): GetProRevocationsResponse diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 1547e3e..07fc5ef 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -20,7 +20,7 @@ import java.time.Instant * only marshal primitives, each type keeps a `@Keep` secondary constructor taking the raw epoch * integers (the shape the C++ builds via `NewObject`); it converts once — here at the glue boundary — * into the typed primary constructor, mapping the `0` "unset" sentinel to null. The types are - * `@Serializable` so the app can persist them directly (e.g. its SQLite pro-details cache). + * `@Serializable` so the app can persist them directly (e.g. its SQLite pro-status cache). */ /** Epoch **seconds** as an [Instant], or null when unset (`0`). */ @@ -109,7 +109,7 @@ data class ProProofResponse( val proof: ProProof?, ) : ProResponse -/** One payment/subscription record from get-details. */ +/** One payment/subscription record from get-pro-status. */ @Serializable @Keep data class ProPaymentItem( @@ -164,14 +164,18 @@ data class ProPaymentItem( ) } -/** Response to get-details. */ +/** + * Response to get-pro-status (endpoint `get_pro_status`, Delta #15 — the split-out "am I Pro?" call). + * Carries the account status plus its single most-recent payment; the full payment history is a + * separate (library-only) query and is not wired here. + */ @Serializable @Keep -data class GetProDetailsResponse( +data class GetProStatusResponse( override val header: ProResponseHeader, - val items: List, val userStatus: String, // opaque account-status slug: never/active/expired - val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_DETAILS_ERROR_REPORT + val latestPayment: ProPaymentItem?, // the single most-recent payment, or null when none + val errorReport: Int, // SESSION_PRO_BACKEND_GET_PRO_STATUS_ERROR_REPORT val autoRenewing: Boolean, @Serializable(with = InstantAsEpochMillisSerializer::class) val expiry: Instant?, // account access expiry (incl. grace); null if never subscribed @@ -179,30 +183,28 @@ data class GetProDetailsResponse( val gracePeriod: Duration, // grace included in [expiry] @Serializable(with = InstantAsEpochMillisSerializer::class) val refundRequested: Instant?, // when a refund was requested; null if none - val paymentsTotal: Int, ) : ProResponse { /** Raw-epoch constructor used by the JNI layer (see the file header); converts to the typed fields. */ @Keep constructor( header: ProResponseHeader, - items: List, userStatus: String, + hasLatestPayment: Boolean, + latestPayment: ProPaymentItem?, errorReport: Int, autoRenewing: Boolean, expiryUnixTs: Long, gracePeriodDurationSeconds: Long, refundRequestedUnixTs: Long, - paymentsTotal: Int, ) : this( header = header, - items = items, userStatus = userStatus, + latestPayment = if (hasLatestPayment) latestPayment else null, errorReport = errorReport, autoRenewing = autoRenewing, expiry = expiryUnixTs.secondsToInstantOrNull(), gracePeriod = Duration.ofSeconds(gracePeriodDurationSeconds), refundRequested = refundRequestedUnixTs.secondsToInstantOrNull(), - paymentsTotal = paymentsTotal, ) } diff --git a/libsession-util b/libsession-util index c6e5a21..3f8aace 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit c6e5a2107b6c8adc6fba9afc6bb92f2e77101249 +Subproject commit 3f8aace0ea118da3e3102959e4b7c991f25f4b53 From 2cd8c2a978796cb81894efe41e0ed6480e4b1bc8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 01:55:38 -0300 Subject: [PATCH 11/14] Emit plan as (planCount, planUnit) instead of a re-stringified slug The C API already exposes the parsed ProPlanPeriod (count + unit); stop re-stringifying it back to a compact "" slug that the app then re-parses. ProPaymentItem now carries planCount:Int + planUnit:String (lowercase unit name, matching the nodejs glue), so clients hold (count, unit) directly with no wire re-parsing and no canonicalization. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 26 +++++++++---------- .../pro/ProBackendResponses.kt | 9 ++++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 87d900a..a9b5fb2 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -60,16 +60,16 @@ JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderURL jstring_from_optional(env, u.cancel_subscription_url).get())}; } -// Delta #14/#15: `plan` is now a parsed ProPlanPeriod (count + unit) rather than a raw slug. Render it -// back to the canonical "" slug (or "lifetime") the Kotlin/app layer still consumes as a -// String, so the app-facing shape is unchanged. -std::string plan_to_string(const pb::ProPlanPeriod& plan) { - switch (plan.unit) { - case pb::ProPlanUnit::second: return std::to_string(plan.count) + "s"; - case pb::ProPlanUnit::day: return std::to_string(plan.count) + "d"; - case pb::ProPlanUnit::week: return std::to_string(plan.count) + "w"; - case pb::ProPlanUnit::month: return std::to_string(plan.count) + "m"; - case pb::ProPlanUnit::year: return std::to_string(plan.count) + "y"; +// Delta #14: `plan` is a parsed ProPlanPeriod (count + unit). We hand the app the structured pair — +// count as an int, unit as a lowercase name — so nothing downstream has to re-parse a slug (matching +// the nodejs glue's {planCount, planUnit}). The unit is preserved exactly, never canonicalized. +std::string_view plan_unit_to_string(pb::ProPlanUnit unit) { + switch (unit) { + case pb::ProPlanUnit::second: return "second"; + case pb::ProPlanUnit::day: return "day"; + case pb::ProPlanUnit::week: return "week"; + case pb::ProPlanUnit::month: return "month"; + case pb::ProPlanUnit::year: return "year"; case pb::ProPlanUnit::lifetime: return "lifetime"; } return ""; @@ -77,11 +77,11 @@ std::string plan_to_string(const pb::ProPlanPeriod& plan) { JavaLocalRef serialize_payment_item(JNIEnv* env, const pb::ProPaymentItem& it) { static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProPaymentItem", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); - auto plan = plan_to_string(it.plan); + "(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ZJJJJJJJLjava/lang/String;)V"); return {env, env->NewObject(cls.java_class, cls.constructor, jstring_from_optional(env, std::string_view(it.status)).get(), // opaque status code string - jstring_from_optional(env, std::string_view(plan)).get(), + static_cast(it.plan.count), + jstring_from_optional(env, plan_unit_to_string(it.plan.unit)).get(), jstring_from_optional(env, std::string_view(it.payment_provider)).get(), static_cast(it.auto_renewing), static_cast(it.purchased_at.time_since_epoch().count()), // ms diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 07fc5ef..63394b2 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -114,7 +114,8 @@ data class ProProofResponse( @Keep data class ProPaymentItem( val status: String, // opaque per-payment status slug: unredeemed/redeemed/expired/revoked - val plan: String, // period-code slug, e.g. "1m"/"3m"/"1y" + val planCount: Int, // parsed billing-period count; >= 1 for periodic units, 0 for "lifetime" + val planUnit: String, // parsed unit name: second/day/week/month/year/lifetime (never canonicalized) val paymentProvider: String, // provider slug, e.g. "google_play" val autoRenewing: Boolean, @Serializable(with = InstantAsEpochMillisSerializer::class) @@ -137,7 +138,8 @@ data class ProPaymentItem( @Keep constructor( status: String, - plan: String, + planCount: Int, + planUnit: String, paymentProvider: String, autoRenewing: Boolean, purchasedUnixTsMs: Long, @@ -150,7 +152,8 @@ data class ProPaymentItem( paymentId: String, ) : this( status = status, - plan = plan, + planCount = planCount, + planUnit = planUnit, paymentProvider = paymentProvider, autoRenewing = autoRenewing, purchased = Instant.ofEpochMilli(purchasedUnixTsMs), From 77725561873a0c2409805a6c8d252e836e74f592 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 14:51:05 -0300 Subject: [PATCH 12/14] =?UTF-8?q?Drop=20stale=20Delta-N=20spec=20citations?= =?UTF-8?q?;=20reference=20current=20=C2=A7=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wire spec (docs/pro-wire-protocol.md) dropped its Delta-N change-history annotations, so every 'Delta #N' citation was a dangling reference. Reworded each to the current section (envelope -> §5/§5.1/§5.2, plan grammar -> §1, get_pro_status -> §3.4) or dropped the parenthetical where there is no section. Verified all remaining pro-wire-protocol § references resolve. Co-Authored-By: Claude Opus 4.8 (1M context) --- library/src/main/cpp/pro_backend.cpp | 6 +++--- .../messenger/libsession_util/pro/ProBackendResponses.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index a9b5fb2..9b6e10a 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -24,7 +24,7 @@ std::span string_to_span(std::string_view s) { // --- struct -> Kotlin marshalling helpers --- JavaLocalRef serialize_response_header(JNIEnv* env, const pb::ResponseBase& r) { - // Delta #12: status is now an enum (Ok/Fail/Error), plus an optional machine slug (error_code) + // §5: status is an enum (Ok/Fail/Error), plus an optional machine slug (error_code, §5.1) // and an optional English diagnostic (error); no more errors[] array. We marshal status as its // ordinal (int) — the Kotlin @Keep ctor maps it to the ProResponseStatus enum. static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProResponseHeader", @@ -60,7 +60,7 @@ JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderURL jstring_from_optional(env, u.cancel_subscription_url).get())}; } -// Delta #14: `plan` is a parsed ProPlanPeriod (count + unit). We hand the app the structured pair — +// §1: `plan` is a parsed ProPlanPeriod (count + unit). We hand the app the structured pair — // count as an int, unit as a lowercase name — so nothing downstream has to re-parse a slug (matching // the nodejs glue's {planCount, planUnit}). The unit is preserved exactly, never canonicalized. std::string_view plan_unit_to_string(pb::ProPlanUnit unit) { @@ -115,7 +115,7 @@ jobject serialize_pro_proof_response(JNIEnv* env, const pb::ProProofResponse& re } jobject serialize_pro_status_response(JNIEnv* env, const pb::ProStatusResponse& resp) { - // Delta #15: get_pro_details split into get_pro_status; the response now carries a single optional + // §3.4: get_pro_status carries a single optional // latest_payment (has-flag + nullable item) instead of an items[] list + payments_total. static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/GetProStatusResponse", "(Lnetwork/loki/messenger/libsession_util/pro/ProResponseHeader;Ljava/lang/String;Z" diff --git a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt index 63394b2..0ee9508 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/pro/ProBackendResponses.kt @@ -64,7 +64,7 @@ data class ProviderUrls( ) /** - * Outcome category for a Pro backend response (Delta #12). CLOSED set: [Ok] = success (payload fields + * Outcome category for a Pro backend response (§5). CLOSED set: [Ok] = success (payload fields * set); [Fail] = rejected on client input / a precondition; [Error] = backend fault (the same request * may succeed later, i.e. retryable). Ordinals match the C `SESSION_PRO_BACKEND_RESPONSE_STATUS` enum. */ @@ -72,7 +72,7 @@ data class ProviderUrls( enum class ProResponseStatus { Ok, Fail, Error } /** - * Common response header (Delta #12). Check [isSuccess] first. + * Common response header (§5). Check [isSuccess] first. * * On non-[ProResponseStatus.Ok]: [errorCode] is a stable machine-readable slug (spec §5.1) — map known * ones to a localized (Crowdin) string; an unknown slug is forward-compatible (falls through). [error] is @@ -168,7 +168,7 @@ data class ProPaymentItem( } /** - * Response to get-pro-status (endpoint `get_pro_status`, Delta #15 — the split-out "am I Pro?" call). + * Response to get-pro-status (endpoint `get_pro_status`, §3.4 — the "am I Pro?" read). * Carries the account status plus its single most-recent payment; the full payment history is a * separate (library-only) query and is not wired here. */ From 0cf392317610770587e23fbeb6a1d8abb0209930 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 23 Jul 2026 23:05:10 -0300 Subject: [PATCH 13/14] Re-pin libsession-util to merged dev (d63deea5); restore canonical submodule URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refactor merged upstream (session-foundation/libsession-util dev, to become v1.7.0). Point the submodule back at the canonical session-foundation repo (undoing the WIP switch to the jagerman fork in b2f1430 — the target commit only exists on canonical) and re-pin off the now-orphaned WIP 3f8aace0 to the dev tip d63deea5, which includes the refactor plus Pro backend integration tests + CI. The pro API is identical to 3f8aace0 (only Delta-citation comments differ), so no glue code changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitmodules | 2 +- libsession-util | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6df0cbe..d6ca3b1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libsession-util"] path = libsession-util - url = https://github.com/jagerman/libsession-util.git + url = https://github.com/session-foundation/libsession-util.git diff --git a/libsession-util b/libsession-util index 3f8aace..d63deea 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit 3f8aace0ea118da3e3102959e4b7c991f25f4b53 +Subproject commit d63deea51c7bf399f1764d88fe207871fd6c4d97 From 1cdf0476ec0bd41988dd5b6a325bdaa898247dff Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 27 Jul 2026 21:29:24 -0300 Subject: [PATCH 14/14] Bump libsession-util pin d63deea5 -> 7e4d7641 (revocation retry/retain clamp) Pulls in the revocation-list retry_in/retain_for clamp (7d422cfd) now done in libsession's parse_revocations, plus its merge (#102) and the seed_payment obfuscated-account-id test fix. Lets clients drop their own client-side clamps. Co-Authored-By: Claude Opus 4.8 (1M context) --- libsession-util | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession-util b/libsession-util index d63deea..7e4d764 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit d63deea51c7bf399f1764d88fe207871fd6c4d97 +Subproject commit 7e4d7641a8243baa9ca6ec3a749f251fdd18488b