Skip to content

Commit

Permalink
Feature: Remove resend, stripe, cron env variable dependency (Codehag…
Browse files Browse the repository at this point in the history
  • Loading branch information
ousszizou authored Mar 21, 2024
1 parent aaab29a commit 2d27ab3
Show file tree
Hide file tree
Showing 25 changed files with 305 additions and 242 deletions.
6 changes: 2 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
# -----------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXTJS_URL=http://localhost:3000
CRON_SECRET=csec_

# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_"
Expand All @@ -22,11 +21,10 @@ DATABASE_HOST=eu-west.connect.psdb.
DATABASE_NAME=YOUR_DB_NAME

# -----------------------------------------------------------------------------
# Email (Resend)
# Stripe
# -----------------------------------------------------------------------------
RESEND_API_KEY=re_

# Stripe
USE_STRIPE=false
STRIPE_API_KEY="sk_test_"
STRIPE_WEBHOOK_SECRET="whsec_"
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID="prod_"
Expand Down
12 changes: 1 addition & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ cp .env.example .env.local

1. Create [Clerk](https://clerk.com) Account
2. Create [Planet Scale](https://planetscale.com/) Account
3. Create [Resend](https://resend.com) Account
4. Create [Stripe](https://stripe.com) Account and download [Stripe CLI](https://docs.stripe.com/stripe-cli)
5. Secure [CRON](https://dev.to/chrisnowicki/how-to-secure-vercel-cron-job-routes-in-nextjs-13-9g8) jobs
3. Create [Stripe](https://stripe.com) Account and download [Stripe CLI](https://docs.stripe.com/stripe-cli)

5. Start the development server from either yarn or turbo:

Expand Down Expand Up @@ -121,14 +119,6 @@ You can also use `docker-compose` to have a Mysql database locally, instead of r
2. run `docker-compose --env-file .env.local up` to start the DB.
3. run `pnpm run db:push`.

## Email provider

This project uses [Resend](https://resend.com/) to handle transactional emails. You need to add create an account and get an api key needed for authentication.

Please be aware that the Resend is designed to send test emails exclusively to the email address registered with the account, or to `delivered@resend.dev`, where they are logged on their dashboard.

The default setting for `TEST_EMAIL_ADDRESS` is `delivered@resend.dev` but you have the option to change it to the email address that is associated with your Resend account.

## Roadmap

- [x] ~Initial setup~
Expand Down
16 changes: 7 additions & 9 deletions apps/www/actions/generate-user-stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
import { api } from "@/trpc/server";
import { currentUser } from "@clerk/nextjs";

import { stripe } from "@projectx/stripe";
import { withStripe } from "@projectx/stripe";

import { absoluteUrl } from "@/lib/utils";

Expand All @@ -18,10 +18,10 @@ const billingUrl = absoluteUrl("/pricing");

export async function generateUserStripe(
priceId: string,
): Promise<responseAction> {
let redirectUrl: string = "";
): Promise<responseAction | null> {
return withStripe<responseAction>(async (stripe) => {
let redirectUrl = "";

try {
const user = await currentUser();

if (!user || !user.emailAddresses) {
Expand Down Expand Up @@ -60,10 +60,8 @@ export async function generateUserStripe(

redirectUrl = stripeSession.url as string;
}
} catch (error) {
throw new Error("Failed to generate user stripe session");
}

// no revalidatePath because redirect
redirect(redirectUrl);
// no revalidatePath because redirect
redirect(redirectUrl);
});
}
97 changes: 53 additions & 44 deletions apps/www/app/(dashboard)/_components/workspace-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useOrganization, useOrganizationList, useUser } from "@clerk/nextjs";
import { toDecimal } from "dinero.js";
import { Check, ChevronDown, ChevronsUpDown, PlusCircle } from "lucide-react";

import { env } from "@projectx/stripe/env";
import type { ExtendedPlanInfo, PlansResponse } from "@projectx/stripe/plans";
import type { PurchaseOrg } from "@projectx/validators";
import { purchaseOrgSchema } from "@projectx/validators";

Expand Down Expand Up @@ -244,7 +246,12 @@ export function WorkspaceSwitcher({ isCollapsed }: WorkspaceSwitcherProps) {
}

function NewOrganizationDialog(props: { closeDialog: () => void }) {
const plans = React.use(api.stripe.plans.query());
const useStripe = env.USE_STRIPE === "true";

let plans: any | null = null;
if (useStripe) {
plans = api.stripe.plans.query();
}

const form = useZodForm({ schema: purchaseOrgSchema });

Expand All @@ -255,7 +262,7 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
.mutate(data)
.catch(() => ({ success: false as const }));

if (response.success) window.location.href = response.url;
if (response?.success) window.location.href = response.url as string;
else
toaster.toast({
title: "Error",
Expand Down Expand Up @@ -293,49 +300,51 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
)}
/>

<FormField
control={form.control}
name="planId"
render={({ field }) => (
<FormItem>
<div className="flex justify-between">
<FormLabel>Subscription plan *</FormLabel>
<Link
href="/pricing"
className="text-xs text-muted-foreground hover:underline"
{useStripe && (
<FormField
control={form.control}
name="planId"
render={({ field }) => (
<FormItem>
<div className="flex justify-between">
<FormLabel>Subscription plan *</FormLabel>
<Link
href="/pricing"
className="text-xs text-muted-foreground hover:underline"
>
What&apos;s included in each plan?
</Link>
</div>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
What&apos;s included in each plan?
</Link>
</div>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
</FormControl>
<SelectContent>
{plans.map((plan) => (
<SelectItem key={plan.priceId} value={plan.priceId}>
<span className="font-medium">{plan.name}</span> -{" "}
<span className="text-muted-foreground">
{toDecimal(
plan.price,
({ value, currency }) =>
`${currencySymbol(currency.code)}${value}`,
)}{" "}
per month
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
</FormControl>
<SelectContent>
{plans?.map((plan: ExtendedPlanInfo) => (
<SelectItem key={plan.priceId} value={plan.priceId}>
<span className="font-medium">{plan.name}</span> -{" "}
<span className="text-muted-foreground">
{toDecimal(
plan.price,
({ value, currency }) =>
`${currencySymbol(currency.code)}${value}`,
)}{" "}
per month
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}

<DialogFooter>
<Button variant="outline" onClick={() => props.closeDialog()}>
Expand Down
21 changes: 18 additions & 3 deletions apps/www/app/(marketing)/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { api } from "@/trpc/server";
import { currentUser } from "@clerk/nextjs";
import { User } from "@clerk/nextjs/server";

import { env } from "@projectx/stripe/env";

import { PricingCards } from "@/components/pricing-cards";
import { PricingFaq } from "@/components/pricing-faq";
Expand All @@ -11,12 +14,24 @@ export const metadata = {
};

export default async function PricingPage() {
const user = await currentUser();
const subscriptionPlan = await api.auth.mySubscription.query();
const useStripe = env.USE_STRIPE === "true";

let user: User | null = null;
let subscriptionPlan: any = null;

if (useStripe) {
user = await currentUser();
subscriptionPlan = await api.auth.mySubscription.query();
}

// const user = await currentUser();
// const subscriptionPlan = await api.auth.mySubscription.query();

return (
<div className="flex w-full flex-col gap-16 py-8 md:py-8">
<PricingCards userId={user?.id} subscriptionPlan={subscriptionPlan} />
{useStripe && (
<PricingCards userId={user?.id} subscriptionPlan={subscriptionPlan} />
)}
<hr className="container" />
<PricingFaq />
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/www/app/api/cron/update-bank-account-data/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export async function POST(req: NextRequest) {
.at(1);

// if not found OR the bearer token does NOT equal the CRON_SECRET
if (!authToken || authToken !== env.CRON_SECRET) {
// TODO: Later we'll add the 2nd part of condition authToken !== env.CRON_SECRET
if (!authToken) {
return NextResponse.json(
{ error: "Unauthorized" },
{
Expand Down
3 changes: 2 additions & 1 deletion apps/www/app/api/cron/update-integrations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export async function POST(req: NextRequest) {
.at(1);

// if not found OR the bearer token does NOT equal the CRON_SECRET
if (!authToken || authToken !== env.CRON_SECRET) {
// TODO: Later we'll add the 2nd part of condition authToken !== env.CRON_SECRET
if (!authToken) {
return NextResponse.json(
{ error: "Unauthorized" },
{
Expand Down
14 changes: 5 additions & 9 deletions apps/www/app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

import { handleEvent, stripe } from "@projectx/stripe";
import { handleEvent, withStripe } from "@projectx/stripe";

import { env } from "@/env";

export async function POST(req: NextRequest) {
const payload = await req.text();
const signature = req.headers.get("Stripe-Signature")!;
const signature = req.headers.get("Stripe-Signature") as string;

try {
return withStripe(async (stripe) => {
const event = stripe.webhooks.constructEvent(
payload,
signature,
env.STRIPE_WEBHOOK_SECRET,
env.STRIPE_WEBHOOK_SECRET as string,
);

await handleEvent(event);

console.log("✅ Handled Stripe Event", event.type);
return NextResponse.json({ received: true }, { status: 200 });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.log(`❌ Error when handling Stripe Event: ${message}`);
return NextResponse.json({ error: message }, { status: 400 });
}
});
}
2 changes: 1 addition & 1 deletion apps/www/config/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SubscriptionPlan } from "@/types";
import type { SubscriptionPlan } from "@/types";

import { env } from "@/env";

Expand Down
14 changes: 6 additions & 8 deletions apps/www/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,22 @@ export const env = createEnv({
* This way you can ensure the app isn't built with invalid env vars.
*/
server: {
CRON_SECRET: z.string().min(1),
DATABASE_HOST: z.string().min(1),
DATABASE_USERNAME: z.string().min(1),
DATABASE_PASSWORD: z.string().min(1),
RESEND_API_KEY: z.string().min(1),
STRIPE_API_KEY: z.string().min(1),
STRIPE_WEBHOOK_SECRET: z.string().min(1),
STRIPE_API_KEY: z.string().min(1).optional(),
STRIPE_WEBHOOK_SECRET: z.string().min(1).optional(),
},
/**
* Specify your client-side environment variables schema here.
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
NEXT_PUBLIC_APP_URL: z.string().min(1),
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID: z.string().min(1),
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID: z.string().min(1),
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string().min(1),
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string().min(1),
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID: z.string().min(1).optional(),
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID: z.string().min(1).optional(),
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID: z.string().min(1).optional(),
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID: z.string().min(1).optional(),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
Expand Down
1 change: 0 additions & 1 deletion apps/www/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,3 @@ export const formatNumberWithSpaces = (value: number | string) => {
}
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
};

6 changes: 3 additions & 3 deletions apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"@clerk/themes": "^1.7.9",
"@dinero.js/currencies": "2.0.0-alpha.14",
"@hookform/resolvers": "^3.3.2",
"@next/mdx": "^14.1.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "^14.1.0",
"@projectx/api": "workspace:^0.1.0",
"@projectx/connector-core": "workspace:^0.1.0",
"@projectx/connector-gocardless": "workspace:^0.1.0",
Expand Down Expand Up @@ -85,9 +85,9 @@
"gray-matter": "^4.0.3",
"jotai": "^2.6.1",
"lucide-react": "^0.354.0",
"next-mdx-remote": "^4.4.1",
"ms": "^2.1.3",
"next": "^14.1.0",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"nodemailer": "^6.9.8",
"openai": "^4.16.1",
Expand Down Expand Up @@ -116,8 +116,8 @@
"@projectx/tailwind-config": "workspace:^0.1.0",
"@projectx/tsconfig": "workspace:^0.1.0",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.8.9",
"@types/mdx": "^2.0.11",
"@types/node": "^20.8.9",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.14",
"eslint": "^8.57.0",
Expand Down
6 changes: 2 additions & 4 deletions apps/www/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { Icon } from "lucide-react";

import { Icons } from "@/components/shared/icons";
import type { Icons } from "@/components/shared/icons";

export type SidebarNavItem = {
title: string;
Expand Down Expand Up @@ -48,6 +46,6 @@ export type SubscriptionPlan = {
monthly: number;
};
stripeIds: {
monthly: string | null;
monthly: string | null | undefined;
};
};
Loading

0 comments on commit 2d27ab3

Please sign in to comment.