mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fmt(js): npm run fix on merge (#66834)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
b51fbc738b
commit
33300cdc7f
16 changed files with 403 additions and 153 deletions
|
|
@ -107,11 +107,11 @@ describe('BillingOverlay — step-up screen (Enable terminal billing)', () => {
|
|||
describe('BillingOverlay — overview (reordered, dollars)', () => {
|
||||
it('leads with balance in the title, Add funds first, no "credits"', () => {
|
||||
const out = render(overlay('overview'))
|
||||
expect(out).toContain('Top up · balance $12.00') // balance in the title
|
||||
expect(out).toContain('Add funds') // buy action, renamed
|
||||
expect(out).toContain('Top up · balance $12.00') // balance in the title
|
||||
expect(out).toContain('Add funds') // buy action, renamed
|
||||
expect(out).toContain('Auto-reload')
|
||||
expect(out).toContain('Manage on portal')
|
||||
expect(out.toLowerCase()).not.toContain('credits') // dollars only
|
||||
expect(out.toLowerCase()).not.toContain('credits') // dollars only
|
||||
// No standalone "Enable terminal billing" item — discovered at pay time.
|
||||
expect(out).not.toContain('Enable terminal billing')
|
||||
})
|
||||
|
|
@ -126,8 +126,22 @@ describe('BillingOverlay — overview (reordered, dollars)', () => {
|
|||
status: 'healthy',
|
||||
plan_name: 'Plus',
|
||||
has_topup: true,
|
||||
plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 },
|
||||
topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 }
|
||||
plan_bar: {
|
||||
kind: 'plan',
|
||||
remaining_display: '$14.00',
|
||||
total_display: '$20.00',
|
||||
spent_display: '$6.00',
|
||||
pct_used: 30,
|
||||
fill_fraction: 0.7
|
||||
},
|
||||
topup_bar: {
|
||||
kind: 'topup',
|
||||
remaining_display: '$12.00',
|
||||
total_display: '$12.00',
|
||||
spent_display: '$0.00',
|
||||
pct_used: null,
|
||||
fill_fraction: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ import { DEFAULT_THEME } from '../theme.js'
|
|||
const t = DEFAULT_THEME
|
||||
|
||||
/** Mount a SubscriptionOverlay via renderSync + PassThrough. */
|
||||
function mount(overlay: SubscriptionOverlayState, onPatch: (next: Partial<SubscriptionOverlayState>) => void = () => {}) {
|
||||
function mount(
|
||||
overlay: SubscriptionOverlayState,
|
||||
onPatch: (next: Partial<SubscriptionOverlayState>) => void = () => {}
|
||||
) {
|
||||
const stdout = new PassThrough()
|
||||
const stdin = new PassThrough()
|
||||
const stderr = new PassThrough()
|
||||
|
|
@ -46,15 +49,13 @@ function mount(overlay: SubscriptionOverlayState, onPatch: (next: Partial<Subscr
|
|||
|
||||
inputHarness.handler = undefined
|
||||
const element = React.createElement(SubscriptionOverlay, { onClose: () => {}, onPatch, overlay, t })
|
||||
const instance = renderSync(
|
||||
element,
|
||||
{
|
||||
patchConsole: false,
|
||||
stderr: stderr as NodeJS.WriteStream,
|
||||
stdin: stdin as NodeJS.ReadStream,
|
||||
stdout: stdout as NodeJS.WriteStream
|
||||
}
|
||||
)
|
||||
|
||||
const instance = renderSync(element, {
|
||||
patchConsole: false,
|
||||
stderr: stderr as NodeJS.WriteStream,
|
||||
stdin: stdin as NodeJS.ReadStream,
|
||||
stdout: stdout as NodeJS.WriteStream
|
||||
})
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
|
|
@ -76,9 +77,33 @@ function render(overlay: SubscriptionOverlayState): string {
|
|||
}
|
||||
|
||||
const TIERS = [
|
||||
{ tier_id: 'free', name: 'Free', tier_order: 0, dollars_per_month_display: '$0', monthly_credits: '0', is_current: false, is_enabled: true },
|
||||
{ tier_id: 'plus', name: 'Plus', tier_order: 1, dollars_per_month_display: '$20', monthly_credits: '1000', is_current: true, is_enabled: true },
|
||||
{ tier_id: 'ultra', name: 'Ultra', tier_order: 2, dollars_per_month_display: '$40', monthly_credits: '3000', is_current: false, is_enabled: true }
|
||||
{
|
||||
tier_id: 'free',
|
||||
name: 'Free',
|
||||
tier_order: 0,
|
||||
dollars_per_month_display: '$0',
|
||||
monthly_credits: '0',
|
||||
is_current: false,
|
||||
is_enabled: true
|
||||
},
|
||||
{
|
||||
tier_id: 'plus',
|
||||
name: 'Plus',
|
||||
tier_order: 1,
|
||||
dollars_per_month_display: '$20',
|
||||
monthly_credits: '1000',
|
||||
is_current: true,
|
||||
is_enabled: true
|
||||
},
|
||||
{
|
||||
tier_id: 'ultra',
|
||||
name: 'Ultra',
|
||||
tier_order: 2,
|
||||
dollars_per_month_display: '$40',
|
||||
monthly_credits: '3000',
|
||||
is_current: false,
|
||||
is_enabled: true
|
||||
}
|
||||
]
|
||||
|
||||
const state = (overrides: Partial<SubscriptionStateResponse> = {}): SubscriptionStateResponse => ({
|
||||
|
|
@ -128,7 +153,15 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
const out = render(
|
||||
overlay(
|
||||
state({
|
||||
current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '700', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null },
|
||||
current: {
|
||||
tier_id: 'pro',
|
||||
tier_name: 'Pro',
|
||||
monthly_credits: '1000',
|
||||
credits_remaining: '700',
|
||||
cycle_ends_at: '2026-07-01',
|
||||
pending_downgrade_tier_name: null,
|
||||
pending_downgrade_at: null
|
||||
},
|
||||
usage: {
|
||||
available: true,
|
||||
status: 'healthy',
|
||||
|
|
@ -136,8 +169,22 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
renews_display: 'Jul 1, 2026',
|
||||
total_spendable_display: '$26.00',
|
||||
has_topup: true,
|
||||
plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 },
|
||||
topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 }
|
||||
plan_bar: {
|
||||
kind: 'plan',
|
||||
remaining_display: '$14.00',
|
||||
total_display: '$20.00',
|
||||
spent_display: '$6.00',
|
||||
pct_used: 30,
|
||||
fill_fraction: 0.7
|
||||
},
|
||||
topup_bar: {
|
||||
kind: 'topup',
|
||||
remaining_display: '$12.00',
|
||||
total_display: '$12.00',
|
||||
spent_display: '$0.00',
|
||||
pct_used: null,
|
||||
fill_fraction: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -155,8 +202,29 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
const out = render(
|
||||
overlay(
|
||||
state({
|
||||
current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '170', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null },
|
||||
usage: { available: true, status: 'low', plan_name: 'Pro', total_spendable_display: '$3.40', plan_bar: { kind: 'plan', remaining_display: '$3.40', total_display: '$20.00', spent_display: '$16.60', pct_used: 83, fill_fraction: 0.17 } }
|
||||
current: {
|
||||
tier_id: 'pro',
|
||||
tier_name: 'Pro',
|
||||
monthly_credits: '1000',
|
||||
credits_remaining: '170',
|
||||
cycle_ends_at: '2026-07-01',
|
||||
pending_downgrade_tier_name: null,
|
||||
pending_downgrade_at: null
|
||||
},
|
||||
usage: {
|
||||
available: true,
|
||||
status: 'low',
|
||||
plan_name: 'Pro',
|
||||
total_spendable_display: '$3.40',
|
||||
plan_bar: {
|
||||
kind: 'plan',
|
||||
remaining_display: '$3.40',
|
||||
total_display: '$20.00',
|
||||
spent_display: '$16.60',
|
||||
pct_used: 83,
|
||||
fill_fraction: 0.17
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
|
@ -172,7 +240,15 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
is_admin: false,
|
||||
can_change_plan: false,
|
||||
role: 'MEMBER',
|
||||
current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '500', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: null, pending_downgrade_at: null },
|
||||
current: {
|
||||
tier_id: 'pro',
|
||||
tier_name: 'Pro',
|
||||
monthly_credits: '1000',
|
||||
credits_remaining: '500',
|
||||
cycle_ends_at: '2026-07-01',
|
||||
pending_downgrade_tier_name: null,
|
||||
pending_downgrade_at: null
|
||||
},
|
||||
usage: { available: true, status: 'healthy', plan_name: 'Pro' }
|
||||
})
|
||||
)
|
||||
|
|
@ -186,7 +262,16 @@ describe('SubscriptionOverlay — overview', () => {
|
|||
const out = render(
|
||||
overlay(
|
||||
state({
|
||||
current: { tier_id: 'pro', tier_name: 'Pro', monthly_credits: '1000', credits_remaining: '500', cycle_ends_at: '2026-07-01', pending_downgrade_tier_name: 'Free', pending_downgrade_at: '2026-07-15', pending_downgrade_display: 'Jul 15, 2026' },
|
||||
current: {
|
||||
tier_id: 'pro',
|
||||
tier_name: 'Pro',
|
||||
monthly_credits: '1000',
|
||||
credits_remaining: '500',
|
||||
cycle_ends_at: '2026-07-01',
|
||||
pending_downgrade_tier_name: 'Free',
|
||||
pending_downgrade_at: '2026-07-15',
|
||||
pending_downgrade_display: 'Jul 15, 2026'
|
||||
},
|
||||
usage: { available: true, status: 'healthy', plan_name: 'Pro' }
|
||||
})
|
||||
)
|
||||
|
|
@ -296,7 +381,13 @@ describe('SubscriptionOverlay — confirm', () => {
|
|||
pending: {
|
||||
kind: 'upgrade',
|
||||
targetTierId: 'ultra',
|
||||
preview: { ok: true, effect: 'charge_now', target_tier_name: 'Ultra', amount_due_now_cents: 1234, monthly_credits_delta: '2000' }
|
||||
preview: {
|
||||
ok: true,
|
||||
effect: 'charge_now',
|
||||
target_tier_name: 'Ultra',
|
||||
amount_due_now_cents: 1234,
|
||||
monthly_credits_delta: '2000'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -311,7 +402,13 @@ describe('SubscriptionOverlay — confirm', () => {
|
|||
pending: {
|
||||
kind: 'tier_change',
|
||||
targetTierId: 'plus',
|
||||
preview: { ok: true, effect: 'scheduled', target_tier_name: 'Plus', effective_at: '2026-08-01T00:00:00Z', amount_due_now_cents: null }
|
||||
preview: {
|
||||
ok: true,
|
||||
effect: 'scheduled',
|
||||
target_tier_name: 'Plus',
|
||||
effective_at: '2026-08-01T00:00:00Z',
|
||||
amount_due_now_cents: null
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -322,7 +419,9 @@ describe('SubscriptionOverlay — confirm', () => {
|
|||
})
|
||||
|
||||
it('cancellation: shows cancel-at-period-end copy', () => {
|
||||
const out = render(at('confirm', subscriber(), { pending: { kind: 'cancellation', targetTierId: null, preview: null } }))
|
||||
const out = render(
|
||||
at('confirm', subscriber(), { pending: { kind: 'cancellation', targetTierId: null, preview: null } })
|
||||
)
|
||||
|
||||
expect(out).toContain('Confirm cancellation')
|
||||
expect(out).toContain('will not renew')
|
||||
|
|
@ -331,7 +430,11 @@ describe('SubscriptionOverlay — confirm', () => {
|
|||
it('blocked: shows the reason + Manage on portal', () => {
|
||||
const out = render(
|
||||
at('confirm', subscriber(), {
|
||||
pending: { kind: 'tier_change', targetTierId: 'ultra', preview: { ok: true, effect: 'blocked', reason: 'Retract the cancellation before upgrading.' } }
|
||||
pending: {
|
||||
kind: 'tier_change',
|
||||
targetTierId: 'ultra',
|
||||
preview: { ok: true, effect: 'blocked', reason: 'Retract the cancellation before upgrading.' }
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -352,7 +455,11 @@ describe('SubscriptionOverlay — result', () => {
|
|||
it('error with recovery: shows the message + Open the portal', () => {
|
||||
const out = render(
|
||||
at('result', subscriber(), {
|
||||
result: { ok: false, message: 'This upgrade needs extra verification (3DS).', recoveryUrl: 'https://portal.example/x' }
|
||||
result: {
|
||||
ok: false,
|
||||
message: 'This upgrade needs extra verification (3DS).',
|
||||
recoveryUrl: 'https://portal.example/x'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -366,6 +473,7 @@ describe('SubscriptionOverlay — upgrade response mapping', () => {
|
|||
const applyUpgrade = async (response: unknown) => {
|
||||
const onPatch = vi.fn()
|
||||
const upgrade = vi.fn(() => Promise.resolve(response))
|
||||
|
||||
const mounted = mount(
|
||||
at('confirm', subscriber(), {
|
||||
ctx: { ...ctx, upgrade } as SubscriptionOverlayState['ctx'],
|
||||
|
|
@ -433,17 +541,22 @@ describe('SubscriptionOverlay — upgrade response mapping', () => {
|
|||
vi.useFakeTimers()
|
||||
|
||||
try {
|
||||
const refreshState = vi.fn(() => Promise.resolve(subscriber({
|
||||
current: {
|
||||
tier_id: 'ultra',
|
||||
tier_name: 'Ultra',
|
||||
monthly_credits: '3000',
|
||||
credits_remaining: '3000',
|
||||
cycle_ends_at: '2026-08-01',
|
||||
pending_downgrade_tier_name: null,
|
||||
pending_downgrade_at: null
|
||||
}
|
||||
})))
|
||||
const refreshState = vi.fn(() =>
|
||||
Promise.resolve(
|
||||
subscriber({
|
||||
current: {
|
||||
tier_id: 'ultra',
|
||||
tier_name: 'Ultra',
|
||||
monthly_credits: '3000',
|
||||
credits_remaining: '3000',
|
||||
cycle_ends_at: '2026-08-01',
|
||||
pending_downgrade_tier_name: null,
|
||||
pending_downgrade_at: null
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const result = { message: 'Upgraded to Ultra.', ok: true, pendingTierId: 'ultra' }
|
||||
const mounted = mount(at('result', subscriber(), { ctx: { ...ctx, refreshState }, result }))
|
||||
|
||||
|
|
|
|||
|
|
@ -313,19 +313,28 @@ describe('/billing slash command (overlay-driven)', () => {
|
|||
it.each([
|
||||
['admin', 'An admin turned off terminal billing for this terminal'],
|
||||
['self', 'You turned off terminal billing for this terminal']
|
||||
])('ctx.charge remote_spending_revoked (%s) → clears the overlay (no zombie button) + actor copy', async (actor, copy) => {
|
||||
const { run, sys } = buildCtx({
|
||||
'billing.state': ownerState(),
|
||||
'billing.charge': { ok: false, error: 'remote_spending_revoked', actor, recovery: 'reconnect', idempotency_key: 'k' }
|
||||
})
|
||||
])(
|
||||
'ctx.charge remote_spending_revoked (%s) → clears the overlay (no zombie button) + actor copy',
|
||||
async (actor, copy) => {
|
||||
const { run, sys } = buildCtx({
|
||||
'billing.state': ownerState(),
|
||||
'billing.charge': {
|
||||
ok: false,
|
||||
error: 'remote_spending_revoked',
|
||||
actor,
|
||||
recovery: 'reconnect',
|
||||
idempotency_key: 'k'
|
||||
}
|
||||
})
|
||||
|
||||
await run('')
|
||||
getOverlayState().billing!.ctx.charge('100')
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(printed(sys)).toContain(copy)
|
||||
expect(getOverlayState().billing).toBeNull()
|
||||
})
|
||||
await run('')
|
||||
getOverlayState().billing!.ctx.charge('100')
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(printed(sys)).toContain(copy)
|
||||
expect(getOverlayState().billing).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
it('ctx.charge session_revoked → clears overlay + re-login (not reconnect) copy', async () => {
|
||||
const { run, sys } = buildCtx({
|
||||
|
|
@ -353,9 +362,7 @@ describe('/billing slash command (overlay-driven)', () => {
|
|||
return Promise.reject(new Error('socket closed'))
|
||||
}
|
||||
|
||||
return Promise.resolve(
|
||||
method === 'billing.charge' ? { ok: true, charge_id: 'ch_1', idempotency_key: 'k' } : null
|
||||
)
|
||||
return Promise.resolve(method === 'billing.charge' ? { ok: true, charge_id: 'ch_1', idempotency_key: 'k' } : null)
|
||||
})
|
||||
await getOverlayState().billing!.ctx.charge('100')
|
||||
await vi.waitFor(() => expect(printed(sys)).toContain('outcome is unconfirmed'))
|
||||
|
|
|
|||
|
|
@ -35,7 +35,12 @@ describe('turnController.startMessage — flash-and-yield notices clear on next
|
|||
|
||||
it('leaves a sticky credits.depleted notice across a new turn', () => {
|
||||
patchUiState({
|
||||
notice: { key: 'credits.depleted', kind: 'sticky', level: 'error', text: '✕ Credit access paused · run /topup to top up' }
|
||||
notice: {
|
||||
key: 'credits.depleted',
|
||||
kind: 'sticky',
|
||||
level: 'error',
|
||||
text: '✕ Credit access paused · run /topup to top up'
|
||||
}
|
||||
})
|
||||
turnController.startMessage()
|
||||
expect(getUiState().notice?.key).toBe('credits.depleted')
|
||||
|
|
|
|||
|
|
@ -79,8 +79,22 @@ describe('/usage slash command', () => {
|
|||
renews_display: 'Jul 1, 2026',
|
||||
total_spendable_display: '$26.00',
|
||||
has_topup: true,
|
||||
plan_bar: { kind: 'plan', remaining_display: '$14.00', total_display: '$20.00', spent_display: '$6.00', pct_used: 30, fill_fraction: 0.7 },
|
||||
topup_bar: { kind: 'topup', remaining_display: '$12.00', total_display: '$12.00', spent_display: '$0.00', pct_used: null, fill_fraction: 1 }
|
||||
plan_bar: {
|
||||
kind: 'plan',
|
||||
remaining_display: '$14.00',
|
||||
total_display: '$20.00',
|
||||
spent_display: '$6.00',
|
||||
pct_used: 30,
|
||||
fill_fraction: 0.7
|
||||
},
|
||||
topup_bar: {
|
||||
kind: 'topup',
|
||||
remaining_display: '$12.00',
|
||||
total_display: '$12.00',
|
||||
spent_display: '$0.00',
|
||||
pct_used: null,
|
||||
fill_fraction: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,16 @@ import type { MutableRefObject, ReactNode, RefObject, SetStateAction } from 'rea
|
|||
|
||||
import type { PasteEvent } from '../components/textInput.js'
|
||||
import type { GatewayClient } from '../gatewayClient.js'
|
||||
import type { BillingCardInfo, BillingMutationResponse, BillingStateResponse, ImageAttachResponse, SessionCloseResponse, SubscriptionPreviewResponse, SubscriptionStateResponse, SubscriptionUpgradeResponse } from '../gatewayTypes.js'
|
||||
import type {
|
||||
BillingCardInfo,
|
||||
BillingMutationResponse,
|
||||
BillingStateResponse,
|
||||
ImageAttachResponse,
|
||||
SessionCloseResponse,
|
||||
SubscriptionPreviewResponse,
|
||||
SubscriptionStateResponse,
|
||||
SubscriptionUpgradeResponse
|
||||
} from '../gatewayTypes.js'
|
||||
import type { ParsedVoiceRecordKey } from '../lib/platform.js'
|
||||
import type { RpcResult } from '../lib/rpc.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
|
@ -96,9 +105,9 @@ export type BillingScreen = 'autoreload' | 'buy' | 'confirm' | 'limit' | 'overvi
|
|||
|
||||
/** Outcome of a charge attempt — lets the overlay route without tearing down. */
|
||||
export type BillingChargeOutcome =
|
||||
| 'submitted' // 202 accepted; settlement is reported via transcript lines
|
||||
| 'needs_remote_spending' // insufficient_scope → route to the stepup screen
|
||||
| 'error' // any other failure (already surfaced via sys)
|
||||
| 'submitted' // 202 accepted; settlement is reported via transcript lines
|
||||
| 'needs_remote_spending' // insufficient_scope → route to the stepup screen
|
||||
| 'error' // any other failure (already surfaced via sys)
|
||||
|
||||
/**
|
||||
* The functions the overlay needs to talk to the gateway and emit
|
||||
|
|
@ -173,10 +182,7 @@ export type SubscriptionScreen = 'confirm' | 'overview' | 'picker' | 'result' |
|
|||
|
||||
// The action held while the stepup screen grants terminal billing, replayed on
|
||||
// grant: re-preview a tier, re-apply the confirmed pending change, or re-resume.
|
||||
export type SubscriptionStepUpRetry =
|
||||
| { kind: 'apply' }
|
||||
| { kind: 'preview'; tierId: string }
|
||||
| { kind: 'resume' }
|
||||
export type SubscriptionStepUpRetry = { kind: 'apply' } | { kind: 'preview'; tierId: string } | { kind: 'resume' }
|
||||
|
||||
/** Outcome of a terminal-billing step-up: granted, plus the typed denial (for copy). */
|
||||
export interface StepUpResult {
|
||||
|
|
|
|||
|
|
@ -44,20 +44,20 @@ export const $isBlocked = computed(
|
|||
}) =>
|
||||
Boolean(
|
||||
agents ||
|
||||
approval ||
|
||||
billing ||
|
||||
clarify ||
|
||||
confirm ||
|
||||
journey ||
|
||||
modelPicker ||
|
||||
pager ||
|
||||
petPicker ||
|
||||
pluginsHub ||
|
||||
secret ||
|
||||
sessions ||
|
||||
skillsHub ||
|
||||
subscription ||
|
||||
sudo
|
||||
approval ||
|
||||
billing ||
|
||||
clarify ||
|
||||
confirm ||
|
||||
journey ||
|
||||
modelPicker ||
|
||||
pager ||
|
||||
petPicker ||
|
||||
pluginsHub ||
|
||||
secret ||
|
||||
sessions ||
|
||||
skillsHub ||
|
||||
subscription ||
|
||||
sudo
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -598,7 +598,9 @@ export const sessionCommands: SlashCommand[] = [
|
|||
const plan = usageModel.plan_name ?? (usageModel.status === 'free' ? 'Free' : null)
|
||||
|
||||
if (plan) {
|
||||
sections.push({ text: `Plan: ${plan}${usageModel.renews_display ? ` · renews ${usageModel.renews_display}` : ''}` })
|
||||
sections.push({
|
||||
text: `Plan: ${plan}${usageModel.renews_display ? ` · renews ${usageModel.renews_display}` : ''}`
|
||||
})
|
||||
}
|
||||
|
||||
if (barLines.length) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,10 @@ const buildSubscriptionCtx = (
|
|||
// Carry the typed denial (session_revoked / remote_spending_revoked /
|
||||
// rate_limited / …) so the stepup screen shows the right recovery.
|
||||
.then(r => ({ error: r?.error, granted: !!(r && r.ok && r.granted), message: r?.message }))
|
||||
.catch(() => ({ granted: false, message: 'Could not reach the billing service — check your connection, then retry.' })),
|
||||
.catch(() => ({
|
||||
granted: false,
|
||||
message: 'Could not reach the billing service — check your connection, then retry.'
|
||||
})),
|
||||
resume: () =>
|
||||
ctx.gateway
|
||||
.rpc<BillingMutationResponse>('subscription.resume', {})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import type { SlashCommand, SlashRunCtx } from '../types.js'
|
|||
// Poll cadence (plan §5, frozen): 2s interval, 5-minute cap.
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
const POLL_CAP_MS = 5 * 60 * 1000
|
||||
const UNCONFIRMED_CHARGE_MESSAGE = '🟡 Your last charge’s outcome is unconfirmed — check your balance/history before retrying.'
|
||||
const UNCONFIRMED_CHARGE_MESSAGE =
|
||||
'🟡 Your last charge’s outcome is unconfirmed — check your balance/history before retrying.'
|
||||
|
||||
type Sys = (text: string) => void
|
||||
|
||||
|
|
@ -74,7 +75,9 @@ const renderBillingError = (
|
|||
break
|
||||
|
||||
case 'role_required':
|
||||
sys('Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal.')
|
||||
sys(
|
||||
'Adding funds needs someone with billing permissions (owner, admin, or finance admin), or manage this on the portal.'
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
|
|
@ -94,7 +97,9 @@ const renderBillingError = (
|
|||
break
|
||||
|
||||
case 'auto_top_up_disabled_failures':
|
||||
sys('Auto-reload was turned off after repeated charge failures. Fix the card issue, then re-enable it from /topup → Auto-reload.')
|
||||
sys(
|
||||
'Auto-reload was turned off after repeated charge failures. Fix the card issue, then re-enable it from /topup → Auto-reload.'
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
|
|
@ -199,7 +204,11 @@ const pollCharge = (sys: Sys, ctx: SlashRunCtx, chargeId: string, portalUrl?: st
|
|||
ctx.guarded<BillingChargeStatusResponse>(r => {
|
||||
if (!r.ok) {
|
||||
// 429/503 while polling = retry-after, NOT a failure. Back off + continue.
|
||||
if (r.error === 'rate_limited' || r.error === 'temporarily_unavailable' || r.error === 'stripe_unavailable') {
|
||||
if (
|
||||
r.error === 'rate_limited' ||
|
||||
r.error === 'temporarily_unavailable' ||
|
||||
r.error === 'stripe_unavailable'
|
||||
) {
|
||||
if (timedOut()) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,7 +328,8 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
|||
// answering felt like the prompt had locked the entire UI. Explicitly
|
||||
// skip the prompt-overlay early-return for scroll keys so they fall
|
||||
// through to the wheel / PageUp / Shift+arrow handlers below.
|
||||
const promptOverlay = overlay.approval || overlay.billing || overlay.clarify || overlay.confirm || overlay.subscription
|
||||
const promptOverlay =
|
||||
overlay.approval || overlay.billing || overlay.clarify || overlay.confirm || overlay.subscription
|
||||
const fallThroughForScroll = promptOverlay && shouldFallThroughForScroll(key)
|
||||
|
||||
if (promptOverlay && !fallThroughForScroll) {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ export function PromptZone({
|
|||
const current = overlay.subscription
|
||||
|
||||
const onPatch = (next: Partial<typeof current>) =>
|
||||
patchOverlayState(prev => (prev.subscription ? { ...prev, subscription: { ...prev.subscription, ...next } } : prev))
|
||||
patchOverlayState(prev =>
|
||||
prev.subscription ? { ...prev, subscription: { ...prev.subscription, ...next } } : prev
|
||||
)
|
||||
|
||||
const onClose = () => patchOverlayState({ subscription: null })
|
||||
|
||||
|
|
|
|||
|
|
@ -157,7 +157,9 @@ function OverviewScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) {
|
|||
full menu — members/billing-off get the portal note instead. */}
|
||||
{full && (
|
||||
<Text color={t.color.muted}>
|
||||
{s.card ? `Card: ${s.card.display ?? s.card.masked}` : 'No saved card on file — “Add funds” walks you through adding one.'}
|
||||
{s.card
|
||||
? `Card: ${s.card.display ?? s.card.masked}`
|
||||
: 'No saved card on file — “Add funds” walks you through adding one.'}
|
||||
</Text>
|
||||
)}
|
||||
{note && (
|
||||
|
|
@ -354,14 +356,19 @@ function BuyScreen({ ctx, onPatch, s, t }: ScreenProps) {
|
|||
Add funds
|
||||
</Text>
|
||||
<Text color={t.color.text}>No saved card on file.</Text>
|
||||
<Text color={t.color.muted}>Add a card once on the portal billing page — after that you can top up right from the terminal.</Text>
|
||||
<Text color={t.color.muted}>
|
||||
Add a card once on the portal billing page — after that you can top up right from the terminal.
|
||||
</Text>
|
||||
<Text />
|
||||
{rows.map((label, i) => (
|
||||
<MenuRow active={cSel === i} index={i + 1} key={label} label={label} t={t} />
|
||||
))}
|
||||
{error && <Text color={t.color.error}>{error}</Text>}
|
||||
<Text />
|
||||
{footer(checking ? 'Checking for a card…' : `↑/↓ select · 1-${rows.length} quick pick · Enter confirm · Esc back`, t)}
|
||||
{footer(
|
||||
checking ? 'Checking for a card…' : `↑/↓ select · 1-${rows.length} quick pick · Enter confirm · Esc back`,
|
||||
t
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -475,7 +482,9 @@ function ConfirmScreen({
|
|||
<Text color={t.color.muted}>{payLine}</Text>
|
||||
{/* Provenance-less payloads (older NAS) keep the generic line; when the
|
||||
resolver says WHY this card, payLine already carries it. */}
|
||||
{s.card && !s.card.resolved_via && <Text color={t.color.muted}>Your card saved on the portal will be charged.</Text>}
|
||||
{s.card && !s.card.resolved_via && (
|
||||
<Text color={t.color.muted}>Your card saved on the portal will be charged.</Text>
|
||||
)}
|
||||
<Text color={t.color.muted}>By confirming, you allow Nous Research to charge your card.</Text>
|
||||
<Text />
|
||||
<ActionRow active={sel === 0} color={t.color.ok} label={`Pay $${amount} now`} t={t} />
|
||||
|
|
@ -677,9 +686,12 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) {
|
|||
const ar = s.auto_reload
|
||||
const enabled = Boolean(ar?.enabled)
|
||||
const distinctCard = ar?.card.kind === 'distinct' ? ar.card : null
|
||||
|
||||
const distinctCardName = distinctCard
|
||||
? [distinctCard.brand, distinctCard.last4 ? `••${distinctCard.last4}` : null].filter(Boolean).join(' ') || 'a different card'
|
||||
? [distinctCard.brand, distinctCard.last4 ? `••${distinctCard.last4}` : null].filter(Boolean).join(' ') ||
|
||||
'a different card'
|
||||
: null
|
||||
|
||||
const manageCardLabel = 'Use your card on file — manage on portal'
|
||||
|
||||
// Prefill from state (strip the $ from the *_usd raw fields if present).
|
||||
|
|
@ -690,14 +702,17 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) {
|
|||
const [error, setError] = useState<null | string>(null)
|
||||
// focusRow: 0=threshold field, 1=reloadTo field, 2=Agree, 3=Turn off (if enabled), last=Cancel
|
||||
const manageCardRows = distinctCard && s.portal_url ? [manageCardLabel] : []
|
||||
|
||||
const actionRows = enabled
|
||||
? ['Agree and turn on', 'Turn off', ...manageCardRows, 'Cancel']
|
||||
: ['Agree and turn on', ...manageCardRows, 'Cancel']
|
||||
|
||||
const actionColors: Record<string, string> = {
|
||||
'Agree and turn on': t.color.ok,
|
||||
'Turn off': t.color.warn,
|
||||
[manageCardLabel]: t.color.accent
|
||||
}
|
||||
|
||||
const FIELD_ROWS = 2
|
||||
const [row, setRow] = useState(0)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,7 @@ export interface MenuRowSpec {
|
|||
* the key fully handled and skip the default escape/arrow/enter/number
|
||||
* handling for that keypress (e.g. a screen with a text-input sub-mode).
|
||||
*/
|
||||
export function useMenu(
|
||||
rows: MenuRowSpec[],
|
||||
onEscape: () => void,
|
||||
onKey?: (ch: string, key: Key) => boolean
|
||||
): number {
|
||||
export function useMenu(rows: MenuRowSpec[], onEscape: () => void, onKey?: (ch: string, key: Key) => boolean): number {
|
||||
const [sel, setSel] = useState(0)
|
||||
|
||||
useInput((ch, key) => {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,7 @@ import type {
|
|||
SubscriptionResult,
|
||||
SubscriptionStepUpRetry
|
||||
} from '../app/interfaces.js'
|
||||
import type {
|
||||
SubscriptionStateResponse,
|
||||
SubscriptionTierOption,
|
||||
SubscriptionUpgradeResponse
|
||||
} from '../gatewayTypes.js'
|
||||
import type { SubscriptionStateResponse, SubscriptionTierOption, SubscriptionUpgradeResponse } from '../gatewayTypes.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { ActionRow, footer, MenuRow, type MenuRowSpec, UsageBars, useMenu } from './overlayPrimitives.js'
|
||||
|
|
@ -173,7 +169,11 @@ function stepUpDenialResult(res: { error?: string; message?: string }): Subscrip
|
|||
}
|
||||
|
||||
if (res.error === 'remote_spending_revoked') {
|
||||
return { message: res.message || 'Terminal spending was turned off for this session — reconnect from the portal, then retry.', ok: false }
|
||||
return {
|
||||
message:
|
||||
res.message || 'Terminal spending was turned off for this session — reconnect from the portal, then retry.',
|
||||
ok: false
|
||||
}
|
||||
}
|
||||
|
||||
if (res.error === 'rate_limited') {
|
||||
|
|
@ -182,7 +182,8 @@ function stepUpDenialResult(res: { error?: string; message?: string }): Subscrip
|
|||
|
||||
return {
|
||||
message:
|
||||
res.message || 'Terminal billing was not enabled — someone with billing permissions (owner, admin, or finance admin) must allow it for this org. You can also make this change on the portal.',
|
||||
res.message ||
|
||||
'Terminal billing was not enabled — someone with billing permissions (owner, admin, or finance admin) must allow it for this org. You can also make this change on the portal.',
|
||||
ok: false
|
||||
}
|
||||
}
|
||||
|
|
@ -259,28 +260,46 @@ function applyPendingAndRoute(
|
|||
const finish = (result: SubscriptionResult) => onPatch({ result, screen: 'result' })
|
||||
|
||||
if (pending.kind === 'cancellation') {
|
||||
return ctx.scheduleCancellation().then(r =>
|
||||
isScopeDenial(r)
|
||||
? toStepUp()
|
||||
: finish(mutationResult(r, 'Scheduled — your plan stays active until the end of the billing period, then it cancels. Nothing changes today.'))
|
||||
)
|
||||
return ctx
|
||||
.scheduleCancellation()
|
||||
.then(r =>
|
||||
isScopeDenial(r)
|
||||
? toStepUp()
|
||||
: finish(
|
||||
mutationResult(
|
||||
r,
|
||||
'Scheduled — your plan stays active until the end of the billing period, then it cancels. Nothing changes today.'
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (pending.kind === 'upgrade') {
|
||||
return ctx.upgrade(pending.targetTierId ?? '', pending.idempotencyKey).then(r =>
|
||||
isScopeDenial(r) ? toStepUp() : finish(upgradeResult(r, pending.targetTierId))
|
||||
)
|
||||
return ctx
|
||||
.upgrade(pending.targetTierId ?? '', pending.idempotencyKey)
|
||||
.then(r => (isScopeDenial(r) ? toStepUp() : finish(upgradeResult(r, pending.targetTierId))))
|
||||
}
|
||||
|
||||
return ctx.scheduleChange(pending.targetTierId ?? '').then(r =>
|
||||
isScopeDenial(r)
|
||||
? toStepUp()
|
||||
: finish(mutationResult(r, 'Scheduled — your plan doesn’t change today. You keep your current plan until the end of the billing period, then it switches.'))
|
||||
)
|
||||
return ctx
|
||||
.scheduleChange(pending.targetTierId ?? '')
|
||||
.then(r =>
|
||||
isScopeDenial(r)
|
||||
? toStepUp()
|
||||
: finish(
|
||||
mutationResult(
|
||||
r,
|
||||
'Scheduled — your plan doesn’t change today. You keep your current plan until the end of the billing period, then it switches.'
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** Resume (undo the pending change) and route: result (ok/err) or stepup (scope). */
|
||||
function resumeAndRoute(ctx: SubscriptionOverlayState['ctx'], onPatch: ScreenProps['onPatch'], allowStepUp = true): Promise<void> {
|
||||
function resumeAndRoute(
|
||||
ctx: SubscriptionOverlayState['ctx'],
|
||||
onPatch: ScreenProps['onPatch'],
|
||||
allowStepUp = true
|
||||
): Promise<void> {
|
||||
return ctx.resume().then(r => {
|
||||
if (isScopeDenial(r)) {
|
||||
return allowStepUp
|
||||
|
|
@ -288,7 +307,10 @@ function resumeAndRoute(ctx: SubscriptionOverlayState['ctx'], onPatch: ScreenPro
|
|||
: onPatch({ result: scopeStillDeniedResult, screen: 'result' })
|
||||
}
|
||||
|
||||
return onPatch({ result: mutationResult(r, 'Your pending change was undone — you stay on your current plan.'), screen: 'result' })
|
||||
return onPatch({
|
||||
result: mutationResult(r, 'Your pending change was undone — you stay on your current plan.'),
|
||||
screen: 'result'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -594,7 +616,11 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
if (isCancellation) {
|
||||
primary = { color: t.color.warn, label: 'Cancel subscription', run: apply }
|
||||
} else if (effect === 'charge_now') {
|
||||
primary = { color: t.color.ok, label: amount ? `Pay ${amount} & upgrade now` : 'Upgrade now (prorated charge)', run: apply }
|
||||
primary = {
|
||||
color: t.color.ok,
|
||||
label: amount ? `Pay ${amount} & upgrade now` : 'Upgrade now (prorated charge)',
|
||||
run: apply
|
||||
}
|
||||
} else if (effect === 'scheduled') {
|
||||
primary = { color: t.color.ok, label: `Schedule change to ${targetName}`, run: apply }
|
||||
} else if (effect === 'blocked') {
|
||||
|
|
@ -604,7 +630,12 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
const rows: MenuRowSpec[] = primary ? [primary, { label: 'Back', run: back }] : [{ label: 'Back', run: back }]
|
||||
const sel = useMenu(rows, back)
|
||||
// Chip contrasts an immediate charge vs a period-end schedule at a glance.
|
||||
const chip = effect === 'charge_now' ? { color: t.color.ok, label: 'charged now' } : effect === 'scheduled' ? { color: t.color.warn, label: 'scheduled · not today' } : null
|
||||
const chip =
|
||||
effect === 'charge_now'
|
||||
? { color: t.color.ok, label: 'charged now' }
|
||||
: effect === 'scheduled'
|
||||
? { color: t.color.warn, label: 'scheduled · not today' }
|
||||
: null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
|
@ -619,10 +650,12 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
{isCancellation && (
|
||||
<>
|
||||
<Text color={t.color.text}>
|
||||
Cancel {s.current?.tier_name ?? 'your plan'} — it stays active until {shortDate(s.current?.cycle_ends_at)}, then
|
||||
will not renew.
|
||||
Cancel {s.current?.tier_name ?? 'your plan'} — it stays active until {shortDate(s.current?.cycle_ends_at)},
|
||||
then will not renew.
|
||||
</Text>
|
||||
<Text color={t.color.muted}>
|
||||
You keep your remaining credits for this period. You can resume before it ends.
|
||||
</Text>
|
||||
<Text color={t.color.muted}>You keep your remaining credits for this period. You can resume before it ends.</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
@ -636,7 +669,9 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
<Text color={t.color.muted}>Monthly credits change: {preview.monthly_credits_delta}.</Text>
|
||||
)}
|
||||
<Text color={t.color.muted}>
|
||||
{chargeCard ? `${chargeCard} — the card on your subscription — will be charged.` : 'The card on your subscription will be charged.'}
|
||||
{chargeCard
|
||||
? `${chargeCard} — the card on your subscription — will be charged.`
|
||||
: 'The card on your subscription will be charged.'}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -644,8 +679,8 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
{effect === 'scheduled' && !isCancellation && (
|
||||
<>
|
||||
<Text color={t.color.text}>
|
||||
Change to {targetName} — takes effect {shortDate(preview?.effective_at)}. No charge now; you keep your current
|
||||
plan until then.
|
||||
Change to {targetName} — takes effect {shortDate(preview?.effective_at)}. No charge now; you keep your
|
||||
current plan until then.
|
||||
</Text>
|
||||
{preview?.monthly_credits_delta && (
|
||||
<Text color={t.color.muted}>Monthly credits change: {preview.monthly_credits_delta}.</Text>
|
||||
|
|
@ -658,7 +693,9 @@ function ConfirmScreen({ onClose, onPatch, overlay, t }: ScreenProps) {
|
|||
)}
|
||||
|
||||
{effect === 'blocked' && !isCancellation && (
|
||||
<Text color={t.color.warn}>{preview?.reason ?? 'That change cannot be made here — manage it on the portal.'}</Text>
|
||||
<Text color={t.color.warn}>
|
||||
{preview?.reason ?? 'That change cannot be made here — manage it on the portal.'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text />
|
||||
|
|
@ -678,6 +715,7 @@ function ResultScreen({ onClose, overlay, t }: Omit<ScreenProps, 'onPatch'>) {
|
|||
const result = overlay.result ?? null
|
||||
const recoveryUrl = result?.recoveryUrl ?? null
|
||||
const pendingTierId = result?.pendingTierId ?? null
|
||||
|
||||
const [applyState, setApplyState] = useState<'applying' | 'confirmed' | 'timed_out'>(
|
||||
pendingTierId ? 'applying' : 'confirmed'
|
||||
)
|
||||
|
|
@ -738,6 +776,7 @@ function ResultScreen({ onClose, overlay, t }: Omit<ScreenProps, 'onPatch'>) {
|
|||
|
||||
const applying = result?.ok && applyState === 'applying'
|
||||
const timedOut = result?.ok && applyState === 'timed_out'
|
||||
|
||||
const message = timedOut
|
||||
? 'Your upgrade succeeded and is still applying — refresh in a moment.'
|
||||
: (result?.message ?? '')
|
||||
|
|
@ -751,7 +790,10 @@ function ResultScreen({ onClose, overlay, t }: Omit<ScreenProps, 'onPatch'>) {
|
|||
}
|
||||
|
||||
const rows: MenuRowSpec[] = recoveryUrl
|
||||
? [{ color: t.color.accent, label: 'Open the portal to finish', run: openRecovery }, { label: 'Close', run: onClose }]
|
||||
? [
|
||||
{ color: t.color.accent, label: 'Open the portal to finish', run: openRecovery },
|
||||
{ label: 'Close', run: onClose }
|
||||
]
|
||||
: [{ label: 'Close', run: onClose }]
|
||||
|
||||
const sel = useMenu(rows, onClose)
|
||||
|
|
@ -762,7 +804,9 @@ function ResultScreen({ onClose, overlay, t }: Omit<ScreenProps, 'onPatch'>) {
|
|||
{applying ? 'Applying…' : timedOut ? 'Still applying' : result?.ok ? 'Done' : 'Could not complete'}
|
||||
</Text>
|
||||
<Text color={t.color.text}>{message}</Text>
|
||||
{result?.ok && !applying && !timedOut && <Text color={t.color.muted}>Re-run /subscription anytime to review it.</Text>}
|
||||
{result?.ok && !applying && !timedOut && (
|
||||
<Text color={t.color.muted}>Re-run /subscription anytime to review it.</Text>
|
||||
)}
|
||||
<Text />
|
||||
{rows.map((row, i) => (
|
||||
<ActionRow active={sel === i} color={row.color} key={row.label} label={row.label} t={t} />
|
||||
|
|
@ -857,9 +901,15 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) {
|
|||
|
||||
const rows: MenuRowSpec[] =
|
||||
phase === 'granted'
|
||||
? [{ color: t.color.ok, label: retry?.kind === 'apply' ? 'Continue the change' : 'Continue', run: resume }, { label: 'Cancel', run: back }]
|
||||
? [
|
||||
{ color: t.color.ok, label: retry?.kind === 'apply' ? 'Continue the change' : 'Continue', run: resume },
|
||||
{ label: 'Cancel', run: back }
|
||||
]
|
||||
: phase === 'prompt'
|
||||
? [{ color: t.color.ok, label: 'Enable terminal billing', run: enable }, { label: 'Cancel', run: back }]
|
||||
? [
|
||||
{ color: t.color.ok, label: 'Enable terminal billing', run: enable },
|
||||
{ label: 'Cancel', run: back }
|
||||
]
|
||||
: []
|
||||
|
||||
const sel = useMenu(rows, back)
|
||||
|
|
@ -871,12 +921,18 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) {
|
|||
</Text>
|
||||
{phase === 'prompt' && (
|
||||
<>
|
||||
<Text color={t.color.text}>Changing your plan needs terminal billing enabled for this org. Enable it here, then continue.</Text>
|
||||
<Text color={t.color.muted}>Someone with billing permissions (owner, admin, or finance admin) approves it once in the browser.</Text>
|
||||
<Text color={t.color.text}>
|
||||
Changing your plan needs terminal billing enabled for this org. Enable it here, then continue.
|
||||
</Text>
|
||||
<Text color={t.color.muted}>
|
||||
Someone with billing permissions (owner, admin, or finance admin) approves it once in the browser.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{phase === 'waiting' && (
|
||||
<Text color={t.color.muted}>Opening your browser to approve… finish there, then come back — nothing is charged until you continue.</Text>
|
||||
<Text color={t.color.muted}>
|
||||
Opening your browser to approve… finish there, then come back — nothing is charged until you continue.
|
||||
</Text>
|
||||
)}
|
||||
{phase === 'granted' && <Text color={t.color.ok}>Terminal billing enabled. Continue to finish your change.</Text>}
|
||||
{phase === 'resuming' && <Text color={t.color.muted}>Applying your change…</Text>}
|
||||
|
|
@ -885,7 +941,14 @@ function StepUpScreen({ onPatch, overlay, t }: ScreenProps) {
|
|||
<ActionRow active={sel === i} color={row.color} key={row.label} label={row.label} t={t} />
|
||||
))}
|
||||
<Text />
|
||||
{footer(phase === 'waiting' ? 'Waiting for approval… · Esc to cancel' : phase === 'resuming' ? 'Working…' : '↑/↓ select · Enter · Esc back', t)}
|
||||
{footer(
|
||||
phase === 'waiting'
|
||||
? 'Waiting for approval… · Esc to cancel'
|
||||
: phase === 'resuming'
|
||||
? 'Working…'
|
||||
: '↑/↓ select · Enter · Esc back',
|
||||
t
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,36 +157,36 @@ export interface BillingMutationResponse {
|
|||
export interface SubscriptionTierOption {
|
||||
tier_id: string
|
||||
name: string
|
||||
tier_order: number // sorts the picker + upgrade/downgrade hint
|
||||
dollars_per_month_display: string // pre-formatted ($X / $X.YY)
|
||||
tier_order: number // sorts the picker + upgrade/downgrade hint
|
||||
dollars_per_month_display: string // pre-formatted ($X / $X.YY)
|
||||
monthly_credits: string | null
|
||||
is_current: boolean // the active plan: shown, not selectable
|
||||
is_enabled: boolean // false = grandfathered current tier
|
||||
is_current: boolean // the active plan: shown, not selectable
|
||||
is_enabled: boolean // false = grandfathered current tier
|
||||
}
|
||||
|
||||
export interface SubscriptionStateResponse {
|
||||
ok: boolean
|
||||
logged_in: boolean
|
||||
is_admin: boolean
|
||||
can_change_plan: boolean // role gate (ADMIN/OWNER), from NAS
|
||||
can_change_plan: boolean // role gate (ADMIN/OWNER), from NAS
|
||||
org_name: string | null
|
||||
org_id: string | null // org.id from the NAS response
|
||||
org_id: string | null // org.id from the NAS response
|
||||
role: string | null
|
||||
context: 'personal' | 'team' // personal account vs team/org terminal
|
||||
context: 'personal' | 'team' // personal account vs team/org terminal
|
||||
current: {
|
||||
tier_id: string | null // null = free (no active sub)
|
||||
tier_id: string | null // null = free (no active sub)
|
||||
tier_name: string | null
|
||||
monthly_credits: string | null
|
||||
credits_remaining: string | null
|
||||
cycle_ends_at: string | null // ISO
|
||||
cycle_ends_at: string | null // ISO
|
||||
pending_downgrade_tier_name: string | null
|
||||
pending_downgrade_at: string | null
|
||||
pending_downgrade_display: string | null // formatted pending_downgrade_at
|
||||
pending_downgrade_display: string | null // formatted pending_downgrade_at
|
||||
cancel_at_period_end: boolean // subscription scheduled to cancel at period end
|
||||
cancellation_effective_at: string | null // ISO when cancellation takes effect
|
||||
cancellation_effective_display: string | null // formatted cancellation_effective_at
|
||||
cancellation_effective_at: string | null // ISO when cancellation takes effect
|
||||
cancellation_effective_display: string | null // formatted cancellation_effective_at
|
||||
} | null
|
||||
tiers: SubscriptionTierOption[] // selectable catalog for the in-terminal picker
|
||||
tiers: SubscriptionTierOption[] // selectable catalog for the in-terminal picker
|
||||
portal_url: string | null
|
||||
error?: string | null
|
||||
// Shared dollar usage model (two-bar view), embedded by the gateway so the
|
||||
|
|
@ -206,8 +206,8 @@ export interface SubscriptionPreviewResponse {
|
|||
target_tier_id?: string | null
|
||||
target_tier_name?: string | null
|
||||
monthly_credits_delta?: string | null
|
||||
amount_due_now_cents?: number | null // the prorated upfront charge for an upgrade
|
||||
effective_at?: string | null // ISO, when a scheduled change lands
|
||||
amount_due_now_cents?: number | null // the prorated upfront charge for an upgrade
|
||||
effective_at?: string | null // ISO, when a scheduled change lands
|
||||
// typed-error envelope (present when ok=false)
|
||||
error?: string
|
||||
message?: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue