fix stripe configuration

This commit is contained in:
2026-05-26 13:47:43 -04:00
parent 72609755f8
commit 3bcbdae678
35 changed files with 1189 additions and 1727 deletions

View File

@@ -0,0 +1,79 @@
import { createSignal, onMount, Show } from "solid-js";
import type { Stripe, StripeEmbeddedCheckout } from "@stripe/stripe-js";
interface EmbeddedCheckoutProps {
clientSecret: string;
onCheckoutComplete?: () => void;
}
export default function EmbeddedCheckout(props: EmbeddedCheckoutProps) {
const [error, setError] = createSignal<string | null>(null);
const [loading, setLoading] = createSignal(true);
let container: HTMLDivElement | undefined;
onMount(async () => {
let embeddedCheckout: StripeEmbeddedCheckout | null = null;
try {
const mod = await import("@stripe/stripe-js");
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
if (!publishableKey) {
setError("Stripe publishable key not configured");
setLoading(false);
return;
}
const stripe = await mod.loadStripe(publishableKey);
if (!stripe) {
setError("Failed to load Stripe");
setLoading(false);
return;
}
embeddedCheckout = await stripe.createEmbeddedCheckoutPage({
clientSecret: props.clientSecret,
onComplete: () => {
props.onCheckoutComplete?.();
},
});
if (container) {
embeddedCheckout.mount(container);
}
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load checkout");
setLoading(false);
}
// Cleanup on unmount
return () => {
embeddedCheckout?.destroy();
};
});
return (
<div class="w-full">
<Show when={loading()}>
<div class="flex items-center justify-center min-h-[400px]">
<div class="text-center">
<div class="animate-spin h-8 w-8 border-2 border-[var(--color-brand-primary)] border-t-transparent rounded-full mx-auto mb-3" />
<p class="text-sm text-[var(--color-text-secondary)]">Loading checkout...</p>
</div>
</div>
</Show>
<Show when={error() && !loading()}>
<div class="text-center py-8">
<p class="text-[var(--color-error)] mb-4">{error()}</p>
</div>
</Show>
<div
ref={container}
class={loading() ? "hidden" : ""}
/>
</div>
);
}

View File

