diff --git a/apps/readest-app/.env b/apps/readest-app/.env index 82c9198b..1e4b4c25 100644 --- a/apps/readest-app/.env +++ b/apps/readest-app/.env @@ -7,3 +7,6 @@ NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThp NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv" NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw==" + +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64="cGtfdGVzdF81MVJmQmdLRTdSWW5pTWsxc0tDV2RUd2hMZzcySzk4eDRWcjlIdDdsRFBONngzcnpZYmhydGtNQnpDdzZKbHFaRVVITVp5eVNjVXhCZXVkcGppWTk0WXNHcDAweFlRRnRRaUU=" +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64="cGtfbGl2ZV81MVFYN3dRRU5ndjJFOUxQRHpZUlE5TlJJeTNjd09EZ1AzSkNFRHRPWlFtdFJWc3Brd053ZE1NNUpIVnVPTmJWcjZ3VGFCMUNZR1pJMmRPVWppTkY0bHJvVjAwalE4TkpkdWk=" diff --git a/apps/readest-app/next.config.mjs b/apps/readest-app/next.config.mjs index 217d16e9..aac1783c 100644 --- a/apps/readest-app/next.config.mjs +++ b/apps/readest-app/next.config.mjs @@ -8,11 +8,14 @@ if (isDev) { initOpenNextCloudflareForDev(); } +const exportOutput = appPlatform !== 'web' && !isDev; + /** @type {import('next').NextConfig} */ const nextConfig = { // Ensure Next.js uses SSG instead of SSR // https://nextjs.org/docs/pages/building-your-application/deploying/static-exports - output: appPlatform === 'web' || isDev ? undefined : 'export', + output: exportOutput ? 'export' : undefined, + pageExtensions: exportOutput ? ['jsx', 'tsx'] : ['js', 'jsx', 'ts', 'tsx'], // Note: This feature is required to use the Next.js Image component in SSG mode. // See https://nextjs.org/docs/messages/export-image-api for different workarounds. images: { diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index b9928827..569a1760 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -43,6 +43,7 @@ "@ducanh2912/next-pwa": "^10.2.9", "@fabianlars/tauri-plugin-oauth": "2", "@opennextjs/cloudflare": "^1.3.1", + "@stripe/stripe-js": "^7.4.0", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", "@supabase/supabase-js": "^2.49.10", @@ -83,6 +84,7 @@ "react-responsive": "^10.0.0", "react-window": "^1.8.11", "semver": "^7.7.1", + "stripe": "^18.2.1", "tinycolor2": "^1.6.0", "zustand": "5.0.1" }, diff --git a/apps/readest-app/src/app/api/stripe/check/route.ts b/apps/readest-app/src/app/api/stripe/check/route.ts new file mode 100644 index 00000000..7509084e --- /dev/null +++ b/apps/readest-app/src/app/api/stripe/check/route.ts @@ -0,0 +1,36 @@ +import Stripe from 'stripe'; +import { NextResponse } from 'next/server'; +import { getStripe } from '@/libs/stripe/server'; +import { createSubscription } from '@/utils/stripe'; +import { validateUserAndToken } from '@/utils/access'; + +export async function POST(request: Request) { + const { sessionId } = await request.json(); + + const { user, token } = await validateUserAndToken(request.headers.get('authorization')); + if (!user || !token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 403 }); + } + + try { + const stripe = getStripe(); + const session = await stripe.checkout.sessions.retrieve(sessionId); + + if (session.payment_status === 'paid') { + const customerId = session.customer as string; + const subscriptionId = session.subscription as string; + await createSubscription(user.id, customerId, subscriptionId); + } + + return NextResponse.json({ session }); + } catch (error) { + if (error instanceof Stripe.errors.StripeError) { + console.error('Stripe error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 }, + ); + } +} diff --git a/apps/readest-app/src/app/api/stripe/checkout/route.ts b/apps/readest-app/src/app/api/stripe/checkout/route.ts new file mode 100644 index 00000000..2c3667cc --- /dev/null +++ b/apps/readest-app/src/app/api/stripe/checkout/route.ts @@ -0,0 +1,65 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getStripe } from '@/libs/stripe/server'; +import { validateUserAndToken } from '@/utils/access'; +import { supabase } from '@/utils/supabase'; + +export async function POST(request: NextRequest) { + const { priceId, metadata = {} } = await request.json(); + + const { user, token } = await validateUserAndToken(request.headers.get('authorization')); + if (!user || !token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 403 }); + } + + const enhancedMetadata = { + ...metadata, + userId: user.id, + }; + + try { + const { data: customerData } = await supabase + .from('customers') + .select('stripe_customer_id') + .eq('user_id', user.id) + .single(); + + let customerId; + if (!customerData?.stripe_customer_id) { + const stripe = getStripe(); + const customer = await stripe.customers.create({ + email: user.email, + metadata: { + userId: user.id, + }, + }); + customerId = customer.id; + await supabase.from('customers').insert({ + user_id: user.id, + stripe_customer_id: customerId, + }); + } else { + customerId = customerData.stripe_customer_id; + } + + const stripe = getStripe(); + const session = await stripe.checkout.sessions.create({ + customer: customerId, + mode: 'subscription', + payment_method_types: ['card'], + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + metadata: enhancedMetadata, + success_url: `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${request.headers.get('origin')}/user`, + }); + + return NextResponse.json({ sessionId: session.id, url: session.url }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: 'Error creating checkout session' }, { status: 500 }); + } +} diff --git a/apps/readest-app/src/app/api/stripe/plans/route.ts b/apps/readest-app/src/app/api/stripe/plans/route.ts new file mode 100644 index 00000000..68d76cd7 --- /dev/null +++ b/apps/readest-app/src/app/api/stripe/plans/route.ts @@ -0,0 +1,39 @@ +import Stripe from 'stripe'; +import { NextResponse } from 'next/server'; +import { getStripe } from '@/libs/stripe/server'; +import { UserPlan } from '@/types/user'; + +export async function GET() { + try { + const stripe = getStripe(); + const prices = await stripe.prices.list({ + expand: ['data.product'], + active: true, + type: 'recurring', + }); + + const plans = prices.data + .filter((price) => { + const product = price.product as Stripe.Product; + return product.active === true; + }) + .map((price) => { + const product = price.product as Stripe.Product & { + metadata: { plan: UserPlan }; + }; + return { + plan: product.metadata.plan, + price_id: price.id, + price: price.unit_amount, + currency: price.currency, + interval: price.recurring?.interval, + product: price.product, + }; + }); + + return NextResponse.json(plans); + } catch (error) { + console.error(error); + return NextResponse.json({ error: 'Error fetching subscription plans' }, { status: 500 }); + } +} diff --git a/apps/readest-app/src/app/api/stripe/portal/route.ts b/apps/readest-app/src/app/api/stripe/portal/route.ts new file mode 100644 index 00000000..459feac5 --- /dev/null +++ b/apps/readest-app/src/app/api/stripe/portal/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getStripe } from '@/libs/stripe/server'; +import { validateUserAndToken } from '@/utils/access'; +import { supabase } from '@/utils/supabase'; + +export async function POST(request: NextRequest) { + const { user, token } = await validateUserAndToken(request.headers.get('authorization')); + if (!user || !token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 403 }); + } + + try { + const { data: customerData } = await supabase + .from('customers') + .select('stripe_customer_id') + .eq('user_id', user.id) + .single(); + + if (!customerData?.stripe_customer_id) { + throw new Error('Customer not found'); + } + + const stripe = getStripe(); + const session = await stripe.billingPortal.sessions.create({ + customer: customerData.stripe_customer_id, + return_url: `${request.headers.get('origin')}/user`, + }); + + return NextResponse.json({ url: session.url }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: 'Error creating portal session' }, { status: 500 }); + } +} diff --git a/apps/readest-app/src/app/api/stripe/webhook/route.ts b/apps/readest-app/src/app/api/stripe/webhook/route.ts new file mode 100644 index 00000000..e4afeef8 --- /dev/null +++ b/apps/readest-app/src/app/api/stripe/webhook/route.ts @@ -0,0 +1,196 @@ +import Stripe from 'stripe'; +import { NextRequest, NextResponse } from 'next/server'; +import { getStripe } from '@/libs/stripe/server'; +import { createSubscription } from '@/utils/stripe'; +import { createSupabaseAdminClient } from '@/utils/supabase'; + +export async function POST(request: NextRequest) { + try { + const body = await request.text(); + const signature = request.headers.get('stripe-signature'); + + if (!signature) { + return NextResponse.json({ error: 'Missing Stripe signature' }, { status: 401 }); + } + + const stripe = getStripe(); + + let event; + try { + event = stripe.webhooks.constructEvent( + body, + signature, + process.env['STRIPE_WEBHOOK_SECRET']!, + ); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + console.error(`Webhook signature verification failed: ${message}`); + return NextResponse.json( + { + error: `Webhook signature verification failed: ${message}`, + }, + { status: 400 }, + ); + } + + switch (event.type) { + case 'checkout.session.completed': + const session = event.data.object; + const userId = session.metadata?.['userId']; + if (userId) { + if (session.mode === 'subscription') { + await handleSuccessfulSubscription(session, userId); + } else { + await handleSuccessfulPayment(session, userId); + } + } + break; + + case 'invoice.paid': + await handleSuccessfulInvoice(event.data.object); + break; + + case 'invoice.payment_failed': + await handleFailedInvoice(event.data.object); + break; + + case 'customer.subscription.deleted': + await handleSubscriptionCancelled(event.data.object); + break; + } + + return NextResponse.json({ received: true }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error('Webhook error:', message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +async function handleSuccessfulPayment(session: Stripe.Checkout.Session, userId: string) { + const supabase = createSupabaseAdminClient(); + await supabase.from('payments').insert({ + user_id: userId, + stripe_checkout_id: session.id, + amount: session.amount_total, + currency: session.currency, + status: 'completed', + payment_intent: session.payment_intent, + payment_method: session.payment_method_types?.[0] || 'unknown', + created_at: new Date().toISOString(), + }); +} + +async function handleSuccessfulSubscription(session: Stripe.Checkout.Session, userId: string) { + const customerId = session.customer as string; + const subscriptionId = session.subscription as string; + + await createSubscription(userId, customerId, subscriptionId); +} + +async function handleSuccessfulInvoice(invoice: Stripe.Invoice) { + const customerId = invoice.customer; + const subscriptionId = invoice.parent?.subscription_details?.subscription; + + if (!subscriptionId) { + return; + } + + const supabase = createSupabaseAdminClient(); + const { data: customerData } = await supabase + .from('customers') + .select('user_id') + .eq('stripe_customer_id', customerId) + .single(); + + if (!customerData?.user_id) { + console.error('Customer not found:', customerId); + return; + } + + await supabase + .from('subscriptions') + .update({ + status: 'active', + current_period_end: new Date(invoice.lines.data[0]!.period.end * 1000).toISOString(), + }) + .eq('stripe_subscription_id', subscriptionId); + + await supabase + .from('plans') + .update({ + status: 'active', + }) + .eq('user_id', customerData.user_id); +} + +async function handleFailedInvoice(invoice: Stripe.Invoice) { + const customerId = invoice.customer; + const subscriptionId = invoice.parent?.subscription_details?.subscription; + + if (!subscriptionId) { + return; + } + + const supabase = createSupabaseAdminClient(); + const { data: customerData } = await supabase + .from('customers') + .select('user_id') + .eq('stripe_customer_id', customerId) + .single(); + + if (!customerData?.user_id) { + console.error('Customer not found:', customerId); + return; + } + + await supabase + .from('subscriptions') + .update({ + status: 'past_due', + }) + .eq('stripe_subscription_id', subscriptionId); + + await supabase + .from('plans') + .update({ + status: 'past_due', + }) + .eq('id', customerData.user_id); +} + +async function handleSubscriptionCancelled(subscription: Stripe.Subscription) { + const subscriptionId = subscription.id; + + const supabase = createSupabaseAdminClient(); + await supabase + .from('subscriptions') + .update({ + status: 'cancelled', + cancelled_at: new Date().toISOString(), + }) + .eq('stripe_subscription_id', subscriptionId); + + const { data: subscriptionData } = await supabase + .from('subscriptions') + .select('user_id') + .eq('stripe_subscription_id', subscriptionId) + .single(); + + if (subscriptionData?.user_id) { + await supabase + .from('plans') + .update({ + plan: 'free', + status: 'cancelled', + }) + .eq('id', subscriptionData.user_id); + } +} + +// This is needed to parse the body as a stream for the webhook signature verification +export const config = { + api: { + bodyParser: false, + }, +}; diff --git a/apps/readest-app/src/libs/stripe/client.ts b/apps/readest-app/src/libs/stripe/client.ts new file mode 100644 index 00000000..ad6f583d --- /dev/null +++ b/apps/readest-app/src/libs/stripe/client.ts @@ -0,0 +1,14 @@ +import { loadStripe, Stripe } from '@stripe/stripe-js'; + +let stripePromise: Promise; + +export const getStripe = () => { + if (!stripePromise) { + const publishableKey = + process.env.NODE_ENV === 'production' + ? process.env['NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64'] + : process.env['NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64']; + stripePromise = loadStripe(atob(publishableKey!)); + } + return stripePromise; +}; diff --git a/apps/readest-app/src/libs/stripe/server.ts b/apps/readest-app/src/libs/stripe/server.ts new file mode 100644 index 00000000..c649d4ec --- /dev/null +++ b/apps/readest-app/src/libs/stripe/server.ts @@ -0,0 +1,16 @@ +import Stripe from 'stripe'; + +let stripe: Stripe | null; + +export const getStripe = () => { + if (!stripe) { + const stripeSecretKey = + process.env.NODE_ENV === 'production' + ? process.env['STRIPE_SECRET_KEY'] + : process.env['STRIPE_SECRET_KEY_DEV']; + stripe = new Stripe(stripeSecretKey!, { + httpClient: Stripe.createFetchHttpClient(), + }); + } + return stripe; +}; diff --git a/apps/readest-app/src/middleware.ts b/apps/readest-app/src/middleware.ts new file mode 100644 index 00000000..b025f12b --- /dev/null +++ b/apps/readest-app/src/middleware.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const allowedOrigins = [ + 'https://web.readest.com', + 'https://tauri.localhost', + 'http://tauri.localhost', + 'tauri://localhost', + 'http://localhost:3000', + 'http://localhost:3001', +]; + +const corsOptions = { + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', +}; + +export function middleware(request: NextRequest) { + const origin = request.headers.get('origin') ?? ''; + const isAllowedOrigin = allowedOrigins.includes(origin); + + if (request.method === 'OPTIONS') { + const preflightHeaders = new Headers({ + ...corsOptions, + ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }), + }); + + return new NextResponse(null, { + status: 200, + headers: preflightHeaders, + }); + } + + const response = NextResponse.next(); + + if (isAllowedOrigin) { + response.headers.set('Access-Control-Allow-Origin', origin); + } + + Object.entries(corsOptions).forEach(([key, value]) => { + response.headers.set(key, value); + }); + + return response; +} + +export const config = { + matcher: ['/api/stripe/:path*'], +}; diff --git a/apps/readest-app/src/pages/api/deepl/translate.ts b/apps/readest-app/src/pages/api/deepl/translate.ts index 47eb1498..05fd0f34 100644 --- a/apps/readest-app/src/pages/api/deepl/translate.ts +++ b/apps/readest-app/src/pages/api/deepl/translate.ts @@ -1,9 +1,8 @@ import crypto from 'crypto'; -import { supabase } from '@/utils/supabase'; import { NextApiRequest, NextApiResponse } from 'next'; import { corsAllMethods, runMiddleware } from '@/utils/cors'; import { getCloudflareContext } from '@opennextjs/cloudflare'; -import { getDailyTranslationPlanData, getUserPlan } from '@/utils/access'; +import { getDailyTranslationPlanData, getUserPlan, validateUserAndToken } from '@/utils/access'; import { ErrorCodes } from '@/services/translators'; const DEFAULT_DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate'; @@ -19,19 +18,6 @@ interface CloudflareEnv { TRANSLATIONS_KV?: KVNamespace; } -const getUserAndToken = async (authHeader: string | undefined) => { - if (!authHeader) return {}; - - const token = authHeader.replace('Bearer ', ''); - const { - data: { user }, - error, - } = await supabase.auth.getUser(token); - - if (error || !user) return {}; - return { user, token }; -}; - const LANG_V2_V1_MAP: Record = { 'ZH-HANS': 'ZH', 'ZH-HANT': 'ZH-TW', @@ -58,7 +44,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const env = (getCloudflareContext().env || {}) as CloudflareEnv; const hasKVCache = !!env['TRANSLATIONS_KV']; - const { user, token } = await getUserAndToken(req.headers['authorization']); + const { user, token } = await validateUserAndToken(req.headers['authorization']); const { DEEPL_PRO_API, DEEPL_FREE_API } = process.env; const deepFreeApiUrl = DEEPL_FREE_API || DEFAULT_DEEPL_FREE_API; const deeplProApiUrl = DEEPL_PRO_API || DEFAULT_DEEPL_PRO_API; diff --git a/apps/readest-app/src/pages/api/storage/download.ts b/apps/readest-app/src/pages/api/storage/download.ts index 3cf6f58d..11620870 100644 --- a/apps/readest-app/src/pages/api/storage/download.ts +++ b/apps/readest-app/src/pages/api/storage/download.ts @@ -1,20 +1,8 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase, createSupabaseClient } from '@/utils/supabase'; +import { createSupabaseClient } from '@/utils/supabase'; import { corsAllMethods, runMiddleware } from '@/utils/cors'; import { getDownloadSignedUrl } from '@/utils/object'; - -const getUserAndToken = async (authHeader: string | undefined) => { - if (!authHeader) return {}; - - const token = authHeader.replace('Bearer ', ''); - const { - data: { user }, - error, - } = await supabase.auth.getUser(token); - - if (error || !user) return {}; - return { user, token }; -}; +import { validateUserAndToken } from '@/utils/access'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { await runMiddleware(req, res, corsAllMethods); @@ -24,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } try { - const { user, token } = await getUserAndToken(req.headers['authorization']); + const { user, token } = await validateUserAndToken(req.headers['authorization']); if (!user || !token) { return res.status(403).json({ error: 'Not authenticated' }); } diff --git a/apps/readest-app/src/pages/api/storage/upload.ts b/apps/readest-app/src/pages/api/storage/upload.ts index 8977bc3e..48020726 100644 --- a/apps/readest-app/src/pages/api/storage/upload.ts +++ b/apps/readest-app/src/pages/api/storage/upload.ts @@ -1,22 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { supabase, createSupabaseClient } from '@/utils/supabase'; +import { createSupabaseClient } from '@/utils/supabase'; import { corsAllMethods, runMiddleware } from '@/utils/cors'; -import { getStoragePlanData } from '@/utils/access'; +import { getStoragePlanData, validateUserAndToken } from '@/utils/access'; import { getUploadSignedUrl } from '@/utils/object'; -const getUserAndToken = async (authHeader: string | undefined) => { - if (!authHeader) return {}; - - const token = authHeader.replace('Bearer ', ''); - const { - data: { user }, - error, - } = await supabase.auth.getUser(token); - - if (error || !user) return {}; - return { user, token }; -}; - export default async function handler(req: NextApiRequest, res: NextApiResponse) { await runMiddleware(req, res, corsAllMethods); @@ -25,7 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } try { - const { user, token } = await getUserAndToken(req.headers['authorization']); + const { user, token } = await validateUserAndToken(req.headers['authorization']); if (!user || !token) { return res.status(403).json({ error: 'Not authenticated' }); } @@ -74,11 +61,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } try { - const uploadUrl = await getUploadSignedUrl( - fileKey, - objSize, - 1800, - ); + const uploadUrl = await getUploadSignedUrl(fileKey, objSize, 1800); res.status(200).json({ uploadUrl, diff --git a/apps/readest-app/src/pages/api/sync.ts b/apps/readest-app/src/pages/api/sync.ts index 47f3326e..a221019a 100644 --- a/apps/readest-app/src/pages/api/sync.ts +++ b/apps/readest-app/src/pages/api/sync.ts @@ -1,13 +1,14 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { NextRequest, NextResponse } from 'next/server'; import { PostgrestError } from '@supabase/supabase-js'; -import { supabase, createSupabaseClient } from '@/utils/supabase'; +import { createSupabaseClient } from '@/utils/supabase'; import { BookDataRecord } from '@/types/book'; import { transformBookConfigToDB } from '@/utils/transform'; import { transformBookNoteToDB } from '@/utils/transform'; import { transformBookToDB } from '@/utils/transform'; import { runMiddleware, corsAllMethods } from '@/utils/cors'; import { SyncData, SyncResult, SyncType } from '@/libs/sync'; +import { validateUserAndToken } from '@/utils/access'; const transformsToDB = { books: transformBookToDB, @@ -25,31 +26,10 @@ type TableName = keyof typeof transformsToDB; type DBError = { table: TableName; error: PostgrestError }; -const getUserAndToken = async (req: NextRequest) => { - const authHeader = req.headers.get('authorization'); - if (!authHeader) return {}; - - const token = authHeader.replace('Bearer ', ''); - try { - const { - data: { user }, - error, - } = await supabase.auth.getUser(token); - if (error?.message === 'fetch failed') { - return { error: 'Network error' }; - } else if (error || !user) { - return { error: 'Not authenticated' }; - } - return { user, token }; - } catch { - return { error: 'Network error' }; - } -}; - export async function GET(req: NextRequest) { - const { user, token, error } = await getUserAndToken(req); - if (!user || !token || error) { - return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 }); + const { user, token } = await validateUserAndToken(req.headers.get('authorization')); + if (!user || !token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 403 }); } const supabase = createSupabaseClient(token); @@ -121,9 +101,9 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { - const { user, token, error } = await getUserAndToken(req); - if (!user || !token || error) { - return NextResponse.json({ error: error || 'Unknown error' }, { status: 401 }); + const { user, token } = await validateUserAndToken(req.headers.get('authorization')); + if (!user || !token) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 403 }); } const supabase = createSupabaseClient(token); diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index e5f756e9..8ffd5690 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -555,9 +555,9 @@ export const DEFAULT_STORAGE_QUOTA: UserStorageQuota = { }; export const DEFAULT_DAILY_TRANSLATION_QUOTA: UserDailyTranslationQuota = { - free: 50 * 1024, - plus: 500 * 1024, - pro: 1024 * 1024, + free: 10 * 1024, + plus: 50 * 1024, + pro: 200 * 1024, }; export const DOUBLE_CLICK_INTERVAL_THRESHOLD_MS = 250; diff --git a/apps/readest-app/src/utils/access.ts b/apps/readest-app/src/utils/access.ts index fdea5c7a..5c13128c 100644 --- a/apps/readest-app/src/utils/access.ts +++ b/apps/readest-app/src/utils/access.ts @@ -76,7 +76,7 @@ export const getUserID = async (): Promise => { return data?.session?.user?.id ?? null; }; -export const validateUserAndToken = async (authHeader: string | undefined) => { +export const validateUserAndToken = async (authHeader: string | null | undefined) => { if (!authHeader) return {}; const token = authHeader.replace('Bearer ', ''); diff --git a/apps/readest-app/src/utils/stripe.ts b/apps/readest-app/src/utils/stripe.ts new file mode 100644 index 00000000..faf59ce1 --- /dev/null +++ b/apps/readest-app/src/utils/stripe.ts @@ -0,0 +1,65 @@ +import Stripe from 'stripe'; +import { getStripe } from '@/libs/stripe/server'; +import { createSupabaseAdminClient } from './supabase'; +import { UserPlan } from '@/types/user'; + +export const createSubscription = async ( + userId: string, + customerId: string, + subscriptionId: string, +) => { + const stripe = getStripe(); + const supabase = createSupabaseAdminClient(); + + const subscription = await stripe.subscriptions.retrieve(subscriptionId, { + expand: ['items.data.price.product'], + }); + const subscriptionItem = subscription.items.data[0]!; + const priceId = subscriptionItem.price.id; + const product = subscriptionItem.price.product as Stripe.Product & { + metadata: { plan: UserPlan }; + }; + const plan = product.metadata['plan'] || 'free'; + + try { + const { data: existingSubscription } = await supabase + .from('subscriptions') + .select('*') + .eq('stripe_subscription_id', subscriptionId) + .single(); + + const period_start = new Date(subscriptionItem.current_period_start * 1000).toISOString(); + const period_end = new Date(subscriptionItem.current_period_end * 1000).toISOString(); + if (existingSubscription) { + await supabase + .from('subscriptions') + .update({ + status: subscription.status, + current_period_start: period_start, + current_period_end: period_end, + }) + .eq('id', existingSubscription.id); + } else { + await supabase.from('subscriptions').insert({ + user_id: userId, + stripe_customer_id: customerId, + stripe_subscription_id: subscriptionId, + stripe_price_id: priceId, + status: subscription.status, + current_period_start: period_start, + current_period_end: period_end, + created_at: new Date().toISOString(), + }); + } + } catch (error) { + console.error('Error checking existing subscription:', error); + } + + await supabase + .from('plans') + .update({ + plan: plan, + status: subscription.status, + }) + .eq('id', userId); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13412ece..e785b2b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@opennextjs/cloudflare': specifier: ^1.3.1 version: 1.3.1(wrangler@4.21.0) + '@stripe/stripe-js': + specifier: ^7.4.0 + version: 7.4.0 '@supabase/auth-ui-react': specifier: ^0.4.7 version: 0.4.7(@supabase/supabase-js@2.49.10) @@ -173,6 +176,9 @@ importers: semver: specifier: ^7.7.1 version: 7.7.2 + stripe: + specifier: ^18.2.1 + version: 18.2.1(@types/node@22.15.31) tinycolor2: specifier: ^1.6.0 version: 1.6.0 @@ -329,24 +335,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.35.0': resolution: {integrity: sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.35.0': resolution: {integrity: sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.35.0': resolution: {integrity: sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.35.0': resolution: {integrity: sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg==} @@ -1551,138 +1561,163 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm64@1.1.0': resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.1.0': resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.1.0': resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.1.0': resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.1.0': resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.1.0': resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.1.0': resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm64@0.34.2': resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.2': resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.2': resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.2': resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-arm64@0.34.2': resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.2': resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -1781,30 +1816,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.70': resolution: {integrity: sha512-wBTOllEYNfJCHOdZj9v8gLzZ4oY3oyPX8MSRvaxPm/s7RfEXxCyZ8OhJ5xAyicsDdbE5YBZqdmaaeP5+xKxvtg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.70': resolution: {integrity: sha512-GVUUPC8TuuFqHip0rxHkUqArQnlzmlXmTEBuXAWdgCv85zTCFH8nOHk/YCF5yo0Z2eOm8nOi90aWs0leJ4OE5Q==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.70': resolution: {integrity: sha512-/kvUa2lZRwGNyfznSn5t1ShWJnr/m5acSlhTV3eXECafObjl0VBuA1HJw0QrilLpb4Fe0VLywkpD1NsMoVDROQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.70': resolution: {integrity: sha512-aqlv8MLpycoMKRmds7JWCfVwNf1fiZxaU7JwJs9/ExjTD8lX2KjsO7CTeAj5Cl4aEuzxUWbJPUUE2Qu9cZ1vfg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.70': resolution: {integrity: sha512-Q9QU3WIpwBTVHk4cPfBjGHGU4U0llQYRXgJtFtYqqGNEOKVN4OT6PQ+ve63xwIPODMpZ0HHyj/KLGc9CWc3EtQ==} @@ -1839,24 +1879,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@15.3.3': resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@15.3.3': resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@15.3.3': resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@15.3.3': resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==} @@ -2007,46 +2051,55 @@ packages: resolution: {integrity: sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.28.0': resolution: {integrity: sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.28.0': resolution: {integrity: sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.28.0': resolution: {integrity: sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.28.0': resolution: {integrity: sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.28.0': resolution: {integrity: sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.28.0': resolution: {integrity: sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.28.0': resolution: {integrity: sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.28.0': resolution: {integrity: sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.28.0': resolution: {integrity: sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==} @@ -2492,6 +2545,10 @@ packages: '@stitches/core@1.2.8': resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==} + '@stripe/stripe-js@7.4.0': + resolution: {integrity: sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ==} + engines: {node: '>=12.16'} + '@supabase/auth-js@2.69.1': resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==} @@ -2564,30 +2621,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.6.0': resolution: {integrity: sha512-lAEzj40VGrn/dPtly9E2dfng82t1xuEdoq0fwjEk6J4oeoKJwD/Kss7vysQHSHq36nKGFoO7gpKa5mxisBGFog==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.6.0': resolution: {integrity: sha512-IWaMX0tQQHFzOudJ0OyC3wamFUr/Qje3qwPSu2okSiHmP1+5enSN3WZZJV5vZ979n/Mak2TFQ8+ivPeSW2fn6g==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.6.0': resolution: {integrity: sha512-w/zlc2H3c9D4GEjAQOOh/DT701SRT9L8IvuhEMwdWWG6BQEH/+u5tgYR3sVJNDv9WXGNxTCiFV0bd/6pgreC1Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.6.0': resolution: {integrity: sha512-XSeuj7AWuP6Abg532wvzcgIXLlkZLhFXNDRISp7Zeb/ZNHrYTd4BcO88IPb5LVw5vVmvV5r6iTxDd9oVw/FrmA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.6.0': resolution: {integrity: sha512-gvmpQkj2vsU3fvJNgSzrX2mw/0DRnkfrg+1GH3wHsCoZq0/xVXG1mXZAxDg4r6e4yCeVuM6Xv2OHpF+Az9+cXA==} @@ -5312,10 +5374,6 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} - side-channel@1.1.0: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} @@ -5448,6 +5506,15 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stripe@18.2.1: + resolution: {integrity: sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -9314,6 +9381,8 @@ snapshots: '@stitches/core@1.2.8': {} + '@stripe/stripe-js@7.4.0': {} + '@supabase/auth-js@2.69.1': dependencies: '@supabase/node-fetch': 2.6.15 @@ -11205,7 +11274,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 ipaddr.js@1.9.1: {} @@ -12359,13 +12428,6 @@ snapshots: object-inspect: 1.13.3 side-channel-map: 1.0.1 - side-channel@1.0.6: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.2.4 - object-inspect: 1.13.3 - side-channel@1.1.0: dependencies: es-errors: 1.3.0 @@ -12459,7 +12521,7 @@ snapshots: internal-slot: 1.0.7 regexp.prototype.flags: 1.5.3 set-function-name: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 string.prototype.repeat@1.0.0: dependencies: @@ -12515,6 +12577,12 @@ snapshots: strip-json-comments@3.1.1: {} + stripe@18.2.1(@types/node@22.15.31): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 22.15.31 + strnum@1.0.5: {} styled-jsx@5.1.6(@babel/core@7.26.7)(react@19.0.0):