Skip to content

[PM-33517] feat: Add Plan row to Settings and premium subscription management#6794

Draft
SaintPatrck wants to merge 1 commit intopremium-upgrade/PM-33946-dynamic-pricing-implfrom
premium-upgrade/PM-33517-settings-premium_bak
Draft

[PM-33517] feat: Add Plan row to Settings and premium subscription management#6794
SaintPatrck wants to merge 1 commit intopremium-upgrade/PM-33946-dynamic-pricing-implfrom
premium-upgrade/PM-33517-settings-premium_bak

Conversation

@SaintPatrck
Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-33517

📔 Objective

Add a Plan row to the Settings screen and implement premium subscription management for existing premium users.

Stacked on #6793 (PM-33946 dynamic pricing)

Changes

  • Add "Plan" row to Settings screen that navigates to the PlanScreen in Standard (push) mode
  • Extend PlanScreen with a Premium ViewState showing subscription details (billing amount, next renewal date)
  • Add premium user dialogs: cancel confirmation, portal loading, portal error
  • Add premium user actions: ManagePlanClick, CancelPremiumClick, ConfirmCancelClick, portal URL fetching
  • Wire up customer portal navigation via BillingRepository.getPortalUrl()
  • Add ic_plan.xml drawable for the settings row
  • Full test coverage for all new ViewModel logic and Screen composables

@SaintPatrck SaintPatrck added the ai-review-vnext Request a Claude code review using the vNext workflow label Apr 14, 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 Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Code Review: PM-33517 feat: Add Plan row to Settings and premium subscription management

PR: #6794 | Author: @SaintPatrck | Reviewed by: Claude

Overall Assessment

Well-structured PR that extends the existing PlanScreen / PlanViewModel to support premium subscription management alongside the free-user upgrade flow. The code follows established codebase patterns (UDF, BaseViewModel, internal actions for async results, PlanHandlers), includes comprehensive test coverage, and the feature flag gating is correctly implemented.

Findings

🟡 suggestion — Pricing error forces navigation back for premium users

handleClosePricingErrorClick() (line 204-207 in PlanViewModel.kt) unconditionally calls sendEvent(PlanEvent.NavigateBack) when the user dismisses the pricing error dialog. For free users this makes sense (the screen is useless without pricing), but for premium users the subscription management UI (manage plan, cancel) is still functional without the rate — the placeholder "--" is shown. Navigating back is unexpected in that context.

Suggestion: Consider only navigating back for Free view state, or dismissing the dialog without navigating for premium users.

🟢 nit — PLACEHOLDER_RENEWAL_DATE KDoc visibility

The KDoc on PLACEHOLDER_RENEWAL_DATE (line 48-50) says "Placeholder renewal date until the API provides real data" — but there's no indication in this PR of when/how the real renewal date will be populated. This is fine if it's planned for a follow-up, but worth confirming.

🟢 nit — Billing amount string concatenation in Composable

In PremiumContent (line 450-452 of PlanScreen.kt):

billingAmount = "${viewState.rate} ${stringResource(id = BitwardenString.per_month)}"

String concatenation with a localized resource can cause issues for RTL languages or locales with different formatting conventions. Consider using a parameterized string resource (e.g., billing_rate_per_month with %1$s / month) for better i18n support.

Summary

  • Architecture: Clean separation of free/premium paths in the ViewModel via onFreeContent/onPremiumContent guards. The shared pricing fetch logic is correctly polymorphic over both view states.
  • Navigation: Standard/Modal plan modes and settings graph integration look correct.
  • Feature flag: Properly gated behind FlagKey.MobilePremiumUpgrade with reactive flow updates.
  • Tests: Comprehensive coverage for both ViewModel and Screen, covering all new actions, dialog states, and navigation events.
  • Security: Portal URL is fetched server-side and launched via intent — no sensitive data exposed in state.

Verdict: Approve — The one suggestion about pricing error behavior for premium users is worth considering but not blocking.

