mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
Implement setup task for worker admin environment and add new database schema and snapshot files
This commit is contained in:
@@ -1,28 +1,177 @@
|
||||
import { FlowgladProvider } from "@flowglad/react"
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from "react"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
|
||||
type UsageSnapshot = {
|
||||
used: number
|
||||
limit: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
type StorageUsage = {
|
||||
archives?: UsageSnapshot
|
||||
storage?: UsageSnapshot
|
||||
}
|
||||
|
||||
type BillingStatus = {
|
||||
isGuest: boolean
|
||||
isPaid: boolean
|
||||
planName: string
|
||||
currentPeriodEnd?: string
|
||||
cancelAtPeriodEnd?: boolean
|
||||
isLoading: boolean
|
||||
error?: string
|
||||
usage?: StorageUsage
|
||||
}
|
||||
|
||||
type BillingContextValue = BillingStatus & {
|
||||
refresh: () => Promise<void>
|
||||
openCheckout: () => Promise<void>
|
||||
openPortal: () => Promise<void>
|
||||
}
|
||||
|
||||
const BillingContext = createContext<BillingContextValue | null>(null)
|
||||
|
||||
export function useBilling() {
|
||||
const context = useContext(BillingContext)
|
||||
if (!context) {
|
||||
return {
|
||||
isGuest: true,
|
||||
isPaid: false,
|
||||
planName: "Guest",
|
||||
isLoading: false,
|
||||
usage: undefined,
|
||||
refresh: async () => {},
|
||||
openCheckout: async () => {},
|
||||
openPortal: async () => {},
|
||||
} as BillingContextValue
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
type BillingProviderProps = {
|
||||
children: React.ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function BillingProvider({ children }: BillingProviderProps) {
|
||||
const flowgladEnabled = import.meta.env.VITE_FLOWGLAD_ENABLED === "true"
|
||||
|
||||
// Skip billing entirely when Flowglad isn't configured
|
||||
if (!flowgladEnabled) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
const { data: session, isPending } = authClient.useSession()
|
||||
const [status, setStatus] = useState<BillingStatus>({
|
||||
isGuest: true,
|
||||
isPaid: false,
|
||||
planName: "Guest",
|
||||
isLoading: true,
|
||||
usage: undefined,
|
||||
})
|
||||
|
||||
// Don't load billing until we know auth state
|
||||
if (isPending) {
|
||||
return <>{children}</>
|
||||
const fetchBillingStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/stripe/billing")
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as Partial<BillingStatus>
|
||||
setStatus({
|
||||
isGuest: data.isGuest ?? true,
|
||||
isPaid: data.isPaid ?? false,
|
||||
planName: data.planName ?? "Guest",
|
||||
usage: data.usage,
|
||||
currentPeriodEnd: data.currentPeriodEnd,
|
||||
cancelAtPeriodEnd: data.cancelAtPeriodEnd,
|
||||
isLoading: false,
|
||||
})
|
||||
} else {
|
||||
setStatus((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: "Failed to load billing status",
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[billing] Failed to fetch status:", error)
|
||||
setStatus((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: "Failed to load billing status",
|
||||
}))
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending) return
|
||||
|
||||
if (!session?.user) {
|
||||
setStatus({
|
||||
isGuest: true,
|
||||
isPaid: false,
|
||||
planName: "Guest",
|
||||
isLoading: false,
|
||||
usage: undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fetchBillingStatus()
|
||||
}, [session?.user, isPending, fetchBillingStatus])
|
||||
|
||||
const openCheckout = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/stripe/checkout", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
successUrl: `${window.location.origin}/archive?billing=success`,
|
||||
cancelUrl: `${window.location.origin}/archive?billing=canceled`,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { url?: string }
|
||||
if (data.url) {
|
||||
window.location.href = data.url
|
||||
}
|
||||
} else {
|
||||
console.error("[billing] Failed to create checkout session")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[billing] Checkout error:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const openPortal = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/stripe/portal", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
returnUrl: window.location.href,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as { url?: string }
|
||||
if (data.url) {
|
||||
window.location.href = data.url
|
||||
}
|
||||
} else {
|
||||
console.error("[billing] Failed to create portal session")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[billing] Portal error:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const value: BillingContextValue = {
|
||||
...status,
|
||||
refresh: fetchBillingStatus,
|
||||
openCheckout,
|
||||
openPortal,
|
||||
}
|
||||
|
||||
return (
|
||||
<FlowgladProvider loadBilling={!!session?.user} serverRoute="/api/flowglad">
|
||||
{children}
|
||||
</FlowgladProvider>
|
||||
<BillingContext.Provider value={value}>{children}</BillingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user