diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt index bb987d3b0f..bb1d973df2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanNonOriginating.kt @@ -15,6 +15,8 @@ import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_KEY import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_STORE_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatus +import org.thoughtcrime.securesms.pro.ProUrls +import org.thoughtcrime.securesms.pro.getPlatformDisplayName import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider @@ -24,6 +26,10 @@ import org.thoughtcrime.securesms.ui.theme.ThemeColors @Composable fun RefundPlanNonOriginating( subscription: ProStatus.Active.WithPlan, + /// Whether the store's own quick-refund window is still open. Decides all three of the button, the + /// body copy and the link, exactly as it does on the originating screen — while the store will take + /// the request it is sent there, and once it will not, only Session can action it. + isQuickRefund: Boolean, sendCommand: (ProSettingsViewModel.Commands) -> Unit, onBack: () -> Unit, ){ @@ -34,18 +40,42 @@ fun RefundPlanNonOriginating( disabled = true, onBack = onBack, headerTitle = stringResource(R.string.proRefundDescription), - buttonText = Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, subscription.providerData.platform) - .format().toString(), + buttonText = if (isQuickRefund) + // See RefundPlanScreen: "Google Play Store", not "Google". + Phrase.from(context.getText(R.string.openPlatformWebsite)) + .put(PLATFORM_KEY, subscription.providerData.getPlatformDisplayName()) + .format().toString() + else stringResource(R.string.requestRefund), dangerButton = true, onButtonClick = { - sendCommand(ShowOpenUrlDialog(subscription.providerData.refundSupportUrl)) + // The store route's url is libsession's, read through `providerUrls` — it owns the + // per-provider table and says so. Note the slot: for Google Play its `refund_support_url` + // IS the Session short link that redirects into the Play store, so the value we want for + // the window-OPEN route sits under libsession's "support" name. The window-closed route + // uses `ProUrls.SUPPORT`, which reads libsession's `url_pro_support`. + // + // No provider check on top of the window: `providerData` is already the ORIGINATING + // provider's table, so an Apple plan yields Apple's own refund page here rather than the + // Play-store link. Gating on the provider as well would replace that correct page with + // Session's form. + sendCommand( + ShowOpenUrlDialog( + if (isQuickRefund) subscription.providerData.refundSupportUrl + else ProUrls.SUPPORT + ) + ) }, contentTitle = Phrase.from(context.getText(R.string.proRefunding)) .format().toString(), - contentDescription = Phrase.from(context.getText(R.string.proPlanPlatformRefund)) + // Past the window the request is Session's to handle, and the copy has to say so — this is the + // same sentence the originating screen shows, with the non-originating premise kept. + contentDescription = if (isQuickRefund) + Phrase.from(context.getText(R.string.proPlanPlatformRefund)) + .put(PLATFORM_STORE_KEY, subscription.providerData.store) + .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) + .format() + else Phrase.from(context.getText(R.string.proPlanPlatformRefundLong)) .put(PLATFORM_STORE_KEY, subscription.providerData.store) - .put(PLATFORM_ACCOUNT_KEY, subscription.providerData.platformAccount) .format(), linkCellsInfo = stringResource(R.string.refundRequestOptions), linkCells = listOf( @@ -88,6 +118,7 @@ private fun PreviewUpdatePlan( val context = LocalContext.current RefundPlanNonOriginating ( subscription = previewAutoRenewingApple, + isQuickRefund = true, sendCommand = {}, onBack = {}, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt index 9349b32977..c3ea7d486a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/RefundPlanScreen.kt @@ -20,6 +20,8 @@ import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.PLATFORM_KEY import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatus +import org.thoughtcrime.securesms.pro.ProUrls +import org.thoughtcrime.securesms.pro.getPlatformDisplayName import org.thoughtcrime.securesms.pro.isFromAnotherPlatform import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.ui.qaTag @@ -66,6 +68,10 @@ fun RefundPlanScreen( || !refundData.hasValidSubscription -> RefundPlanNonOriginating( subscription = activePlan, + // The window governs this screen too, and it was resolved right here — this branch + // simply never passed it on, so one screen served both the <48h and >48h states with + // the former's button and the latter's link. + isQuickRefund = refundData.isQuickRefund, sendCommand = viewModel::onCommand, onBack = onBack, ) @@ -98,16 +104,33 @@ fun RefundPlan( disabled = true, onBack = onBack, buttonText = if(isQuickRefund) Phrase.from(context.getText(R.string.openPlatformWebsite)) - .put(PLATFORM_KEY, data.providerData.platform) + // `getPlatformDisplayName`, not `platform`: the designs spell the rule out - Apple reads + // "Apple" but our own store reads "Google Play Store", never "Google". iOS and Desktop + // both branch the same way; these two refund screens were the only sites still passing + // the raw platform name. + .put(PLATFORM_KEY, data.providerData.getPlatformDisplayName()) .format().toString() else stringResource(R.string.requestRefund), dangerButton = true, onButtonClick = { - if(isQuickRefund && !quickRefundUrl.isNullOrEmpty()){ - sendCommand(ShowOpenUrlDialog(quickRefundUrl)) - } else { - sendCommand(ShowOpenUrlDialog(data.providerData.refundSupportUrl)) - } + // Two Session-owned links, chosen on the window alone - not the provider's own + // `refund_platform_url`/`refund_support_url`. Being ours, the destinations can be + // repointed without a client release, and all three clients agree on them. + // + // The window is what decides who can act: while it is open the store takes the request, + // and once it closes only Session can, which is what this screen's copy promises. + // The store route's url is libsession's, read through `providerUrls` — it owns the + // per-provider table and says so. Note the slot: for Google Play its `refund_support_url` + // IS the Session short link that redirects into the Play store, so the value we want for + // the window-OPEN route sits under libsession's "support" name. The window-closed route + // uses `ProUrls.SUPPORT`, which reads libsession's `url_pro_support`. + // + // No provider gate here: this screen only ever shows a plan bought on this store. + sendCommand( + ShowOpenUrlDialog( + if (isQuickRefund) data.providerData.refundSupportUrl else ProUrls.SUPPORT + ) + ) }, title = stringResource(R.string.proRefundDescription), ){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProProofs.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProProofs.kt index 9014dad6a9..46c73fce8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProProofs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProProofs.kt @@ -6,11 +6,22 @@ import org.session.protos.SessionProtos /** * Copies values from a libsession ProProof into a protobuf-based ProProof. + * + * There is deliberately no version field. A proof's format is bound into its signature by the + * 16-byte domain prefix the signer selects (`ProProof_v0_____`), so the version was never part of + * the signed payload — carrying it alongside as a plain value proved nothing and could disagree + * with the format the signature actually attests to. Verification derives the format from the + * prefix instead, and libsession-util has dropped `ProProof::version` accordingly. + * + * Protobuf field tag 1 on `ProProof` is retired and must never be reused: peers built before the + * removal still put a `uint32` there, so a new field on tag 1 would be fed their stale version + * numbers. A peer that sends one is handled by libsession, which reports the proof as + * `ProStatus::UnsupportedVersion` and lets the message through without Pro content rather than + * dropping it. */ fun SessionProtos.ProProof.Builder.copyFromLibSession( proProof: ProProof -): SessionProtos.ProProof.Builder = setVersion(proProof.version) - .setExpiryUnixTs(proProof.expirySeconds) +): SessionProtos.ProProof.Builder = setExpiryUnixTs(proProof.expirySeconds) .setRevocationTag(ByteString.copyFrom(proProof.revocationTagHex.hexToByteArray())) .setRotatingPublicKey(ByteString.copyFrom(proProof.rotatingPubKeyHex.hexToByteArray())) .setSig(ByteString.copyFrom(proProof.signatureHex.hexToByteArray())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/pro/ProUrls.kt b/app/src/main/java/org/thoughtcrime/securesms/pro/ProUrls.kt index cdf9c62a22..6a4e17d084 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pro/ProUrls.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pro/ProUrls.kt @@ -1,20 +1,11 @@ package org.thoughtcrime.securesms.pro -/** - * The Pro destinations, mirroring libsession's URL registry - * (`session_protocol.cpp`, the `url_pro_*` fields). - * - * Copies rather than reads: 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. - * - * Being copies, they can drift from it, and they have — every value here was once wrong at the use site - * that needed it. They are defined together so that comparing this file against the registry is the whole - * check; correcting a single link where it happens to be used is what let them diverge one at a time. - */ +import network.loki.messenger.libsession_util.pro.BackendRequests + object ProUrls { - const val FAQ = "https://getsession.org/pro#faq" - const val PRIVACY_POLICY = "https://getsession.org/pro-privacy" - const val ROADMAP = "https://getsession.org/pro#roadmap" - const val SUPPORT = "https://getsession.org/pro-support" - const val TERMS_OF_SERVICE = "https://getsession.org/pro-terms" + val FAQ: String get() = BackendRequests.proUrls().faq + val PRIVACY_POLICY: String get() = BackendRequests.proUrls().privacyPolicy + val ROADMAP: String get() = BackendRequests.proUrls().roadmap + val SUPPORT: String get() = BackendRequests.proUrls().support + val TERMS_OF_SERVICE: String get() = BackendRequests.proUrls().termsOfService } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae22a8c3bb..aa73cabd63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ kotlinxImmutableVersion = "0.5.1" kryoVersion = "5.6.2" kspVersion = "2.3.11" legacySupportV13Version = "1.0.0" -libsessionUtilAndroidVersion = "1.1.0-49-g5dbfffc" +libsessionUtilAndroidVersion = "1.1.0-53-gc8906a1" media3ExoplayerVersion = "1.10.0" mockitoCoreVersion = "5.23.0" navVersion = "2.9.8"