Comment on lines +998 to +1001
coEvery {
mockBillingRepository.getPremiumPlanPricing()
} returns PremiumPlanPricingResult.Success(
monthlyRate = "$2.00",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Test will not compile -- PremiumPlanPricingResult.Success has no monthlyRate parameter

Details and fix

PremiumPlanPricingResult.Success only accepts annualPrice: Double, but this test constructs it with monthlyRate = "$2.00" (a String parameter that does not exist on the class).

Additionally, even if the parameter existed and compiled, handlePricingResultReceive uses onFreeContent which is a no-op when the viewState is Premium. The pricing result would be silently dropped, so the rate would remain as the placeholder "--" rather than updating to "$2.00".

This likely needs two fixes:

  1. Add a Premium branch to handlePricingResultReceive (or a similar mechanism) so that the pricing fetch updates the rate for Premium users as well.
  2. Update this test to use the correct constructor parameter (annualPrice: Double) and assert the properly formatted rate.

Comment on lines +74 to +78
PlanState(
planMode = planMode,
viewState = if (isPremium) {
PlanState.ViewState.Premium(
rate = PLACEHOLDER_RATE,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: Premium user rate will stay as placeholder "--" because pricing fetch result is ignored

Details and fix

The init block unconditionally fetches pricing via billingRepository.getPremiumPlanPricing(), but handlePricingResultReceive wraps its logic in onFreeContent, which is a no-op when viewState is Premium. The pricing result is silently dropped, and rate remains PLACEHOLDER_RATE ("--").

This means the "Billing amount" row in the Premium subscription card will display "-- per month" instead of the actual rate.

Consider adding a Premium branch in handlePricingResultReceive that updates ViewState.Premium.rate when the pricing result succeeds, similar to how the Free branch updates ViewState.Free.rate.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Logo
Checkmarx One – Scan Summary & Detailsc889b8b9-be75-4577-bd0f-555e1a973224

Great job! No new security vulnerabilities introduced in this pull request

@SaintPatrck SaintPatrck force-pushed the premium-upgrade/PM-33517-settings-premium_bak branch from eda128d to 581f032 Compare April 14, 2026 03:30
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 93.40659% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.66%. Comparing base (4a6de0d) to head (581f032).

Files with missing lines Patch % Lines
.../ui/platform/feature/premium/plan/PlanViewModel.kt 90.32% 4 Missing and 5 partials ⚠️
...den/ui/platform/feature/premium/plan/PlanScreen.kt 95.45% 0 Missing and 6 partials ⚠️
...ui/platform/feature/settings/SettingsNavigation.kt 33.33% 2 Missing ⚠️
...form/feature/premium/plan/handlers/PlanHandlers.kt 94.11% 1 Missing ⚠️
Additional details and impacted files
@@                                Coverage Diff                                @@
##           premium-upgrade/PM-33946-dynamic-pricing-impl    #6794      +/-   ##
=================================================================================
- Coverage                                          85.69%   85.66%   -0.03%     
=================================================================================
  Files                                                828      831       +3     
  Lines                                              58730    59011     +281     
  Branches                                            8595     8633      +38     
=================================================================================
+ Hits                                               50327    50553     +226     
- Misses                                              5422     5468      +46     
- Partials                                            2981     2990       +9     
Flag Coverage Δ
app-data 17.39% <0.00%> (-0.08%) ⬇️
app-ui-auth-tools 20.26% <0.00%> (-0.09%) ⬇️
app-ui-platform 15.75% <93.40%> (+0.32%) ⬆️
app-ui-vault 25.84% <0.00%> (-0.11%) ⬇️
authenticator 6.50% <0.00%> (-0.04%) ⬇️
lib-core-network-bridge 4.26% <0.00%> (-0.02%) ⬇️
lib-data-ui 1.02% <0.00%> (-0.01%) ⬇️

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.

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.

1 participant