Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Remove resend, stripe, cron env variable dependency #217

Merged
merged 10 commits into from
Mar 21, 2024
Merged
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 `[email protected]`, where they are logged on their dashboard.

The default setting for `TEST_EMAIL_ADDRESS` is `[email protected]` 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
Loading