Skip to content
Merged
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
94 changes: 58 additions & 36 deletions components/Pricing.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState } from 'react'
import Link from 'next/link'

import TierSelector from '@components/TierSelector'

import { track, EVENTS } from 'utils/analytics'
import status from '../public/status.json'

Expand Down Expand Up @@ -199,11 +201,15 @@ export default function Pricing({ hostedOffer = null }) {
Choose how you want to start.
</h1>
<p className="mx-auto mt-4 max-w-3xl text-center text-sm leading-relaxed text-ink-2 md:text-base">
Run OpenAdapt yourself, qualify a consequential enterprise
workflow with our team, or subscribe to managed Cloud for an
approved workflow.
Run OpenAdapt yourself, subscribe to managed Cloud for an
approved workflow, or qualify a consequential enterprise
workflow with our team.
</p>

<div className="mx-auto mt-10 max-w-4xl">
<TierSelector />
</div>

<div
role="list"
aria-label="Ways to start with OpenAdapt"
Expand Down Expand Up @@ -233,34 +239,6 @@ export default function Pricing({ hostedOffer = null }) {
</p>
</EntryCard>

<EntryCard
id="pricing-enterprise"
eyebrow="Recommended for enterprise"
title="Workflow Qualification Sprint"
price="From $15,000"
priceDetail="ten-business-day target"
description="Qualify one application, one environment, and one measurable business effect before scaling."
features={[
'Suitability decision and qualified prototype',
'Identity and effect-verification contract',
'Failure, exception, and deployment analysis',
'ROI model and signed go/no-go report',
]}
featured
>
<p className="mt-4 text-xs leading-relaxed text-ink-3">
Native, RDP, and Citrix qualifications are typically
$25,000–$40,000. The sprint is paid even when the
correct outcome is not to automate.
</p>
<Link
href="/qualify"
className="btn-ink mt-5 w-full text-center"
>
Qualify one workflow
</Link>
</EntryCard>

<EntryCard
id="cloud-preview"
eyebrow="Managed self-service"
Expand All @@ -277,22 +255,32 @@ export default function Pricing({ hostedOffer = null }) {
'Live subscription with secure Stripe checkout',
]}
>
<div className="mb-5 flex items-center gap-3">
<div>
<span
className="rounded-full border border-hairline bg-ground px-3 py-1 font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-ink-2"
className="inline-block rounded-full border border-hairline bg-ground px-3 py-1 font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-ink-2"
data-testid="hosted-status-label"
>
{hostedStatusLabel}
</span>
{runCapLabel && (
<span
className="text-xs font-medium text-ink-2"
<p
className="mt-2 text-xs font-medium text-ink-2"
data-testid="hosted-run-cap"
>
{runCapLabel}
</span>
</p>
)}
</div>
<p className="mb-5 mt-3 text-xs leading-relaxed text-ink-3">
Evaluating first? The free{' '}
<a
href="#community"
className="text-accent underline"
>
OpenAdapt Community
</a>{' '}
runtime runs locally at no cost.
</p>
<HostedCheckoutButton
available={hostedOfferAvailable}
/>
Expand Down Expand Up @@ -325,6 +313,34 @@ export default function Pricing({ hostedOffer = null }) {
</Link>
</p>
</EntryCard>

<EntryCard
id="pricing-enterprise"
eyebrow="Recommended for enterprise"
title="Workflow Qualification Sprint"
price="From $15,000"
priceDetail="ten-business-day target"
description="Qualify one application, one environment, and one measurable business effect before scaling."
features={[
'Suitability decision and qualified prototype',
'Identity and effect-verification contract',
'Failure, exception, and deployment analysis',
'ROI model and signed go/no-go report',
]}
featured
>
<p className="mt-4 text-xs leading-relaxed text-ink-3">
Native, RDP, and Citrix qualifications are typically
$25,000–$40,000. The sprint is paid even when the
correct outcome is not to automate.
</p>
<Link
href="/qualify"
className="btn-ink mt-5 w-full text-center"
>
Qualify one workflow
</Link>
</EntryCard>
</div>

<div
Expand Down Expand Up @@ -417,6 +433,12 @@ export default function Pricing({ hostedOffer = null }) {
Explore OpenAdapt Execute
</Link>
</aside>

<p className="mt-4 border-t border-hairline pt-4 text-xs leading-relaxed text-ink-3">
Production contracts are priced per verified
workflow-run volume against the published annual floor
shown above; exact quotes follow qualification.
</p>
</div>

{/*
Expand Down
167 changes: 167 additions & 0 deletions components/TierSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { useState } from 'react'

import styles from './TierSelector.module.css'

/*
* Pure client-side helper at the top of /pricing. Three questions map a
* visitor to one recommended starting tier, with anchor links to the tier
* cards below. Recommendations only restate claims already made by the tier
* cards themselves: Cloud is scoped to browser workflows on non-regulated
* data, and regulated data, desktop, RDP, and Citrix start with
* qualification.
*/

const QUESTIONS = [
{
id: 'substrate',
label: 'Where does the workflow run?',
options: [
{ value: 'browser', label: 'In the browser' },
{ value: 'desktop-or-remote', label: 'Desktop or remote (RDP/Citrix)' },
],
},
{
id: 'regulated',
label: 'Is the data regulated?',
options: [
{ value: 'no', label: 'No' },
{ value: 'yes', label: 'Yes' },
],
},
{
id: 'operator',
label: 'Who operates it?',
options: [
{ value: 'you', label: 'Your team' },
{ value: 'us', label: 'OpenAdapt' },
],
},
]

const TIERS = {
community: {
href: '#community',
name: 'OpenAdapt Community',
why: 'Run the free MIT-licensed local runtime yourself and evaluate at no cost.',
},
cloud: {
href: '#cloud-preview',
name: 'OpenAdapt Cloud',
why: 'Managed browser execution on non-regulated data, with history, evidence, and usage in one control plane.',
},
sprint: {
href: '#pricing-enterprise',
name: 'Workflow Qualification Sprint',
why: 'Regulated data, desktop, RDP/Citrix surfaces, and enterprise scale start with a qualification.',
},
}

const TIER_ORDER = ['community', 'cloud', 'sprint']

function recommend(answers) {
const { substrate, regulated, operator } = answers
if (regulated === 'yes') return 'sprint'
if (substrate === 'desktop-or-remote') return 'sprint'
if (operator === 'us') return 'cloud'
if (operator === 'you') return 'community'
return null
}

export default function TierSelector() {
const [answers, setAnswers] = useState({})

const answeredCount = QUESTIONS.filter(
(question) => answers[question.id]
).length
const selected = recommend(answers)
const complete = answeredCount === QUESTIONS.length

return (
<div className="rounded-2xl border border-hairline bg-panel p-6 md:p-7">
<p className="eyebrow">Not sure where to start?</p>
<h2 className="mt-2 font-display text-lg font-semibold tracking-tight text-ink md:text-xl">
Answer three questions.
</h2>

<div className="mt-5 grid gap-x-8 gap-y-1 md:grid-cols-3">
{QUESTIONS.map((question) => (
<div key={question.id} className={styles.group}>
<p className="text-sm font-medium text-ink">
{question.label}
</p>
<div
className={styles.options}
role="group"
aria-label={question.label}
>
{question.options.map((option) => {
const isActive =
answers[question.id] === option.value
return (
<button
key={option.value}
type="button"
aria-pressed={isActive}
className={[
styles.option,
isActive
? styles.optionSelected
: '',
]
.filter(Boolean)
.join(' ')}
onClick={() =>
setAnswers((current) => ({
...current,
[question.id]: option.value,
}))
}
>
{option.label}
</button>
)
})}
</div>
</div>
))}
</div>

<div aria-live="polite" className="mt-6 min-h-[2.5rem]">
{!complete ? (
<p className="text-xs leading-relaxed text-ink-3">
{answeredCount > 0
? `${answeredCount} of ${QUESTIONS.length} answered.`
: 'Answer all three to see a recommended starting tier.'}
</p>
) : (
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-xl border border-hairline bg-ground px-4 py-3">
<p className="text-sm leading-relaxed text-ink-2">
Recommended starting point:{' '}
<a
href={TIERS[selected].href}
className="font-semibold text-accent underline"
>
{TIERS[selected].name}
</a>
. {TIERS[selected].why}
</p>
<p className="text-xs text-ink-3">
Or compare all three:{' '}
{TIER_ORDER.map((tier, index) => (
<span key={tier}>
{index > 0 && ' · '}
<a
href={TIERS[tier].href}
className="underline hover:text-ink"
>
{TIERS[tier].name}
</a>
</span>
))}
</p>
</div>
)}
</div>
</div>
)
}
42 changes: 42 additions & 0 deletions components/TierSelector.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Stateful segmented-option styling lives here so selected states never rely
on dynamically composed Tailwind class strings. Color values mirror the
theme tokens in tailwind.config.js (hairline, panel, ground, ink, ink-2,
ink-3, accent). */

.group + .group {
margin-top: 1rem;
}

.options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.5rem;
}

.option {
border: 1px solid #dddcd2;
border-radius: 9999px;
background-color: #fdfcf9;
color: #4c523f;
padding: 0.375rem 0.875rem;
font-size: 0.8125rem;
line-height: 1.25rem;
cursor: pointer;
}

.option:hover {
border-color: #23281f;
}

.option:focus-visible {
outline: 2px solid #3e6b4f;
outline-offset: 2px;
}

.optionSelected,
.optionSelected:hover {
border-color: #23281f;
background-color: #23281f;
color: #f2f1ec;
}
Loading