Skip to content

fix: keep canceled subscriptions active until they expire - #1391

Merged
ovitrif merged 9 commits into
masterfrom
fix/subscription-canceled-status
Oct 5, 2026
Merged

ovitrif merged 9 commits into
masterfrom
fix/subscription-canceled-status

Conversation

@ovitrif

@ovitrif ovitrif commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1413
Twin: synonymdev/bitkit-ios#846

Description

  • Fixes a canceled subscription reading "Expired" while its paid period is still running: it now reads like an active one with an end date, STATUS "Active", list subtitle "Expires " and timing cell "EXPIRES ".
  • Lists a canceled subscription under ACTIVE until its paid period ends, so it counts toward the ACTIVE count and the monthly cost like an active one; it then moves to EXPIRED, dimmed, with timing "EXPIRED ".
  • Drives status, row subtitle, section, timing and the refresh timers from one predicate (active, or canceled with its last paid period still ahead), so they cannot disagree. A canceled subscription ends at its last paid period even when a fixed end date is later.
  • Refreshes the list and an open detail screen at the moment the paid period ends, so they flip without being reopened.
  • Hides the timing cell for a canceled subscription that has no end date.
  • Applies the same rule to a canceled subscription you created: while its last paid period is ahead it stays under CREATED with "Expires " (STATUS "Active", timing "EXPIRES "), and when that date passes it moves to EXPIRED, dimmed. A created subscription that was never canceled stays under CREATED as before; created subscriptions never count toward the monthly cost or the ACTIVE count.
  • Hides a canceled subscription from a blocked peer while its paid period runs, like an active one, so it no longer shows under ACTIVE or counts toward the monthly cost.
  • Leaves the detail footer as is: More Info only, no Cancel for a canceled subscription.

Out of Scope

  • SubscriptionsScreen.kt: the Renew button on the detail footer, tracked in Subscriptions: unbuilt controls and the swipe colour rule #1276.
  • PaykitSubscription.kt: lifecycle rules (isActive, isExpired, canCancel) are unchanged.
  • SubscriptionsScreen.kt: an expired subscription with a fixed end date still reads "Expires " in the list, as before.

Design

Subscription Details Renew is the state for a canceled subscription that has not expired yet, and the Subscriptions overview lists it under ACTIVE as "Expires ". The frames show STATUS "Active" and no "Canceled" label.

Preview

Screenshots use a local-only fixture that seeds payer and creator canceled subscriptions in memory (not part of this PR); no real Swipe To Cancel was run. On the same fixture an open detail of a canceled subscription, payer or creator side, switched from STATUS "Active" and "EXPIRES" to "Expired" and "EXPIRED" at the end of its paid period without leaving the screen, and its row moved to EXPIRED, with the counts and monthly cost updating.

Subscriptions you pay

Canceled, paid period not ended: list Canceled, paid period not ended: detail Expired: list Expired: detail

Subscriptions you created

Before the paid period ends: list After it ends: list Paid period not ended: detail Expired: detail

QA Notes

Journeys

  • updated cancel-and-delete.xml — after Swipe To Cancel the detail shows STATUS "Active", "EXPIRES " and no Cancel; back on the list the subscription is in ACTIVE with "Expires "; on the creator it stays under CREATED with the same subtitle until the paid period ends, then moves to EXPIRED. The added steps were not run end to end: the journey needs two Pubky identities on regtest

Manual Tests

N/A

Automated Checks

  • updated SubscriptionsScreenTest.kt — status, row subtitle, ACTIVE/CREATED/EXPIRED grouping for subscriptions you pay and ones you created, monthly cost, timing and the end-of-period refresh for a canceled subscription before and after its paid period ends
  • updated PaykitPaymentRequestRepoSubscriptionTest.kt — a canceled subscription from a blocked peer is hidden while its paid period runs and kept as history after it ends

