From 6bfd117649d93bf9c8106860a1e1ebc2d5fbb3df Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 25 Aug 2026 10:00:54 +1000 Subject: [PATCH] feat: expose libsession's Pro URL registry to Kotlin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session-android hard-codes five Pro URLs in `ProUrls.kt`, and that file documents why: "the registry is a C struct of `const char*` with no accessor exposed to Kotlin, so consuming it directly would mean adding JNI surface for five constants." It also records the cost of copying — "every value here was once wrong at the use site that needed it." libsession owns these values. This removes the excuse. Adds `BackendRequests.proUrls(): ProUrls`, returning all eight `url_pro_*` fields as one object, in the same shape as the existing `providerUrls(slug)`: a `@Keep` data class, a `serialize_pro_urls` JNI helper mirroring `serialize_provider_urls`, and an `extern "C"` entry point beside it. **Glue-only — no C++ change was needed.** `SESSION_PROTOCOL_STRINGS` is already `extern const` in the public header `session/session_protocol.h`, so the JNI layer reads it directly and the vendored submodule is untouched. Worth noting in `ProUrls`: `privacyPolicy` and `termsOfService` are the **Pro** documents (`url_pro_privacy_policy` / `url_pro_terms_of_service`), not Session's general ones. The registry carries both pairs and they are different pages — the equivalent nodejs binding had exactly that bug (session-foundation/libsession-util-nodejs#69). Verified end to end, not just compiled: session-android built against this glue via the `session.libsession_util.project.path` override, with `ProUrls.SUPPORT` switched to `BackendRequests.proUrls().support`, and the full `@android @pro` suite passed 29/29 on a device — including the refund specs that open that URL. Co-Authored-By: Claude Opus 5 --- library/src/main/cpp/pro_backend.cpp | 31 +++++++++++++++++++ .../libsession_util/pro/BackendRequests.kt | 7 +++++ .../pro/ProBackendResponses.kt | 21 +++++++++++++ 3 files changed, 59 insertions(+) diff --git a/library/src/main/cpp/pro_backend.cpp b/library/src/main/cpp/pro_backend.cpp index 38775ab..020197a 100644 --- a/library/src/main/cpp/pro_backend.cpp +++ b/library/src/main/cpp/pro_backend.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "jni_utils.h" #include "util.h" @@ -60,6 +61,28 @@ JavaLocalRef serialize_provider_urls(JNIEnv* env, const pb::ProviderURL jstring_from_optional(env, u.cancel_subscription_url).get())}; } +/// The Pro URL registry as one Kotlin object. Reads `SESSION_PROTOCOL_STRINGS` directly - it is +/// declared `extern const` in the public C header, so no C++ surface had to be added for this. +/// +/// Field order MUST match `ProUrls`'s constructor. The two document urls are the `url_pro_*` ones: +/// the registry also carries generic `url_privacy_policy` / `url_terms_of_service`, which are +/// different pages and do not belong on a Pro object. +JavaLocalRef serialize_pro_urls(JNIEnv* env) { + static BasicJavaClassInfo cls(env, "network/loki/messenger/libsession_util/pro/ProUrls", + "(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"); + const auto& s = SESSION_PROTOCOL_STRINGS; + return {env, env->NewObject(cls.java_class, cls.constructor, + jstring_from_optional(env, std::string_view(s.url_pro_access_not_found)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_faq)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_page)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_privacy_policy)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_roadmap)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_support)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_terms_of_service)).get(), + jstring_from_optional(env, std::string_view(s.url_pro_upgrade)).get())}; +} + // §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. @@ -249,6 +272,14 @@ Java_network_loki_messenger_libsession_1util_pro_BackendRequests_providerUrls( }); } +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_pro_BackendRequests_proUrls(JNIEnv* env, jobject) { + return run_catching_cxx_exception_or_throws(env, [=]() -> jobject { + return serialize_pro_urls(env).release(); + }); +} + // --- Backend identity constants (single source of truth in libsession) --- extern "C" 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 2d029b1..c856082 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 @@ -40,6 +40,13 @@ object BackendRequests : LibSessionUtilCApi() { external fun providerUrls(providerCode: String): ProviderUrls? + /** + * The Pro URL registry. Identical for every user and owned by libsession, so read this rather than + * copying the values — a client-side copy of a libsession constant is a value that can silently + * drift from the one the other clients use. + */ + external fun proUrls(): ProUrls + /** * The purchasable payment-provider slugs to surface to users (single source of truth in libsession; * excludes non-purchasable providers like `stf`). Order is not significant — the client 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 78429d8..7ae3c50 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 @@ -53,6 +53,27 @@ data class ProRequest( val body: String, ) +/** + * The Pro URL registry — the `url_pro_*` fields of libsession's `SESSION_PROTOCOL_STRINGS`. + * + * Not per-provider and not translation data: these are identical for every user, which is why + * libsession owns them. Exposed as one object rather than eight calls, matching [ProviderUrls]. + * + * Note [privacyPolicy] and [termsOfService] are the **Pro** documents (`url_pro_*`), not Session's + * general ones — the registry carries both pairs and they are different pages. + */ +@Keep +data class ProUrls( + val accessNotFound: String, + val faq: String, + val page: String, + val privacyPolicy: String, + val roadmap: String, + val support: String, + val termsOfService: String, + val upgrade: String, +) + /** Per-provider support/management URLs — libsession is the source of truth for these. */ @Keep data class ProviderUrls(