Skip to content

Commit

Permalink
refactor: add some helpers & update metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
qqharry21 committed Mar 5, 2024
1 parent b452cdb commit bd96c60
Show file tree
Hide file tree
Showing 10 changed files with 444 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .env
@@ -1,5 +1,12 @@
NEXT_PUBLIC_SITE_URL=

ACCESS_TOKEN_KEY='user_access_token'
REFRESH_TOKEN_KEY='user_refresh_token'

NEXT_PUBLIC_MEASUREMENT_ID=
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=
SENTRY_DSN=
SENTRY_AUTH_TOKEN=

NEXT_PUBLIC_SUPABASE_URL=https://odwqofjfevmgoakhmaav.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9kd3FvZmpmZXZtZ29ha2htYWF2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDk1NDY3NDYsImV4cCI6MjAyNTEyMjc0Nn0.m0sa2EhmHo0OKBwHJOhuO-YkHA-vtbyH-ah6eBppVpc
10 changes: 10 additions & 0 deletions .env.example
@@ -1,2 +1,12 @@
NEXT_PUBLIC_SITE_URL=

ACCESS_TOKEN_KEY=
REFRESH_TOKEN_KEY=

NEXT_PUBLIC_MEASUREMENT_ID=
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=
SENTRY_DSN=
SENTRY_AUTH_TOKEN=

NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
Binary file added public/og.webp
Binary file not shown.
52 changes: 42 additions & 10 deletions src/app/layout.tsx
Expand Up @@ -3,23 +3,55 @@ import type { Metadata } from 'next';
import { Header } from '@/components/header';
import { Sidebar } from '@/components/sidebar';
import { inter } from '@/styles/fonts';
import { getURL } from '@/utils/helpers';

import { Providers } from './providers';

import '../styles/globals.scss';

export const metadata: Metadata = {
title: {
default: 'HaoMo Dashboard',
template: '%s | HaoMo Dashboard',
},
description: 'HaoMo Dashboard',
keywords: 'HaoMo Dashboard',
icons: {
icon: '/icon.ico',
},
const meta = {
title: 'HaoMo Dashboard',
description: 'Content management system for HaoMo',
cardImage: '/og.webp',
robots: 'follow, index',
favicon: '/icon.ico',
url: getURL(),
};

export async function generateMetadata(): Promise<Metadata> {
return {
title: {
default: meta.title,
template: `%s | ${meta.title}`,
},
description: meta.description,
referrer: 'origin-when-cross-origin',
keywords: ['Vercel', 'Supabase', 'Next.js', 'Dashboard', 'HaoMo'],
authors: [{ name: 'HaoMo', url: 'https://vercel.com/' }],
creator: 'HaoMo',
publisher: 'HaoMo',
robots: meta.robots,
icons: { icon: meta.favicon },
metadataBase: new URL(meta.url),
openGraph: {
url: meta.url,
title: meta.title,
description: meta.description,
images: [meta.cardImage],
type: 'website',
siteName: meta.title,
},
twitter: {
card: 'summary_large_image',
site: '@HarryChen824',
creator: '@HarryChen824',
title: meta.title,
description: meta.description,
images: [meta.cardImage],
},
};
}

export default function RootLayout({ children }: Readonly<PropsWithChildren>) {
return (
<html
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types/environment.d.ts
@@ -1,7 +1,7 @@
declare namespace NodeJS {
export interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test' | 'local';
readonly NEXT_PUBLIC_BASE_URL: string;
readonly NEXT_PUBLIC_SITE_URL: string;
readonly NEXT_PUBLIC_MEASUREMENT_ID: string;
readonly NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID: string;
readonly SENTRY_DSN: string;
Expand Down
21 changes: 21 additions & 0 deletions src/middleware.ts
@@ -0,0 +1,21 @@
import type { NextRequest } from 'next/server';

import { updateSession } from '@/utils/supabase/middleware';

export async function middleware(request: NextRequest) {
return await updateSession(request);
}

export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};
21 changes: 21 additions & 0 deletions src/utils/helpers.ts
@@ -0,0 +1,21 @@
export const getURL = (path: string = '') => {
// Check if NEXT_PUBLIC_SITE_URL is set and non-empty. Set this to your site URL in production env.
let url =
process?.env?.NEXT_PUBLIC_SITE_URL && process.env.NEXT_PUBLIC_SITE_URL.trim() !== ''
? process.env.NEXT_PUBLIC_SITE_URL
: // If not set, check for NEXT_PUBLIC_VERCEL_URL, which is automatically set by Vercel.
process?.env?.NEXT_PUBLIC_VERCEL_URL && process.env.NEXT_PUBLIC_VERCEL_URL.trim() !== ''
? process.env.NEXT_PUBLIC_VERCEL_URL
: // If neither is set, default to localhost for local development.
'http://localhost:3000/';

// Trim the URL and remove trailing slash if exists.
url = url.replace(/\/+$/, '');
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Ensure path starts without a slash to avoid double slashes in the final URL.
path = path.replace(/^\/+/, '');

// Concatenate the URL and the path.
return path ? `${url}/${path}` : url;
};
245 changes: 245 additions & 0 deletions src/utils/supabase/admin.ts
@@ -0,0 +1,245 @@
// import { createClient } from '@supabase/supabase-js';
// import type Stripe from 'stripe';
// import type { Database, Tables, TablesInsert } from 'types_db';

// import { toDateTime } from '@/utils/helpers';
// import { stripe } from '@/utils/stripe/config';

// type Product = Tables<'products'>;
// type Price = Tables<'prices'>;

// // Change to control trial period length
// const TRIAL_PERIOD_DAYS = 0;

// // Note: supabaseAdmin uses the SERVICE_ROLE_KEY which you must only use in a secure server-side context
// // as it has admin privileges and overwrites RLS policies!
// const supabaseAdmin = createClient<Database>(
// process.env.NEXT_PUBLIC_SUPABASE_URL || '',
// process.env.SUPABASE_SERVICE_ROLE_KEY || ''
// );

// const upsertProductRecord = async (product: Stripe.Product) => {
// const productData: Product = {
// id: product.id,
// active: product.active,
// name: product.name,
// description: product.description ?? null,
// image: product.images?.[0] ?? null,
// metadata: product.metadata,
// };

// const { error: upsertError } = await supabaseAdmin.from('products').upsert([productData]);
// if (upsertError) throw new Error(`Product insert/update failed: ${upsertError.message}`);
// console.log(`Product inserted/updated: ${product.id}`);
// };

// const upsertPriceRecord = async (price: Stripe.Price, retryCount = 0, maxRetries = 3) => {
// const priceData: Price = {
// id: price.id,
// product_id: typeof price.product === 'string' ? price.product : '',
// active: price.active,
// currency: price.currency,
// type: price.type,
// unit_amount: price.unit_amount ?? null,
// interval: price.recurring?.interval ?? null,
// interval_count: price.recurring?.interval_count ?? null,
// trial_period_days: price.recurring?.trial_period_days ?? TRIAL_PERIOD_DAYS,
// };

// const { error: upsertError } = await supabaseAdmin.from('prices').upsert([priceData]);

// if (upsertError?.message.includes('foreign key constraint')) {
// if (retryCount < maxRetries) {
// console.log(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`);
// await new Promise((resolve) => setTimeout(resolve, 2000));
// await upsertPriceRecord(price, retryCount + 1, maxRetries);
// } else {
// throw new Error(
// `Price insert/update failed after ${maxRetries} retries: ${upsertError.message}`
// );
// }
// } else if (upsertError) {
// throw new Error(`Price insert/update failed: ${upsertError.message}`);
// } else {
// console.log(`Price inserted/updated: ${price.id}`);
// }
// };

// const deleteProductRecord = async (product: Stripe.Product) => {
// const { error: deletionError } = await supabaseAdmin
// .from('products')
// .delete()
// .eq('id', product.id);
// if (deletionError) throw new Error(`Product deletion failed: ${deletionError.message}`);
// console.log(`Product deleted: ${product.id}`);
// };

// const deletePriceRecord = async (price: Stripe.Price) => {
// const { error: deletionError } = await supabaseAdmin.from('prices').delete().eq('id', price.id);
// if (deletionError) throw new Error(`Price deletion failed: ${deletionError.message}`);
// console.log(`Price deleted: ${price.id}`);
// };

// const upsertCustomerToSupabase = async (uuid: string, customerId: string) => {
// const { error: upsertError } = await supabaseAdmin
// .from('customers')
// .upsert([{ id: uuid, stripe_customer_id: customerId }]);

// if (upsertError)
// throw new Error(`Supabase customer record creation failed: ${upsertError.message}`);

// return customerId;
// };

// const createCustomerInStripe = async (uuid: string, email: string) => {
// const customerData = { metadata: { supabaseUUID: uuid }, email: email };
// const newCustomer = await stripe.customers.create(customerData);
// if (!newCustomer) throw new Error('Stripe customer creation failed.');

// return newCustomer.id;
// };

// const createOrRetrieveCustomer = async ({ email, uuid }: { email: string; uuid: string }) => {
// // Check if the customer already exists in Supabase
// const { data: existingSupabaseCustomer, error: queryError } = await supabaseAdmin
// .from('customers')
// .select('*')
// .eq('id', uuid)
// .maybeSingle();

// if (queryError) {
// throw new Error(`Supabase customer lookup failed: ${queryError.message}`);
// }

// // Retrieve the Stripe customer ID using the Supabase customer ID, with email fallback
// let stripeCustomerId: string | undefined;
// if (existingSupabaseCustomer?.stripe_customer_id) {
// const existingStripeCustomer = await stripe.customers.retrieve(
// existingSupabaseCustomer.stripe_customer_id
// );
// stripeCustomerId = existingStripeCustomer.id;
// } else {
// // If Stripe ID is missing from Supabase, try to retrieve Stripe customer ID by email
// const stripeCustomers = await stripe.customers.list({ email: email });
// stripeCustomerId = stripeCustomers.data.length > 0 ? stripeCustomers.data[0].id : undefined;
// }

// // If still no stripeCustomerId, create a new customer in Stripe
// const stripeIdToInsert = stripeCustomerId
// ? stripeCustomerId
// : await createCustomerInStripe(uuid, email);
// if (!stripeIdToInsert) throw new Error('Stripe customer creation failed.');

// if (existingSupabaseCustomer && stripeCustomerId) {
// // If Supabase has a record but doesn't match Stripe, update Supabase record
// if (existingSupabaseCustomer.stripe_customer_id !== stripeCustomerId) {
// const { error: updateError } = await supabaseAdmin
// .from('customers')
// .update({ stripe_customer_id: stripeCustomerId })
// .eq('id', uuid);

// if (updateError)
// throw new Error(`Supabase customer record update failed: ${updateError.message}`);
// console.warn(`Supabase customer record mismatched Stripe ID. Supabase record updated.`);
// }
// // If Supabase has a record and matches Stripe, return Stripe customer ID
// return stripeCustomerId;
// } else {
// console.warn(`Supabase customer record was missing. A new record was created.`);

// // If Supabase has no record, create a new record and return Stripe customer ID
// const upsertedStripeCustomer = await upsertCustomerToSupabase(uuid, stripeIdToInsert);
// if (!upsertedStripeCustomer) throw new Error('Supabase customer record creation failed.');

// return upsertedStripeCustomer;
// }
// };

// /**
// * Copies the billing details from the payment method to the customer object.
// */
// const copyBillingDetailsToCustomer = async (uuid: string, payment_method: Stripe.PaymentMethod) => {
// //Todo: check this assertion
// const customer = payment_method.customer as string;
// const { name, phone, address } = payment_method.billing_details;
// if (!name || !phone || !address) return;

// await stripe.customers.update(customer, { name, phone, address });
// const { error: updateError } = await supabaseAdmin
// .from('users')
// .update({
// billing_address: { ...address },
// payment_method: { ...payment_method[payment_method.type] },
// })
// .eq('id', uuid);
// if (updateError) throw new Error(`Customer update failed: ${updateError.message}`);
// };

// const manageSubscriptionStatusChange = async (
// subscriptionId: string,
// customerId: string,
// createAction = false
// ) => {
// // Get customer's UUID from mapping table.
// const { data: customerData, error: noCustomerError } = await supabaseAdmin
// .from('customers')
// .select('id')
// .eq('stripe_customer_id', customerId)
// .single();

// if (noCustomerError) throw new Error(`Customer lookup failed: ${noCustomerError.message}`);

// const { id: uuid } = customerData!;

// const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
// expand: ['default_payment_method'],
// });
// // Upsert the latest status of the subscription object.
// const subscriptionData: TablesInsert<'subscriptions'> = {
// id: subscription.id,
// user_id: uuid,
// metadata: subscription.metadata,
// status: subscription.status,
// price_id: subscription.items.data[0].price.id,
// //TODO check quantity on subscription

// quantity: subscription.quantity,
// cancel_at_period_end: subscription.cancel_at_period_end,
// cancel_at: subscription.cancel_at ? toDateTime(subscription.cancel_at).toISOString() : null,
// canceled_at: subscription.canceled_at
// ? toDateTime(subscription.canceled_at).toISOString()
// : null,
// current_period_start: toDateTime(subscription.current_period_start).toISOString(),
// current_period_end: toDateTime(subscription.current_period_end).toISOString(),
// created: toDateTime(subscription.created).toISOString(),
// ended_at: subscription.ended_at ? toDateTime(subscription.ended_at).toISOString() : null,
// trial_start: subscription.trial_start
// ? toDateTime(subscription.trial_start).toISOString()
// : null,
// trial_end: subscription.trial_end ? toDateTime(subscription.trial_end).toISOString() : null,
// };

// const { error: upsertError } = await supabaseAdmin
// .from('subscriptions')
// .upsert([subscriptionData]);
// if (upsertError) throw new Error(`Subscription insert/update failed: ${upsertError.message}`);
// console.log(`Inserted/updated subscription [${subscription.id}] for user [${uuid}]`);

// // For a new subscription copy the billing details to the customer object.
// // NOTE: This is a costly operation and should happen at the very end.
// if (createAction && subscription.default_payment_method && uuid)
// //@ts-ignore
// await copyBillingDetailsToCustomer(
// uuid,
// subscription.default_payment_method as Stripe.PaymentMethod
// );
// };

// export {
// createOrRetrieveCustomer,
// deletePriceRecord,
// deleteProductRecord,
// manageSubscriptionStatusChange,
// upsertPriceRecord,
// upsertProductRecord,
// };

0 comments on commit bd96c60

Please sign in to comment.