@ovitrif ovitrif added this to the 2.7.0 milestone Sep 30, 2026
@ovitrif ovitrif self-assigned this Sep 30, 2026
@ovitrif
ovitrif force-pushed the fix/subscription-canceled-status branch from f20f41a to 972d4f5 Compare October 1, 2026 19:45
@ovitrif
ovitrif force-pushed the fix/subscription-canceled-status branch from 972d4f5 to c392424 Compare October 1, 2026 19:50
@ovitrif ovitrif removed this from the 2.7.0 milestone Oct 1, 2026
@ovitrif ovitrif changed the title fix: show canceled subscriptions as canceled fix: keep canceled subscriptions active until they expire Oct 1, 2026
@ovitrif
ovitrif marked this pull request as ready for review October 1, 2026 22:07
@ovitrif
ovitrif requested review from a team, coreyphillips and pwltr and removed request for a team October 1, 2026 22:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T22:11:03.013809Z c392424 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Changes how canceled subscriptions display and behave.

This should not merge until canceled fixed-end subscriptions stop appearing active beyond their last paid period.

Findings

  1. P1 Canceled subscription stays active too long ▶

Summary

This PR keeps canceled subscriptions in ACTIVE through their paid period and schedules list and detail refreshes at expiration.

  • It aligns status, timing, section placement, and monthly cost around a shared predicate.
  • The predicate can instead use a later fixed plan end when one exists, extending ACTIVE beyond the paid period.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Canceled subscription] --> B[expiryDate]
  B --> C{Future date?}
  C -->|Yes| D[ACTIVE status, section and cost]
  C -->|No| E[EXPIRED status and section]
  F[Fixed plan end] --> B
  G[Last paid-period end] --> B
Loading

Reviews (1) · Last reviewed commit: "fix: keep canceled subscriptions active ..."

