diff --git a/.server-changes/hipaa-addon-pricing-cta.md b/.server-changes/hipaa-addon-pricing-cta.md new file mode 100644 index 0000000000..8dc4a41f8b --- /dev/null +++ b/.server-changes/hipaa-addon-pricing-cta.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Request a HIPAA BAA add-on directly from any paid pricing tier in the dashboard. diff --git a/apps/webapp/app/components/DefinitionTooltip.tsx b/apps/webapp/app/components/DefinitionTooltip.tsx index 5bb3a71399..d91cce92c9 100644 --- a/apps/webapp/app/components/DefinitionTooltip.tsx +++ b/apps/webapp/app/components/DefinitionTooltip.tsx @@ -6,14 +6,16 @@ export function DefinitionTip({ content, children, title, + disableHoverableContent = true, }: { content: React.ReactNode; children: React.ReactNode; title: React.ReactNode; + disableHoverableContent?: boolean; }) { return ( - + {children} diff --git a/apps/webapp/app/components/Feedback.tsx b/apps/webapp/app/components/Feedback.tsx index ecfd4e88c9..0848359e21 100644 --- a/apps/webapp/app/components/Feedback.tsx +++ b/apps/webapp/app/components/Feedback.tsx @@ -1,10 +1,10 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { InformationCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"; -import { EnvelopeIcon } from "@heroicons/react/24/solid"; +import { EnvelopeIcon, ShieldCheckIcon } from "@heroicons/react/24/solid"; import { Form, useActionData, useLocation, useNavigation, useSearchParams } from "@remix-run/react"; import { type ReactNode, useEffect, useState } from "react"; -import { type FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback"; +import { type FeedbackType, feedbackTypes, schema } from "~/routes/resources.feedback"; import { Button } from "./primitives/Buttons"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog"; import { Fieldset } from "./primitives/Fieldset"; @@ -84,9 +84,12 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac How can we help? We read every message and will respond as quickly as we can. - {!(type === "feature" || type === "help" || type === "concurrency") && ( -
- )} + {!( + type === "feature" || + type === "help" || + type === "concurrency" || + type === "hipaa" + ) &&
}
@@ -132,6 +135,19 @@ export function Feedback({ button, defaultValue = "bug", onOpenChange }: Feedbac )} + {type === "hipaa" && ( + + + We offer a signed Business Associate Agreement (BAA) as a paid add-on on any + paid plan. To help us get back to you quickly, please include your company + name, and a brief description of the PHI workload you plan to run. + + + )} diff --git a/apps/webapp/app/routes/resources.feedback.ts b/apps/webapp/app/routes/resources.feedback.ts index a6271c9d5a..bc6e3c3084 100644 --- a/apps/webapp/app/routes/resources.feedback.ts +++ b/apps/webapp/app/routes/resources.feedback.ts @@ -8,19 +8,55 @@ import { sendToPlain } from "~/utils/plain.server"; let client: PlainClient | undefined; -export const feedbackTypeLabel = { - bug: "Bug report", - feature: "Feature request", - help: "Help me out", - enterprise: "Enterprise enquiry", - feedback: "General feedback", - concurrency: "Increase my concurrency", - region: "Suggest a new region", -}; +export const feedbackTypes = { + bug: { + label: "Bug report", + labelTypeId: "lt_01HB920BTPFS36KH1JT9C36YVY", + threadTitle: "Contact form: Bug report", + }, + feature: { + label: "Feature request", + labelTypeId: "lt_01HB920BV8CJGYXVE15WWN6P07", + threadTitle: "Contact form: Feature request", + }, + help: { + label: "Help me out", + labelTypeId: "lt_01KTVCAPZY5ZJ0SS4ACMXWYYT3", + threadTitle: "Contact form: Help me out", + }, + enterprise: { + label: "Enterprise enquiry", + labelTypeId: "lt_01K7PF5EV2877EH4SZYB667FW4", + threadTitle: "Contact form: Enterprise enquiry", + }, + feedback: { + label: "General feedback", + labelTypeId: "lt_01HB920BSRZ3RA1ETHBVEB5ST2", + threadTitle: "Contact form: General feedback", + }, + concurrency: { + label: "Increase my concurrency", + labelTypeId: "lt_01KTVCCY2PDE5V6WV2PQ8N85K2", + threadTitle: "Contact form: Increase my concurrency", + }, + region: { + label: "Suggest a new region", + labelTypeId: "lt_01KTVCDPYYBW6KS9H5V8MTQ0GG", + threadTitle: "Contact form: Suggest a new region", + }, + hipaa: { + label: "HIPAA BAA request", + labelTypeId: "lt_01KS54WBRYKE6DY369KPK2SS4W", + threadTitle: "Contact form: HIPAA BAA request", + }, +} as const satisfies Record< + string, + { label: string; labelTypeId?: string; threadTitle: string } +>; -export type FeedbackType = keyof typeof feedbackTypeLabel; +export type FeedbackType = keyof typeof feedbackTypes; -const feedbackTypeLiterals = Object.keys(feedbackTypeLabel).map((key) => z.literal(key)); +const feedbackTypeLiterals = Object.keys(feedbackTypes).map((key) => z.literal(key)); const feedbackType = z.union( [feedbackTypeLiterals[0], feedbackTypeLiterals[1], ...feedbackTypeLiterals.slice(2)], @@ -46,16 +82,17 @@ export async function action({ request }: ActionFunctionArgs) { return json(submission); } - const title = feedbackTypeLabel[submission.value.feedbackType as FeedbackType]; + const inquiry = feedbackTypes[submission.value.feedbackType as FeedbackType]; try { await sendToPlain({ userId: user.id, email: user.email, name: user.name ?? user.displayName ?? user.email, - title, + title: inquiry.threadTitle, + labelTypeIds: inquiry.labelTypeId ? [inquiry.labelTypeId] : undefined, components: [ uiComponent.text({ - text: `New ${title} reported by ${user.name} (${user.email})`, + text: `New ${inquiry.label} reported by ${user.name} (${user.email})`, }), uiComponent.divider({ spacingSize: "M" }), uiComponent.text({ diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index e5a9dce742..2ba2c761d7 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -1,5 +1,4 @@ import { - ArrowUpRightIcon, CheckIcon, ExclamationTriangleIcon, ShieldCheckIcon, @@ -37,6 +36,7 @@ import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Spinner } from "~/components/primitives/Spinner"; import { TextArea } from "~/components/primitives/TextArea"; +import { TextLink } from "~/components/primitives/TextLink"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { prisma } from "~/db.server"; import { redirectWithErrorMessage } from "~/models/message.server"; @@ -237,6 +237,11 @@ const pricingDefinitions = { title: "Query period", content: "The maximum number of days a query can look back when analyzing your task data.", }, + hipaaBaa: { + title: "HIPAA BAA", + content: + "A signed Business Associate Agreement (BAA) is required to run tasks that process Protected Health Information (PHI) on Trigger.dev Cloud.", + }, }; type PricingPlansProps = { @@ -317,9 +322,6 @@ export function TierFree({
- - ${plan.limits.includedUsage / 100} free monthly usage - {showGithubVerificationBadge && status === "approved" && ( {status === "rejected" ? (
-
@@ -533,6 +534,16 @@ export function TierFree({ )}
    + + + ${plan.limits.includedUsage / 100} / month free credits + + Unlimited{" "} @@ -584,9 +595,6 @@ export function TierHobby({ return ( - - ${plan.limits.includedUsage / 100} usage included -
    @@ -652,6 +660,24 @@ export function TierHobby({ )}
      + + + This plan includes ${plan.limits.includedUsage / 100} of compute each month. After + that's used, tasks keep running and you're billed per our{" "} + + compute usage rates + + . + + } + > + ${plan.limits.includedUsage / 100} / month credits included + + Unlimited{" "} @@ -672,6 +698,16 @@ export function TierHobby({ + + + Request a BAA + + } + /> +
    ); @@ -701,9 +737,6 @@ export function TierPro({ return ( - - ${plan.limits.includedUsage / 100} usage included -
    @@ -773,6 +806,24 @@ export function TierPro({
      + + + This plan includes ${plan.limits.includedUsage / 100} of compute each month. After + that's used, tasks keep running and you're billed per our{" "} + + compute usage rates + + . + + } + > + ${plan.limits.includedUsage / 100} / month credits included + + {`Then ${formatCurrency(concurrencyAddOnPricing.centsPerStep / 100, true)}/month per ${ concurrencyAddOnPricing.stepSize @@ -801,6 +852,16 @@ export function TierPro({ {pricingDefinitions.additionalRealtimeConnections.content} + + + Request a BAA + + } + /> +
    ); @@ -809,41 +870,12 @@ export function TierPro({ export function TierEnterprise() { return ( -
    -
    -
    -

    Enterprise

    -
    -

    - Tailor a custom plan -

    -
    -
      - - All Pro plan features + - - - Custom log retention - -
    -
      - - Priority support - - - Role-based access control - -
    -
      - - SOC 2 report - - - SSO - -
    +
    +
    +

    Enterprise

    +

    Tailor a custom plan

    -
    +
    +
    +
      + + All Pro plan features + + + + Custom log retention + +
    +
      + + Priority support + + + Role-based access control + +
    +
      + + SOC 2 report + + + SSO + +
    +
      + + + Request a BAA + + } + /> + +
    +
    ); } @@ -921,36 +991,6 @@ function PricingHeader({ ); } -function TierLimit({ children, href }: { children: React.ReactNode; href?: string }) { - return ( - <> -
    - {href ? ( - - {children} - - } - content={ -
    - View compute pricing information - -
    - } - /> - ) : ( -
    {children}
    - )} - - ); -} - function FeatureItem({ checked, checkedColor = "primary", @@ -1206,6 +1246,31 @@ function QueryPeriod({ limits }: { limits: Limits }) { ); } +function HIPAAAddOn({ + children, + checkedColor = "primary", +}: { + children?: React.ReactNode; + checkedColor?: "primary" | "bright"; +}) { + return ( + +
    +
    + + HIPAA BAA + {" "} + add-on +
    + {children && {children}} +
    +
    + ); +} + function Branches({ limits, children }: { limits: Limits; children?: React.ReactNode }) { return ( 0}> diff --git a/apps/webapp/app/utils/plain.server.ts b/apps/webapp/app/utils/plain.server.ts index 4e8ce63073..ee205374e1 100644 --- a/apps/webapp/app/utils/plain.server.ts +++ b/apps/webapp/app/utils/plain.server.ts @@ -7,9 +7,17 @@ type Input = { name: string; title: string; components: ReturnType[]; + labelTypeIds?: string[]; }; -export async function sendToPlain({ userId, email, name, title, components }: Input) { +export async function sendToPlain({ + userId, + email, + name, + title, + components, + labelTypeIds, +}: Input) { if (!env.PLAIN_API_KEY) { return; } @@ -51,6 +59,7 @@ export async function sendToPlain({ userId, email, name, title, components }: In }, title: title, components: components, + labelTypeIds, }); if (createThreadRes.error) {