mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(billing): plan chips and rows deep-link their tier (#68666)
This commit is contained in:
parent
11ae6bf0e3
commit
5ed7137fc6
6 changed files with 55 additions and 15 deletions
|
|
@ -227,8 +227,8 @@ describe('deriveBillingView', () => {
|
|||
|
||||
expect(subscription?.description).toBe('Paid models need a subscription — pick a plan to start it on the portal.')
|
||||
expect(subscription?.chips).toEqual([
|
||||
{ disabled: false, label: 'Plus · $20/mo · $1,000 credits/mo', url: subscription?.action?.url },
|
||||
{ disabled: false, label: 'Ultra · $40/mo · $3,000 credits/mo', url: subscription?.action?.url }
|
||||
{ disabled: false, label: 'Plus · $20/mo · $1,000 credits/mo', url: `${subscription?.action?.url}&plan=plus` },
|
||||
{ disabled: false, label: 'Ultra · $40/mo · $3,000 credits/mo', url: `${subscription?.action?.url}&plan=ultra` }
|
||||
])
|
||||
})
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ describe('deriveBillingView', () => {
|
|||
|
||||
expect(subscription?.chips).toEqual([
|
||||
{ disabled: true, label: '✓ Plus · $20/mo · $1,000 credits/mo' },
|
||||
{ disabled: false, label: 'Ultra · $40/mo · $3,000 credits/mo', url: subscription?.action?.url }
|
||||
{ disabled: false, label: 'Ultra · $40/mo · $3,000 credits/mo', url: `${subscription?.action?.url}&plan=ultra` }
|
||||
])
|
||||
})
|
||||
|
||||
|
|
@ -398,4 +398,27 @@ describe('buildManageSubscriptionUrl', () => {
|
|||
})
|
||||
).toBe('https://portal.nousresearch.com/manage-subscription?org_id=org_123')
|
||||
})
|
||||
|
||||
it('appends the tier as a plan query param when provided', () => {
|
||||
expect(
|
||||
buildManageSubscriptionUrl(
|
||||
{
|
||||
org_id: 'org_123',
|
||||
portal_url: 'https://portal.nousresearch.com/billing'
|
||||
},
|
||||
undefined,
|
||||
'ultra'
|
||||
)
|
||||
).toBe('https://portal.nousresearch.com/manage-subscription?org_id=org_123&plan=ultra')
|
||||
})
|
||||
|
||||
it('omits the plan param when no tierId is given', () => {
|
||||
expect(
|
||||
buildManageSubscriptionUrl(
|
||||
{ org_id: null, portal_url: 'https://portal.nousresearch.com/billing' },
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
).toBe('https://portal.nousresearch.com/manage-subscription')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@ export function deriveBillingView(
|
|||
|
||||
export function buildManageSubscriptionUrl(
|
||||
subscription?: null | Pick<SubscriptionStateResponse, 'org_id' | 'portal_url'>,
|
||||
fallbackPortalUrl?: null | string
|
||||
fallbackPortalUrl?: null | string,
|
||||
tierId?: string
|
||||
): string {
|
||||
const portalUrls = [subscription?.portal_url, fallbackPortalUrl].filter(
|
||||
(url): url is string => typeof url === 'string' && url.length > 0
|
||||
|
|
@ -176,6 +177,10 @@ export function buildManageSubscriptionUrl(
|
|||
url.searchParams.set('org_id', subscription.org_id)
|
||||
}
|
||||
|
||||
if (tierId) {
|
||||
url.searchParams.set('plan', tierId)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
} catch {
|
||||
// Try the next candidate; malformed portal URLs should not break settings.
|
||||
|
|
@ -276,11 +281,12 @@ function paymentMethodRow(billing: BillingStateResponse): BillingAccountRowView
|
|||
|
||||
/**
|
||||
* Tier catalog as chips for accounts that can change plans; the current plan is
|
||||
* inert, every other opens the portal where the change/start happens.
|
||||
* inert, every other opens the portal where the change/start happens, deep-linked
|
||||
* to that tier via `?plan=`.
|
||||
*/
|
||||
function subscriptionTierChips(
|
||||
subscription: null | SubscriptionStateResponse,
|
||||
manageUrl: string
|
||||
fallbackPortalUrl?: null | string
|
||||
): BillingChipView[] | undefined {
|
||||
// Teams have no personal subscription to sell into.
|
||||
if (!subscription?.can_change_plan || subscription.context === 'team') {
|
||||
|
|
@ -301,7 +307,9 @@ function subscriptionTierChips(
|
|||
const suffix = Number.isFinite(credits) && credits > 0 ? ` · $${credits.toLocaleString('en-US')} credits/mo` : ''
|
||||
const label = `${tier.name} · ${tier.dollars_per_month_display}/mo${suffix}`
|
||||
|
||||
return tier.is_current ? { disabled: true, label: `✓ ${label}` } : { disabled: false, label, url: manageUrl }
|
||||
return tier.is_current
|
||||
? { disabled: true, label: `✓ ${label}` }
|
||||
: { disabled: false, label, url: buildManageSubscriptionUrl(subscription, fallbackPortalUrl, tier.tier_id) }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -310,13 +318,14 @@ function subscriptionRow(
|
|||
subscription: null | SubscriptionStateResponse,
|
||||
subscriptionResult?: BillingResult<SubscriptionStateResponse>
|
||||
): BillingAccountRowView {
|
||||
const manageUrl = buildManageSubscriptionUrl(subscription, subscription?.portal_url ?? billing.portal_url)
|
||||
const fallbackPortalUrl = subscription?.portal_url ?? billing.portal_url
|
||||
const manageUrl = buildManageSubscriptionUrl(subscription, fallbackPortalUrl)
|
||||
const current = subscription?.current
|
||||
const fallbackPlan = billing.usage?.plan_name ?? EMPTY_BILLING_VALUE
|
||||
const value = current?.tier_name ?? fallbackPlan
|
||||
const renewal = formatBillingDate(current?.cycle_ends_at ?? billing.usage?.renews_at)
|
||||
const unavailable = subscriptionResult && !subscriptionResult.ok
|
||||
const chips = subscriptionTierChips(subscription, manageUrl)
|
||||
const chips = subscriptionTierChips(subscription, fallbackPortalUrl)
|
||||
|
||||
return {
|
||||
action: { label: 'Adjust plan ↗', url: manageUrl },
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
mounted.cleanup()
|
||||
|
||||
expect(openManageLink).toHaveBeenCalledTimes(1)
|
||||
expect(openManageLink).toHaveBeenCalledWith('plus')
|
||||
expect(preview).not.toHaveBeenCalled()
|
||||
// openManageLink narrates the handoff itself.
|
||||
expect(sys).not.toHaveBeenCalled()
|
||||
|
|
|
|||
|
|
@ -198,8 +198,11 @@ export interface SubscriptionOverlayCtx {
|
|||
* the server doesn't say (older NAS): the confirm keeps its generic line.
|
||||
*/
|
||||
fetchCard: () => Promise<BillingCardInfo | null>
|
||||
/** Build {portal}/manage-subscription?org_id=… locally and open it. Resolves ok/false. */
|
||||
openManageLink: () => Promise<boolean>
|
||||
/**
|
||||
* Build {portal}/manage-subscription?org_id=… locally and open it. Resolves
|
||||
* ok/false. Pass `tierId` to deep-link a specific plan via `?plan=`.
|
||||
*/
|
||||
openManageLink: (tierId?: string) => Promise<boolean>
|
||||
/** Open an arbitrary portal recovery URL (e.g. an upgrade's SCA handoff). */
|
||||
openPortal: (url: string) => void
|
||||
/** Re-fetch subscription.state. */
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type Sys = (text: string) => void
|
|||
* `org_id` pins the page to the correct account in multi-org situations.
|
||||
* Falls back to bare `/manage-subscription` if org_id is absent.
|
||||
*/
|
||||
function buildManageUrl(s: SubscriptionStateResponse): string | null {
|
||||
function buildManageUrl(s: SubscriptionStateResponse, tierId?: string): string | null {
|
||||
// portal_url is already an absolute URL resolved by resolve_portal_base_url()
|
||||
// on the Python side (e.g. https://portal.nousresearch.com/billing). Strip any
|
||||
// path so we can attach /manage-subscription cleanly.
|
||||
|
|
@ -46,6 +46,10 @@ function buildManageUrl(s: SubscriptionStateResponse): string | null {
|
|||
url.searchParams.set('org_id', s.org_id)
|
||||
}
|
||||
|
||||
if (tierId) {
|
||||
url.searchParams.set('plan', tierId)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +68,8 @@ const buildSubscriptionCtx = (
|
|||
.rpc<BillingStateResponse>('billing.state', {})
|
||||
.then(r => (r?.ok ? (r.card ?? null) : null))
|
||||
.catch(() => null),
|
||||
openManageLink: () => {
|
||||
const url = buildManageUrl(initialState)
|
||||
openManageLink: (tierId?: string) => {
|
||||
const url = buildManageUrl(initialState, tierId)
|
||||
|
||||
if (!url) {
|
||||
sys('Could not build manage URL — is your portal configured?')
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ function OverviewScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
}
|
||||
|
||||
busyRef.current = true
|
||||
void ctx.openManageLink()
|
||||
void ctx.openManageLink(tier.tier_id)
|
||||
onClose()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue