From 5dc5da61b24e46ce1487e07ffb78fcbfa5cacb17 Mon Sep 17 00:00:00 2001 From: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:29:19 +0000 Subject: [PATCH] docs: record that a badge subscription cannot be restored or bought again Restoring a subscription on a new device or under another profile fails for two independent reasons, the client's restore reading only unfinished transactions on iOS and the service answering any key but the one it credited receipt_used, and a lapsed iOS subscriber cannot subscribe again on the same install either. Both gaps files record it in their own format, and both restorePurchases() say why reading the store's entitlements alone would not help. Run: 010-subscription-lifecycle, slice S2c --- apps/ios/Shared/Views/Badges/BadgeStore.swift | 2 ++ apps/ios/product/gaps.md | 10 +++++++ .../simplex/common/views/badges/BadgeStore.kt | 2 ++ apps/multiplatform/product/gaps.md | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/apps/ios/Shared/Views/Badges/BadgeStore.swift b/apps/ios/Shared/Views/Badges/BadgeStore.swift index 30838b7787..37ba01b4e3 100644 --- a/apps/ios/Shared/Views/Badges/BadgeStore.swift +++ b/apps/ios/Shared/Views/Badges/BadgeStore.swift @@ -409,6 +409,8 @@ final class BadgeStore: ObservableObject { // Hands over as the device's own whatever the store lists, so a purchase no record here has is claimed by this profile. // Answers whether a badge is shown, or a purchase is being added or awaits approval, afterwards. + // Reading currentEntitlements would not restore a subscription on another install or profile: the service credited + // the payment to the purchase key of the install that bought it, and answers any other key receipt_used. func restorePurchases() async throws -> Bool { try await AppStore.sync() var handOverError: Error? = nil diff --git a/apps/ios/product/gaps.md b/apps/ios/product/gaps.md index 50d6bf2938..40c13d904d 100644 --- a/apps/ios/product/gaps.md +++ b/apps/ios/product/gaps.md @@ -62,3 +62,13 @@ The Haskell Store modules (`Store/Direct.hs`, `Store/Groups.hs`, `Store/Messages --- +## Badges: Store Purchases + +### GAP: A badge subscription cannot be restored or bought again +**Source:** [Shared/Views/Badges/BadgeStore.swift](../Shared/Views/Badges/BadgeStore.swift) +Restoring a badge subscription on a new device or under another profile does not work, for two independent reasons. `restorePurchases()` reads `Transaction.unfinished`, so a purchase that was already finished is never found. And the service has credited the payment to the purchase key of the install that bought it, so a new key is answered `receipt_used`. Fixing only the client would turn "Nothing to restore" into a refusal, which is worse. On the same install a lapsed subscriber cannot subscribe again either: the app's gate refuses (`subscribedBefore`, scanning `Transaction.all`), and core would not credit the new charge anyway, since Apple keeps the original transaction id and core answers the hand-over from the old credited record. + +**REC:** A transfer design, in the service and the client, for new devices and profiles. For the same install, a resume path in core that credits a later charge of a retired subscription to its own purchase, then a gate narrowed to it. + +--- + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt index 5446018ddc..842cac454b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt @@ -438,6 +438,8 @@ object BadgeStore { // Hands over as the device's own whatever the store lists, so a purchase no record here has is claimed by this profile. // Answers whether a badge is shown, or a purchase is being added or awaits approval, afterwards. + // Play's list does include the subscription, yet it is not restored on another install or profile: the service credited + // the payment to the purchase key of the install that bought it, and answers any other key receipt_used. suspend fun restorePurchases(): Boolean { var handOverError: Exception? = null val purchases = if (useBadgeTestProducts) emptyList() else platform.androidUnfinishedBadgePurchases() diff --git a/apps/multiplatform/product/gaps.md b/apps/multiplatform/product/gaps.md index 7e8f841775..fc7315a458 100644 --- a/apps/multiplatform/product/gaps.md +++ b/apps/multiplatform/product/gaps.md @@ -15,6 +15,7 @@ This document catalogs known gaps in the multiplatform codebase (Android and Des 7. [Desktop: Cryptor Not Implemented](#gap-07-desktop-cryptor-not-implemented) 8. [Desktop: App Link Registration](#gap-08-desktop-app-link-registration) 9. [Desktop: Emoji Picker Without Skin Tones and Localized Search](#gap-09-desktop-emoji-picker-without-skin-tones-and-localized-search) +10. [Badges: Restoring a Subscription on a New Device or Profile](#gap-10-badges-restoring-a-subscription-on-a-new-device-or-profile) --- @@ -349,3 +350,28 @@ Desktop users cannot pick a skin tone in the picker, and search in other languag 1. Keep the skin tone variants in the generated data, grouped by base emoji, and add a skin tone choice to the picker. 2. Add localized names (CLDR annotations) to the generated data for search. + +--- + +## GAP-10: Badges: Restoring a Subscription on a New Device or Profile + +**Severity:** Medium +**Category:** Feature / Payments +**Platforms:** Android + +### Description + +Play gives a resubscription a new purchase token, so subscribing again on the same install is credited as a new purchase. On a new device or under another profile, Restore finds the subscription in Play's list and hands it over, but the service has credited the payment to the purchase key of the install that bought it and answers the new key `receipt_used`. + +### Affected Locations + +- `common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeStore.kt` -- `restorePurchases()` +- `../../apps/simplex-badge-service/src/BadgeService/Service.hs` -- a credited receipt answered to another key + +### Impact + +A subscriber who moves to a new device or profile keeps paying but cannot show the badge there, and Restore reports a refusal. + +### Recommendation + +1. A transfer design, in the service and the client and shared with iOS, that moves a credited subscription to a new purchase key.