Comment thread app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c392424fe8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread journeys/subscriptions/cancel-and-delete.xml Outdated
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from dcd8a5b (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 5e05970: a canceled subscription now stops running at its last paid period even when it has a later fixed end date (status, subtitle, section, monthly cost, timing and the refresh timer all use that date), with a test for it, and the journey's last step now reads "Navigate back" so it matches the iOS twin. Answers the two bot threads above.

@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed ae6525c: the paid-through treatment now applies only to subscriptions the user pays. A canceled subscription the user created stays in CREATED and reads as expired as before (status, timing, dimming), covered by a new test that failed on the previous head. It also never moved sections on Android, because ACTIVE and EXPIRED already filter on the payer role. Same change as the iOS twin.

@coreyphillips

Copy link
Copy Markdown
Contributor

Two independent reviews, nothing blocking a merge.

worth doing, does not block

  • Ended canceled row shows the fixed end date while detail shows the paid-through date (app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt:1070). For a subscription you pay that was canceled and has a fixed recurrence.endsAt later than its last paid period, the list row and the detail screen show different dates once the paid period ends. Before that moment the row reads "Expires Feb 1", which matches the detail. At Feb 1 the row moves to EXPIRED. rowSubtitleSpec then falls through to the else branch (SubscriptionsScreen.kt:1070), which uses recurrence.endsAt, so the row now reads "Expires June 1". The detail uses renewalText -> expiryDate() -> canceledPaidThrough(), so it reads "EXPIRED February 1". The row's date jumps later at the moment the subscription expires, and the description's claim that the two "cannot disagree" does not hold for this case. Before this PR, both sides used endsAt and agreed. Confirmed by tracing the fixture from the test canceled subscription stops running at its last paid period even with a later end date through rowSubtitleSpec(paidThrough). That test checks statusRes and expiryDate but not the row subtitle. This is non-blocking because the payer's own Cancel is hidden when endsAt is set (canCancel requires endsAt == null), so the state is only reachable if the counterparty cancels. A fix is to use canceledPaidThrough() ?: recurrence.endsAt in the else branch, or to call expiryDate() there for canceled subscriptions.

coreyphillips
coreyphillips previously approved these changes Oct 2, 2026
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coreyphillips Pushed fa6fb4c for your note on the ended canceled row:

the list row and the detail screen show different dates once the paid period ends

The row now uses the last paid period's end for a canceled subscription you pay, so it reads "Expires February 1" like the detail's "EXPIRED February 1" instead of jumping to the later end date. The test for that case now asserts the row subtitle before and after the paid period ends, and it failed on the previous head. A canceled subscription without a fixed end date still reads "Expired" in the row.

@ovitrif
ovitrif requested a review from coreyphillips October 2, 2026 09:36
coreyphillips
coreyphillips previously approved these changes Oct 2, 2026
…nceled-status

# Conflicts:
#	app/src/main/java/to/bitkit/ui/screens/subscriptions/SubscriptionsScreen.kt
@ovitrif

ovitrif commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed bd66ef7: merged master into the branch to resolve the conflict in the subscription transition timer, keeping master's acceptance-time handling and this PR's paid-through refresh. No visible behavior change. Related unit tests, detekt and the dev build pass locally.

@ovitrif
ovitrif requested a review from coreyphillips October 5, 2026 12:39
@coreyphillips

Copy link
Copy Markdown
Contributor

Two independent reviews, nothing blocking a merge.

worth doing, does not block

  • Canceled subscription with a blocked peer now shows under ACTIVE (app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt:944). A canceled subscription from a blocked peer that still has a paid period running now shows under ACTIVE and counts toward the monthly cost. An active subscription from a blocked peer stays hidden. The cause is the visibility filter in PaykitPaymentRequestRepo (around line 944). It hides blocked-peer subscriptions only when paidPeriods.isEmpty() || isProposalVisible(now) || isActive(now). A CANCELED subscription with paid periods fails all three checks, so it stays in the list. Before this PR it landed under EXPIRED as history. Now runsUntilPaidThrough puts it under ACTIVE with "Expires " and STATUS "Active". Blocking a peer does not change the lifecycle state: PaykitSdkService.blockPeer only records the peer state. So a user who cancels and then blocks the counterparty sees it as active, which is the very state the filter hides for ACTIVE_RECURRING ones. I confirmed this by reading the code, not on a device. A fix is to use runsUntilPaidThrough in the repo filter, which would mean moving or duplicating it outside the screen file. The other option is to state that this asymmetry is intended.
  • Journey expects the list while still on details (journeys/subscriptions/cancel-and-delete.xml:20). After Swipe To Cancel, hideSheet() reveals the detail screen that launched the sheet, but the new step immediately expects the ACTIVE list row and the next step tries to tap it. Following lines 15 through 19 leaves no such row on screen, so the added QA path cannot reach its detail assertion. Verify the updated detail state first, navigate back, then check and tap the row.

coreyphillips
coreyphillips previously approved these changes Oct 5, 2026
@ovitrif

ovitrif commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 4e451e7 for the creator-side listing and @coreyphillips' remaining notes:

Ended canceled row shows the fixed end date while detail shows the paid-through date

@coreyphillips Already fixed in fa6fb4c (SubscriptionsScreen.kt:1075); the row subtitle is asserted before and after the paid period in SubscriptionsScreenTest.kt, and it failed before the fix.

Canceled subscription with a blocked peer now shows under ACTIVE

@coreyphillips Fixed in f75355d: the blocked-peer filter in PaykitPaymentRequestRepo.kt now uses runsUntilPaidThrough, which moved next to PaykitSubscription so the screen and the repo share one copy. A canceled subscription from a blocked peer is hidden while its paid period runs and stays as history afterwards; the new repo test failed before the fix.

Journey expects the list while still on details

@coreyphillips Fixed in 4e451e7: after Swipe To Cancel cancel-and-delete.xml asserts the detail state first, then navigates back, then checks and taps the ACTIVE row.

Also in this push (cf5acce): a canceled subscription you created follows the same rule as one you pay. While its paid period is ahead it stays under CREATED with "Expires "; once that date passes it moves to EXPIRED. One that was never canceled is unchanged. Tests failed on the previous head and pass now, and I checked both sides on an emulator with a seeded fixture (the Preview table has the screens).

@ovitrif
ovitrif requested a review from coreyphillips October 5, 2026 13:36
@ovitrif

ovitrif commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coreyphillips @pwltr this one needs to merge for 2.6.0: its iOS twin synonymdev/bitkit-ios#846 is merged and ships in 2.6.0, so Android should match. Since the last approval it adds the creator-side listing (a canceled subscription you created stays under CREATED with "Expires " until its paid period ends, then moves to EXPIRED), hides a canceled blocked-peer subscription while it runs, and fixes the journey step order. CI is green on dcd8a5b and the branch is 0 behind master. Could one of you review it today?

@ovitrif
ovitrif merged commit c58b155 into master Oct 5, 2026
21 checks passed
@ovitrif
ovitrif deleted the fix/subscription-canceled-status branch October 5, 2026 16:49
@jvsena42 jvsena42 mentioned this pull request Oct 5, 2026
6 of 23 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: a canceled subscription reads expired while its paid period runs

2 participants