Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import SwiftUI
import UniformTypeIdentifiers
import WebKit

/// Single edit point for the Operator price quoted in the deprecation-banner
/// fallback (used only when the backend omits `deprecationMessage`).
let operatorDeprecationFallbackPrice = "$49/mo"

enum SubscriptionPlanPresentation {
static func selectionLabel(planTitle: String, startingPrice: String?) -> String {
guard let startingPrice, !startingPrice.isEmpty else {
Expand Down Expand Up @@ -134,7 +138,7 @@ extension SettingsContentView {
return "\(prefix) on \(formatter.string(from: date))"
}

func planSubtitle(for planId: String) -> String? {
static func planSubtitle(for planId: String) -> String? {
switch planId {
case "unlimited":
return "200 questions per month"
Expand Down Expand Up @@ -175,7 +179,7 @@ extension SettingsContentView {
return prices.first
}

func planEyebrow(for planId: String) -> String {
static func planEyebrow(for planId: String) -> String {
switch planId {
case "unlimited":
return "Starter"
Expand All @@ -188,10 +192,10 @@ extension SettingsContentView {
}
}

func planDescription(for planId: String) -> String {
static func planDescription(for planId: String) -> String {
switch planId {
case "unlimited":
return "100 chat questions per month. Shared with mobile and web."
return "200 chat questions per month. Shared with mobile and web."
case "operator":
return "500 chat questions per month. Shared with mobile and web."
case "architect":
Expand Down Expand Up @@ -236,7 +240,7 @@ extension SettingsContentView {
SubscriptionPlanCatalogMerger.merge(primary: primary, fallback: fallback)
}

func fallbackFeatures(for planId: String) -> [String] {
static func fallbackFeatures(for planId: String) -> [String] {
switch planId {
case "architect":
return [
Expand Down Expand Up @@ -313,7 +317,7 @@ extension SettingsContentView {
return SubscriptionPlanOption(
id: planId,
title: title,
features: fallbackFeatures(for: planId),
features: Self.fallbackFeatures(for: planId),
prices: mappedPrices
)
}
Expand Down Expand Up @@ -342,7 +346,7 @@ extension SettingsContentView {
Circle()
.fill(accent)
.frame(width: 6, height: 6)
Text((plan.eyebrow ?? planEyebrow(for: plan.id)).uppercased())
Text((plan.eyebrow ?? Self.planEyebrow(for: plan.id)).uppercased())
.scaledFont(size: OmiType.micro, weight: .bold)
.foregroundColor(Ink.secondary)
.tracking(0.8)
Expand All @@ -352,7 +356,7 @@ extension SettingsContentView {
.scaledFont(size: OmiType.heading, weight: .bold)
.foregroundColor(Ink.primary)

if let subtitle = plan.subtitle ?? planSubtitle(for: plan.id) {
if let subtitle = plan.subtitle ?? Self.planSubtitle(for: plan.id) {
Text(subtitle)
.scaledFont(size: OmiType.caption)
.foregroundColor(Ink.secondary)
Expand All @@ -379,7 +383,7 @@ extension SettingsContentView {
.fixedSize(horizontal: true, vertical: false)
}

Text(plan.description ?? planDescription(for: plan.id))
Text(plan.description ?? Self.planDescription(for: plan.id))
.scaledFont(size: OmiType.body)
.foregroundColor(Ink.secondary)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ extension SettingsContentView {

Text(
subscription.deprecationMessage
?? "Your Unlimited plan is being retired. Try the new Operator plan — same great features at $49/mo."
?? "Your Unlimited plan is being retired. Try the new Operator plan — same great features at \(operatorDeprecationFallbackPrice)."
)
.scaledFont(size: OmiType.body)
.foregroundColor(Ink.secondary)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import XCTest

@testable import Omi_Computer

/// Fallback billing copy shown only when the backend catalog omits a plan's
/// subtitle/description (`SettingsContentView.planSubtitle`/`planDescription`).
/// These shipped disagreeing for the "unlimited" (Neo) plan id — the subtitle
/// said 200 questions/month while the description said 100 — so a user could
/// see either number depending which card region they read.
@MainActor
final class SettingsContentViewBillingFallbackTests: XCTestCase {
private func leadingQuestionCount(in text: String?) -> String? {
guard let first = text?.split(separator: " ").first, first.allSatisfy(\.isNumber) else {
return nil
}
return String(first)
}

func testUnlimitedFallbackSubtitleAndDescriptionAgreeOnQuestionCount() {
let subtitle = SettingsContentView.planSubtitle(for: "unlimited")
let description = SettingsContentView.planDescription(for: "unlimited")
XCTAssertEqual(leadingQuestionCount(in: subtitle), leadingQuestionCount(in: description))
XCTAssertEqual(leadingQuestionCount(in: description), "200")
}

func testOperatorFallbackSubtitleAndDescriptionAgreeOnQuestionCount() {
let subtitle = SettingsContentView.planSubtitle(for: "operator")
let description = SettingsContentView.planDescription(for: "operator")
XCTAssertEqual(leadingQuestionCount(in: subtitle), leadingQuestionCount(in: description))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"change": "Fixed the Unlimited plan's fallback description showing the wrong monthly question count"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
startCheckout,
createCheckoutSession,
upgradeSubscription,
openCustomerPortal
openCustomerPortal,
OPERATOR_DEPRECATION_FALLBACK_PRICE
} from '../../../lib/billing'
import type {
UserSubscriptionResponse,
Expand Down Expand Up @@ -225,7 +226,7 @@ export function PlanUsageTab(): React.JSX.Element {
title="Plan Retiring"
subtitle={
subscription.deprecation_message ??
'Your Unlimited plan is being retired. Try the new Operator plan — same great features at $49/mo.'
`Your Unlimited plan is being retired. Try the new Operator plan — same great features at ${OPERATOR_DEPRECATION_FALLBACK_PRICE}.`
}
trailing={
showCatalog ? (
Expand Down
9 changes: 9 additions & 0 deletions desktop/windows/src/renderer/src/lib/billing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,15 @@ describe('plan catalog helpers', () => {
'd'
])
})
it('keeps the Neo (unlimited) fallback subtitle and description question counts in sync', () => {
// Regression: subtitle said 200 questions/month while description said 100 for the
// same plan id, so a user could see either number depending which card region they read.
const p: SubscriptionPlan = { id: 'unlimited', title: 'Neo' }
const subtitleCount = planSubtitle(p).match(/\d+/)?.[0]
const descriptionCount = planDescription(p).match(/\d+/)?.[0]
expect(descriptionCount).toBe(subtitleCount)
expect(descriptionCount).toBe('200')
})
it('sorts prices month-first and reads the starting price', () => {
expect(sortedPrices(CATALOG[1]).map((p) => p.title)).toEqual(['Monthly', 'Annual'])
expect(planStartingPrice(CATALOG[1])).toBe('$49/mo')
Expand Down
6 changes: 5 additions & 1 deletion desktop/windows/src/renderer/src/lib/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ export function orderedCatalog(
.map(({ p }) => p)
}

// Single edit point for the Operator price quoted in the deprecation-banner
// fallback (used only when the API omits `deprecation_message`).
export const OPERATOR_DEPRECATION_FALLBACK_PRICE = '$49/mo'

// Per-plan-id fallbacks (BillingHelpers planEyebrow/planSubtitle/planDescription/
// fallback features), used only when the catalog omits the field.
const PLAN_FALLBACKS: Record<
Expand All @@ -362,7 +366,7 @@ const PLAN_FALLBACKS: Record<
unlimited: {
eyebrow: 'Starter',
subtitle: '200 questions per month',
description: '100 chat questions per month. Shared with mobile and web.',
description: '200 chat questions per month. Shared with mobile and web.',
features: [
'200 chat questions per month',
'Unlimited listening and transcription',
Expand Down
Loading