[PM-39708] fix: Show correct UI for unpaid subscription status#2862
[PM-39708] fix: Show correct UI for unpaid subscription status#2862KatherineInCode wants to merge 11 commits into
Conversation
Unpaid is a lapsed state (premium = FALSE after repeated payment failures) that was being aliased to updatePayment, causing the vault list to show the generic "Upgrade to Premium" card and the Plan screen to show "Update payment" details instead of unpaid-specific copy. - Add PremiumPlanStatus.unpaid as a first-class case (danger badge, isTroubleState = true) - Add isPaymentProblemState to PremiumPlanStatus covering pastDue, unpaid, and updatePayment — drives the attention card - Show "subscription needs attention" card for unpaid; suppress the upgrade card when attention card is visible (extracted to refreshPremiumActionCards()) - Remove doesActiveAccountHavePremiumPersonally() guard from refreshSubscriptionAttentionCard so lapsed users are surfaced on every sync regardless of premium flag (matches Android behavior) - Plan screen: unpaid description, hidden cancel button, danger badge
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2862 +/- ##
===========================================
- Coverage 81.27% 40.60% -40.68%
===========================================
Files 1028 357 -671
Lines 66164 16657 -49507
===========================================
- Hits 53773 6763 -47010
+ Misses 12391 9894 -2497 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the addition of Code Review DetailsNo blocking findings. Notes considered and cleared during review:
|
| func descriptionText_unpaid() { | ||
| var state = PremiumPlanState() | ||
| state.planStatus = .unpaid | ||
| state.subscription = .fixture(status: .unpaid, suspension: testDate) |
There was a problem hiding this comment.
❌ CRITICAL: Assigning to state.subscription will not compile.
Details and fix
subscription is declared as a private, getter-only computed property in PremiumPlanState.swift:
private var subscription: PremiumSubscription? { loadingState.data }@testable import elevates internal access but does not expose private members, and a computed property with no setter is never assignable. This line breaks compilation of the BitwardenShared test target. Every other test in this file sets the subscription through loadingState, matching the source-of-truth pattern:
| state.subscription = .fixture(status: .unpaid, suspension: testDate) | |
| state.loadingState = .data(.fixture(status: .unpaid, suspension: testDate)) |
The same issue exists at PremiumPlanView+SnapshotTests.swift:88 (processor.state.subscription = .fixture(...)) — that method is disabletest_-prefixed, but disabling only skips execution, not compilation, so it must be fixed the same way.
| state.shouldShowUpgradedToPremiumActionCard = await services.billingService | ||
| .shouldShowUpgradedToPremiumActionCard() |
There was a problem hiding this comment.
🤔 Should we include this in refreshPremiumActionCards() as well?
🎟️ Tracking
PM-39708
📔 Objective
SubscriptionStatus.unpaidis the state Stripe assigns after ~8 failed payment attempts, at which point the server setspremium = FALSE. The app was aliasing it toupdatePayment(a dunning state where premium is still active), causing two bugs:Changes:
PremiumPlanStatus: Added.unpaidas a first-class case (danger badge,isTroubleState = true); addedisPaymentProblemStateproperty (true forpastDue/unpaid/updatePayment) to drive the attention cardBillingService.refreshSubscriptionAttentionCard: RemoveddoesActiveAccountHavePremiumPersonally()guard so lapsed users are surfaced on every sync regardless of premium flag; free users hit a silent 404 (GetSubscriptionRequestError). Matches Android's explicit design decision inPremiumStateManagerImplVaultListProcessor: ExtractedrefreshPremiumActionCards()— attention card and upgrade card are now mutually exclusive; attention card takes priority📸 Screenshots