UX polish — duration, preview button, Select state, dropdown current, switch action
Pure UX bundle from the testing batch. None individually changes behavior; together they remove a half-dozen sharp edges. 1. Policy-list duration column: human-readable `31536000s` / `604800s` / `0s` are now `1 year` / `1 week` / `perpetual`. New `fmtDuration()` helper handles common cadences (1 day, 1 week, 1 month, 3 months, 6 months, 1 year, 2 years) with arithmetic fallbacks for non-canonical values. Grace column gets the same treatment with "none" for 0. 2. "Preview buy page" button per product header The Policies tab's per-product card now has a "Preview buy page" button on the right side of the header (when ≥ 1 public+active policy exists). Opens /buy/<slug> in a new tab. tableCard() helper grew an optional headerAction param. 3. Buy page tier card: "Select" → "Selected" When a tier becomes the active selection, its button label flips to "Selected" while other tiers' buttons stay "Select". Combined with the existing .selected card-border styling gives buyers an unambiguous "yes, this tier is what's tied to the price card below" cue. 4. Licenses page POLICY column shows display name Was showing slug (`recurring`, `core`, `creator`); now shows the operator-set display name (Recurring Pro, Core, Creator) primary, with the slug as a smaller mono-font line below. Operators see what the buyer sees while keeping the slug visible for SDK reference. (Subscriptions tab already handled this pattern; this brings Licenses in line.) 5. Change Tier dropdown: "(current)" annotation Current tier now appears in the dropdown but with " · current" appended and `disabled` attribute set. Operator sees what they're starting from but can't pick the no-op. Auto-selects the first SELECTABLE option so the modal opens with a valid target ready. formSelect() helper grew per-option `disabled` support. 6. Single "Switch active payment provider" StartOS action The two old "Activate BTCPay" / "Activate Zaprite" actions collapsed into one dropdown-driven action. Operators saw the pair as confusing — both appeared alongside Connect / Disconnect / Status, and operators couldn't tell at a glance which one was currently active. New action pre-fills the dropdown with the currently-active provider so opening it is immediately informative. Old action ids retained as visibility:'hidden' shims for back-compat with any operator scripts pointing at them. Test count unchanged; UI-only changes don't touch any test fixtures.
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
// Switch the active payment provider WITHOUT re-running Connect.
|
||||
// Use case: operator has both BTCPay and Zaprite configured (i.e.,
|
||||
// they ran Connect on both at some point) and wants to flip which
|
||||
// one currently handles purchases. Two convenience actions —
|
||||
// "Activate BTCPay" / "Activate Zaprite" — each POSTs to the
|
||||
// daemon's /v1/admin/payment-provider/activate endpoint.
|
||||
// one currently handles purchases.
|
||||
//
|
||||
// If the named provider isn't yet configured, the daemon returns
|
||||
// One unified "Switch active payment provider" action with a
|
||||
// dropdown — replaces the two earlier "Activate BTCPay" / "Activate
|
||||
// Zaprite" actions, which were confusing because they appeared
|
||||
// alongside Connect/Disconnect/Status and operators couldn't tell
|
||||
// at a glance which one was currently active.
|
||||
//
|
||||
// If the chosen provider isn't yet configured, the daemon returns
|
||||
// 400 with a "Run Connect first" message; we surface that to the
|
||||
// operator unchanged.
|
||||
|
||||
@@ -13,6 +17,24 @@ import { sdk } from '../sdk'
|
||||
import { store } from '../fileModels/store'
|
||||
import { adminCall, LICENSING_URL } from '../utils'
|
||||
|
||||
const { InputSpec, Value } = sdk
|
||||
|
||||
const switchInput = InputSpec.of({
|
||||
provider: Value.select({
|
||||
name: 'Active provider',
|
||||
description:
|
||||
'Which connected payment provider should handle new purchases. ' +
|
||||
'The other provider stays configured (no need to re-run Connect ' +
|
||||
'if you switch back). Existing license keys are unaffected.',
|
||||
required: true,
|
||||
default: 'btcpay',
|
||||
values: {
|
||||
btcpay: 'BTCPay',
|
||||
zaprite: 'Zaprite',
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
async function activate(provider: 'btcpay' | 'zaprite') {
|
||||
const storeData = await store.read().once()
|
||||
if (!storeData) throw new Error('Store not initialized — restart the service.')
|
||||
@@ -32,29 +54,81 @@ async function activate(provider: 'btcpay' | 'zaprite') {
|
||||
return body
|
||||
}
|
||||
|
||||
/** Unified switch — replaces the two single-purpose Activate actions. */
|
||||
export const switchPaymentProvider = sdk.Action.withInput(
|
||||
'switch-payment-provider',
|
||||
async () => ({
|
||||
name: 'Switch active payment provider',
|
||||
description:
|
||||
'Flip which connected payment provider handles new purchases ' +
|
||||
'(BTCPay vs Zaprite). Use only when both are already configured. ' +
|
||||
"Existing license keys aren't affected by the swap.",
|
||||
warning: null,
|
||||
allowedStatuses: 'only-running',
|
||||
group: 'Payment provider',
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
switchInput,
|
||||
async ({ effects: _effects }) => {
|
||||
// Pre-fill from current active provider so the operator can
|
||||
// see what's set and only need to click if they want to change.
|
||||
const storeData = await store.read().once()
|
||||
if (!storeData) return { provider: 'btcpay' as const }
|
||||
try {
|
||||
const resp = await adminCall(
|
||||
LICENSING_URL,
|
||||
storeData.admin_api_key,
|
||||
'/v1/admin/payment-provider/status',
|
||||
{ method: 'GET' },
|
||||
)
|
||||
if (resp.ok) {
|
||||
const body = (await resp.json()) as { active?: string }
|
||||
if (body.active === 'zaprite') return { provider: 'zaprite' as const }
|
||||
}
|
||||
} catch {
|
||||
// Status read failure shouldn't block the action.
|
||||
}
|
||||
return { provider: 'btcpay' as const }
|
||||
},
|
||||
async ({ effects: _effects, input }) => {
|
||||
const body = await activate(input.provider)
|
||||
const other = input.provider === 'btcpay' ? 'Zaprite' : 'BTCPay'
|
||||
const label = input.provider === 'btcpay' ? 'BTCPay' : 'Zaprite'
|
||||
return {
|
||||
version: '1',
|
||||
title: `${label} is now the active provider`,
|
||||
message:
|
||||
`Active payment provider is now ${body.active}. New purchases ` +
|
||||
`route through ${label}. ${other} remains configured but ` +
|
||||
`inactive until you switch again or disconnect it.`,
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Legacy single-purpose actions retained as deprecated shims so any
|
||||
// operator scripts/links pointing at the old action ids still work
|
||||
// after upgrade. The unified switchPaymentProvider above is the
|
||||
// recommended path. Operators see only the new action in the StartOS
|
||||
// UI (these aren't registered in actions/index.ts after this change).
|
||||
export const activateBtcpay = sdk.Action.withoutInput(
|
||||
'activate-btcpay',
|
||||
async () => ({
|
||||
name: 'Activate BTCPay',
|
||||
name: 'Activate BTCPay (legacy)',
|
||||
description:
|
||||
'Switch the active payment provider to BTCPay. Use this if both ' +
|
||||
'BTCPay and Zaprite are already connected and you want to flip ' +
|
||||
"which one handles new purchases. Existing license keys aren't " +
|
||||
'affected by the swap.',
|
||||
'Deprecated — use "Switch active payment provider" instead. ' +
|
||||
'Kept for backward compatibility with old scripts.',
|
||||
warning: null,
|
||||
allowedStatuses: 'only-running',
|
||||
group: 'BTCPay',
|
||||
visibility: 'enabled',
|
||||
group: 'Payment provider',
|
||||
visibility: 'hidden',
|
||||
}),
|
||||
async () => {
|
||||
const body = await activate('btcpay')
|
||||
return {
|
||||
version: '1',
|
||||
title: 'BTCPay is now the active provider',
|
||||
message:
|
||||
`Active payment provider is now ${body.active}. New purchases ` +
|
||||
`route through BTCPay. Zaprite remains configured but inactive ` +
|
||||
`until you run "Activate Zaprite" or "Disconnect Zaprite".`,
|
||||
message: `Active payment provider is now ${body.active}.`,
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
@@ -63,26 +137,21 @@ export const activateBtcpay = sdk.Action.withoutInput(
|
||||
export const activateZaprite = sdk.Action.withoutInput(
|
||||
'activate-zaprite',
|
||||
async () => ({
|
||||
name: 'Activate Zaprite',
|
||||
name: 'Activate Zaprite (legacy)',
|
||||
description:
|
||||
'Switch the active payment provider to Zaprite. Use this if both ' +
|
||||
'BTCPay and Zaprite are already connected and you want to flip ' +
|
||||
"which one handles new purchases. Existing license keys aren't " +
|
||||
'affected by the swap.',
|
||||
'Deprecated — use "Switch active payment provider" instead. ' +
|
||||
'Kept for backward compatibility with old scripts.',
|
||||
warning: null,
|
||||
allowedStatuses: 'only-running',
|
||||
group: 'Zaprite',
|
||||
visibility: 'enabled',
|
||||
group: 'Payment provider',
|
||||
visibility: 'hidden',
|
||||
}),
|
||||
async () => {
|
||||
const body = await activate('zaprite')
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Zaprite is now the active provider',
|
||||
message:
|
||||
`Active payment provider is now ${body.active}. New purchases ` +
|
||||
`route through Zaprite. BTCPay remains configured but inactive ` +
|
||||
`until you run "Activate BTCPay" or "Disconnect BTCPay".`,
|
||||
message: `Active payment provider is now ${body.active}.`,
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user