Skip to content

[PM-36969] feat: Surface subscription substate to premium gates#6931

Draft
SaintPatrck wants to merge 7 commits into
mainfrom
premium-upgrade/pm-36969-subscription-status-flow
Draft

[PM-36969] feat: Surface subscription substate to premium gates#6931
SaintPatrck wants to merge 7 commits into
mainfrom
premium-upgrade/pm-36969-subscription-status-flow

Conversation

@SaintPatrck
Copy link
Copy Markdown
Contributor

@SaintPatrck SaintPatrck commented May 15, 2026

🎟️ Tracking

📔 Objective

The server keeps Profile.Premium=true during the grace window after a subscription enters a recovery or terminal Stripe state. Surfaces that gate purely on isPremium either suppress the Plan-screen badge that would explain the user's situation, or hide the upgrade CTA users need to recover their account.

Adds PremiumStateManager.subscriptionStatusStateFlow (Loading/NoSubscription/Available/Error) so callers can derive "effectively premium" from both the account flag and the Stripe substate. The upgrade banner now flips back on when the active subscription is in a trouble state (past_due, update_payment, canceled, paused) even while the server still reports premium. The Plan screen routes free-with-trouble-substate users to the Premium view so they see the right badge and Manage/Resubscribe affordances.

Renames OVERDUE_PAYMENT → UPDATE_PAYMENT so the badge label matches the Figma frame. A new SubscriptionResult.NotFound maps the 404 returned for users without a GatewaySubscriptionId to "free, show upgrade CTA" instead of an error dialog.

📸 Screenshots

Figma Actual

@SaintPatrck SaintPatrck added the ai-review-vnext Request a Claude code review using the vNext workflow label May 15, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature Change Type - Feature Development labels May 15, 2026
@SaintPatrck SaintPatrck removed the app:authenticator Bitwarden Authenticator app context label May 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

This PR surfaces the Stripe subscription substate to premium-gating callers via a new PremiumStateManager.subscriptionStatusStateFlow, renames OVERDUE_PAYMENT to UPDATE_PAYMENT, maps the subscription endpoint's 404 to a new SubscriptionResult.NotFound (free user) instead of an error, and emphasizes dates in subscription status descriptions using the existing <annotation emphasis="bold"> mechanism. The Plan screen now routes free-with-trouble-substate users to the Premium view, and the upgrade banner flips back on for users whose server isPremium flag is stale during a Stripe recovery state. Test coverage is comprehensive across the manager, repository, view model, and screen layers, including the new SubscriptionResult.NotFound and trouble-state-routing paths. The latest commit drops the AuthRepository dependency from PremiumStateManagerImpl in favor of reading raw profile fields off AuthDiskSource directly.

Code Review Details

No new findings beyond the existing reviewer threads. Six unresolved threads from david-livefront remain open and are awaiting author action (compressed-doc style on SubscriptionStatusState.kt:13, 404-mapping placement in BillingRepositoryImpl.kt:71, and loading-state question on PlanViewModel.kt:394, plus three outdated/superseded comments). Earlier critical and important findings were resolved in commits edf7875e (isPremium-gate fix), b6abedce (duplicate property declaration), and 408cdb52 (AuthDiskSource refactor that addressed the userStateFlow extraction suggestions).

@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

❌ Patch coverage is 90.73171% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.38%. Comparing base (1f8280f) to head (408cdb5).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...arden/ui/platform/base/util/StringResExtensions.kt 76.47% 1 Missing and 7 partials ⚠️
.../ui/platform/feature/premium/plan/PlanViewModel.kt 90.90% 1 Missing and 4 partials ⚠️
...en/data/billing/manager/PremiumStateManagerImpl.kt 94.52% 0 Missing and 4 partials ⚠️
...den/ui/platform/feature/premium/plan/PlanScreen.kt 94.28% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6931      +/-   ##
==========================================
+ Coverage   86.25%   86.38%   +0.12%     
==========================================
  Files         909      864      -45     
  Lines       64380    62769    -1611     
  Branches     9146     9125      -21     
==========================================
- Hits        55533    54223    -1310     
+ Misses       5704     5406     -298     
+ Partials     3143     3140       -3     
Flag Coverage Δ
app-data 17.21% <36.58%> (+0.35%) ⬆️
app-ui-auth-tools 19.31% <5.10%> (-0.23%) ⬇️
app-ui-platform 15.63% <54.59%> (-0.45%) ⬇️
app-ui-vault 28.17% <5.10%> (-0.62%) ⬇️
authenticator 6.30% <5.10%> (-0.01%) ⬇️
lib-core-network-bridge 4.09% <0.00%> (-0.03%) ⬇️
lib-data-ui 1.16% <13.26%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot added the app:authenticator Bitwarden Authenticator app context label May 18, 2026
Comment thread ui/src/main/res/values/strings.xml Outdated
<string name="premium_next_charge_summary">Your next charge is for %1$s USD due on %2$s.</string>
<string name="subscription_canceled_description">Your subscription was canceled on %1$s. Resubscribe to continue using premium features.</string>
<string name="subscription_overdue_description">We couldn’t process your payment. Update your payment before your subscription ends on %1$s.</string>
<string name="subscription_update_payment_description">We couldn’t process your payment. Update your payment before your subscription ends on %1$s.</string>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should the injected date also be made bold?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Thanks!