@@ -1,38 +0,0 @@
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
interface CTABannerSectionProps {
class?: string;
}
export default function CTABannerSection(props: CTABannerSectionProps) {
return (
<section id="cta" class={cn("py-20 md:py-28 scroll-mt-16", props.class)}>
<PageContainer py="py-8">
<div class="gradient-card border border-(--color-border)/50 rounded-2xl p-10 md:p-16 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
Join thousands of users who trust Kordant to keep their digital
identity safe from emerging threats.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<A href="/signup">
<Button variant="primary" size="lg">
Create Account
</Button>
</A>
<A href="/login">
<Button variant="secondary" size="lg">
Sign In
</Button>
</A>
</div>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,249 +0,0 @@
import { For } from "solid-js";
import type { JSX } from "solid-js";
import { cn } from "~/lib/utils";
import Card from "~/components/ui/Card";
import PageContainer from "~/components/layout/PageContainer";
interface Feature {
title: string;
description: string;
icon: () => JSX.Element;
}
function DarkWatchIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />
</svg>
);
}
function VoicePrintIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M12 4a4 4 0 00-4 4v8a4 4 0 008 0V8a4 4 0 00-4-4z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
d="M4 11v2m16-2v2M8 18.5A6 6 0 0016 18.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
);
}
function SpamShieldIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10 10l4 4m0-4l-4 4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
);
}
function HomeTitleIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function RemoveBrokersIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M3 6h18M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2m-4 4v6m-4-6v6m-3-8v10a2 2 0 002 2h10a2 2 0 002-2V8H8z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function FamilyPlansIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M12 12a4 4 0 100-8 4 4 0 000 8zM5 22v-2a4 4 0 014-4h6a4 4 0 014 4v2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
d="M20 9a3 3 0 100-6 3 3 0 000 6zM16 22v-2a3 3 0 00-3-3h-2a3 3 0 00-3 3v2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
opacity="0.6"
/>
</svg>
);
}
const features: Feature[] = [
{
title: "DarkWatch",
description:
"Continuous dark web monitoring to detect your exposed credentials and personal data.",
icon: DarkWatchIcon,
},
{
title: "VoicePrint",
description:
"AI-powered voice clone detection to protect against deepfake voice scams.",
icon: VoicePrintIcon,
},
{
title: "SpamShield",
description:
"Intelligent spam and scam call blocking that learns your patterns over time.",
icon: SpamShieldIcon,
},
{
title: "HomeTitle",
description:
"Property fraud alerts that notify you of unauthorized changes to your home records.",
icon: HomeTitleIcon,
},
{
title: "RemoveBrokers",
description:
"Automatic data broker removal to minimize your personal data footprint online.",
icon: RemoveBrokersIcon,
},
{
title: "Family Plans",
description:
"Protect your whole household with shared monitoring, alerts, and management tools.",
icon: FamilyPlansIcon,
},
];
interface FeatureCardProps {
feature: Feature;
}
function FeatureCard(props: FeatureCardProps) {
const Icon = props.feature.icon;
return (
<Card class="hover:shadow-lg transition-shadow duration-300">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 p-2 rounded-lg bg-[var(--color-bg-secondary)]">
<Icon />
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">
{props.feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] leading-relaxed">
{props.feature.description}
</p>
</div>
</div>
</Card>
);
}
interface FeaturesGridSectionProps {
class?: string;
}
export default function FeaturesGridSection(props: FeaturesGridSectionProps) {
return (
<section
id="features"
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
>
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Platform Features
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Comprehensive protection powered by AI and real-time monitoring
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={features}>
{(feature) => <FeatureCard feature={feature} />}
</For>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,154 +0,0 @@
import { For } from "solid-js";
import type { JSX } from "solid-js";
import { cn } from "~/lib/utils";
import Card from "~/components/ui/Card";
import PageContainer from "~/components/layout/PageContainer";
function CheckIcon() {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0"
>
<path
d="M4 9l3 3 7-7"
stroke="var(--color-success)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function IndividualIcon() {
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-[var(--color-brand-primary)]"
>
<circle cx="20" cy="14" r="6" fill="currentColor" />
<path
d="M8 32c0-6.6 5.4-12 12-12s12 5.4 12 12H8z"
fill="currentColor"
/>
</svg>
);
}
function FamilyIcon() {
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-10 h-10 text-[var(--color-brand-primary)]"
>
<circle cx="14" cy="12" r="5" fill="currentColor" />
<circle cx="26" cy="12" r="4" fill="currentColor" opacity="0.7" />
<path
d="M2 30c0-5 4-9 9-9 1.5 0 3 .4 4.2 1.1C16.5 21.5 18 21 20 21s3.5.5 4.8 1.1C26 21.4 27.5 21 29 21c5 0 9 4 9 9H2z"
fill="currentColor"
/>
</svg>
);
}
interface PanelProps {
title: string;
description: string;
items: string[];
icon: () => JSX.Element;
}
function Panel(props: PanelProps) {
const Icon = props.icon;
return (
<Card class="h-full">
<div class="flex flex-col h-full">
<div class="mb-4">
<Icon />
</div>
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-2">
{props.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-6">
{props.description}
</p>
<ul class="space-y-3 flex-1">
<For each={props.items}>
{(item) => (
<li class="flex items-start gap-2.5">
<CheckIcon />
<span class="text-[var(--color-text-secondary)] text-sm">
{item}
</span>
</li>
)}
</For>
</ul>
</div>
</Card>
);
}
interface ForUsersSectionProps {
class?: string;
}
export default function ForUsersSection(props: ForUsersSectionProps) {
return (
<section
id="for-users"
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
>
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
For Everyone
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Whether you're protecting yourself or your whole family
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<Panel
title="For Individuals"
description="Personal identity protection tailored to your digital footprint."
icon={IndividualIcon}
items={[
"Monitor personal email and phone numbers",
"Dark web credential scanning",
"Voiceprint clone detection",
"Spam and scam call filtering",
"Data broker opt-out service",
]}
/>
<Panel
title="For Families"
description="Group management tools to keep every household member safe."
icon={FamilyIcon}
items={[
"Add unlimited family members",
"Shared alert dashboard",
"Child account monitoring",
"Family-wide dark web scans",
"Centralized threat notifications",
]}
/>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,125 +0,0 @@
import { onMount } from "solid-js";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
import PageContainer from "~/components/layout/PageContainer";
function ShieldIcon() {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-12 h-12 md:w-16 md:h-16"
>
<circle cx="24" cy="24" r="24" fill="var(--color-brand-primary)" />
<path
d="M24 10L16 14v6.5c0 5.1 3.4 9.9 8 11.5 4.6-1.6 8-6.4 8-11.5V14l-8-4z"
fill="white"
fill-opacity="0.9"
/>
<path
d="M20 24l3 2.5 5-5"
stroke="var(--color-brand-primary)"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
interface HeroSectionProps {
class?: string;
}
export default function HeroSection(props: HeroSectionProps) {
let heroRef: HTMLDivElement | undefined;
onMount(() => {
if (heroRef) {
heroRef.style.opacity = "1";
heroRef.style.transform = "translateY(0)";
}
});
return (
<section class={cn("relative", props.class)}>
<PageContainer>
<div
ref={heroRef}
class="flex flex-col items-center text-center py-20 md:py-32 transition-all duration-700 ease-out"
style="opacity: 0; transform: translateY(20px);"
>
<div class="mb-6 shadow-glow-primary rounded-full p-3">
<ShieldIcon />
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-6 max-w-4xl">
<Typewriter speed={50} delay={400} keepAlive={false}>
<span class="text-text-primary">AI-Powered </span>
<span class="text-gradient-primary">Identity Protection</span>
<br />
<span class="text-text-primary">for Everyone</span>
</Typewriter>
</h1>
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
Threat actors are using AI in multifaceted attacks. Kordant evens
the playing field using advanced AI to monitor, detect, and prevent
identity threats in real-time.
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<A href="/signup">
<Button variant="primary" size="lg">
Get Started
</Button>
</A>
<A href="#features">
<Button variant="ghost" size="lg">
Learn More
</Button>
</A>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
<span class="flex items-center gap-1.5">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z"
fill="var(--color-success)"
/>
</svg>
No credit card required
</span>
<span class="flex items-center gap-1.5">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z"
fill="var(--color-success)"
/>
</svg>
Free tier available
</span>
</div>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,197 +0,0 @@
import { For } from "solid-js";
import type { JSX } from "solid-js";
import { cn } from "~/lib/utils";
import PageContainer from "~/components/layout/PageContainer";
interface Step {
number: number;
title: string;
description: string;
icon: () => JSX.Element;
}
function EnrollIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 text-white"
>
<path
d="M12 4a4 4 0 100 8 4 4 0 000-8zM6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
d="M17 8l2 2 4-4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function MonitorIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 text-white"
>
<path
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
stroke="currentColor"
stroke-width="1.5"
/>
<path
d="M12 8v4l3 3"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 12h3m15 0h3M12 3v3m0 15v3"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
opacity="0.5"
/>
</svg>
);
}
function AlertIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 text-white"
>
<path
d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 22a2 2 0 01-2-2h4a2 2 0 01-2 2z"
fill="currentColor"
/>
<path
d="M12 11v3m0-6v1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
);
}
const steps: Step[] = [
{
number: 1,
title: "Enroll Your Identity",
description:
"Sign up and add your emails, phone numbers, and family members to create your protection profile.",
icon: EnrollIcon,
},
{
number: 2,
title: "We Monitor 24/7",
description:
"Our system runs continuous dark web scans, voiceprint detection, and spam filtering to catch threats early.",
icon: MonitorIcon,
},
{
number: 3,
title: "Get Instant Alerts",
description:
"Receive real-time notifications the moment a threat is detected, with clear guidance on what to do next.",
icon: AlertIcon,
},
];
interface StepBlockProps {
step: Step;
index: number;
}
function StepBlock(props: StepBlockProps) {
const isEven = props.index % 2 === 0;
const Icon = props.step.icon;
return (
<div
class={cn(
"flex gap-8 md:flex-row flex-col",
isEven ? "" : "md:flex-row-reverse",
)}
>
<div class="flex-1">
<div class="flex items-start gap-5">
<div class="w-14 h-14 rounded-full gradient-primary shadow-glow-primary flex items-center justify-center shrink-0">
<Icon />
</div>
<div>
<div class="inline-flex items-center gap-2 mb-1.5">
<span class="text-sm font-semibold text-[var(--color-brand-primary)]">
Step {props.step.number}
</span>
</div>
<h3 class="text-xl md:text-2xl font-bold text-[var(--color-text-primary)] mb-2">
{props.step.title}
</h3>
<p class="text-base text-[var(--color-text-secondary)] leading-relaxed">
{props.step.description}
</p>
</div>
</div>
</div>
<div class="flex-1 hidden md:block" />
</div>
);
}
interface HowItWorksSectionProps {
class?: string;
}
export default function HowItWorksSection(props: HowItWorksSectionProps) {
return (
<section
id="how-it-works"
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
>
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
How It Works
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Three simple steps to full identity protection
</p>
</div>
<div class="flex flex-col gap-12 md:gap-16">
<For each={steps}>
{(step, index) => <StepBlock step={step} index={index()} />}
</For>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,217 +0,0 @@
import { For } from "solid-js";
import type { JSX } from "solid-js";
import { cn } from "~/lib/utils";
import Card from "~/components/ui/Card";
import PageContainer from "~/components/layout/PageContainer";
function CheckIcon() {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0"
>
<path
d="M4 9l3 3 7-7"
stroke="var(--color-success)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function ProactiveIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M13 3l-2 6h5l-3 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M4 14l5-5m0 0l5 5m-5-5v12"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function AIIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
<path
d="M8 16l2 2-2 2M16 16l-2 2 2 2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
function PrivacyIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-[var(--color-brand-primary)]"
>
<path
d="M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4z"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M9 12l2 2 4-4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}
interface ValueProp {
title: string;
description: string;
items: string[];
icon: () => JSX.Element;
}
const valueProps: ValueProp[] = [
{
title: "Proactive, Not Reactive",
description:
"We detect threats before they cause damage, so you can act early.",
icon: ProactiveIcon,
items: [
"Real-time dark web scanning",
"Pre-breach alerts and warnings",
"Automated threat response",
],
},
{
title: "AI-Powered Detection",
description:
"Machine learning models trained on real scam data to catch the latest threats.",
icon: AIIcon,
items: [
"Deepfake voice identification",
"Pattern-based scam detection",
"Continuous model improvement",
],
},
{
title: "Privacy First",
description:
"Your data stays encrypted and private. We never sell your information.",
icon: PrivacyIcon,
items: [
"End-to-end encrypted data",
"GDPR and CCPA compliant",
"Zero data selling policy",
],
},
];
interface ValueCardProps {
prop: ValueProp;
}
function ValueCard(props: ValueCardProps) {
const Icon = props.prop.icon;
return (
<Card class="backdrop-blur-2xl">
<div class="flex flex-col h-full">
<div class="mb-3 p-2 rounded-lg bg-[var(--color-bg-secondary)] w-fit">
<Icon />
</div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
{props.prop.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-4 leading-relaxed">
{props.prop.description}
</p>
<ul class="space-y-2 flex-1">
<For each={props.prop.items}>
{(item) => (
<li class="flex items-start gap-2.5">
<CheckIcon />
<span class="text-[var(--color-text-secondary)] text-sm">
{item}
</span>
</li>
)}
</For>
</ul>
</div>
</Card>
);
}
interface WhyKordantSectionProps {
class?: string;
}
export default function WhyKordantSection(props: WhyKordantSectionProps) {
return (
<section
id="why-kordant"
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
>
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Why Kordant
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Built on cutting-edge technology with your privacy at the core
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<For each={valueProps}>
{(prop) => <ValueCard prop={prop} />}
</For>
</div>
</PageContainer>
</section>
);
}

View File

@@ -1,120 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render } from "solid-js/web";
import type { JSX } from "solid-js";
vi.mock("@solidjs/router", () => ({
A: (props: { href?: string; children?: JSX.Element }) => {
const href = props.href || "#";
return (
<a href={href}>
{props.children}
</a>
);
},
}));
import HeroSection from "./HeroSection";
function mount(comp: () => JSX.Element): HTMLDivElement {
const container = document.createElement("div");
document.body.appendChild(container);
render(() => comp(), container);
return container;
}
beforeEach(() => {
document.body.innerHTML = "";
});
afterEach(() => {
document.body.innerHTML = "";
});
describe("HeroSection", () => {
it("renders the headline with AI-Powered Identity Protection", () => {
mount(() => <HeroSection />);
expect(document.body.textContent).toContain("AI-Powered");
expect(document.body.textContent).toContain("Identity Protection");
expect(document.body.textContent).toContain("for Everyone");
});
it("renders the subheadline", () => {
mount(() => <HeroSection />);
expect(document.body.textContent).toContain("Kordant uses advanced AI");
});
it("renders the Get Started CTA", () => {
mount(() => <HeroSection />);
expect(document.body.textContent).toContain("Get Started");
const primaryBtn = document.querySelector("button.gradient-primary");
expect(primaryBtn).toBeTruthy();
});
it("renders the Learn More CTA", () => {
mount(() => <HeroSection />);
expect(document.body.textContent).toContain("Learn More");
});
it("renders trust indicators", () => {
mount(() => <HeroSection />);
expect(document.body.textContent).toContain("No credit card required");
expect(document.body.textContent).toContain("Free tier available");
});
it("renders the shield icon SVG", () => {
mount(() => <HeroSection />);
const svg = document.querySelector("svg");
expect(svg).toBeTruthy();
});
it("wraps content in PageContainer", () => {
mount(() => <HeroSection />);
const container = document.querySelector(".max-w-7xl");
expect(container).toBeTruthy();
});
it("renders two buttons for CTAs", () => {
mount(() => <HeroSection />);
const buttons = document.querySelectorAll("button");
expect(buttons.length).toBe(2);
});
it("has Get Started button wrapped in link to /signup", () => {
mount(() => <HeroSection />);
const links = document.querySelectorAll("a");
const signupLink = Array.from(links).find(
(a) => a.getAttribute("href") === "/signup",
);
expect(signupLink).toBeTruthy();
expect(signupLink!.textContent).toContain("Get Started");
});
it("has Learn More button wrapped in link to #features", () => {
mount(() => <HeroSection />);
const links = document.querySelectorAll("a");
const featuresLink = Array.from(links).find(
(a) => a.getAttribute("href") === "#features",
);
expect(featuresLink).toBeTruthy();
expect(featuresLink!.textContent).toContain("Learn More");
});
it("applies custom class prop", () => {
mount(() => <HeroSection class="custom-hero" />);
const section = document.querySelector("section.custom-hero");
expect(section).toBeTruthy();
});
it("has centered text layout", () => {
mount(() => <HeroSection />);
const inner = document.querySelector(".text-center");
expect(inner).toBeTruthy();
});
it("has responsive vertical padding", () => {
mount(() => <HeroSection />);
const inner = document.querySelector(".py-20");
expect(inner).toBeTruthy();
expect(inner!.className).toContain("md:py-32");
});
});

View File

@@ -1,379 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render } from "solid-js/web";
import type { JSX } from "solid-js";
vi.mock("@solidjs/router", () => ({
A: (props: { href?: string; children?: JSX.Element }) => {
const href = props.href || "#";
return (
<a href={href}>
{props.children}
</a>
);
},
}));
import HowItWorksSection from "./HowItWorksSection";
import FeaturesGridSection from "./FeaturesGridSection";
import ForUsersSection from "./ForUsersSection";
import WhyKordantSection from "./WhyKordantSection";
import CTABannerSection from "./CTABannerSection";
function mount(comp: () => JSX.Element): HTMLDivElement {
const container = document.createElement("div");
document.body.appendChild(container);
render(() => comp(), container);
return container;
}
beforeEach(() => {
document.body.innerHTML = "";
});
afterEach(() => {
document.body.innerHTML = "";
});
describe("HowItWorksSection", () => {
it("renders the section heading", () => {
mount(() => <HowItWorksSection />);
expect(document.body.textContent).toContain("How It Works");
});
it("renders the section subheading", () => {
mount(() => <HowItWorksSection />);
expect(document.body.textContent).toContain(
"Three simple steps to full identity protection",
);
});
it("renders all 3 steps", () => {
mount(() => <HowItWorksSection />);
expect(document.body.textContent).toContain("Enroll Your Identity");
expect(document.body.textContent).toContain("We Monitor 24/7");
expect(document.body.textContent).toContain("Get Instant Alerts");
});
it("renders step descriptions", () => {
mount(() => <HowItWorksSection />);
expect(document.body.textContent).toContain(
"Sign up and add your emails",
);
expect(document.body.textContent).toContain("dark web scans");
expect(document.body.textContent).toContain("real-time notifications");
});
it("renders 3 numbered circles with gradient-primary", () => {
mount(() => <HowItWorksSection />);
const circles = document.querySelectorAll(".gradient-primary");
expect(circles.length).toBe(3);
});
it("has the anchor ID for smooth scrolling", () => {
mount(() => <HowItWorksSection />);
const section = document.querySelector('#how-it-works');
expect(section).toBeTruthy();
});
it("applies custom class prop", () => {
mount(() => <HowItWorksSection class="custom-how" />);
const section = document.querySelector("section.custom-how");
expect(section).toBeTruthy();
});
it("wraps content in PageContainer", () => {
mount(() => <HowItWorksSection />);
const container = document.querySelector(".max-w-7xl");
expect(container).toBeTruthy();
});
});
describe("FeaturesGridSection", () => {
it("renders the section heading", () => {
mount(() => <FeaturesGridSection />);
expect(document.body.textContent).toContain("Platform Features");
});
it("renders the section subheading", () => {
mount(() => <FeaturesGridSection />);
expect(document.body.textContent).toContain("Comprehensive protection");
});
it("renders all 6 feature cards", () => {
mount(() => <FeaturesGridSection />);
expect(document.body.textContent).toContain("DarkWatch");
expect(document.body.textContent).toContain("VoicePrint");
expect(document.body.textContent).toContain("SpamShield");
expect(document.body.textContent).toContain("HomeTitle");
expect(document.body.textContent).toContain("RemoveBrokers");
expect(document.body.textContent).toContain("Family Plans");
});
it("renders 6 Card components", () => {
mount(() => <FeaturesGridSection />);
const cards = document.querySelectorAll(".gradient-card");
expect(cards.length).toBe(6);
});
it("renders feature descriptions", () => {
mount(() => <FeaturesGridSection />);
expect(document.body.textContent).toContain("dark web monitoring");
expect(document.body.textContent).toContain("voice clone detection");
expect(document.body.textContent).toContain("scam call blocking");
});
it("has the anchor ID for smooth scrolling", () => {
mount(() => <FeaturesGridSection />);
const section = document.querySelector('#features');
expect(section).toBeTruthy();
});
it("applies custom class prop", () => {
mount(() => <FeaturesGridSection class="custom-features" />);
const section = document.querySelector("section.custom-features");
expect(section).toBeTruthy();
});
it("uses responsive grid layout", () => {
mount(() => <FeaturesGridSection />);
const grid = document.querySelector(".grid-cols-1");
expect(grid).toBeTruthy();
expect(grid!.className).toContain("md:grid-cols-2");
expect(grid!.className).toContain("lg:grid-cols-3");
});
});
describe("ForUsersSection", () => {
it("renders the section heading", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain("For Everyone");
});
it("renders the section subheading", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain(
"Whether you're protecting yourself",
);
});
it("renders both panels", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain("For Individuals");
expect(document.body.textContent).toContain("For Families");
});
it("renders individual panel description", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain(
"Personal identity protection",
);
});
it("renders family panel description", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain("Group management tools");
});
it("renders bullet items for individuals", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain(
"Monitor personal email and phone numbers",
);
expect(document.body.textContent).toContain(
"Dark web credential scanning",
);
});
it("renders bullet items for families", () => {
mount(() => <ForUsersSection />);
expect(document.body.textContent).toContain(
"Add unlimited family members",
);
expect(document.body.textContent).toContain("Shared alert dashboard");
});
it("renders checkmark icons", () => {
mount(() => <ForUsersSection />);
const checkmarks = document.querySelectorAll(
'svg path[fill="var(--color-success)"]',
);
expect(checkmarks.length).toBeGreaterThan(0);
});
it("renders 2 Card components for panels", () => {
mount(() => <ForUsersSection />);
const cards = document.querySelectorAll(".gradient-card");
expect(cards.length).toBe(2);
});
it("has the anchor ID for smooth scrolling", () => {
mount(() => <ForUsersSection />);
const section = document.querySelector('#for-users');
expect(section).toBeTruthy();
});
it("applies custom class prop", () => {
mount(() => <ForUsersSection class="custom-users" />);
const section = document.querySelector("section.custom-users");
expect(section).toBeTruthy();
});
it("uses two-column grid on desktop", () => {
mount(() => <ForUsersSection />);
const grid = document.querySelector(".grid-cols-1");
expect(grid).toBeTruthy();
expect(grid!.className).toContain("md:grid-cols-2");
});
});
describe("WhyKordantSection", () => {
it("renders the section heading", () => {
mount(() => <WhyKordantSection />);
expect(document.body.textContent).toContain("Why Kordant");
});
it("renders the section subheading", () => {
mount(() => <WhyKordantSection />);
expect(document.body.textContent).toContain(
"Built on cutting-edge technology",
);
});
it("renders all 3 value prop cards", () => {
mount(() => <WhyKordantSection />);
expect(document.body.textContent).toContain("Proactive, Not Reactive");
expect(document.body.textContent).toContain("AI-Powered Detection");
expect(document.body.textContent).toContain("Privacy First");
});
it("renders value prop descriptions", () => {
mount(() => <WhyKordantSection />);
expect(document.body.textContent).toContain(
"detect threats before they cause damage",
);
expect(document.body.textContent).toContain(
"Machine learning models trained",
);
expect(document.body.textContent).toContain("encrypted and private");
});
it("renders bullet items for each card", () => {
mount(() => <WhyKordantSection />);
expect(document.body.textContent).toContain(
"Real-time dark web scanning",
);
expect(document.body.textContent).toContain(
"Deepfake voice identification",
);
expect(document.body.textContent).toContain("End-to-end encrypted data");
});
it("renders 3 Card components", () => {
mount(() => <WhyKordantSection />);
const cards = document.querySelectorAll(".gradient-card");
expect(cards.length).toBe(3);
});
it("has the anchor ID for smooth scrolling", () => {
mount(() => <WhyKordantSection />);
const section = document.querySelector('#why-kordant');
expect(section).toBeTruthy();
});
it("applies custom class prop", () => {
mount(() => <WhyKordantSection class="custom-why" />);
const section = document.querySelector("section.custom-why");
expect(section).toBeTruthy();
});
it("uses three-column grid on desktop", () => {
mount(() => <WhyKordantSection />);
const grid = document.querySelector(".grid-cols-1");
expect(grid).toBeTruthy();
expect(grid!.className).toContain("md:grid-cols-3");
});
});
describe("CTABannerSection", () => {
it("renders the CTA headline", () => {
mount(() => <CTABannerSection />);
expect(document.body.textContent).toContain(
"Ready to protect your identity?",
);
});
it("renders the CTA subtext", () => {
mount(() => <CTABannerSection />);
expect(document.body.textContent).toContain(
"Join thousands of users",
);
});
it("renders Create Account button", () => {
mount(() => <CTABannerSection />);
expect(document.body.textContent).toContain("Create Account");
const primaryBtn = document.querySelector("button.gradient-primary");
expect(primaryBtn).toBeTruthy();
});
it("renders Sign In button", () => {
mount(() => <CTABannerSection />);
expect(document.body.textContent).toContain("Sign In");
});
it("has Create Account link to /signup", () => {
mount(() => <CTABannerSection />);
const links = document.querySelectorAll("a");
const signupLink = Array.from(links).find(
(a) => a.getAttribute("href") === "/signup",
);
expect(signupLink).toBeTruthy();
expect(signupLink!.textContent).toContain("Create Account");
});
it("has Sign In link to /login", () => {
mount(() => <CTABannerSection />);
const links = document.querySelectorAll("a");
const loginLink = Array.from(links).find(
(a) => a.getAttribute("href") === "/login",
);
expect(loginLink).toBeTruthy();
expect(loginLink!.textContent).toContain("Sign In");
});
it("renders 2 buttons", () => {
mount(() => <CTABannerSection />);
const buttons = document.querySelectorAll("button");
expect(buttons.length).toBe(2);
});
it("has the anchor ID for smooth scrolling", () => {
mount(() => <CTABannerSection />);
const section = document.querySelector('#cta');
expect(section).toBeTruthy();
});
it("applies custom class prop", () => {
mount(() => <CTABannerSection class="custom-cta" />);
const section = document.querySelector("section.custom-cta");
expect(section).toBeTruthy();
});
it("uses centered text layout", () => {
mount(() => <CTABannerSection />);
const inner = document.querySelector(".text-center");
expect(inner).toBeTruthy();
});
it("wraps content in PageContainer", () => {
mount(() => <CTABannerSection />);
const container = document.querySelector(".max-w-7xl");
expect(container).toBeTruthy();
});
it("uses gradient card for CTA banner", () => {
mount(() => <CTABannerSection />);
const card = document.querySelector(".gradient-card");
expect(card).toBeTruthy();
});
});

