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
This commit is contained in:
spacedandy-agents-simplex committed 2026-10-20 03:29:19 +00:00
1 parent 55d69781e8
commit 5dc5da61b2
4 files changed
+40

No files matched your search

@@ -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
+10
View File
@@ -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.
---
@@ -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()
+26
View File
@@ -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.