mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat(marketing): say no card where the trial is actually claimed
Three places still carried the old promise. The register form, which is where the CTA lands and where the trial is granted, said nothing about it at all, so the landing page's claim had to be taken on faith across a page load. The comparison profile summary still described the trial without mentioning the card. And the CtaLink comment quoted a button label that no longer exists. The hero and closing CTAs now read 'Open dashboard' for a signed-in visitor. That button already pointed at /dashboard for them, and offering a free trial to someone who is mid-subscription reads as a bug. The pricing card keeps the short label because it sits directly under 'No credit card' and the card narrows to about 230px at the md breakpoint, where the long label would wrap out of a fixed-height button.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { after } from 'next/server';
|
import { after } from 'next/server';
|
||||||
import { readPageVisitor, recordVisitorEvent } from '@/lib/analytics/visitor';
|
import { readPageVisitor, recordVisitorEvent } from '@/lib/analytics/visitor';
|
||||||
import { isInviteCodeRequired } from '@/lib/feature-flags';
|
import { isInviteCodeRequired, isStripeFeatureEnabled } from '@/lib/feature-flags';
|
||||||
import { getInvitationPreviewByToken } from '@/lib/invitations';
|
import { getInvitationPreviewByToken } from '@/lib/invitations';
|
||||||
import { isInvitationPreviewAllowed } from '@/lib/invitation-preview-limit';
|
import { isInvitationPreviewAllowed } from '@/lib/invitation-preview-limit';
|
||||||
import RegisterPageClient from './register-page-client';
|
import RegisterPageClient from './register-page-client';
|
||||||
@@ -40,6 +40,7 @@ export default async function RegisterPage({ searchParams }: RegisterPageProps)
|
|||||||
return (
|
return (
|
||||||
<RegisterPageClient
|
<RegisterPageClient
|
||||||
requireInviteCode={isInviteCodeRequired()}
|
requireInviteCode={isInviteCodeRequired()}
|
||||||
|
trialOnSignup={isStripeFeatureEnabled()}
|
||||||
googleEnabled={googleEnabled}
|
googleEnabled={googleEnabled}
|
||||||
githubEnabled={githubEnabled}
|
githubEnabled={githubEnabled}
|
||||||
invitation={invitation}
|
invitation={invitation}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export interface RegisterInvitation {
|
|||||||
|
|
||||||
interface RegisterPageClientProps {
|
interface RegisterPageClientProps {
|
||||||
requireInviteCode: boolean;
|
requireInviteCode: boolean;
|
||||||
|
/**
|
||||||
|
* Billing is on, so this account starts a free trial. False on a self-hosted
|
||||||
|
* instance, where there is nothing to trial and the promise would be a lie.
|
||||||
|
*/
|
||||||
|
trialOnSignup?: boolean;
|
||||||
googleEnabled: boolean;
|
googleEnabled: boolean;
|
||||||
githubEnabled: boolean;
|
githubEnabled: boolean;
|
||||||
invitation?: RegisterInvitation | null;
|
invitation?: RegisterInvitation | null;
|
||||||
@@ -30,6 +35,7 @@ interface RegisterPageClientProps {
|
|||||||
|
|
||||||
export default function RegisterPageClient({
|
export default function RegisterPageClient({
|
||||||
requireInviteCode,
|
requireInviteCode,
|
||||||
|
trialOnSignup = false,
|
||||||
googleEnabled,
|
googleEnabled,
|
||||||
githubEnabled,
|
githubEnabled,
|
||||||
invitation = null,
|
invitation = null,
|
||||||
@@ -156,6 +162,15 @@ export default function RegisterPageClient({
|
|||||||
Create Account
|
Create Account
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Join OpenFrame to collaborate on video projects</CardDescription>
|
<CardDescription>Join OpenFrame to collaborate on video projects</CardDescription>
|
||||||
|
{/* The landing page CTA promises a trial and no card. Say it again here,
|
||||||
|
where the promise is actually kept, rather than making people take the
|
||||||
|
previous page's word for it. Invitees are joining someone else's
|
||||||
|
workspace, so the trial is not what brought them. */}
|
||||||
|
{trialOnSignup && !isInvitationFlow && (
|
||||||
|
<CardDescription>
|
||||||
|
Your 7-day free trial starts as soon as you create the account. No credit card.
|
||||||
|
</CardDescription>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{/* OAuth Buttons */}
|
{/* OAuth Buttons */}
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
|||||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
const navbarRef = useRef<HTMLElement | null>(null);
|
const navbarRef = useRef<HTMLElement | null>(null);
|
||||||
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
||||||
|
// Same button, two audiences. Once you are signed in it goes to the dashboard,
|
||||||
|
// and offering a trial to someone who is already using the product reads as a
|
||||||
|
// mistake rather than as an offer.
|
||||||
|
const hostedCtaLabel = isLoggedIn ? 'Open dashboard' : 'Start 7-day free trial, no card required';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanupHandlers: Array<() => void> = [];
|
const cleanupHandlers: Array<() => void> = [];
|
||||||
@@ -263,7 +267,7 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
|||||||
href={hostedCtaHref}
|
href={hostedCtaHref}
|
||||||
className="group relative isolate inline-flex h-12 min-w-max items-center justify-center overflow-hidden border border-primary bg-primary px-10 text-sm font-medium whitespace-nowrap text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
|
className="group relative isolate inline-flex h-12 min-w-max items-center justify-center overflow-hidden border border-primary bg-primary px-10 text-sm font-medium whitespace-nowrap text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
|
||||||
>
|
>
|
||||||
Start 7-day free trial, no card required
|
{hostedCtaLabel}
|
||||||
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||||
</CtaLink>
|
</CtaLink>
|
||||||
|
|
||||||
@@ -722,7 +726,7 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
|||||||
href={hostedCtaHref}
|
href={hostedCtaHref}
|
||||||
className="mt-auto group relative isolate inline-flex h-12 w-full items-center justify-center overflow-hidden bg-[#06b6d4] font-medium text-black transition-colors hover:bg-[#06b6d4]/90 text-sm"
|
className="mt-auto group relative isolate inline-flex h-12 w-full items-center justify-center overflow-hidden bg-[#06b6d4] font-medium text-black transition-colors hover:bg-[#06b6d4]/90 text-sm"
|
||||||
>
|
>
|
||||||
Start free trial
|
{isLoggedIn ? 'Open dashboard' : 'Start free trial'}
|
||||||
</CtaLink>
|
</CtaLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -906,7 +910,7 @@ export function LandingPage({ isLoggedIn }: LandingPageProps) {
|
|||||||
href={hostedCtaHref}
|
href={hostedCtaHref}
|
||||||
className="group relative isolate inline-flex h-12 min-w-max items-center justify-center overflow-hidden border border-primary bg-primary px-10 text-sm font-medium whitespace-nowrap text-primary-foreground transition-transform duration-300 hover:scale-[1.02] md:min-w-[240px]"
|
className="group relative isolate inline-flex h-12 min-w-max items-center justify-center overflow-hidden border border-primary bg-primary px-10 text-sm font-medium whitespace-nowrap text-primary-foreground transition-transform duration-300 hover:scale-[1.02] md:min-w-[240px]"
|
||||||
>
|
>
|
||||||
Start 7-day free trial, no card required
|
{hostedCtaLabel}
|
||||||
</CtaLink>
|
</CtaLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface ComparisonPageProps {
|
|||||||
|
|
||||||
export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
|
export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
|
||||||
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
const hostedCtaHref = isLoggedIn ? '/dashboard' : '/register';
|
||||||
|
const hostedCtaLabel = isLoggedIn ? 'Open dashboard' : 'Start 7-day free trial, no card required';
|
||||||
const competitorName = getCompetitorName(page) ?? 'Alternatives';
|
const competitorName = getCompetitorName(page) ?? 'Alternatives';
|
||||||
const relatedSlugs = page.relatedSlugs.filter((slug) => slug in comparisonPageMap);
|
const relatedSlugs = page.relatedSlugs.filter((slug) => slug in comparisonPageMap);
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
|
|||||||
href={hostedCtaHref}
|
href={hostedCtaHref}
|
||||||
className="group relative isolate inline-flex h-12 items-center justify-center overflow-hidden border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
|
className="group relative isolate inline-flex h-12 items-center justify-center overflow-hidden border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground transition-transform duration-300 hover:scale-[1.02]"
|
||||||
>
|
>
|
||||||
Start 7-day free trial, no card required
|
{hostedCtaLabel}
|
||||||
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
<MoveRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
|
||||||
</CtaLink>
|
</CtaLink>
|
||||||
<a
|
<a
|
||||||
@@ -205,7 +206,7 @@ export function ComparisonPage({ page, isLoggedIn }: ComparisonPageProps) {
|
|||||||
href={hostedCtaHref}
|
href={hostedCtaHref}
|
||||||
className="inline-flex h-12 items-center justify-center border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground"
|
className="inline-flex h-12 items-center justify-center border border-primary bg-primary px-8 text-sm font-medium text-primary-foreground"
|
||||||
>
|
>
|
||||||
Start 7-day free trial, no card required
|
{hostedCtaLabel}
|
||||||
</CtaLink>
|
</CtaLink>
|
||||||
<a
|
<a
|
||||||
href={seoConfig.githubUrl}
|
href={seoConfig.githubUrl}
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ interface CtaLinkProps {
|
|||||||
/**
|
/**
|
||||||
* A marketing call to action that reports the click.
|
* A marketing call to action that reports the click.
|
||||||
*
|
*
|
||||||
* Only the signup CTA counts. The same button reads "Start free trial" and
|
* Only the signup CTA counts. The same button points at `/dashboard` once you
|
||||||
* points at `/dashboard` once you are signed in, and an existing customer
|
* are signed in, and an existing customer clicking through to their own
|
||||||
* clicking through to their own dashboard is not a step in the acquisition
|
* dashboard is not a step in the acquisition funnel.
|
||||||
* funnel.
|
|
||||||
*/
|
*/
|
||||||
export function CtaLink({ href, className, children }: CtaLinkProps) {
|
export function CtaLink({ href, className, children }: CtaLinkProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ export const competitorProfiles: Record<string, CompetitorProfile> = {
|
|||||||
export const openFrameProfile = {
|
export const openFrameProfile = {
|
||||||
name: 'OpenFrame',
|
name: 'OpenFrame',
|
||||||
tagline: 'Fair-source video review and approval with hosted and self-hosted options.',
|
tagline: 'Fair-source video review and approval with hosted and self-hosted options.',
|
||||||
pricingSummary: 'Self-hosted free · Hosted $10/mo after 7-day trial',
|
pricingSummary: 'Self-hosted free · Hosted $10/mo after a 7-day no-card trial',
|
||||||
trial: '7-day free trial on hosted plan',
|
trial: '7-day free trial on hosted plan',
|
||||||
strengths: [
|
strengths: [
|
||||||
'Timestamped text, voice, image, and drawn annotations',
|
'Timestamped text, voice, image, and drawn annotations',
|
||||||
|
|||||||
Reference in New Issue
Block a user