View File

@@ -143,7 +143,7 @@ function RealtimeIndicator() {
<button
type="button"
onClick={clearUnread}
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-[var(--color-error)] text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-(--color-error) text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
aria-label={`${unreadCount()} unread alerts`}
>
{unreadCount() > 99 ? "99+" : unreadCount()}
@@ -152,17 +152,24 @@ function RealtimeIndicator() {
<Show
when={connectionStatus() === "connected"}
fallback={
connectionStatus() === "reconnecting" || connectionStatus() === "connecting" ? (
<div class="flex items-center gap-1 text-[10px] text-[var(--color-warning)]" aria-label="Reconnecting">
connectionStatus() === "reconnecting" ||
connectionStatus() === "connecting" ? (
<div
class="flex items-center gap-1 text-[10px] text-(--color-warning)"
aria-label="Reconnecting"
>
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-warning)] opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-warning)]" />
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-warning) opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-warning)" />
</span>
<span class="hidden sm:inline">Reconnecting</span>
</div>
) : (
<div class="flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]" aria-label="Offline">
<span class="inline-flex rounded-full h-2 w-2 bg-[var(--color-text-muted)]" />
<div
class="flex items-center gap-1 text-[10px] text-(--color-text-muted)"
aria-label="Offline"
>
<span class="inline-flex rounded-full h-2 w-2 bg-(--color-text-muted)" />
<span class="hidden sm:inline">Offline</span>
</div>
)
@@ -170,8 +177,8 @@ function RealtimeIndicator() {
>
<div class="flex items-center gap-1" aria-label="Connected">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-success)] opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-success)]" />
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-success) opacity-75" />
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-success)" />
</span>
</div>
</Show>
@@ -197,7 +204,11 @@ export default function Navbar() {
return location.pathname.startsWith(href);
};
const NavLink = (props: { href: string; label: string; mobile?: boolean }) => (
const NavLink = (props: {
href: string;
label: string;
mobile?: boolean;
}) => (
<A
href={props.href}
class={cn(
@@ -205,9 +216,11 @@ export default function Navbar() {
? "block px-3 py-2 rounded-lg text-base font-medium transition-colors"
: "text-sm font-medium transition-colors",
isActive(props.href)
? "text-[var(--color-text-primary)]"
? "text-text-primary"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
props.mobile && !isActive(props.href) && "hover:bg-[var(--color-bg-secondary)]",
props.mobile &&
!isActive(props.href) &&
"hover:bg-[var(--color-bg-secondary)]",
)}
onClick={() => props.mobile && setMobileOpen(false)}
>
@@ -234,10 +247,14 @@ export default function Navbar() {
<div class="hidden md:flex items-center gap-6">
<SignedOut>
{marketingLinks.map(link => <NavLink href={link.href} label={link.label} />)}
{marketingLinks.map((link) => (
<NavLink href={link.href} label={link.label} />
))}
</SignedOut>
<SignedIn>
{productLinks.map(link => <NavLink href={link.href} label={link.label} />)}
{productLinks.map((link) => (
<NavLink href={link.href} label={link.label} />
))}
</SignedIn>
</div>
@@ -263,7 +280,7 @@ export default function Navbar() {
type="button"
aria-label="Toggle menu"
class="p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
onClick={() => setMobileOpen(v => !v)}
onClick={() => setMobileOpen((v) => !v)}
>
<Show
when={mobileOpen()}
@@ -304,12 +321,12 @@ export default function Navbar() {
<div class="md:hidden glass border-t border-[var(--color-border)]">
<div class="px-4 py-4 space-y-1">
<SignedOut>
{marketingLinks.map(link => (
{marketingLinks.map((link) => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedOut>
<SignedIn>
{productLinks.map(link => (
{productLinks.map((link) => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedIn>

View File

@@ -4,29 +4,20 @@ export interface OnboardingData {
familyInvites: string[];
}
export function getCheckoutUrl(plan: string): string | null {
const prices: Record<string, string | undefined> = {
basic: process.env.STRIPE_PRICE_BASIC,
plus: process.env.STRIPE_PRICE_PLUS,
premium: process.env.STRIPE_PRICE_PREMIUM,
};
const priceId = prices[plan];
if (!priceId) return null;
return `/billing/checkout?priceId=${priceId}`;
}
export async function submitOnboarding(data: OnboardingData) {
const { api } = await import("~/lib/api");
if (data.plan) {
try {
const prices: Record<string, string | undefined> = {
basic: process.env.STRIPE_PRICE_BASIC,
plus: process.env.STRIPE_PRICE_PLUS,
premium: process.env.STRIPE_PRICE_PREMIUM,
};
const priceId = prices[data.plan];
if (priceId) {
await api.billing.createCheckoutSession.mutate({
priceId,
successUrl: `${window.location.origin}/dashboard`,
cancelUrl: `${window.location.origin}/onboarding`,
});
}
} catch {
// billing setup not required for free plan
}
}
for (const item of data.watchlistItems) {
const type = item.includes("@") ? "EMAIL" : "PHONE";
try {

View File

@@ -2,7 +2,7 @@ import { createSignal, For, Show } from "solid-js";
import { Title } from "@solidjs/meta";
import { useNavigate } from "@solidjs/router";
import { Button, Input, Badge } from "~/components/ui";
import { submitOnboarding } from "~/lib/auth";
import { submitOnboarding, getCheckoutUrl } from "~/lib/auth";
import type { OnboardingData } from "~/lib/auth";
const plans = [
@@ -86,6 +86,12 @@ export default function OnboardingPage() {
}
async function completeOnboarding() {
const checkoutUrl = getCheckoutUrl(plan());
if (checkoutUrl) {
navigate(checkoutUrl);
return;
}
setSubmitting(true);
try {
const data: OnboardingData = {
@@ -392,23 +398,12 @@ export default function OnboardingPage() {
selected items and will alert you of any threats.
</p>
</div>
<div class="flex flex-col gap-2 w-full max-w-xs">
<Button
onClick={() => navigate("/dashboard", { replace: true })}
class="w-full"
>
Go to Dashboard
</Button>
{!submitting && (
<Button
variant="ghost"
onClick={completeOnboarding}
class="w-full"
>
Submit onboarding data
</Button>
)}
</div>
<Button
onClick={() => navigate("/dashboard", { replace: true })}
class="w-full max-w-xs"
>
Go to Dashboard
</Button>
</div>
</Show>
</div>

View File

@@ -0,0 +1,34 @@
import type { APIEvent } from "@solidjs/start/server";
import { stripe } from "~/server/stripe";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const sessionId = url.searchParams.get("session_id");
if (!sessionId) {
return new Response(JSON.stringify({ error: "Missing session_id" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
try {
const session = await stripe.checkout.sessions.retrieve(sessionId, {
expand: ["customer_details.email"],
});
return new Response(JSON.stringify({
status: session.status,
customer_email: typeof session.customer_details === "string"
? undefined
: session.customer_details?.email,
}), {
headers: { "Content-Type": "application/json" },
});
} catch {
return new Response(JSON.stringify({ error: "Failed to retrieve session" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
}

View File

@@ -0,0 +1,89 @@
import { createSignal, onMount, Show } from "solid-js";
import { Title } from "@solidjs/meta";
import { useNavigate, useSearchParams } from "@solidjs/router";
import EmbeddedCheckout from "~/components/EmbeddedCheckout";
import { api } from "~/lib/api";
import PageContainer from "~/components/layout/PageContainer";
const priceMap: Record<string, string> = {
basic: process.env.STRIPE_PRICE_BASIC ?? "",
plus: process.env.STRIPE_PRICE_PLUS ?? "",
premium: process.env.STRIPE_PRICE_PREMIUM ?? "",
};
export default function CheckoutPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [clientSecret, setClientSecret] = createSignal("");
const [error, setError] = createSignal<string | null>(null);
const [loading, setLoading] = createSignal(true);
onMount(async () => {
const plan = Array.isArray(searchParams.plan) ? searchParams.plan[0] : searchParams.plan;
const priceIdParam = Array.isArray(searchParams.priceId) ? searchParams.priceId[0] : searchParams.priceId;
const priceId = priceIdParam ?? (plan ? priceMap[plan] : "");
if (!priceId) {
setError("No plan selected. Please select a plan to continue.");
setLoading(false);
return;
}
try {
const returnUrl = `${window.location.origin}/billing/return`;
const result = await api.billing.createCheckoutSession.mutate({
priceId,
returnUrl,
});
setClientSecret(result.clientSecret);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create checkout session");
} finally {
setLoading(false);
}
});
return (
<main class="min-h-screen py-8 md:py-12">
<Title>Checkout Kordant</Title>
<PageContainer>
<div class="max-w-2xl mx-auto">
<div class="mb-8">
<h1 class="text-2xl font-bold text-[var(--color-text-primary)]">Complete your purchase</h1>
<p class="text-sm text-[var(--color-text-secondary)] mt-1">
Secure payment powered by Stripe
</p>
</div>
<Show when={loading()}>
<div class="flex items-center justify-center min-h-[300px]">
<div class="text-center">
<div class="animate-spin h-8 w-8 border-2 border-[var(--color-brand-primary)] border-t-transparent rounded-full mx-auto mb-3" />
<p class="text-sm text-[var(--color-text-secondary)]">Preparing checkout...</p>
</div>
</div>
</Show>
<Show when={error() && !loading()}>
<div class="text-center py-8 border border-[var(--color-border)] rounded-xl">
<p class="text-[var(--color-error)] mb-4">{error()}</p>
<button
onClick={() => navigate("/pricing")}
class="text-sm text-[var(--color-brand-primary)] hover:underline"
>
Back to pricing
</button>
</div>
</Show>
<Show when={clientSecret() && !loading() && !error()}>
<EmbeddedCheckout
clientSecret={clientSecret()}
onCheckoutComplete={() => navigate("/dashboard")}
/>
</Show>
</div>
</PageContainer>
</main>
);
}

View File

@@ -0,0 +1,131 @@
import { createSignal, onMount, Show } from "solid-js";
import { Title } from "@solidjs/meta";
import { useNavigate, useSearchParams } from "@solidjs/router";
import PageContainer from "~/components/layout/PageContainer";
export default function ReturnPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [status, setStatus] = createSignal<"loading" | "complete" | "open" | "error">("loading");
const [customerEmail, setCustomerEmail] = createSignal("");
onMount(async () => {
const sessionId = Array.isArray(searchParams.session_id)
? searchParams.session_id[0]
: searchParams.session_id;
if (!sessionId) {
setStatus("error");
return;
}
try {
const response = await fetch(`/api/stripe/session-status?session_id=${sessionId}`);
if (!response.ok) throw new Error("Failed to check session status");
const data = await response.json();
setStatus(data.status);
setCustomerEmail(data.customer_email ?? "");
} catch {
setStatus("error");
}
});
const isComplete = status() === "complete";
const isOpen = status() === "open";
const isError = status() === "error";
return (
<main class="min-h-screen py-8 md:py-12">
<Title>Payment Status Kordant</Title>
<PageContainer>
<div class="max-w-xl mx-auto text-center">
<Show when={status() === "loading"}>
<div class="flex items-center justify-center min-h-[300px]">
<div class="text-center">
<div class="animate-spin h-8 w-8 border-2 border-[var(--color-brand-primary)] border-t-transparent rounded-full mx-auto mb-3" />
<p class="text-sm text-[var(--color-text-secondary)]">Checking payment status...</p>
</div>
</div>
</Show>
<Show when={isOpen}>
<div class="py-8">
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
Payment in progress
</h1>
<p class="text-[var(--color-text-secondary)] mb-6">
Your payment session is still open. Please complete the checkout process.
</p>
<button
onClick={() => navigate("/billing/checkout")}
class="text-sm text-[var(--color-brand-primary)] hover:underline"
>
Return to checkout
</button>
</div>
</Show>
<Show when={isComplete}>
<div class="py-8">
<div class="h-16 w-16 rounded-full gradient-primary flex items-center justify-center mx-auto mb-6">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 text-white"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M20 6L9 17L4 12" />
</svg>
</div>
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
Payment successful!
</h1>
<p class="text-[var(--color-text-secondary)] mb-2">
We appreciate your business!
</p>
<Show when={customerEmail()}>
<p class="text-sm text-[var(--color-text-tertiary)] mb-6">
A confirmation email will be sent to {customerEmail()}.
</p>
</Show>
<button
onClick={() => navigate("/dashboard", { replace: true })}
class="px-6 py-3 rounded-lg gradient-primary text-white font-medium hover:opacity-90 transition-opacity"
>
Go to Dashboard
</button>
</div>
</Show>
<Show when={isError}>
<div class="py-8">
<h1 class="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
Something went wrong
</h1>
<p class="text-[var(--color-text-secondary)] mb-6">
We couldn't verify your payment status. Please try again.
</p>
<div class="flex gap-3 justify-center">
<button
onClick={() => navigate("/billing/checkout")}
class="text-sm text-[var(--color-brand-primary)] hover:underline"
>
Try checkout again
</button>
<span class="text-[var(--color-text-tertiary)]">or</span>
<button
onClick={() => navigate("/pricing")}
class="text-sm text-[var(--color-brand-primary)] hover:underline"
>
View plans
</button>
</div>
</div>
</Show>
</div>
</PageContainer>
</main>
);
}

View File

@@ -21,7 +21,7 @@ export default function BlogPage() {
// Fetch all published posts
const allPosts = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res => {
return api.blog.list.query({ limit: "100" }).then((res) => {
setLoading(false);
return res.posts;
});
@@ -32,9 +32,9 @@ export default function BlogPage() {
// Fetch featured post
const featuredPost = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res =>
res.posts.find((p: any) => p.featured) ?? null
);
return api.blog.list
.query({ limit: "100" })
.then((res) => res.posts.find((p: any) => p.featured) ?? null);
});
// Filtered + visible posts
@@ -51,8 +51,8 @@ export default function BlogPage() {
return filtered.slice(0, visibleCount());
});
const filtered = createMemo(() => {
const posts = allPosts();
const filtered = createMemo(async () => {
const posts = await allPosts();
if (!posts) return [];
const tag = selectedTag();
if (!tag) return posts;
@@ -77,7 +77,8 @@ export default function BlogPage() {
Kordant Blog
</h1>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Insights on identity protection, AI safety, and the latest digital threats
Insights on identity protection, AI safety, and the latest digital
threats
</p>
</div>
</PageContainer>
@@ -92,19 +93,37 @@ export default function BlogPage() {
<A href={`/blog/${fp().slug}`}>
<Card class="flex flex-col md:flex-row gap-6 p-6 hover:shadow-lg transition-shadow">
<div class="md:w-64 h-40 md:h-auto bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg flex-shrink-0 flex items-center justify-center">
<svg width="40" height="40" viewBox="0 0 32 32" fill="none" class="text-[var(--color-brand-primary)] opacity-40">
<path d="M14 2L2 8v8c0 7 6 12 12 13 6-1 12-6 12-13V8L14 2z" stroke="currentColor" stroke-width="2"/>
<svg
width="40"
height="40"
viewBox="0 0 32 32"
fill="none"
class="text-[var(--color-brand-primary)] opacity-40"
>
<path
d="M14 2L2 8v8c0 7 6 12 12 13 6-1 12-6 12-13V8L14 2z"
stroke="currentColor"
stroke-width="2"
/>
</svg>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Badge variant="info" class="text-xs">Featured</Badge>
<Badge variant="info" class="text-xs">
Featured
</Badge>
<span class="text-xs text-[var(--color-text-tertiary)]">
{fp().publishedAt ? new Date(fp().publishedAt).toLocaleDateString() : ""}
{fp().publishedAt
? new Date(fp().publishedAt).toLocaleDateString()
: ""}
</span>
</div>
<h2 class="text-xl font-bold text-[var(--color-text-primary)] mb-2">{fp().title}</h2>
<p class="text-sm text-[var(--color-text-secondary)] line-clamp-2">{fp().excerpt}</p>
<h2 class="text-xl font-bold text-[var(--color-text-primary)] mb-2">
{fp().title}
</h2>
<p class="text-sm text-[var(--color-text-secondary)] line-clamp-2">
{fp().excerpt}
</p>
</div>
</Card>
</A>
@@ -121,7 +140,10 @@ export default function BlogPage() {
<div class="flex flex-wrap items-center gap-2 mb-10">
<button
type="button"
onClick={() => { setSelectedTag(null); setVisibleCount(POSTS_PER_PAGE); }}
onClick={() => {
setSelectedTag(null);
setVisibleCount(POSTS_PER_PAGE);
}}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
!selectedTag()
@@ -135,7 +157,10 @@ export default function BlogPage() {
{({ tag, count }) => (
<button
type="button"
onClick={() => { setSelectedTag(tag); setVisibleCount(POSTS_PER_PAGE); }}
onClick={() => {
setSelectedTag(tag);
setVisibleCount(POSTS_PER_PAGE);
}}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
selectedTag() === tag
@@ -157,9 +182,27 @@ export default function BlogPage() {
<A href={`/blog/${post.slug}`}>
<Card class="h-full hover:shadow-lg transition-shadow duration-300">
<div class="h-40 bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg mb-4 flex items-center justify-center text-[var(--color-text-tertiary)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" class="opacity-40">
<rect x="4" y="6" width="24" height="20" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M4 12h24M12 6v20" stroke="currentColor" stroke-width="2"/>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
class="opacity-40"
>
<rect
x="4"
y="6"
width="24"
height="20"
rx="2"
stroke="currentColor"
stroke-width="2"
/>
<path
d="M4 12h24M12 6v20"
stroke="currentColor"
stroke-width="2"
/>
</svg>
</div>
<div class="flex flex-wrap gap-1.5 mb-3">
@@ -176,7 +219,9 @@ export default function BlogPage() {
<div class="flex items-center justify-between text-xs text-[var(--color-text-tertiary)] mt-auto">
<span>{post.authorName || "Kordant"}</span>
<span>
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : ""}
{post.publishedAt
? new Date(post.publishedAt).toLocaleDateString()
: ""}
{" · "}
{readingTime(post.content)}
</span>
@@ -189,7 +234,9 @@ export default function BlogPage() {
<Show when={visible().length === 0}>
<div class="text-center py-16">
<p class="text-[var(--color-text-secondary)] text-lg">No posts found{selectedTag() ? ` for "${selectedTag()}"` : ""}</p>
<p class="text-[var(--color-text-secondary)] text-lg">
No posts found{selectedTag() ? ` for "${selectedTag()}"` : ""}
</p>
</div>
</Show>

View File

@@ -228,73 +228,74 @@ export default function Home() {
});
return (
<main class="overflow-hidden" style="--cut: clamp(16px, 2.5vw, 40px)">
<div style="--cut: clamp(16px, 2.5vw, 40px)">
<Title>Kordant AI-Powered Identity Protection</Title>
{/* Hero */}
{/* Fixed background — stays locked while content scrolls over it */}
<ColorWaveBackground yOffset={-0.1} scale={0.65} speed={0.5} />
<div class="relative z-10">
<section>
<PageContainer>
<div
ref={heroRef}
class="flex flex-col items-center text-center py-20 md:py-32 transition-all duration-700 ease-out"
style="opacity: 0; transform: translateY(20px);"
>
<div class="mb-6 shadow-glow-primary rounded-full p-3">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 md:w-16 md:h-16">
<circle cx="24" cy="24" r="24" fill="var(--color-brand-primary)" />
<path d="M24 10L16 14v6.5c0 5.1 3.4 9.9 8 11.5 4.6-1.6 8-6.4 8-11.5V14l-8-4z" fill="white" fill-opacity="0.9" />
<path d="M20 24l3 2.5 5-5" stroke="var(--color-brand-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-6 max-w-4xl">
<Typewriter speed={50} delay={400} keepAlive={false}>
<span class="text-text-primary">AI-Powered </span>
<span class="text-gradient-primary">Identity Protection</span>
<br />
<span class="text-text-primary">for Everyone</span>
</Typewriter>
</h1>
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
Threat actors are using AI in multifaceted attacks. Kordant evens
the playing field using advanced AI to monitor, detect, and prevent
identity threats in real-time.
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<A href="/signup">
<Button variant="primary" size="lg">Get Started</Button>
</A>
<A href="#features">
<Button variant="ghost" size="lg">Learn More</Button>
</A>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
No credit card required
</span>
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
Free tier available
</span>
</div>
{/* Hero */}
<section class="relative z-10">
<PageContainer>
<div
ref={heroRef}
class="flex flex-col items-center text-center py-20 md:py-32 transition-all duration-700 ease-out"
style="opacity: 0; transform: translateY(20px);"
>
<div class="mb-6 shadow-glow-primary rounded-full p-3">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 md:w-16 md:h-16">
<circle cx="24" cy="24" r="24" fill="var(--color-brand-primary)" />
<path d="M24 10L16 14v6.5c0 5.1 3.4 9.9 8 11.5 4.6-1.6 8-6.4 8-11.5V14l-8-4z" fill="white" fill-opacity="0.9" />
<path d="M20 24l3 2.5 5-5" stroke="var(--color-brand-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</PageContainer>
</section>
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-6 max-w-4xl">
<Typewriter speed={50} delay={400} keepAlive={false}>
<span class="text-text-primary">AI-Powered </span>
<span class="text-gradient-primary">Identity Protection</span>
<br />
<span class="text-text-primary">for Everyone</span>
</Typewriter>
</h1>
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
Threat actors are using AI in multifaceted attacks. Kordant evens
the playing field using advanced AI to monitor, detect, and prevent
identity threats in real-time.
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<A href="/signup">
<Button variant="primary" size="lg">Get Started</Button>
</A>
<A href="#features">
<Button variant="ghost" size="lg">Learn More</Button>
</A>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
No credit card required
</span>
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
Free tier available
</span>
</div>
</div>
</PageContainer>
</section>
{/* How It Works */}
<div
class="bg-dot-grid relative z-10"
<section
id="how-it-works"
class="relative z-10 bg-dot-grid scroll-mt-16"
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<section id="how-it-works" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<PageContainer py="py-8">
<div class="py-20 md:py-28">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
How It Works
@@ -333,17 +334,18 @@ export default function Home() {
}}
</For>
</div>
</PageContainer>
</section>
</div>
</div>
</PageContainer>
</section>
{/* Platform Features */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 py-8 -my-10"
<section
id="features"
class="relative z-10 backdrop-blur-2xl bg-bg/40 py-8 -my-10 scroll-mt-16"
style={{ "clip-path": "polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), 0 100%)" }}
>
<section id="features" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<PageContainer py="py-8">
<div class="py-20 md:py-28">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Platform Features
@@ -373,17 +375,18 @@ export default function Home() {
)}
</For>
</div>
</PageContainer>
</section>
</div>
</div>
</PageContainer>
</section>
{/* For Everyone */}
<div
class="bg-dot-grid"
<section
id="for-users"
class="relative z-10 bg-dot-grid scroll-mt-16"
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<section id="for-users" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<PageContainer py="py-8">
<div class="py-20 md:py-28">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
For Everyone
@@ -421,17 +424,18 @@ export default function Home() {
)}
</For>
</div>
</PageContainer>
</section>
</div>
</div>
</PageContainer>
</section>
{/* Why Kordant + CTA */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 pt-8 -mt-10"
<section
id="why-kordant"
class="relative z-10 backdrop-blur-2xl bg-bg/40 pt-8 -mt-10 scroll-mt-16"
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<section id="why-kordant" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<PageContainer py="py-8">
<div class="py-20 md:py-28">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Why Kordant
@@ -469,31 +473,33 @@ export default function Home() {
)}
</For>
</div>
</PageContainer>
</section>
</div>
</PageContainer>
<section id="cta" class="py-20 md:py-28 scroll-mt-16">
<section id="cta" class="scroll-mt-16">
<PageContainer py="py-8">
<div class="gradient-card border border-(--color-border)/50 rounded-2xl p-10 md:p-16 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
Join thousands of users who trust Kordant to keep their digital
identity safe from emerging threats.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<A href="/signup">
<Button variant="primary" size="lg">Create Account</Button>
</A>
<A href="/login">
<Button variant="secondary" size="lg">Sign In</Button>
</A>
<div class="py-20 md:py-28">
<div class="gradient-card border border-(--color-border)/50 rounded-2xl p-10 md:p-16 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
Join thousands of users who trust Kordant to keep their digital
identity safe from emerging threats.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<A href="/signup">
<Button variant="primary" size="lg">Create Account</Button>
</A>
<A href="/login">
<Button variant="secondary" size="lg">Sign In</Button>
</A>
</div>
</div>
</div>
</PageContainer>
</section>
</div>
</main>
</section>
</div>
);
}

View File

@@ -26,7 +26,12 @@ const plans: Plan[] = [
price: "$9",
period: "/month",
description: "Essential identity protection for individuals",
features: ["Dark web monitoring", "Email breach alerts", "Basic scam call blocking", "Monthly reports"],
features: [
"Dark web monitoring",
"Email breach alerts",
"Basic scam call blocking",
"Monthly reports",
],
cta: "Start Free Trial",
popular: false,
},
@@ -35,7 +40,13 @@ const plans: Plan[] = [
price: "$19",
period: "/month",
description: "Advanced protection for you and your family",
features: ["Everything in Basic", "VoicePrint AI detection", "HomeTitle fraud alerts", "RemoveBrokers automation", "Family sharing (up to 5)"],
features: [
"Everything in Basic",
"VoicePrint AI detection",
"HomeTitle fraud alerts",
"RemoveBrokers automation",
"Family sharing (up to 5)",
],
cta: "Start Free Trial",
popular: true,
},
@@ -44,7 +55,14 @@ const plans: Plan[] = [
price: "$39",
period: "/month",
description: "Maximum security for the whole household",
features: ["Everything in Plus", "Unlimited family members", "Priority support 24/7", "Real-time alert correlation", "Advanced analytics dashboard", "Data broker suppression"],
features: [
"Everything in Plus",
"Unlimited family members",
"Priority support 24/7",
"Real-time alert correlation",
"Advanced analytics dashboard",
"Data broker suppression",
],
cta: "Start Free Trial",
popular: false,
},
@@ -80,30 +98,90 @@ const faqs: FAQ[] = [
const comparisonFeatures = [
{ feature: "Dark web monitoring", basic: true, plus: true, premium: true },
{ feature: "Email breach alerts", basic: true, plus: true, premium: true },
{ feature: "Basic scam call blocking", basic: true, plus: true, premium: true },
{
feature: "Basic scam call blocking",
basic: true,
plus: true,
premium: true,
},
{ feature: "Monthly reports", basic: true, plus: true, premium: true },
{ feature: "VoicePrint AI detection", basic: false, plus: true, premium: true },
{ feature: "HomeTitle fraud alerts", basic: false, plus: true, premium: true },
{ feature: "RemoveBrokers automation", basic: false, plus: true, premium: true },
{ feature: "Family sharing", basic: false, plus: "Up to 5", premium: "Unlimited" },
{ feature: "Priority support 24/7", basic: false, plus: false, premium: true },
{ feature: "Real-time alert correlation", basic: false, plus: false, premium: true },
{
feature: "VoicePrint AI detection",
basic: false,
plus: true,
premium: true,
},
{
feature: "HomeTitle fraud alerts",
basic: false,
plus: true,
premium: true,
},
{
feature: "RemoveBrokers automation",
basic: false,
plus: true,
premium: true,
},
{
feature: "Family sharing",
basic: false,
plus: "Up to 5",
premium: "Unlimited",
},
{
feature: "Priority support 24/7",
basic: false,
plus: false,
premium: true,
},
{
feature: "Real-time alert correlation",
basic: false,
plus: false,
premium: true,
},
{ feature: "Advanced analytics", basic: false, plus: false, premium: true },
{ feature: "Data broker suppression", basic: false, plus: false, premium: true },
{
feature: "Data broker suppression",
basic: false,
plus: false,
premium: true,
},
];
function CheckIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M6.5 11.5L3 8l1.1-1.1L6.5 9.3l5.9-5.9L13.5 4.5l-7 7z" fill="var(--color-success)"/>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class="flex-shrink-0"
>
<path
d="M6.5 11.5L3 8l1.1-1.1L6.5 9.3l5.9-5.9L13.5 4.5l-7 7z"
fill="var(--color-success)"
/>
</svg>
);
}
function XIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M4 4l8 8M12 4l-8 8" stroke="var(--color-text-muted)" stroke-width="1.5" stroke-linecap="round"/>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class="flex-shrink-0"
>
<path
d="M4 4l8 8M12 4l-8 8"
stroke="var(--color-text-muted)"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
);
}
@@ -112,33 +190,52 @@ export default function PricingPage() {
const [searchParams] = useSearchParams();
const [openFaq, setOpenFaq] = createSignal<string | null>(null);
const signupUrl = () => `/signup${searchParams.utm_source ? `?utm_source=${searchParams.utm_source}&utm_medium=${searchParams.utm_medium || ""}&utm_campaign=${searchParams.utm_campaign || ""}` : ""}`;
const signupUrl = () =>
`/signup${
searchParams.utm_source
? `?utm_source=${searchParams.utm_source}&utm_medium=${
searchParams.utm_medium || ""
}&utm_campaign=${searchParams.utm_campaign || ""}`
: ""
}`;
const planToId: Record<string, string> = {
Basic: "basic",
Plus: "plus",
Premium: "premium",
};
return (
<main>
<Title>Kordant Pricing AI-Powered Identity Protection Plans</Title>
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<div class="absolute inset-0 bg-linear-to-b from-(--color-brand-primary)/5 to-transparent" />
<PageContainer class="relative z-10">
<div class="text-center max-w-3xl mx-auto">
<Badge variant="info" class="mb-4">Simple Pricing</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-6">
<Badge variant="info" class="mb-4">
Simple Pricing
</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-text-primary mb-6">
Protection That Fits{" "}
<span class="text-gradient-primary">Your Budget</span>
</h1>
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Start with a 14-day free trial. No credit card required. Cancel anytime.
<p class="text-xl text-text-secondary mb-8 max-w-2xl mx-auto">
Start with a 14-day free trial. No credit card required. Cancel
anytime.
</p>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-[var(--color-text-tertiary)]">
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
<span class="flex items-center gap-1.5">
<CheckIcon />14-day free trial
<CheckIcon />
14-day free trial
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />No credit card required
<CheckIcon />
No credit card required
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />Cancel anytime
<CheckIcon />
Cancel anytime
</span>
</div>
</div>
@@ -153,7 +250,8 @@ export default function PricingPage() {
<Card
class={cn(
"relative flex flex-col",
plan.popular && "ring-2 ring-[var(--color-brand-primary)] shadow-glow-primary",
plan.popular &&
"ring-2 ring-(--color-brand-primary) shadow-glow-primary",
)}
>
<Show when={plan.popular}>
@@ -162,25 +260,36 @@ export default function PricingPage() {
</div>
</Show>
<div class="mb-6">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">{plan.name}</h3>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">{plan.description}</p>
<h3 class="text-lg font-semibold text-text-primary mb-1">
{plan.name}
</h3>
<p class="text-sm text-text-secondary mb-4">
{plan.description}
</p>
<div class="flex items-baseline gap-0.5">
<span class="text-4xl font-bold text-[var(--color-text-primary)]">{plan.price}</span>
<span class="text-sm text-[var(--color-text-tertiary)]">{plan.period}</span>
<span class="text-4xl font-bold text-text-primary">
{plan.price}
</span>
<span class="text-sm text-text-tertiary">
{plan.period}
</span>
</div>
</div>
<ul class="space-y-3 mb-8 flex-1">
<For each={plan.features}>
{(feature) => (
<li class="flex items-start gap-2 text-sm text-[var(--color-text-secondary)]">
<li class="flex items-start gap-2 text-sm text-text-secondary">
<CheckIcon />
{feature}
</li>
)}
</For>
</ul>
<A href={signupUrl()}>
<Button variant={plan.popular ? "primary" : "secondary"} class="w-full">
<A href={`/billing/checkout?plan=${planToId[plan.name]}`}>
<Button
variant={plan.popular ? "primary" : "secondary"}
class="w-full"
>
{plan.cta}
</Button>
</A>
@@ -202,25 +311,59 @@ export default function PricingPage() {
<table class="w-full">
<thead>
<tr class="border-b-2 border-[var(--color-border)]">
<th class="text-left px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Feature</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Basic</th>
<th class="text-center px-4 py-3 text-sm font-semibold text-[var(--color-brand-primary)]">Plus</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Premium</th>
<th class="text-left px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">
Feature
</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">
Basic
</th>
<th class="text-center px-4 py-3 text-sm font-semibold text-[var(--color-brand-primary)]">
Plus
</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">
Premium
</th>
</tr>
</thead>
<tbody>
<For each={comparisonFeatures}>
{(row) => (
<tr class="border-b border-[var(--color-border)]">
<td class="px-4 py-3 text-sm text-[var(--color-text-primary)]">{row.feature}</td>
<td class="text-center px-4 py-3">
{row.basic === true ? <CheckIcon /> : row.basic === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.basic}</span>}
<td class="px-4 py-3 text-sm text-[var(--color-text-primary)]">
{row.feature}
</td>
<td class="text-center px-4 py-3">
{row.plus === true ? <CheckIcon /> : row.plus === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.plus}</span>}
{row.basic === true ? (
<CheckIcon />
) : row.basic === false ? (
<XIcon />
) : (
<span class="text-xs text-[var(--color-text-secondary)]">
{row.basic}
</span>
)}
</td>
<td class="text-center px-4 py-3">
{row.premium === true ? <CheckIcon /> : row.premium === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.premium}</span>}
{row.plus === true ? (
<CheckIcon />
) : row.plus === false ? (
<XIcon />
) : (
<span class="text-xs text-[var(--color-text-secondary)]">
{row.plus}
</span>
)}
</td>
<td class="text-center px-4 py-3">
{row.premium === true ? (
<CheckIcon />
) : row.premium === false ? (
<XIcon />
) : (
<span class="text-xs text-[var(--color-text-secondary)]">
{row.premium}
</span>
)}
</td>
</tr>
)}
@@ -244,7 +387,7 @@ export default function PricingPage() {
{(faq) => {
const isOpen = () => openFaq() === faq.q;
return (
<div class="border border-[var(--color-border)] rounded-xl overflow-hidden">
<div class="border border-(--color-border) rounded-xl overflow-hidden">
<button
type="button"
class="w-full flex items-center justify-between px-5 py-4 text-left text-sm font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
@@ -256,9 +399,18 @@ export default function PricingPage() {
height="16"
viewBox="0 0 16 16"
fill="none"
class={cn("transition-transform duration-200", isOpen() && "rotate-180")}
class={cn(
"transition-transform duration-200",
isOpen() && "rotate-180",
)}
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M4 6l4 4 4-4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<Show when={isOpen()}>
@@ -275,17 +427,22 @@ export default function PricingPage() {
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-brand-primary)]">
<section class="py-16 bg-(--color-brand-primary)">
<PageContainer>
<div class="text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-white/80 mb-8 max-w-2xl mx-auto">
Join 50,000+ users who trust Kordant for AI-powered identity protection.
Join 50,000+ users who trust Kordant for AI-powered identity
protection.
</p>
<A href={signupUrl()}>
<Button variant="primary" size="lg" class="bg-white text-[var(--color-brand-primary)] hover:bg-white/90 shadow-lg">
<Button
variant="primary"
size="lg"
class="bg-white text-[var(--color-brand-primary)] hover:bg-white/90 shadow-lg"
>
Get Started Free
</Button>
</A>

View File

@@ -84,8 +84,8 @@ function createCaller(user: User | null) {
createCheckoutSession: t.procedure.use(isAuthed)
.input(wrap(CreateCheckoutSessionSchema))
.mutation(async ({ ctx, input }) => {
const i = input as { priceId: string; successUrl: string; cancelUrl: string };
return mockCreateCheckoutSession(ctx.user.id, ctx.user.email, i.priceId, i.successUrl, i.cancelUrl);
const i = input as { priceId: string; returnUrl: string };
return mockCreateCheckoutSession(ctx.user.id, ctx.user.email, i.priceId, i.returnUrl);
}),
createPortalSession: t.procedure.use(isAuthed)
.input(wrap(CreatePortalSessionSchema))
@@ -159,20 +159,20 @@ describe("billing.getSubscription", () => {
});
describe("billing.createCheckoutSession", () => {
it("creates checkout session and returns URL", async () => {
it("creates checkout session and returns clientSecret", async () => {
mockCreateCheckoutSession.mockResolvedValue({
url: "https://checkout.stripe.com/session_123",
clientSecret: "cs_123_secret",
sessionId: "session_123",
});
const api = createCaller(makeUser());
const result = await api.createCheckoutSession({
priceId: "price_basic",
successUrl: "https://example.com/success",
cancelUrl: "https://example.com/cancel",
returnUrl: "https://example.com/return",
});
expect(result.url).toBe("https://checkout.stripe.com/session_123");
expect(result.clientSecret).toBe("cs_123_secret");
expect(result.sessionId).toBe("session_123");
});
});

View File

@@ -48,8 +48,7 @@ export const billingRouter = createTRPCRouter({
ctx.user.id,
ctx.user.email,
input.priceId,
input.successUrl,
input.cancelUrl,
input.returnUrl,
);
}),

View File

@@ -2,8 +2,7 @@ import { object, string, url, minLength, optional, picklist } from "valibot";
export const CreateCheckoutSessionSchema = object({
priceId: string([minLength(1)]),
successUrl: string([url()]),
cancelUrl: string([url()]),
returnUrl: string([url()]),
});
export const CreatePortalSessionSchema = object({

View File

@@ -100,7 +100,7 @@ describe("getOrCreateCustomer", () => {
});
describe("createCheckoutSession", () => {
it("creates a Stripe checkout session and returns URL", async () => {
it("creates an embedded Stripe checkout session and returns clientSecret", async () => {
(db.select as ReturnType<typeof vi.fn>).mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
@@ -112,20 +112,25 @@ describe("createCheckoutSession", () => {
});
(stripe.checkout.sessions.create as ReturnType<typeof vi.fn>).mockResolvedValue({
url: "https://checkout.stripe.com/session_123",
id: "session_123",
client_secret: "cs_123_secret",
});
const result = await createCheckoutSession(
"u1",
"a@b.com",
"price_basic",
"https://example.com/success",
"https://example.com/cancel",
"https://example.com/return",
);
expect(result.url).toBe("https://checkout.stripe.com/session_123");
expect(result.clientSecret).toBe("cs_123_secret");
expect(result.sessionId).toBe("session_123");
expect(stripe.checkout.sessions.create).toHaveBeenCalledWith(
expect.objectContaining({
ui_mode: "embedded_page",
return_url: "https://example.com/return?session_id={CHECKOUT_SESSION_ID}",
}),
);
});
});

View File

@@ -40,21 +40,20 @@ export async function createCheckoutSession(
userId: string,
email: string,
priceId: string,
successUrl: string,
cancelUrl: string,
returnUrl: string,
) {
const customerId = await getOrCreateCustomer(userId, email);
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
ui_mode: "embedded_page",
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl,
cancel_url: cancelUrl,
return_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`,
metadata: { userId },
});
return { url: session.url, sessionId: session.id };
return { clientSecret: session.client_secret ?? "", sessionId: session.id };
}
export async function createPortalSession(customerId: string, returnUrl: string) {