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
14 changes: 0 additions & 14 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 @@ -21,19 +20,6 @@ DATABASE_PASSWORD=pscale_pw_
DATABASE_HOST=eu-west.connect.psdb.
DATABASE_NAME=YOUR_DB_NAME

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

# Stripe
STRIPE_API_KEY="sk_test_"
STRIPE_WEBHOOK_SECRET="whsec_"
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID="prod_"
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID="price_"
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID="prod_"
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID="price_"

# -----------------------------------------------------------------------------
# OpenBanking
# -----------------------------------------------------------------------------
Expand Down
26 changes: 0 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ 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

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

Expand All @@ -90,21 +87,6 @@ cp .env.example .env.local
pnpm run dev:web
```

## Stripe

To set up Stripe locally with environment variables:

1. Create a [Stripe](https://stripe.com/in) account.
2. After signing in, go to the dashboard and switch to Test mode.
3. In the dashboard, switch to the API keys section.
4. Reveal your secret key and paste it into your `.env.local` file.
5. For the webhook key, switch to the Webhooks tab, add an endpoint to reveal the secret key.
6. To get the `PRODUCT_ID` and `PRICE_ID`, head over to [Stripe's API Docs](https://docs.stripe.com/api/prices/object).
7. From the docs, use the API with your `STRIPE_API_KEY` to create a product & price object.
8. The response object from the API contains two keys: `id` and `product`.
9. Use the `id` as your `PRICE_ID` and `product` as your `PRODUCT_ID`.
10. You can use the same keys for the STD and PRO variables.

## Database

This project uses MySQL database on PlanetScale. To setup a DB for your local dev:
Expand All @@ -121,14 +103,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
6 changes: 3 additions & 3 deletions apps/www/app/(dashboard)/_components/workspace-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function WorkspaceSwitcher({ isCollapsed }: WorkspaceSwitcherProps) {
}

function NewOrganizationDialog(props: { closeDialog: () => void }) {
const plans = React.use(api.stripe.plans.query());
// const plans = React.use(api.stripe.plans.query());

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

Expand Down Expand Up @@ -293,7 +293,7 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
)}
/>

<FormField
{/* <FormField
control={form.control}
name="planId"
render={({ field }) => (
Expand Down Expand Up @@ -335,7 +335,7 @@ function NewOrganizationDialog(props: { closeDialog: () => void }) {
<FormMessage />
</FormItem>
)}
/>
/> */}

<DialogFooter>
<Button variant="outline" onClick={() => props.closeDialog()}>
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
4 changes: 1 addition & 3 deletions apps/www/app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type { NextRequest } from "next/server";

import { handleEvent, stripe } 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")!;
Expand All @@ -13,7 +11,7 @@ export async function POST(req: NextRequest) {
const event = stripe.webhooks.constructEvent(
payload,
signature,
env.STRIPE_WEBHOOK_SECRET,
"", // INFO: later this should be the env variable env.STRIPE_WEBHOOK_SECRET
);

await handleEvent(event);
Expand Down
8 changes: 1 addition & 7 deletions apps/www/components/pricing-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,7 @@ export function PricingCards({ userId, subscriptionPlan }: PricingCardsProps) {
>
Go to dashboard
</Link>
) : (
<BillingFormButton
year={isYearly}
offer={offer}
subscriptionPlan={subscriptionPlan}
/>
)
) : null
) : (
<Link href="/signin">
<Button className="relative rounded-lg">Sign In</Button>
Expand Down
8 changes: 3 additions & 5 deletions apps/www/config/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { SubscriptionPlan } from "@/types";

import { env } from "@/env";
import type { SubscriptionPlan } from "@/types";

export const pricingData: SubscriptionPlan[] = [
{
Expand Down Expand Up @@ -42,7 +40,7 @@ export const pricingData: SubscriptionPlan[] = [
monthly: 10,
},
stripeIds: {
monthly: env.NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID,
monthly: "", // INFO: Later this will be env.NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID
},
},
{
Expand All @@ -60,7 +58,7 @@ export const pricingData: SubscriptionPlan[] = [
monthly: 20,
},
stripeIds: {
monthly: env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
monthly: "", // INFO: Later this will be env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID
},
},
];
16 changes: 0 additions & 16 deletions apps/www/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,16 @@ 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),
},
/**
* 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),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
Expand All @@ -39,14 +31,6 @@ export const env = createEnv({
VERCEL_ENV: process.env.VERCEL_ENV,
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID:
process.env.NEXT_PUBLIC_STRIPE_STD_PRODUCT_ID,
NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID:
process.env.NEXT_PUBLIC_STRIPE_STD_MONTHLY_PRICE_ID,
NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID:
process.env.NEXT_PUBLIC_STRIPE_PRO_PRODUCT_ID,
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID:
process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PRICE_ID,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
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, " ");
};

1 change: 0 additions & 1 deletion apps/www/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const config = {
"@projectx/api",
"@projectx/db",
"@projectx/openbanking",
"@projectx/stripe",
"@projectx/transactional",
"@projectx/validators",
],
Expand Down
7 changes: 3 additions & 4 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 All @@ -103,7 +103,6 @@
"react-textarea-autosize": "^8.5.3",
"react-wrap-balancer": "^1.1.0",
"recharts": "^2.10.3",
"stripe": "^14.15.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -116,8 +115,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
4 changes: 1 addition & 3 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
89 changes: 49 additions & 40 deletions packages/api/src/router/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,48 @@ export const stripeRouter = createTRPCRouter({
.from(schema.customer)
.where(eq(schema.customer.clerkUserId, userId));

const returnUrl = env.NEXTJS_URL + "/dashboard";
const returnUrl = `${env.NEXTJS_URL}/dashboard`;

if (customer?.[0] && customer[0].plan !== "FREE") {
/**
* User is subscribed, create a billing portal session
*/
const session = await stripe.billingPortal.sessions.create({
customer: customer[0].stripeId,
return_url: returnUrl,
});
return { success: true as const, url: session.url };
// const session = await stripe.billingPortal.sessions.create({
// customer: customer[0].stripeId,
// return_url: returnUrl,
// });
return {
success: true as const,
url: returnUrl, // INFO: Later we'll return the session.url
};
}

/**
* User is not subscribed, create a checkout session
* Use existing email address if available
*/

const user = await currentUser();
const email = user?.emailAddresses.find(
(addr) => addr.id === user?.primaryEmailAddressId,
)?.emailAddress;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
customer_email: email,
client_reference_id: userId,
subscription_data: { metadata: { userId } },
cancel_url: returnUrl,
success_url: returnUrl,
line_items: [{ price: PLANS.PRO?.priceId, quantity: 1 }],
});

if (!session.url) return { success: false as const };
return { success: true as const, url: session.url };
// const user = await currentUser();
// const email = user?.emailAddresses.find(
// (addr) => addr.id === user?.primaryEmailAddressId,
// )?.emailAddress;

// const session = await stripe.checkout.sessions.create({
// mode: "subscription",
// payment_method_types: ["card"],
// customer_email: email,
// client_reference_id: userId,
// subscription_data: { metadata: { userId } },
// cancel_url: returnUrl,
// success_url: returnUrl,
// line_items: [{ price: PLANS.PRO?.priceId, quantity: 1 }],
// });

// if (!session.url) return { success: false as const };
return {
success: true as const,
url: returnUrl, // INFO: Later we'll return the session.url
};
}),

plans: publicProcedure.query(async () => {
Expand Down Expand Up @@ -100,24 +106,27 @@ export const stripeRouter = createTRPCRouter({
purchaseOrg: protectedProcedure
.input(purchaseOrgSchema)
.mutation(async (opts) => {
const { userId } = opts.ctx.auth;
const { orgName, planId } = opts.input;
// const { userId } = opts.ctx.auth;
// const { orgName } = opts.input;

const baseUrl = new URL(opts.ctx.req?.nextUrl ?? env.NEXTJS_URL).origin;

const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
client_reference_id: userId,
subscription_data: {
metadata: { userId, organizationName: orgName },
},
success_url: baseUrl + "/onboarding",
cancel_url: baseUrl,
line_items: [{ price: planId, quantity: 1 }],
});

if (!session.url) return { success: false as const };
return { success: true as const, url: session.url };
// const session = await stripe.checkout.sessions.create({
// mode: "subscription",
// payment_method_types: ["card"],
// client_reference_id: userId,
// subscription_data: {
// metadata: { userId, organizationName: orgName },
// },
// success_url: `${baseUrl}/onboarding`,
// cancel_url: baseUrl,
// line_items: [{ price: planId, quantity: 1 }],
// });

// if (!session.url) return { success: false as const };
return {
success: true as const,
url: `${baseUrl}/onboarding`, // INFO: Later we'll return the session.url
};
}),
});
Loading
Loading