Skip to content

Commit

Permalink
add pro plan
Browse files Browse the repository at this point in the history
  • Loading branch information
xvvvyz committed Jul 31, 2024
1 parent f356a7f commit f6421d6
Show file tree
Hide file tree
Showing 19 changed files with 270 additions and 104 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ on:
branches-ignore:
- main
env:
LEMON_SQUEEZY_API_KEY: ${{ secrets.LEMON_SQUEEZY_API_KEY }}
LEMON_SQUEEZY_STORE_ID: ${{ secrets.LEMON_SQUEEZY_STORE_ID }}
LEMON_SQUEEZY_VARIANT_ID: ${{ secrets.LEMON_SQUEEZY_VARIANT_ID }}
LEMON_SQUEEZY_WEBHOOK_SECRET: ${{ secrets.LEMON_SQUEEZY_WEBHOOK_SECRET }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
NEXT_PUBLIC_SUPABASE_PRO: ${{ secrets.NEXT_PUBLIC_SUPABASE_PRO }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD_INLINE: ${{ secrets.SUPABASE_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ on:
branches:
- main
env:
LEMON_SQUEEZY_API_KEY: ${{ secrets.LEMON_SQUEEZY_API_KEY }}
LEMON_SQUEEZY_VARIANT_ID: ${{ secrets.LEMON_SQUEEZY_VARIANT_ID }}
LEMON_SQUEEZY_STORE_ID: ${{ secrets.LEMON_SQUEEZY_STORE_ID }}
LEMON_SQUEEZY_WEBHOOK_SECRET: ${{ secrets.LEMON_SQUEEZY_WEBHOOK_SECRET }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
NEXT_PUBLIC_SUPABASE_PRO: ${{ secrets.NEXT_PUBLIC_SUPABASE_PRO }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_DB_PASSWORD_INLINE: ${{ secrets.SUPABASE_DB_PASSWORD }}
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
Expand Down
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@ bun db:start # outputs supabase url & key
Add the following to your `.env` file:

```dotenv
# required
NEXT_PUBLIC_SUPABASE_ANON_KEY=<SUPABASE_ANON_KEY>
NEXT_PUBLIC_SUPABASE_URL=<SUPABSE_API_URL>
# optional
NEXT_PUBLIC_SUPABASE_PRO=1
```

Generate types and start the dev server:
Expand All @@ -40,16 +36,40 @@ bun db:diff -- -f migration-description

## Production Notes

- Update next.config.js remotePatterns
Vercel common secrets:

- LEMON_SQUEEZY_STORE_ID

Vercel environment secrets:

- LEMON_SQUEEZY_API_KEY
- LEMON_SQUEEZY_VARIANT_ID
- LEMON_SQUEEZY_WEBHOOK_SECRET
- SUPABASE_SERVICE_KEY

GitHub repo secrets:

- SUPABASE_ACCESS_TOKEN
- VERCEL_ORG_ID
- VERCEL_PROJECT_ID
- VERCEL_TOKEN

GitHub environment secrets:

- NEXT_PUBLIC_SUPABASE_ANON_KEY
- NEXT_PUBLIC_SUPABASE_URL
- SUPABASE_DB_PASSWORD
- SUPABASE_PROJECT_ID

Supabase settings:

- Add custom SMTP server
- Update auth providers
- Update email templates
- Update url config
- Add custom SMTP server
- Enable realtime (notifications)
- Enable realtime (notifications table)
- Remove GraphQL api
- Github environment secrets:
- NEXT_PUBLIC_SUPABASE_ANON_KEY
- NEXT_PUBLIC_SUPABASE_PRO
- NEXT_PUBLIC_SUPABASE_URL
- SUPABASE_DB_PASSWORD
- SUPABASE_PROJECT_ID

Other settings:

- Update next.config.js remotePatterns
6 changes: 4 additions & 2 deletions app/(pages)/(with-nav)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IconButton from '@/_components/icon-button';
import Subscriptions from '@/_components/subscriptions';
import countNotifications from '@/_queries/count-notifications';
import getCurrentUser from '@/_queries/get-current-user';
import getCustomer from '@/_queries/get-customer';
import BellIcon from '@heroicons/react/24/outline/BellIcon';
import { ReactNode } from 'react';

Expand All @@ -12,9 +13,10 @@ interface LayoutProps {
}

const Layout = async ({ children }: LayoutProps) => {
const [{ count }, user] = await Promise.all([
const [{ count }, user, { data: customer }] = await Promise.all([
countNotifications(),
getCurrentUser(),
getCustomer(),
]);

if (!user) return null;
Expand Down Expand Up @@ -63,7 +65,7 @@ const Layout = async ({ children }: LayoutProps) => {
}
scroll={false}
/>
<AccountMenu user={user} />
<AccountMenu customer={customer} user={user} />
</div>
</nav>
)}
Expand Down
49 changes: 44 additions & 5 deletions app/_components/account-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@

import Avatar from '@/_components/avatar';
import DropdownMenu from '@/_components/dropdown-menu';
import createCustomerCheckout from '@/_mutations/create-customer-checkout';
import signOut from '@/_mutations/sign-out';
import { GetCustomerData } from '@/_queries/get-customer';
import getCustomerBillingPortal from '@/_queries/get-customer-billing-portal';
import ArrowLeftStartOnRectangleIcon from '@heroicons/react/24/outline/ArrowLeftStartOnRectangleIcon';
import ArrowUpCircleIcon from '@heroicons/react/24/outline/ArrowUpCircleIcon';
import Bars3Icon from '@heroicons/react/24/outline/Bars3Icon';
import Cog6ToothIcon from '@heroicons/react/24/outline/Cog6ToothIcon';
import CreditCardIcon from '@heroicons/react/24/outline/CreditCardIcon';
import { User } from '@supabase/supabase-js';
import { useTransition } from 'react';
import { useState, useTransition } from 'react';

interface AccountMenuProps {
customer: GetCustomerData;
user: User | null;
}

const AccountMenu = ({ user }: AccountMenuProps) => {
const [isTransitioning, startTransition] = useTransition();
const AccountMenu = ({ customer, user }: AccountMenuProps) => {
const [isSignOutTransitioning, startSignOutTransition] = useTransition();

const [isBillingRedirectLoading, setIsBillingRedirectLoading] =
useState(false);

const isSubscribed = customer?.subscription_status === 'active';

return (
<DropdownMenu
Expand All @@ -35,11 +46,39 @@ const AccountMenu = ({ user }: AccountMenuProps) => {
Account settings
</DropdownMenu.Button>
<DropdownMenu.Button
loading={isTransitioning}
loading={isBillingRedirectLoading}
loadingText="Redirecting…"
onClick={async (e) => {
e.preventDefault();
setIsBillingRedirectLoading(true);

const { url } = await (isSubscribed
? getCustomerBillingPortal()
: createCustomerCheckout());

if (url) location.href = url;
else setIsBillingRedirectLoading(false);
}}
>
{isSubscribed ? (
<>
<CreditCardIcon className="w-5 text-fg-4" />
Manage subscription
</>
) : (
<>
<ArrowUpCircleIcon className="w-5 text-fg-4" />
Upgrade to pro
</>
)}
</DropdownMenu.Button>
<DropdownMenu.Separator />
<DropdownMenu.Button
loading={isSignOutTransitioning}
loadingText="Signing out…"
onClick={(e) => {
e.preventDefault();
startTransition(signOut);
startSignOutTransition(signOut);
}}
>
<ArrowLeftStartOnRectangleIcon className="w-5 text-fg-4" />
Expand Down
2 changes: 0 additions & 2 deletions app/_components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import formatImageUrl from '@/_utilities/format-image-url';
import generateImageLoader from '@/_utilities/generate-image-loader';
import Image from 'next/image';
import { twMerge } from 'tailwind-merge';

Expand Down Expand Up @@ -34,7 +33,6 @@ const Avatar = ({ className, file, id = '', size = 'md' }: AvatarProps) => {
alt=""
className="object-cover object-center"
fill
loader={generateImageLoader({ aspectRatio: '1:1' })}
sizes={sizes[size].imgSizes}
src={
src ??
Expand Down
5 changes: 1 addition & 4 deletions app/_components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(
{loading || (type === 'submit' && pending) ? (
<>
{variant !== 'link' && (
<Spinner
className="-ml-0.5"
color={spinnerColorSchemes[colorScheme]}
/>
<Spinner color={spinnerColorSchemes[colorScheme]} />
)}
{loadingText ?? children}
</>
Expand Down
30 changes: 30 additions & 0 deletions app/_mutations/create-customer-checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use server';

import getCurrentUser from '@/_queries/get-current-user';

import {
createCheckout,
lemonSqueezySetup,
} from '@lemonsqueezy/lemonsqueezy.js';

const createCustomerCheckout = async () => {
const user = await getCurrentUser();
if (!user) return { url: null };
lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY! });

const res = await createCheckout(
process.env.LEMON_SQUEEZY_STORE_ID!,
process.env.LEMON_SQUEEZY_VARIANT_ID!,
{
checkoutData: {
custom: { user_id: user.id },
email: user.email,
name: `${user.user_metadata.first_name} ${user.user_metadata.last_name}`,
},
},
);

return { url: res.data?.data.attributes.url ?? null };
};

export default createCustomerCheckout;
18 changes: 18 additions & 0 deletions app/_queries/get-customer-billing-portal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use server';

import getCustomer from '@/_queries/get-customer';

import {
getCustomer as lemonSqueezyGetCustomer,
lemonSqueezySetup,
} from '@lemonsqueezy/lemonsqueezy.js';

const getCustomerBillingPortal = async () => {
const { data: customer } = await getCustomer();
if (!customer) return { url: null };
lemonSqueezySetup({ apiKey: process.env.LEMON_SQUEEZY_API_KEY! });
const res = await lemonSqueezyGetCustomer(customer.customer_id);
return { url: res.data?.data.attributes.urls.customer_portal ?? null };
};

export default getCustomerBillingPortal;
11 changes: 11 additions & 0 deletions app/_queries/get-customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import createServerSupabaseClient from '@/_utilities/create-server-supabase-client';

const getCustomer = () =>
createServerSupabaseClient()
.from('customers')
.select('customer_id, subscription_status')
.single();

export type GetCustomerData = Awaited<ReturnType<typeof getCustomer>>['data'];

export default getCustomer;
27 changes: 12 additions & 15 deletions app/_utilities/create-server-supabase-client.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import { Database } from '@/_types/database';
import { CookieOptions, createServerClient } from '@supabase/ssr';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

const createServerSupabaseClient = () => {
const createServerSupabaseClient = ({
apiKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
} = {}) => {
const cookieStore = cookies();

return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
apiKey,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
getAll() {
return cookieStore.getAll();
},
remove(name: string, options: CookieOptions) {
setAll(cookiesToSet) {
try {
cookieStore.set({ name, value: '', ...options });
} catch (e) {
// noop
}
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (e) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
);
} catch {
// noop
}
},
Expand Down
7 changes: 1 addition & 6 deletions app/_utilities/format-image-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ const formatImageUrl = (file?: string | File | null) => {

if (typeof file === 'string') {
if (file.startsWith('http')) return file;

const pathPart = process.env.NEXT_PUBLIC_SUPABASE_PRO
? 'render/image'
: 'object';

return `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/${pathPart}/public/${file}`;
return `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${file}`;
}

if (file instanceof File) {
Expand Down
21 changes: 0 additions & 21 deletions app/_utilities/generate-image-loader.ts

This file was deleted.

Loading

0 comments on commit f6421d6

Please sign in to comment.