SaintPatrck and others added 5 commits May 18, 2026 17:26
The server keeps `Profile.Premium=true` during the grace window after a
subscription enters a recovery or terminal state, so callers that gate
purely on `isPremium` either route users away from the Plan-screen badge
that explains their situation (PM-36969, PM-36970, PM-37181) or suppress
the upgrade CTA users need to recover their account (PM-37093, PM-37177,
PM-37180). Expose the Stripe substate via a new
`PremiumStateManager.subscriptionStatusStateFlow` and use it to compute
"effectively premium" for banner eligibility and Plan-screen routing.

Renames the misleading `OVERDUE_PAYMENT` enum value to `UPDATE_PAYMENT` so
the badge label matches Figma. The subscription endpoint 404s for free
users (no `GatewaySubscriptionId`); a new `SubscriptionResult.NotFound`
maps that case to "free, show upgrade CTA" instead of an error dialog.
Avoids a guaranteed 404 round-trip to GET /accounts/subscription for
every free user on app launch and on every premium-status push. Re-keys
on (userId, isPremium) so the flow refetches when an account transitions
to premium or when a push arrives for an already-premium user.
The isPremium gate prevented users whose Stripe subscription had moved
to canceled / incomplete_expired (with the server flipping isPremium to
false) from reaching the Premium view in Settings. They landed on the
Free view with no way to see their canceled status or resubscribe.

Drop the gate so the subscription fetch runs whenever there is an
active user; the response layer already translates 404 to
NoSubscription, so genuine free users still bypass the Premium view.
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
The "Apply suggestions from code review" commit accidentally pasted the
property declaration twice, breaking compile. Restore the single
declaration and move the fetch-keying / 404 details onto the
implementation so the interface only carries the caller-facing contract.
@SaintPatrck SaintPatrck force-pushed the premium-upgrade/pm-36969-subscription-status-flow branch from ad72ae6 to d9f1828 Compare May 18, 2026 21:32
Design calls for the date in each Plan-screen subscription-status
description to be visually emphasized so users can quickly spot when
their next charge, cancellation, or grace period takes effect.
Standardize on the codebase's existing annotation-tag pattern
(`<annotation emphasis="bold"><annotation arg="N">`) rather than
inventing a new mechanism. Premium ViewState now carries raw data
fields so the Composable can render an AnnotatedString with the
appropriate bold span per subscription status.
@SaintPatrck SaintPatrck force-pushed the premium-upgrade/pm-36969-subscription-status-flow branch from d9f1828 to c5990ff Compare May 18, 2026 21:37
authRepository
.userStateFlow
.map { state -> state?.activeAccount?.userId }
.distinctUntilChanged()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We have an extension for this on AuthDiskSource

authDiskSource
    .activeUserIdChangesFlow
    .flatMapLatest { userId ->

clock = clock,
)
val itemCount = inputs.vaultDataState.activeVaultItemCount()
val isEffectivelyPremium = activeAccount.isPremium &&
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like the creationDate and isPremium status are the only things that we are pulling from the UserState.

What do you think about just using the AuthDiskSource.userStateFlow to get that data? That would aloow us to remove the dependency on the AuthRepository completely.

*/
sealed class SubscriptionStatusState {

/** No fetch has been attempted yet for the active user. */
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How do you feel about these compressed docs?

I usually prefer the expanded ones:

/**
 * No fetch has been attempted yet for the active user.
 */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't mind them for short docs. For consistency I can make them multi-line.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It does not bother me either way, more of a question

if (bitwardenError is BitwardenError.Http &&
bitwardenError.code == HTTP_CODE_NOT_FOUND
) {
SubscriptionResult.NotFound
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we move this into the service?

checkoutUrl = null,
isAwaitingPremiumStatus = false,
),
dialogState = null,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be a loading state?

The manager only needs the active user id and the raw premium flags
from the persisted profile; routing through AuthRepository.userStateFlow
just to read derived UserState.Account fields adds an unnecessary layer
and a heavier dependency. Read `hasPremiumPersonally` /
`hasPremiumFromOrganization` / `creationDate` straight off the disk-side
profile and drop the AuthRepository dependency entirely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants