Commit Graph
6886 Commits
Author SHA1 Message Date
spaced4ndy 7ea7bfd053 update postgres schema 2026-10-22 21:32:29 +04:00
spaced4ndy c3deb5163b unfocus test 2026-10-22 21:13:41 +04:00
spacedandy-agents-simplex 9d947a983a tests: hold the move with no charge to grant, not with the purchase's store check 2026-10-22 17:11:22 +00:00
spacedandy-agents-simplex 814b98bb4d ios: choose a badge on the site, not in the app 2026-10-22 17:09:40 +00:00
spacedandy-agents-simplex eb2ed9d078 ios, android, desktop: open the badge page on the site again 2026-10-22 17:07:33 +00:00
spaced4ndy 9335eefdf8 fix api 2026-10-22 21:04:54 +04:00
spaced4ndy ee28a4fc83 fix compilation 2026-10-22 21:00:20 +04:00
spaced4ndy 2c8db2b293 core: make the badge subscription tests fail on the defects they name (#7798) 2026-10-22 16:51:24 +00:00
spacedandy-agents-simplex 04e316bec5 fixup! docs: tell the badge subscriptions story with tier changes and restore 2026-10-22 20:39:05 +04:00
spacedandy-agents-simplex 0ac50cd4c3 fixup! docs: describe badge tier changes and restore as the service handles them 2026-10-22 20:39:05 +04:00
spacedandy-agents-simplex a8c1b0e228 fixup! docs: tell the badge subscriptions story with tier changes and restore 2026-10-22 20:39:05 +04:00
spacedandy-agents-simplex e9f859759d fixup! docs: describe badge tier changes and restore as the service handles them 2026-10-22 20:39:05 +04:00
spacedandy-agents-simplex 5e2ac775ff docs: tell the badge subscriptions story with tier changes and restore
The summary now covers the whole lifecycle: buying, renewing, changing
tier, moving to another device or profile, and ending. The badge retires
as soon as the store says the subscription ended, a lapsed iOS subscriber
can resume the retired badge, an App Store raise is granted at once and
a lower tier at the renewal, and a Play plan change is a new purchase.
The known limits and the before-sale list follow the code.

The seven diagrams are updated, with stale file and line references
dropped, and two are added: a tier change and a restore. The page now
points at the diagram files instead of carrying copies of them.
2026-10-22 20:39:05 +04:00
spacedandy-agents-simplex a43a44f2ee docs: describe badge tier changes and restore as the service handles them
The RPC document splits a store tier change by store and direction: the
app never starts one, an App Store raise seen within a day of its charge
is sent as upgradeBadgeSubscription, every other change waits for the
renewal check, and a Play plan change is a new token, a new purchase.
transferBadge and its limits are checked against the handlers, and the
upgrades that are not built are marked so.

The overview admits what Restore discloses: the service links the two
purchase keys, a store receipt moves its subscription to whoever presents
it within the move limit, and a new badge or tier is shown as soon as it
is issued. The Android gap names what remains of Restore.
2026-10-22 20:39:05 +04:00
spaced4ndy 7a93b63385 badge-service: let a restore ask the store without waiting for the purchase's own check 2026-10-22 20:00:25 +04:00
spacedandy-agents-simplex ca4c70c93e core: test that an App Store change to a lower tier waits for the renewal
A Supporter charge handed over while Legend is shown is answered from the record and sends nothing, and the renewal check grants Supporter, shown once the Legend credential expires; a record an older build held for a lower tier settles with no request. Both fail if every change of tier is sent at once again.

Run: 016-tier-change-direction
2026-10-22 08:11:12 +00:00
spacedandy-agents-simplex a0ef455aea core: apply an App Store tier change at once only when it raises the tier
Apple keeps charging the old plan to the end of the period, so applying a downgrade at once took away a badge the subscriber had already paid for. A charge of another tier is now held and sent at once only from supporter to legend; a lower tier, or one that is not ranked (investor, unknown), is answered from the record and the renewal check grants it, as badges-rpc.md says. A record already held for such a change settles silently.

Run: 016-tier-change-direction
2026-10-22 07:24:53 +00:00
spacedandy-agents-simplex 0c901f9917 review: round 3 fixes
- R3-1: a tier change whose answer says the subscription ended is not credited, as a resume is not, so the credit's clearing of a stored ended never drops the one this answer carries.
- R3-2: the changeTier comment in two lines.

Run: 015-badge-fixes, review round 3
2026-10-21 21:37:26 +00:00
spacedandy-agents-simplex cb1134f0bb review: round 2 fixes
- R2-1: a granted tier change clears an ended state the device reported before it. A renewing report never replaces ended, and a charge of the new tier granted proves the subscription has not ended, so without this the worker retired the new tier's badge at its paid-through while the subscription renewed.
- R1-B3: test a tier change whose credential does not verify: the failure stays on the receipt record, not the purchase, and Supporter stays shown.
- R2-3: the refused tier change's test buys Supporter through an invoice, so it shows the refusal announces nothing.
- R2-2: the changeTier comment says what follows each way the record settles.

Run: 015-badge-fixes, review round 2
2026-10-21 21:14:49 +00:00
spacedandy-agents-simplex 5096b3b698 review: round 1 fixes
The App Store tier-change hold, reviewed for the first time:

- R1-B2: a tier change's credit now shows its purchase again (shown_badge_id) in the same transaction, as a resume's does, so a retire that ran while the change was with the service cannot leave a paid purchase unshown and never renewed.
- R1-B5: a tier change of a shown purchase is held whatever other receipt is held, as the worker serves it while the badge is shown; only a resume waits for other held receipts.
- R1-B3: test a tier change the store does not vouch for: refused in silence, the badge kept, and answered from the record after.
- R1-B8: the changeTier comment names when the record settles.

Run: 015-badge-fixes, review round 1
2026-10-21 20:33:29 +00:00
spacedandy-agents-simplex 4a9f851237 core: test that the newest issued badge credential is taken by its ledger row
After a tier change the newer issuance can end before the older one, so ordering issuances by period end would answer the superseded credential to the worker and to the resume and tier-change paths. Pin the row order: the test fails if the query goes back to period_end order.

Run: 015-badge-fixes, slice 3
2026-10-21 19:34:39 +00:00
spacedandy-agents-simplex 80ae767d38 core: test that every Play charge of a month-end subscription funds its month
Play reports only a period's end, and the service dates the charge a plan length before it, so month-end clipping could leave a charge funding less than its month. Walk a monthly and an annual Play plan anchored on 31 January through their renewals at the device's request moments, through the deployed Play verifier: each charge funds exactly its months, paid-through stays at most the grace behind Play's period end (three days at most in this walk), and no charge is left without a grant.

Run: 015-badge-fixes, slice 2
2026-10-21 19:13:44 +00:00
spacedandy-agents-simplex 3d3b410d69 core: tell the apps when a badge request banks a month without issuing it
A pass announced the badge state only when the balance start moved, so a charge granted onto running coverage, or a debit the service wrote, changed months left and paid-through without an event, and the apps kept showing the old renewal date until the screen was reopened. The pass now announces whenever the ledger's last entry is not the one it started from, which a moved balance start implies.

Run: 015-badge-fixes, slice 1
2026-10-21 18:53:41 +00:00
spacedandy-agents-simplex 03cc98f4db Merge branch 'master' into f/badge-subscriptions
# Conflicts:
#	apps/ios/SimpleX.xcodeproj/project.pbxproj
#	simplex-chat.cabal
#	src/Simplex/Chat/Store/SQLite/Migrations.hs
2026-10-21 18:24:09 +00:00
spaced4ndy 1bbbad99cb core, badge-service, ios, android: change badge tier in the store, and restore a subscription on another device (#7795) 2026-10-21 16:53:15 +00:00
EvgenyandEvgeny @ SimpleX Chat 3126d9b544 ios: set default proxy mode (#7790)
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-10-21 14:35:41 +01:00
spaced4ndy f8f64a3047 core, ios, android: explain a failed badge renewal and stop retrying a subscription the store forgot; tests (#7788) 2026-10-21 07:44:20 +00:00
spaced4ndy 9a1d3e4969 ci: test the Android app in an emulator on GitHub's runners (#7786) 2026-10-20 16:55:54 +00:00
spaced4ndy 5834179be6 ci: start app tests only by hand (#7777) 2026-10-20 16:48:47 +00:00
spaced4ndy 6cef6b9e60 badge-service: name the purchase each request came from in the log 2026-10-20 20:32:04 +04:00
spaced4ndy 696abf13ab badge-service: name the purchase each request came from in the log 2026-10-20 20:19:01 +04:00
spaced4ndy b6376b90c7 fix compilation 2026-10-20 19:26:06 +04:00
spaced4ndy a68eb91da1 badge-service: log each request's command and each store check's outcome 2026-10-20 19:16:36 +04:00
spaced4ndy ccf136ca13 core: let a dev build ask the badge service on an interval 2026-10-20 19:03:53 +04:00
spaced4ndy 3c61a3ced4 badge-service: credit test purchases and check the store during coverage in a dev deployment 2026-10-20 18:50:52 +04:00
spaced4ndy 5fd0b5e7fd docs: bound a badge move by the previous credential's expiry 2026-10-20 17:15:48 +04:00
SimpleX Chat fcef2ad008 7.1-beta.7: android 387, desktop 168, ios 363 v7.1.0-beta.7-armv7a v7.1.0-beta.7 2026-10-20 12:34:51 +00:00
spacedandy-agents-simplex 038f50ac30 core, ios, android, desktop: let a lapsed iOS subscriber subscribe again to the badge the profile retired
The iOS app refused any subscription to an Apple ID that ever subscribed, so a lapsed subscriber could never subscribe again in the app. Core now tells the apps each subscription's store reference (BadgeSubscription.transactionRef, optional in JSON), and the iOS gate opens a subscription card only when every earlier subscription of the Apple ID is the active profile's newest purchase, retired, of the card's badge: the one charge core resumes. Kotlin gets the model field only, as Play gives a resubscription a new token and needs no gate.

Run: 011-subscription-resume, slice S2b
2026-10-20 16:33:12 +04:00
spacedandy-agents-simplex 1d72c4c2e7 fixup! core: resume a retired Apple subscription when its store charges it again
Review round 1 fixes:
- R1-1: a resumable charge waiting behind another held receipt of the owner is answered held and left unfinished, rather than answered from the record and finished uncredited.
- R1-3: a resume shows the purchase again only with a credential that has not expired.
- R1-2, R1-8: tests of the resumable-charge rule and the hold's edge, of a held charge whose paid period ends before it is sent, and that a resumed profile shows the newest credential. The resume test is split in two, one environment each.
- R2-V1: a test comment that the reordered steps made false.

Run: 011-subscription-resume, review rounds 1 and 2
2026-10-20 16:33:12 +04:00
spacedandy-agents-simplex 6a00c55c7a core: resume a retired Apple subscription when its store charges it again
A same-install resubscription keeps Apple's original transaction id, so its hand-over found the old credited record and was answered from it with the old credential: the subscriber paid and got nothing. A credited record is now held again when a later charge of its subscription is handed over, the charge is not revoked, its paid period runs and it is dated less than a day ahead of this device's clock, and the record's purchase is its owner's newest, a subscription and not shown, with no other receipt held. The receipt worker then sends the purchase's issueBadge, and the service grants the charge as a renewal. A covered answer shows the purchase again with the newest credential and clears an ended state. A charge the service answers without crediting is deferred an hour and refused a day after its purchase date, so it holds the profile for about a day. A refused resume is remembered on the record (every credit, re-hold and resume clears credit_error), so the app's sweep does not hold the same charge again after its day.

Run: 011-subscription-resume, slice S2a
2026-10-20 16:33:12 +04:00
spacedandy-agents-simplex ed3e236fe9 core: retire on a device's ended report only once the paid period is over
StoreKit can report a subscription expired during Apple's billing retry, and Apple may then recover the charge, so retiring at once on that report left a paying user without the badge. The service's answered ended still retires at once, also while the ledger covers. A stored ended, which is how the device's report arrives, retires once paidThrough has passed.

Run: 011-subscription-resume, slice S1c
2026-10-20 16:33:09 +04:00
spacedandy-agents-simplex 5e642d49a3 core: split the badge request's lock from its body
The receipt worker's attempt already holds the owner's badge lock, which is not reentrant, so it cannot call requestBadgeIssue. The body becomes sendBadgeIssue, and both answer a service error as Left (code, retryAfter) instead of Nothing, so that caller can classify it as it does today. No behaviour change.

Run: 011-subscription-resume, slice S2-0
2026-10-20 15:38:12 +04:00
spacedandy-agents-simplex 0ea43ea59e fixup! core: send a badge request at once from a build for the dev badge service 2026-10-20 15:37:53 +04:00
spacedandy-agents-simplex c0619385e9 fixup! badge-service: let a dev deployment ask its mock store while coverage runs 2026-10-20 15:37:53 +04:00
spacedandy-agents-simplex e570748206 fixup! badge-service: let a dev deployment ask its mock store while coverage runs 2026-10-20 15:37:53 +04:00
spacedandy-agents-simplex 1193ae953f core: send a badge request at once from a build for the dev badge service
/_badge request <userId> sends the profile's badge request now, while
its badge is shown, and runs the badge pass on the answer, so a renewal
or an ended subscription can be watched by hand against the dev
service.

It is allowed only when ChatConfig's badgeDevRequests is set, which no
shipped build does and nothing sets at runtime: the dev-service branch
sets it beside its own badge service overrides. A request off the shared
expiry would tell requests apart, so every other build refuses it, and
it is refused from a paired remote controller.

Run: 011-subscription-tools, slice S3b
2026-10-20 15:37:53 +04:00
spacedandy-agents-simplex e116987e5b badge-service: let a dev deployment ask its mock store while coverage runs
[dev] check_store_during_coverage = on makes every issueBadge for a
subscription ask the dev mock, during paid coverage too, and bank the
month of a charge it can grant, issued once coverage reaches it. So a
renewal can be watched by hand without waiting a month, and a mock that
forgot the plan after a restart answers ended at once.

Off by default, and refused unless accept_unverified_store_receipts is
on, which is itself refused beside a real store: a real store has not
started its next charge while coverage runs, and asking spends the
quota every subscriber shares.

Run: 011-subscription-tools, slice S3a
2026-10-20 15:37:53 +04:00
spaced4ndy ff22df9cdc update plan 2026-10-20 15:27:18 +04:00
spaced4ndy 53683c415d shorter plan 2026-10-20 14:49:13 +04:00
spaced4ndy 77479516d2 transfer plan 2026-10-20 14:35:26 +04:00