Last active
April 1, 2024 05:46
-
-
Save Vetrivel-VP/a7803530b94642a9b6335b57a601dc60 to your computer and use it in GitHub Desktop.
Youtube Spotify Clone
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
STRIPE env Variables | |
---------------------------- | |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= | |
STRIPE_SECRET_KEY= | |
STRIPE_WEBHOOK_SECRET= | |
---------------------------- | |
Stripe Helpers | |
---------------------------- | |
import { Price } from "@/types"; | |
export const getURL = () => { | |
let url = | |
process.env.NEXT_PUBLIC_SITE_URL ?? | |
process.env.NEXT_PUBLIC_VERCEL_URL ?? | |
"http://localhost:3000/"; | |
url = url.includes("http") ? url : `https://${url}`; | |
url = url.charAt(url.length - 1) === "/" ? url : `${url}/`; | |
return url; | |
}; | |
export const postData = async ({ | |
url, | |
data, | |
}: { | |
url: string; | |
data?: { price: Price }; | |
}) => { | |
console.log("POST REQUEST:", url, data); | |
const res: Response = await fetch(url, { | |
method: "POST", | |
headers: new Headers({ "Content-Type": "application/json" }), | |
credentials: "same-origin", | |
body: JSON.stringify(data), | |
}); | |
if (!res.ok) { | |
console.log("Error in POST : ", { url, data, res }); | |
throw new Error(res.statusText); | |
} | |
return res.json(); | |
}; | |
export const toDateTime = (secs: number) => { | |
var t = new Date("1970-01-01T00:30:00Z"); | |
t.setSeconds(secs); | |
return t; | |
}; | |
---------------------------- | |
stripe.ts | |
---------------------------- | |
import Stripe from "stripe"; | |
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", { | |
apiVersion: "2023-10-16", | |
appInfo: { | |
name: "Spotify Clone YT Video", | |
version: "0.1.0", | |
}, | |
}); | |
---------------------------- | |
stripeClient.ts | |
---------------------------- | |
import { loadStripe, Stripe } from "@stripe/stripe-js"; | |
let stripePromise: Promise<Stripe | null>; | |
export const getStripe = () => { | |
if (!stripePromise) { | |
stripePromise = loadStripe( | |
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "" | |
); | |
} | |
return stripePromise; | |
}; | |
---------------------------- | |
stripe supbaseAdmin.ts | |
---------------------------- | |
import Stripe from "stripe"; | |
import { createClient } from "@supabase/supabase-js"; | |
import { Database } from "@/types_db"; | |
import { Price, Product } from "@/types"; | |
import { stripe } from "./stripe"; | |
import { toDateTime } from "./helpers"; | |
export const supabaseAdmin = createClient<Database>( | |
process.env.NEXT_PUBLIC_SUPABASE_URL || "", | |
process.env.SUPABASE_SERVICE_ROLE_KEY || "" | |
); | |
// upsert product record | |
const upserProductRecord = async (product: Stripe.Product) => { | |
const productData: Product = { | |
id: product.id, | |
active: product.active, | |
name: product.name, | |
description: product.description ?? undefined, | |
image: product.images?.[0] ?? null, | |
metadata: product.metadata, | |
}; | |
const { error } = await supabaseAdmin.from("products").upsert([productData]); | |
if (error) { | |
throw error; | |
} | |
console.log("Product inserted/updated : ", product.id); | |
}; | |
const upsertPriceRecord = async (price: Stripe.Price) => { | |
const priceData: Price = { | |
id: price.id, | |
product_id: typeof price.product === "string" ? price.product : "", | |
active: price.active, | |
currency: price.currency, | |
description: price.nickname ?? undefined, | |
type: price.type, | |
unit_amount: price.unit_amount ?? undefined, | |
interval: price.recurring?.interval, | |
interval_count: price.recurring?.interval_count, | |
trial_period_days: price.recurring?.trial_period_days, | |
metadata: price.metadata, | |
}; | |
const { error } = await supabaseAdmin.from("prices").upsert([priceData]); | |
if (error) { | |
throw error; | |
} | |
console.log(`Price inserted/updated : ${price.id}`); | |
}; | |
const createOrRetrieveCustomer = async ({ | |
email, | |
uuid, | |
}: { | |
email: string; | |
uuid: string; | |
}) => { | |
const { data, error } = await supabaseAdmin | |
.from("customers") | |
.select("stripe_customer_id") | |
.eq("id", uuid) | |
.single(); | |
if (error || !data?.stripe_customer_id) { | |
const customerData: { metadata: { supabaseUUID: string }; email?: string } = | |
{ | |
metadata: { | |
supabaseUUID: uuid, | |
}, | |
}; | |
if (email) customerData.email = email; | |
const customer = await stripe.customers.create(customerData); | |
const { error: supabaseError } = await supabaseAdmin | |
.from("customers") | |
.insert([{ id: uuid, stripe_customer_id: customer.id }]); | |
if (supabaseError) { | |
throw supabaseError; | |
} | |
console.log("New Customer Created:", uuid); | |
return customer.id; | |
} | |
// if customer already on plan | |
return data.stripe_customer_id; | |
}; | |
const copyBillingDetailsToCustomer = async ( | |
uuid: string, | |
payment_method: Stripe.PaymentMethod | |
) => { | |
const customer = payment_method.customer as string; | |
const { name, phone, address } = payment_method.billing_details; | |
if (!name || !phone || !address) return; | |
// @ts-ignore | |
await stripe.customers.update(customer, { name, phone, address }); | |
const { error } = await supabaseAdmin | |
.from("users") | |
.update({ | |
billing_address: { ...address }, | |
payment_method: { ...payment_method[payment_method.type] }, | |
}) | |
.eq("id", uuid); | |
if (error) throw error; | |
}; | |
const manageSubscriptionStatusChange = async ( | |
subscriptionId: string, | |
customerId: string, | |
createAction = false | |
) => { | |
const { data: customerData, error: noCustomerError } = await supabaseAdmin | |
.from("customers") | |
.select("id") | |
.eq("stripe_customer_id", customerId) | |
.single(); | |
if (noCustomerError) throw noCustomerError; | |
const { id: uuid } = customerData; | |
const subscription = await stripe.subscriptions.retrieve(subscriptionId, { | |
expand: ["default_payment_method"], | |
}); | |
const subscriptionData: Database["public"]["Tables"]["subscriptions"]["Insert"] = | |
{ | |
id: subscription.id, | |
user_id: uuid, | |
metadata: subscription.metadata, | |
// @ts-ignore | |
status: subscription.status, | |
price_id: subscription.items.data[0].price.id, | |
// @ts-ignore | |
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 } = await supabaseAdmin | |
.from("subscriptions") | |
.upsert([subscriptionData]); | |
if (error) throw error; | |
console.log( | |
`Inserted/ updated subscription [${subscription.id} for ${uuid}]` | |
); | |
if (createAction && subscription.default_payment_method && uuid) { | |
await copyBillingDetailsToCustomer( | |
uuid, | |
subscription.default_payment_method as Stripe.PaymentMethod | |
); | |
} | |
}; | |
export { | |
upserProductRecord, | |
upsertPriceRecord, | |
createOrRetrieveCustomer, | |
manageSubscriptionStatusChange, | |
}; | |
---------------------------- | |
---------------------------- | |
webhook route.ts | |
import Stripe from "stripe"; | |
import { NextResponse } from "next/server"; | |
import { headers } from "next/headers"; | |
import { stripe } from "@/libs/stripe"; | |
import { | |
upserProductRecord, | |
upsertPriceRecord, | |
manageSubscriptionStatusChange, | |
} from "@/libs/supabaseAdmin"; | |
const relevantEvents = new Set([ | |
"product.created", | |
"product.updated", | |
"price.created", | |
"price.updated", | |
"checkout.session.completed", | |
"customer.subscription.created", | |
"customer.subscription.updated", | |
"customer.subscription.deleted", | |
]); | |
export const POST = async (request: Request) => { | |
const body = await request.text(); | |
const sig = headers().get("Stripe-Signature"); | |
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; | |
let event: Stripe.Event; | |
try { | |
if (!sig || !webhookSecret) return; | |
event = stripe.webhooks.constructEvent(body, sig, webhookSecret); | |
} catch (error: any) { | |
console.log(`Error Message : ${error.message}`); | |
return new NextResponse(`Webhook Error : ${error.message}`, { | |
status: 400, | |
}); | |
} | |
if (relevantEvents.has(event.type)) { | |
try { | |
switch (event.type) { | |
case "product.created": | |
case "product.updated": | |
await upserProductRecord(event.data.object as Stripe.Product); | |
break; | |
case "price.created": | |
case "price.updated": | |
await upsertPriceRecord(event.data.object as Stripe.Price); | |
break; | |
case "customer.subscription.created": | |
case "customer.subscription.updated": | |
case "customer.subscription.deleted": | |
const subscription = event.data.object as Stripe.Subscription; | |
await manageSubscriptionStatusChange( | |
subscription.id, | |
subscription.customer as string, | |
event.type === "customer.subscription.created" | |
); | |
break; | |
case "checkout.session.completed": | |
const checkoutSession = event.data.object as Stripe.Checkout.Session; | |
if (checkoutSession.mode === "subscription") { | |
const subscriptionId = checkoutSession.subscription; | |
await manageSubscriptionStatusChange( | |
subscriptionId as string, | |
checkoutSession.customer as string, | |
true | |
); | |
} | |
break; | |
default: | |
throw new Error("Unhandled Relevant event"); | |
} | |
} catch (error) { | |
console.log(error); | |
return new NextResponse(`Webhook error`, { status: 400 }); | |
} | |
} | |
return NextResponse.json({ received: true }, { status: 200 }); | |
}; | |
---------------------------- | |
---------------------------- | |
create-checkout-session | |
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; | |
import { cookies } from "next/headers"; | |
import { NextResponse } from "next/server"; | |
import { stripe } from "@/libs/stripe"; | |
import { getURL } from "@/libs/helpers"; | |
import { createOrRetrieveCustomer } from "@/libs/supabaseAdmin"; | |
export const POST = async (request: Request) => { | |
const { price, quantity = 1, metadata = {} } = await request.json(); | |
try { | |
const supabase = createRouteHandlerClient({ | |
cookies, | |
}); | |
const { | |
data: { user }, | |
} = await supabase.auth.getUser(); | |
const customer = await createOrRetrieveCustomer({ | |
uuid: user?.id || "", | |
email: user?.email || "", | |
}); | |
const session = await stripe.checkout.sessions.create({ | |
payment_method_types: ["card"], | |
billing_address_collection: "required", | |
customer, | |
line_items: [ | |
{ | |
price: price.id, | |
quantity, | |
}, | |
], | |
mode: "subscription", | |
allow_promotion_codes: true, | |
subscription_data: { | |
metadata, | |
}, | |
success_url: `${getURL()}/account`, | |
cancel_url: `${getURL()}/`, | |
}); | |
return NextResponse.json({ sessionId: session.id }); | |
} catch (err: any) { | |
console.log(err); | |
return new NextResponse("Internal Error", { status: 500 }); | |
} | |
}; | |
---------------------------- | |
---------------------------- | |
create-portal-link | |
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; | |
import { cookies } from "next/headers"; | |
import { NextResponse } from "next/server"; | |
import { stripe } from "@/libs/stripe"; | |
import { getURL } from "@/libs/helpers"; | |
import { createOrRetrieveCustomer } from "@/libs/supabaseAdmin"; | |
export const POST = async () => { | |
try { | |
const supabase = createRouteHandlerClient({ cookies }); | |
const { | |
data: { user }, | |
} = await supabase.auth.getUser(); | |
if (!user) throw new Error("Could not get user"); | |
const customer = await createOrRetrieveCustomer({ | |
uuid: user?.id || "", | |
email: user?.email || "", | |
}); | |
if (!customer) throw new Error("Could not get customer"); | |
const { url } = await stripe.billingPortal.sessions.create({ | |
customer, | |
return_url: `${getURL()}/account`, | |
}); | |
return NextResponse.json({ url }); | |
} catch (error) { | |
console.log(error); | |
return new NextResponse("Internal Error", { status: 500 }); | |
} | |
}; | |
---------------------------- | |
types_db.ts | |
---------------------------- | |
export type Json = | |
| string | |
| number | |
| boolean | |
| null | |
| { [key: string]: Json | undefined } | |
| Json[] | |
export type Database = { | |
public: { | |
Tables: { | |
artists: { | |
Row: { | |
author: string | null | |
created_at: string | |
description: string | null | |
facebook: string | null | |
followers: number | null | |
id: number | |
instagram: string | null | |
linkedin: string | null | |
picture: string | null | |
twitter: string | null | |
} | |
Insert: { | |
author?: string | null | |
created_at?: string | |
description?: string | null | |
facebook?: string | null | |
followers?: number | null | |
id?: number | |
instagram?: string | null | |
linkedin?: string | null | |
picture?: string | null | |
twitter?: string | null | |
} | |
Update: { | |
author?: string | null | |
created_at?: string | |
description?: string | null | |
facebook?: string | null | |
followers?: number | null | |
id?: number | |
instagram?: string | null | |
linkedin?: string | null | |
picture?: string | null | |
twitter?: string | null | |
} | |
Relationships: [] | |
} | |
customers: { | |
Row: { | |
id: string | |
stripe_customer_id: string | null | |
} | |
Insert: { | |
id: string | |
stripe_customer_id?: string | null | |
} | |
Update: { | |
id?: string | |
stripe_customer_id?: string | null | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "customers_id_fkey" | |
columns: ["id"] | |
isOneToOne: true | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
favourites: { | |
Row: { | |
created_at: string | |
song_id: number | |
user_id: string | |
} | |
Insert: { | |
created_at?: string | |
song_id: number | |
user_id: string | |
} | |
Update: { | |
created_at?: string | |
song_id?: number | |
user_id?: string | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "public_favourites_song_id_fkey" | |
columns: ["song_id"] | |
isOneToOne: false | |
referencedRelation: "songs" | |
referencedColumns: ["id"] | |
}, | |
{ | |
foreignKeyName: "public_favourites_user_id_fkey" | |
columns: ["user_id"] | |
isOneToOne: false | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
playlists: { | |
Row: { | |
created_at: string | |
id: number | |
title: string | null | |
user_id: string | null | |
} | |
Insert: { | |
created_at?: string | |
id?: number | |
title?: string | null | |
user_id?: string | null | |
} | |
Update: { | |
created_at?: string | |
id?: number | |
title?: string | null | |
user_id?: string | null | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "public_playlists_user_id_fkey" | |
columns: ["user_id"] | |
isOneToOne: false | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
prices: { | |
Row: { | |
active: boolean | null | |
currency: string | null | |
description: string | null | |
id: string | |
interval: Database["public"]["Enums"]["pricing_plan_interval"] | null | |
interval_count: number | null | |
metadata: Json | null | |
product_id: string | null | |
trial_period_days: number | null | |
type: Database["public"]["Enums"]["pricing_type"] | null | |
unit_amount: number | null | |
} | |
Insert: { | |
active?: boolean | null | |
currency?: string | null | |
description?: string | null | |
id: string | |
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null | |
interval_count?: number | null | |
metadata?: Json | null | |
product_id?: string | null | |
trial_period_days?: number | null | |
type?: Database["public"]["Enums"]["pricing_type"] | null | |
unit_amount?: number | null | |
} | |
Update: { | |
active?: boolean | null | |
currency?: string | null | |
description?: string | null | |
id?: string | |
interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null | |
interval_count?: number | null | |
metadata?: Json | null | |
product_id?: string | null | |
trial_period_days?: number | null | |
type?: Database["public"]["Enums"]["pricing_type"] | null | |
unit_amount?: number | null | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "prices_product_id_fkey" | |
columns: ["product_id"] | |
isOneToOne: false | |
referencedRelation: "products" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
products: { | |
Row: { | |
active: boolean | null | |
description: string | null | |
id: string | |
image: string | null | |
metadata: Json | null | |
name: string | null | |
} | |
Insert: { | |
active?: boolean | null | |
description?: string | null | |
id: string | |
image?: string | null | |
metadata?: Json | null | |
name?: string | null | |
} | |
Update: { | |
active?: boolean | null | |
description?: string | null | |
id?: string | |
image?: string | null | |
metadata?: Json | null | |
name?: string | null | |
} | |
Relationships: [] | |
} | |
songs: { | |
Row: { | |
artist_id: number | null | |
created_at: string | |
id: number | |
image_uri: string | null | |
song_uri: string | null | |
title: string | null | |
user_id: string | null | |
} | |
Insert: { | |
artist_id?: number | null | |
created_at?: string | |
id?: number | |
image_uri?: string | null | |
song_uri?: string | null | |
title?: string | null | |
user_id?: string | null | |
} | |
Update: { | |
artist_id?: number | null | |
created_at?: string | |
id?: number | |
image_uri?: string | null | |
song_uri?: string | null | |
title?: string | null | |
user_id?: string | null | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "public_songs_artist_id_fkey" | |
columns: ["artist_id"] | |
isOneToOne: false | |
referencedRelation: "artists" | |
referencedColumns: ["id"] | |
}, | |
{ | |
foreignKeyName: "public_songs_user_id_fkey" | |
columns: ["user_id"] | |
isOneToOne: false | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
subscriptions: { | |
Row: { | |
cancel_at: string | null | |
cancel_at_period_end: boolean | null | |
canceled_at: string | null | |
created: string | |
current_period_end: string | |
current_period_start: string | |
ended_at: string | null | |
id: string | |
metadata: Json | null | |
price_id: string | null | |
quantity: number | null | |
status: Database["public"]["Enums"]["subscription_status"] | null | |
trial_end: string | null | |
trial_start: string | null | |
user_id: string | |
} | |
Insert: { | |
cancel_at?: string | null | |
cancel_at_period_end?: boolean | null | |
canceled_at?: string | null | |
created?: string | |
current_period_end?: string | |
current_period_start?: string | |
ended_at?: string | null | |
id: string | |
metadata?: Json | null | |
price_id?: string | null | |
quantity?: number | null | |
status?: Database["public"]["Enums"]["subscription_status"] | null | |
trial_end?: string | null | |
trial_start?: string | null | |
user_id: string | |
} | |
Update: { | |
cancel_at?: string | null | |
cancel_at_period_end?: boolean | null | |
canceled_at?: string | null | |
created?: string | |
current_period_end?: string | |
current_period_start?: string | |
ended_at?: string | null | |
id?: string | |
metadata?: Json | null | |
price_id?: string | null | |
quantity?: number | null | |
status?: Database["public"]["Enums"]["subscription_status"] | null | |
trial_end?: string | null | |
trial_start?: string | null | |
user_id?: string | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "subscriptions_price_id_fkey" | |
columns: ["price_id"] | |
isOneToOne: false | |
referencedRelation: "prices" | |
referencedColumns: ["id"] | |
}, | |
{ | |
foreignKeyName: "subscriptions_user_id_fkey" | |
columns: ["user_id"] | |
isOneToOne: false | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
users: { | |
Row: { | |
avatar_url: string | null | |
billing_address: Json | null | |
full_name: string | null | |
id: string | |
payment_method: Json | null | |
} | |
Insert: { | |
avatar_url?: string | null | |
billing_address?: Json | null | |
full_name?: string | null | |
id: string | |
payment_method?: Json | null | |
} | |
Update: { | |
avatar_url?: string | null | |
billing_address?: Json | null | |
full_name?: string | null | |
id?: string | |
payment_method?: Json | null | |
} | |
Relationships: [ | |
{ | |
foreignKeyName: "users_id_fkey" | |
columns: ["id"] | |
isOneToOne: true | |
referencedRelation: "users" | |
referencedColumns: ["id"] | |
}, | |
] | |
} | |
} | |
Views: { | |
[_ in never]: never | |
} | |
Functions: { | |
[_ in never]: never | |
} | |
Enums: { | |
pricing_plan_interval: "day" | "week" | "month" | "year" | |
pricing_type: "one_time" | "recurring" | |
subscription_status: | |
| "trialing" | |
| "active" | |
| "canceled" | |
| "incomplete" | |
| "incomplete_expired" | |
| "past_due" | |
| "unpaid" | |
} | |
CompositeTypes: { | |
[_ in never]: never | |
} | |
} | |
} | |
type PublicSchema = Database[Extract<keyof Database, "public">] | |
export type Tables< | |
PublicTableNameOrOptions extends | |
| keyof (PublicSchema["Tables"] & PublicSchema["Views"]) | |
| { schema: keyof Database }, | |
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } | |
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & | |
Database[PublicTableNameOrOptions["schema"]]["Views"]) | |
: never = never, | |
> = PublicTableNameOrOptions extends { schema: keyof Database } | |
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & | |
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { | |
Row: infer R | |
} | |
? R | |
: never | |
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & | |
PublicSchema["Views"]) | |
? (PublicSchema["Tables"] & | |
PublicSchema["Views"])[PublicTableNameOrOptions] extends { | |
Row: infer R | |
} | |
? R | |
: never | |
: never | |
export type TablesInsert< | |
PublicTableNameOrOptions extends | |
| keyof PublicSchema["Tables"] | |
| { schema: keyof Database }, | |
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } | |
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] | |
: never = never, | |
> = PublicTableNameOrOptions extends { schema: keyof Database } | |
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { | |
Insert: infer I | |
} | |
? I | |
: never | |
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"] | |
? PublicSchema["Tables"][PublicTableNameOrOptions] extends { | |
Insert: infer I | |
} | |
? I | |
: never | |
: never | |
export type TablesUpdate< | |
PublicTableNameOrOptions extends | |
| keyof PublicSchema["Tables"] | |
| { schema: keyof Database }, | |
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } | |
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] | |
: never = never, | |
> = PublicTableNameOrOptions extends { schema: keyof Database } | |
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { | |
Update: infer U | |
} | |
? U | |
: never | |
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"] | |
? PublicSchema["Tables"][PublicTableNameOrOptions] extends { | |
Update: infer U | |
} | |
? U | |
: never | |
: never | |
export type Enums< | |
PublicEnumNameOrOptions extends | |
| keyof PublicSchema["Enums"] | |
| { schema: keyof Database }, | |
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } | |
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] | |
: never = never, | |
> = PublicEnumNameOrOptions extends { schema: keyof Database } | |
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] | |
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] | |
? PublicSchema["Enums"][PublicEnumNameOrOptions] | |
: never | |
---------------------------- | |
types.ts | |
----------------------------- | |
import Stripe from "stripe"; | |
export interface UserDetails { | |
id: string; | |
first_name: string; | |
last_name: string; | |
full_name?: string; | |
avatar_url?: string; | |
billing_address?: Stripe.Address; | |
payment_method?: Stripe.PaymentMethod[Stripe.PaymentMethod.Type]; | |
} | |
export interface Artist { | |
id: string; | |
author: string; | |
picture: string; | |
description: string; | |
followers: number; | |
facebook: string; | |
instagram: string; | |
linkedin: string; | |
} | |
export interface Song { | |
id: string; | |
title: string; | |
song_uri: string; | |
image_uri: string; | |
user_id: string; | |
artist_id: string; | |
} | |
export interface Product { | |
id: string; | |
active?: boolean; | |
name?: string; | |
description?: string; | |
image?: string; | |
metadata?: Stripe.Metadata; | |
} | |
export interface Price { | |
id: string; | |
product_id?: string; | |
active?: boolean; | |
description?: string; | |
unit_amount?: number; | |
currency?: string; | |
type?: Stripe.Price.Type; | |
interval?: Stripe.Price.Recurring.Interval; | |
interval_count?: number; | |
trial_period_days?: number | null; | |
metadata?: Stripe.Metadata; | |
products?: Product; | |
} | |
export interface ProductWithPrice extends Product { | |
prices?: Price[]; | |
} | |
export interface Subscription { | |
id: string; | |
user_id: string; | |
status?: Stripe.Subscription.Status; | |
metadata?: Stripe.Metadata; | |
price_id?: string; | |
quantity?: number; | |
cancel_at_period_end?: boolean; | |
created: string; | |
current_period_start: string; | |
current_period_end: string; | |
ended_at?: string; | |
cancel_at?: string; | |
canceled_at?: string; | |
trial_start?: string; | |
trail_end?: string; | |
prices?: Price; | |
} | |
----------------------------- | |
useUser.tsx | |
----------------------------- | |
import { createContext, useContext, useEffect, useState } from "react"; | |
import { User } from "@supabase/auth-helpers-nextjs"; | |
import { | |
useSessionContext, | |
useUser as useSupaUser, | |
} from "@supabase/auth-helpers-react"; | |
import { Subscription, UserDetails } from "@/types"; | |
type UserContextType = { | |
accessToken: string | null; | |
user: User | null; | |
userDetails: UserDetails | null; | |
isLoading: boolean; | |
subscription: Subscription | null; | |
}; | |
export const UserContext = createContext<UserContextType | undefined>( | |
undefined | |
); | |
export interface Props { | |
// like it can be general prop name | |
[propName: string]: any; | |
} | |
export const MyUserContextProvider = (props: Props) => { | |
const { | |
session, | |
isLoading: isUserLoading, | |
supabaseClient: supabase, | |
} = useSessionContext(); | |
const user = useSupaUser(); | |
const accessToken = session?.access_token ?? null; | |
const [isLoadingData, setIsLoadingData] = useState(false); | |
const [userDetails, setUserDetails] = useState<UserDetails | null>(null); | |
const [subscription, setSubscription] = useState<Subscription | null>(null); | |
// function to fetch the user detail | |
const getUserDetails = () => supabase.from("users").select("*").single(); | |
// funtion to fetch the subscription details | |
const getSubscription = () => | |
supabase | |
.from("subscriptions") | |
.select("*, prices(*, products(*))") | |
.in("status", ["trialing", "active"]) | |
.single(); | |
useEffect(() => { | |
if (user && !isLoadingData && !userDetails && !subscription) { | |
setIsLoadingData(true); | |
Promise.allSettled([getUserDetails(), getSubscription()]).then( | |
(results) => { | |
const userDetailsPromise = results[0]; | |
const subscriptionDetailPromise = results[1]; | |
if (userDetailsPromise.status === "fulfilled") { | |
setUserDetails(userDetailsPromise.value.data as UserDetails); | |
} | |
if (subscriptionDetailPromise.status === "fulfilled") { | |
setSubscription( | |
subscriptionDetailPromise.value.data as Subscription | |
); | |
} | |
setIsLoadingData(false); | |
} | |
); | |
} else if (!user && !isLoadingData && !isUserLoading) { | |
setUserDetails(null); | |
setSubscription(null); | |
} | |
}, [user, isUserLoading]); | |
const value = { | |
accessToken, | |
user, | |
userDetails, | |
isLoading: isUserLoading || isLoadingData, | |
subscription, | |
}; | |
return <UserContext.Provider value={value} {...props} />; | |
}; | |
export const userUser = () => { | |
const context = useContext(UserContext); | |
if (context === undefined) { | |
throw new Error("useUser must be used within a MyUserContextProvider"); | |
} | |
return context; | |
}; | |
----------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment