Skip to content

[PM-35757] feat: Show pricing error banner when premium price fetch fails#2590

Merged
andrebispo5 merged 5 commits intopm-35101/complete-stripe-paymentfrom
pm-35757/price-unavailable
Apr 29, 2026
Merged

[PM-35757] feat: Show pricing error banner when premium price fetch fails#2590
andrebispo5 merged 5 commits intopm-35101/complete-stripe-paymentfrom
pm-35757/price-unavailable

Conversation

@andrebispo5
Copy link
Copy Markdown
Contributor

@andrebispo5 andrebispo5 commented Apr 28, 2026

🎟️ Tracking

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

📔 Objective

Fetches the premium plan price from the billing service on appear in PremiumUpgradeView. If the fetch fails, a "Pricing unavailable" error banner is shown with a "Try again" button and the upgrade button is hidden. On success (or retry), the price is displayed and the banner is dismissed.

📸 Screenshots

Simulator Screenshot - iPhone 17 Pro - 2026-04-28 at 23 20 28

@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature labels Apr 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Logo
Checkmarx One – Scan Summary & Detailseae893ba-66dd-4d0f-b7a4-ade7d4ebecee

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.15%. Comparing base (daf4675) to head (0583107).
⚠️ Report is 7 commits behind head on pm-35101/complete-stripe-payment.

Additional details and impacted files
@@                         Coverage Diff                          @@
##           pm-35101/complete-stripe-payment    #2590      +/-   ##
====================================================================
+ Coverage                             86.13%   86.15%   +0.01%     
====================================================================
  Files                                  2124     2124              
  Lines                                182803   182934     +131     
====================================================================
+ Hits                                 157457   157600     +143     
+ Misses                                25346    25334      -12     

☔ 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.

@andrebispo5 andrebispo5 changed the base branch from main to pm-35101/complete-stripe-payment April 28, 2026 22:22
@andrebispo5 andrebispo5 marked this pull request as ready for review April 29, 2026 09:20
Copilot AI review requested due to automatic review settings April 29, 2026 09:20
@andrebispo5 andrebispo5 requested a review from a team as a code owner April 29, 2026 09:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds resilient premium pricing display to PremiumUpgradeView by fetching the premium plan price on appear and showing a retryable “Pricing unavailable” banner when the fetch fails, hiding the upgrade CTA while pricing is unavailable.

Changes:

  • Fetch premium plan pricing via BillingService.getPremiumPlan() on view appear (and retry) and store it in PremiumUpgradeState.
  • Introduce a pricing error banner with “Try again” and dismiss actions; hide the upgrade button while the banner is shown.
  • Add ViewInspector + processor tests and new English localization strings for the banner copy.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeView.swift Shows pricing error banner; hides upgrade CTA when pricing is unavailable; hides price section when premiumPrice is nil.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeView+ViewInspectorTests.swift Adds UI tests for pricing banner visibility and button behaviors; verifies upgrade button hiding.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeState.swift Makes premiumPrice optional and adds showPricingErrorBanner state.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeProcessorTests.swift Adds processor tests for successful/failed price fetch on appear and retry; updates coordinator alert expectation.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeProcessor.swift Implements async price fetch + error banner state transitions; wires retry and dismiss actions.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeEffect.swift Adds .retryFetchPriceTapped effect for the banner retry button.
BitwardenShared/UI/Billing/PremiumUpgrade/PremiumUpgradeAction.swift Adds .dismissPricingErrorBannerTapped action for banner dismissal.
BitwardenResources/Localizations/en.lproj/Localizable.strings Adds English strings for “Pricing unavailable” and “Check your connection and try again.”

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +100
state.premiumPrice = formatter.string(from: NSDecimalNumber(decimal: plan.seat.price))
state.showPricingErrorBanner = false
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

NumberFormatter.string(from:) can return nil (e.g., unexpected locale/currency configuration). Currently that results in premiumPrice == nil while also forcing showPricingErrorBanner = false, leaving the UI with no price and no error. Treat a nil formatted result as a failure (show the banner) and/or provide a safe fallback string.

Suggested change
state.premiumPrice = formatter.string(from: NSDecimalNumber(decimal: plan.seat.price))
state.showPricingErrorBanner = false
if let premiumPrice = formatter.string(from: NSDecimalNumber(decimal: plan.seat.price)) {
state.premiumPrice = premiumPrice
state.showPricingErrorBanner = false
} else {
state.premiumPrice = nil
state.showPricingErrorBanner = true
}

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +104
} catch {
services.errorReporter.log(error: error)
state.showPricingErrorBanner = true
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

On failure you set showPricingErrorBanner = true but never clear state.premiumPrice. If a previously fetched price exists, the view can show a stale price alongside the “Pricing unavailable” banner. Clear premiumPrice when starting a fetch and/or in the catch path so error state can’t display outdated pricing.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@andrebispo5 I don't think this will ever happen, but doesn't harm to clear it as well.

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.

yup this is not going to happen that is why I didn't paid much attention to it.

Comment on lines +53 to +63
/// `perform(_:)` with `.appeared` fetches the premium price and sets it in state on success.
@Test
func perform_appeared_selfHosted() async {
environmentService.region = .selfHosted
func perform_appeared_fetchesPremiumPrice_success() async {
environmentService.region = .unitedStates

await subject.perform(.appeared)

#expect(subject.state.isSelfHosted == true)
#expect(subject.state.premiumPrice != nil)
#expect(subject.state.showPricingErrorBanner == false)
#expect(billingService.getPremiumPlanCalled)
}
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The new tests only assert premiumPrice != nil after fetching, so they won’t catch incorrect cadence/formatting (e.g., showing $19.80 with a “per month” label). Add an assertion for the expected formatted monthly price (using a fixed test locale) to validate the displayed value matches the UI copy.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +100
let plan = try await services.billingService.getPremiumPlan()
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale.current
state.premiumPrice = formatter.string(from: NSDecimalNumber(decimal: plan.seat.price))
state.showPricingErrorBanner = false
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

PremiumUpgradeView labels this value as “per month”, but fetchPremiumPrice() formats plan.seat.price directly. The premium plan fixture/model uses Stripe IDs like premium-annually-* with price: 19.80, so this will display an annual price as a monthly price. Convert the returned price to a monthly amount (or otherwise match the billing cadence) before formatting, and consider asserting the expected formatted value in tests to prevent regressions.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@andrebispo5 Correct me if I'm wrong but premiumPrice will be used in the view which has the monthly calculation, right?

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.

this comment is outdated I moved the formatting to the state and it is being divided as well. :)

@andrebispo5 andrebispo5 requested a review from fedemkr April 29, 2026 20:55
@andrebispo5 andrebispo5 merged commit 3a4e645 into pm-35101/complete-stripe-payment Apr 29, 2026
41 checks passed
@andrebispo5 andrebispo5 deleted the pm-35757/price-unavailable branch April 29, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants