diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx
index 369e81d8..1606c188 100644
--- a/src/components/Nav.tsx
+++ b/src/components/Nav.tsx
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
-import { Link as GLink, Select, Spacer, Text } from "@geist-ui/react";
+import { Button, Link as GLink, Select, Spacer, Text } from "@geist-ui/react";
import Image from "next/image";
import { useRouter } from "next/router";
import React from "react";
@@ -9,6 +9,7 @@ import logo from "../assets/logo/reacher.svg";
import { sentryException } from "@/util/sentry";
import { useUser } from "@/util/useUser";
import styles from "./Nav.module.css";
+import Link from "next/link";
export function Nav(): React.ReactElement {
const { user, userDetails, signOut } = useUser();
@@ -64,7 +65,7 @@ export function Nav(): React.ReactElement {
Help Center
- {user && (
+ {user ? (
+ ) : (
+ <>
+
+
+
+
+
+
+
+ >
)}
);
diff --git a/src/components/ProductCard/Card.tsx b/src/components/ProductCard/Card.tsx
index 1a3ebf69..2a93bce4 100644
--- a/src/components/ProductCard/Card.tsx
+++ b/src/components/ProductCard/Card.tsx
@@ -5,13 +5,14 @@ import React from "react";
import styles from "./Card.module.css";
export interface CardProps extends React.HTMLProps {
- body?: string | React.ReactChild;
- cta?: string | React.ReactChild;
- features?: (string | React.ReactChild)[];
- header: string | React.ReactChild;
- price?: string | React.ReactChild;
- subtitle?: string | React.ReactChild;
- footer?: string | React.ReactChild;
+ body?: React.ReactElement;
+ cta?: React.ReactElement;
+ extra?: React.ReactElement;
+ features?: (string | React.ReactElement)[];
+ header?: string | React.ReactElement;
+ price?: string | React.ReactElement;
+ subtitle?: React.ReactElement;
+ footer?: React.ReactElement;
title: string;
}
@@ -19,6 +20,7 @@ export function Card({
cta,
header,
features,
+ extra,
price,
title,
subtitle,
@@ -52,6 +54,8 @@ export function Card({
+ {extra}
+
What you get:
diff --git a/src/components/ProductCard/Commercial.tsx b/src/components/ProductCard/Commercial.tsx
new file mode 100644
index 00000000..529040fb
--- /dev/null
+++ b/src/components/ProductCard/Commercial.tsx
@@ -0,0 +1,108 @@
+import React from "react";
+import { Check, Info } from "@geist-ui/react-icons";
+import { ProductCard, ProductCardProps } from "./ProductCard";
+import { Spacer, Text } from "@geist-ui/react";
+import styles from "./Card.module.css";
+
+export function Commercial(props: ProductCardProps): React.ReactElement {
+ return (
+
+
+ What you need to do:
+
+
+
+
+
+
+
+ Purchase servers yourself to self-host Reacher.{" "}
+
+ Read how
+
+ .
+
+
+
+
+ }
+ features={features}
+ footer={
+
+
+
+
+
+ Want a free trial before committing?
+ Feel free to try self-hosting with the{" "}
+
+ open-source guide
+
+ , and subscribe once you're ready.
+
+
+ }
+ header={
+
+ 🏠 Self-Hosted
+
+ }
+ subtitle={
+
+ Unlimited email verifications / mo
+
+ }
+ />
+ );
+}
+
+const features = [
+
+ Unlimited{" "}
+
+ full-featured
+ {" "}
+ email verifications.
+ ,
+
+ 💪 Bulk email verifications.{" "}
+
+ Read more.
+
+ ,
+ No data sent back to Reacher.,
+
+
+ Customer support via email/chat.
+ ,
+
+ See{" "}
+
+ full terms and details
+
+ .
+ ,
+];
diff --git a/src/components/ProductCard/FreeTrial.tsx b/src/components/ProductCard/FreeTrial.tsx
index 52bf7365..68272b82 100644
--- a/src/components/ProductCard/FreeTrial.tsx
+++ b/src/components/ProductCard/FreeTrial.tsx
@@ -26,40 +26,37 @@ export function FreeTrial({
{active ? "Current Plan" : "Not available"}
}
- features={[
- "50 email verifications per month.",
-
-
- Full-featured
- {" "}
- email verifications.
- ,
-
- Support via{" "}
-
- Github Issues
-
- .
- ,
- "No credit card required.",
- ]}
+ features={features}
header="Free Forever"
- subtitle={
-
- Use Reacher's servers with
- high IP reputation.
-
- }
+ subtitle={50 email verifications / mo}
title={productName()}
price={priceString}
/>
);
}
+
+const features = [
+ "Use Reacher's servers with high IP reputation.",
+
+
+ Full-featured
+ {" "}
+ email verifications.
+ ,
+
+ Support via{" "}
+
+ Github Issues
+
+ .
+ ,
+ "No credit card required.",
+];
diff --git a/src/components/ProductCard/ProductCard.tsx b/src/components/ProductCard/ProductCard.tsx
new file mode 100644
index 00000000..149009eb
--- /dev/null
+++ b/src/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,114 @@
+import { Button } from "@geist-ui/react";
+import { useRouter } from "next/router";
+import React, { useState } from "react";
+
+import { postData } from "@/util/helpers";
+import { sentryException } from "@/util/sentry";
+import { getStripe } from "@/util/stripeClient";
+import { COMMERCIAL_LICENSE_PRODUCT_ID } from "@/util/subs";
+import type {} from "@/util/supabaseClient";
+import { useUser } from "@/util/useUser";
+import { Card } from "./Card";
+import { Tables } from "@/supabase/database.types";
+import { ProductWithPrice } from "@/supabase/domain.types";
+
+export interface ProductCardProps {
+ currency: string;
+ product: ProductWithPrice;
+ subscription: Tables<"subscriptions"> | null;
+ extra?: React.ReactElement;
+ header?: React.ReactElement;
+ footer?: React.ReactElement;
+ features?: (string | React.ReactElement)[];
+ subtitle?: React.ReactElement;
+}
+
+export function ProductCard({
+ currency,
+ product,
+ subscription,
+ ...props
+}: ProductCardProps): React.ReactElement {
+ const router = useRouter();
+ const [priceIdLoading, setPriceIdLoading] = useState();
+ const { session, user } = useUser();
+
+ const active = !!subscription;
+ const price = product.prices.find(({ currency: c }) => currency === c);
+ if (!price || !price.unit_amount) {
+ return
Error: No price found for product {product.id}.
;
+ }
+
+ const handleCheckout = async (price: Tables<"prices">) => {
+ setPriceIdLoading(price.id);
+
+ if (!session) {
+ router.push("/signup").catch(sentryException);
+
+ return;
+ }
+
+ try {
+ const { sessionId } = await postData<{ sessionId: string }>({
+ url: "/api/stripe/create-checkout-session",
+ data: { price },
+ token: session.access_token,
+ });
+
+ const stripe = await getStripe();
+ if (!stripe) {
+ throw new Error("Empty stripe object at checkout");
+ }
+
+ await stripe.redirectToCheckout({ sessionId });
+ } catch (err) {
+ sentryException(err as Error);
+ alert((err as Error).message);
+ } finally {
+ setPriceIdLoading(false);
+ }
+ };
+
+ const priceString = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: price.currency || undefined,
+ minimumFractionDigits: 0,
+ }).format(price.unit_amount / 100);
+
+ return (
+ {
+ window.sa_event &&
+ window.sa_event(
+ `pricing:${
+ product.id === COMMERCIAL_LICENSE_PRODUCT_ID
+ ? "commercial"
+ : "saas"
+ }`
+ );
+ handleCheckout(price).catch(sentryException);
+ }}
+ type="success"
+ >
+ {priceIdLoading
+ ? session
+ ? "Redirecting to Stripe..."
+ : "Redirecting to sign up page..."
+ : active
+ ? "Current Plan"
+ : user
+ ? "Upgrade Plan"
+ : "Get Started"}
+
+ }
+ key={price.product_id}
+ price={priceString}
+ title={product.name || "No Product Name"} // Latter should never happen
+ {...props}
+ />
+ );
+}
diff --git a/src/components/ProductCard/SaaS100k.tsx b/src/components/ProductCard/SaaS100k.tsx
new file mode 100644
index 00000000..91dbaf39
--- /dev/null
+++ b/src/components/ProductCard/SaaS100k.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { Text } from "@geist-ui/react";
+import { ProductCard } from "./ProductCard";
+import type { ProductCardProps } from "./ProductCard";
+
+export function SaaS100k(props: ProductCardProps): React.ReactElement {
+ return (
+
+ ✨ New ✨
+
+ }
+ subtitle={100,000 email verifications / mo}
+ />
+ );
+}
+
+const features = [
+ "Use Reacher's servers with high IP reputation.",
+
+
+ Full-featured
+ {" "}
+ email verifications.
+ ,
+
+ Customer support via email/chat.
+ ,
+ "Cancel anytime.",
+];
diff --git a/src/components/ProductCard/SaaS10k.tsx b/src/components/ProductCard/SaaS10k.tsx
new file mode 100644
index 00000000..c10cce4c
--- /dev/null
+++ b/src/components/ProductCard/SaaS10k.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { Text } from "@geist-ui/react";
+import { ProductCard } from "./ProductCard";
+import type { ProductCardProps } from "./ProductCard";
+
+export function SaaS10k(props: ProductCardProps): React.ReactElement {
+ return (
+
+ For individuals
+
+ }
+ features={features}
+ subtitle={10,000 email verifications / mo}
+ />
+ );
+}
+
+const features = [
+ "Use Reacher's servers with high IP reputation.",
+
+
+ Full-featured
+ {" "}
+ email verifications.
+ ,
+
+ Customer support via email/chat.
+ ,
+ "Cancel anytime.",
+];
diff --git a/src/components/ProductCard/Sub.tsx b/src/components/ProductCard/Sub.tsx
deleted file mode 100644
index 970738d8..00000000
--- a/src/components/ProductCard/Sub.tsx
+++ /dev/null
@@ -1,232 +0,0 @@
-import { Button, Text } from "@geist-ui/react";
-import { Info } from "@geist-ui/react-icons";
-import { useRouter } from "next/router";
-import React, { useState } from "react";
-
-import { postData } from "@/util/helpers";
-import { sentryException } from "@/util/sentry";
-import { getStripe } from "@/util/stripeClient";
-import { COMMERCIAL_LICENSE_PRODUCT_ID } from "@/util/subs";
-import type {} from "@/util/supabaseClient";
-import { useUser } from "@/util/useUser";
-import { Card } from "./Card";
-import styles from "./Card.module.css";
-import { Tables } from "@/supabase/database.types";
-import { ProductWithPrice } from "@/supabase/domain.types";
-
-export interface ProductCardProps {
- currency: string;
- product: ProductWithPrice;
- subscription: Tables<"subscriptions"> | null;
-}
-
-export function ProductCard({
- currency,
- product,
- subscription,
-}: ProductCardProps): React.ReactElement {
- const router = useRouter();
- const [priceIdLoading, setPriceIdLoading] = useState();
- const { session, user } = useUser();
-
- const active = !!subscription;
- const price = product.prices.find(({ currency: c }) => currency === c);
- if (!price || !price.unit_amount) {
- return
-
- Want a free trial before
- committing? Feel free to try self-hosting with the{" "}
-
- open-source guide
-
- , and subscribe once you're ready.
-
-
- ) : undefined
- }
- header={
- product.id === COMMERCIAL_LICENSE_PRODUCT_ID ? (
-
- For self-hosting
-
- ) : (
-
- Most popular
-
- )
- }
- key={price.product_id}
- price={priceString}
- subtitle={
- product.id === COMMERCIAL_LICENSE_PRODUCT_ID ? (
-
- Self-host Reacher
- with your own infrastructure.
-
- ) : (
-
- Use Reacher's servers with
- high IP reputation.
-
- )
- }
- title={product.name || "No Product Name"} // Latter should never happen
- />
- );
-}
-
-function saasFeatures(): (string | React.ReactElement)[] {
- return [
- "10000 email verifications per month.",
-
-
- Full-featured
- {" "}
- email verifications.
- ,
- "Customer support via email/chat.",
- "Cancel anytime.",
- ];
-}
-
-function licenseFeatures(): (string | React.ReactElement)[] {
- return [
-
- Unlimited email verifications per month.
- ,
-
- 💪 Bulk email verification.{" "}
-
- Read more.
-
- ,
-
- Self-host in your commercial apps. No data sent
- back to Reacher.
- ,
-
-
- Full-featured
- {" "}
- email verifications.
- ,
- "Customer support via email/chat.",
-
- Comes with{" "}
-
- self-host guides
- {" "}
- (Heroku, Docker).
- ,
-
- See{" "}
-
- full terms and details
-
- .
- ,
- ];
-}
diff --git a/src/components/ProductCard/index.ts b/src/components/ProductCard/index.ts
index 58e93393..d5df76bd 100644
--- a/src/components/ProductCard/index.ts
+++ b/src/components/ProductCard/index.ts
@@ -1,2 +1,4 @@
+export * from "./Commercial";
export * from "./FreeTrial";
-export * from "./Sub";
+export * from "./SaaS10k";
+export * from "./SaaS100k";
diff --git a/src/components/SubGetStarted/GetStartedLicense.tsx b/src/components/SubGetStarted/GetStartedLicense.tsx
index d31285d8..4a1b19a0 100644
--- a/src/components/SubGetStarted/GetStartedLicense.tsx
+++ b/src/components/SubGetStarted/GetStartedLicense.tsx
@@ -4,7 +4,7 @@ import React from "react";
export function GetStartedLicense(): React.ReactElement {
return (
- How to get started with email verifications?
+ How to get started with the Commercial License?
To get started with self-hosting, please refer to our{" "}
diff --git a/src/components/SubGetStarted/GetStartedSaas.tsx b/src/components/SubGetStarted/GetStartedSaas.tsx
index c8b46251..ca2dc4bc 100644
--- a/src/components/SubGetStarted/GetStartedSaas.tsx
+++ b/src/components/SubGetStarted/GetStartedSaas.tsx
@@ -103,7 +103,10 @@ export function GetStartedSaas(): React.ReactElement {
for using the API, with a program called Postman. If you still
have questions, just use the chat widget on the bottom right
corner to send me a message, or shoot me an email at{" "}
- amaury@reacher.email.
+
+ 📧 amaury@reacher.email
+
+ .
);
diff --git a/src/pages/pricing.tsx b/src/pages/pricing.tsx
index 9bda9f69..705c6e40 100644
--- a/src/pages/pricing.tsx
+++ b/src/pages/pricing.tsx
@@ -1,15 +1,18 @@
-import { Grid, Select, Spacer, Text } from "@geist-ui/react";
+import { Collapse, Grid, Page, Select, Spacer, Text } from "@geist-ui/react";
import { GetStaticProps } from "next";
import React, { useState } from "react";
-import { FreeTrial, Nav, ProductCard } from "../components";
+import { Nav } from "../components/Nav";
+import { SaaS10k, SaaS100k, Commercial } from "../components/ProductCard";
import {
COMMERCIAL_LICENSE_PRODUCT_ID,
+ SAAS_100K_PRODUCT_ID,
SAAS_10K_PRODUCT_ID,
} from "@/util/subs";
import { getActiveProductWithPrices } from "@/util/supabaseClient";
import { useUser } from "@/util/useUser";
import { ProductWithPrice } from "@/supabase/domain.types";
+import Link from "next/link";
export const getStaticProps: GetStaticProps = async () => {
const products = await getActiveProductWithPrices();
@@ -34,12 +37,17 @@ export default function Pricing({
subscriptionCurrency || "eur"
);
- const saasProduct = products.find(({ id }) => id === SAAS_10K_PRODUCT_ID);
- const licenseProduct = products.find(
+ const saas10kProduct = products.find(
+ ({ id }) => id === SAAS_10K_PRODUCT_ID
+ );
+ const saas100kProduct = products.find(
+ ({ id }) => id === SAAS_100K_PRODUCT_ID
+ );
+ const commercialProduct = products.find(
({ id }) => id === COMMERCIAL_LICENSE_PRODUCT_ID
);
- if (!saasProduct || !licenseProduct) {
- throw new Error("Pricing: saasProduct or licenseProduct not found.");
+ if (!saas10kProduct || !saas100kProduct || !commercialProduct) {
+ throw new Error("Pricing: saasProduct or commercialProduct not found.");
}
return (
@@ -66,24 +74,33 @@ export default function Pricing({
-
+
-
-
+
+
+
+
+ Frequently Asked Questions
+
+
+
+
+ Reacher currently does not offer a
+ SaaS plan for verifying 1 million or more emails. If
+ you need to verify 1 million or more emails, please
+ check the Commercial License plan. You will need to
+ purchase your own servers and self-host Reacher.
+
+
+ Yes. Simple{" "}
+ create an account and
+ you can use the textbox to verify your email.
+
+
+ Yes. Follow these steps in the{" "}
+
+ self-host guide
+
+ .
+
+
+ Send me an email to{" "}
+
+ 📧 amaury@reacher.email
+
+ , I reply pretty fast.
+
+
+
>
);
diff --git a/src/pages/signup.tsx b/src/pages/signup.tsx
index 95cf7ffe..0a72a40a 100644
--- a/src/pages/signup.tsx
+++ b/src/pages/signup.tsx
@@ -117,6 +117,11 @@ export default function SignUp(): React.ReactElement {
return (
+
+
+ 50 free email verifications when you sign up.
+
+