diff --git a/.gitignore b/.gitignore index 32c5e88..db544e5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,12 +30,17 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* +certificates + # env files (can opt-in for committing if needed) .env* # vercel .vercel +# claude code +.claude + # typescript *.tsbuildinfo next-env.d.ts @@ -53,4 +58,4 @@ next-env.d.ts .wrangler .dev.vars .dev.vars* -!.dev.vars.example \ No newline at end of file +!.dev.vars.example diff --git a/biome.json b/biome.json index e0d7566..3a8be83 100644 --- a/biome.json +++ b/biome.json @@ -23,6 +23,7 @@ "src/components/tailark/*.tsx", "src/app/[[]locale]/preview/**", "src/payment/types.ts", + "src/credits/types.ts", "src/types/index.d.ts", "public/sw.js" ] @@ -81,6 +82,7 @@ "src/components/tailark/*.tsx", "src/app/[[]locale]/preview/**", "src/payment/types.ts", + "src/credits/types.ts", "src/types/index.d.ts", "public/sw.js" ] diff --git a/env.example b/env.example index e158843..6422e72 100644 --- a/env.example +++ b/env.example @@ -63,6 +63,7 @@ STORAGE_PUBLIC_URL="" # https://mksaas.com/docs/payment#setup # Get Stripe key and secret from https://dashboard.stripe.com # ----------------------------------------------------------------------------- +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" STRIPE_SECRET_KEY="" STRIPE_WEBHOOK_SECRET="" # Pro plan - monthly subscription @@ -71,6 +72,14 @@ NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY="" NEXT_PUBLIC_STRIPE_PRICE_PRO_YEARLY="" # Lifetime plan - one-time payment NEXT_PUBLIC_STRIPE_PRICE_LIFETIME="" +# Credit package - basic +NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC="" +# Credit package - standard +NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD="" +# Credit package - premium +NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM="" +# Credit package - enterprise +NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE="" # ----------------------------------------------------------------------------- # Configurations @@ -153,6 +162,12 @@ NEXT_PUBLIC_AFFILIATE_PROMOTEKIT_ID="" NEXT_PUBLIC_TURNSTILE_SITE_KEY="" TURNSTILE_SECRET_KEY="" +# ----------------------------------------------------------------------------- +# Inngest +# https://mksaas.com/docs/jobs#setup +# ----------------------------------------------------------------------------- +INNGEST_SIGNING_KEY="" + # ----------------------------------------------------------------------------- # AI # https://mksaas.com/docs/ai diff --git a/messages/en.json b/messages/en.json index 7429307..baa595b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -28,7 +28,21 @@ "save": "Save", "loading": "Loading...", "cancel": "Cancel", - "logoutFailed": "Failed to log out" + "logoutFailed": "Failed to log out", + "table": { + "totalRecords": "Total {count} records", + "noResults": "No results", + "loading": "Loading...", + "columns": "Columns", + "rowsPerPage": "Rows per page", + "page": "Page", + "firstPage": "First Page", + "lastPage": "Last Page", + "nextPage": "Next Page", + "previousPage": "Previous Page", + "ascending": "Asc", + "descending": "Desc" + } }, "PricingPage": { "title": "Pricing", @@ -99,6 +113,24 @@ } } }, + "CreditPackages": { + "basic": { + "name": "Basic", + "description": "Basic credits package description" + }, + "standard": { + "name": "Standard", + "description": "Standard credits package description" + }, + "premium": { + "name": "Premium", + "description": "Premium credits package description" + }, + "enterprise": { + "name": "Enterprise", + "description": "Enterprise credits package description" + } + }, "NotFoundPage": { "title": "404", "message": "Sorry, the page you are looking for does not exist.", @@ -432,6 +464,7 @@ "avatar": { "dashboard": "Dashboard", "billing": "Billing", + "credits": "Credits", "settings": "Settings" } }, @@ -457,14 +490,6 @@ "banReason": "Ban Reason", "banExpires": "Ban Expires" }, - "noResults": "No results", - "firstPage": "First Page", - "lastPage": "Last Page", - "nextPage": "Next Page", - "previousPage": "Previous Page", - "rowsPerPage": "Rows per page", - "page": "Page", - "loading": "Loading...", "admin": "Admin", "user": "User", "email": { @@ -474,8 +499,8 @@ "emailCopied": "Email copied to clipboard", "banned": "Banned", "active": "Active", - "joined": "Joined", - "updated": "Updated", + "joined": "Joined at", + "updated": "Updated at", "ban": { "reason": "Ban Reason", "reasonPlaceholder": "Enter the reason for banning this user", @@ -546,6 +571,7 @@ "createCustomerPortalFailed": "Failed to open Stripe customer portal" }, "price": "Price:", + "periodStartDate": "Period start date:", "nextBillingDate": "Next billing date:", "trialEnds": "Trial ends:", "freePlanMessage": "You are currently on the free plan with limited features", @@ -554,7 +580,75 @@ "manageBilling": "Manage Billing", "upgradePlan": "Upgrade Plan", "retry": "Retry", - "errorMessage": "Failed to get data" + "errorMessage": "Failed to get data", + "paymentSuccess": "Payment successful" + }, + "credits": { + "title": "Credits", + "description": "Manage your credit transactions", + "balance": { + "title": "Credit Balance", + "description": "Your credit balance", + "credits": "Credits", + "creditsDescription": "You have {credits} credits", + "creditsExpired": "Credits expired", + "creditsAdded": "Credits have been added to your account", + "viewTransactions": "View Credit Transactions", + "retry": "Retry", + "subscriptionCredits": "{credits} credits from subscription this month", + "lifetimeCredits": "{credits} credits from lifetime plan this month", + "expiringCredits": "{credits} credits expiring on {date}" + }, + "packages": { + "title": "Credit Packages", + "description": "Purchase additional credits to use our services", + "purchase": "Purchase", + "processing": "Processing...", + "popular": "Popular", + "completePurchase": "Complete Your Purchase", + "failedToFetchCredits": "Failed to fetch credits", + "failedToCreatePaymentIntent": "Failed to create payment intent", + "failedToInitiatePayment": "Failed to initiate payment", + "cancel": "Cancel", + "purchaseFailed": "Purchase credits failed", + "checkoutFailed": "Failed to create checkout session", + "loading": "Loading...", + "pay": "Pay" + }, + "transactions": { + "title": "Credit Transactions", + "error": "Failed to get credit transactions", + "search": "Search credit transactions...", + "paymentIdCopied": "Payment ID copied to clipboard", + "columns": { + "columns": "Columns", + "id": "ID", + "type": "Type", + "description": "Description", + "amount": "Amount", + "remainingAmount": "Remaining Amount", + "paymentId": "Payment ID", + "expirationDate": "Expiration Date", + "expirationDateProcessedAt": "Expiration Date Processed At", + "createdAt": "Created At", + "updatedAt": "Updated At" + }, + "types": { + "MONTHLY_REFRESH": "Monthly Refresh", + "REGISTER_GIFT": "Register Gift", + "PURCHASE": "Purchased Credits", + "USAGE": "Consumed Credits", + "EXPIRE": "Expired Credits", + "SUBSCRIPTION_RENEWAL": "Subscription Renewal", + "LIFETIME_MONTHLY": "Lifetime Monthly" + }, + "detailViewer": { + "title": "Credit Transaction Detail", + "close": "Close" + }, + "expired": "Expired", + "never": "Never" + } }, "notification": { "title": "Notification", diff --git a/messages/zh.json b/messages/zh.json index cf11293..fb75177 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -28,7 +28,21 @@ "saving": "保存中...", "loading": "加载中...", "cancel": "取消", - "logoutFailed": "退出失败" + "logoutFailed": "退出失败", + "table": { + "totalRecords": "总共 {count} 条记录", + "noResults": "无结果", + "loading": "加载中...", + "columns": "列", + "rowsPerPage": "每页行数", + "page": "页", + "firstPage": "第一页", + "lastPage": "最后一页", + "nextPage": "下一页", + "previousPage": "上一页", + "ascending": "升序", + "descending": "降序" + } }, "PricingPage": { "title": "价格", @@ -94,12 +108,29 @@ "feature-3": "专属支持", "feature-4": "企业级安全", "feature-5": "高级集成", - "feature-6": "自定义域名", - "feature-7": "自定义品牌", - "feature-8": "终身更新" + "feature-6": "自定义品牌", + "feature-7": "终身更新" } } }, + "CreditPackages": { + "basic": { + "name": "基础版", + "description": "基础版功能介绍放这里" + }, + "standard": { + "name": "标准版", + "description": "标准版功能介绍放这里" + }, + "premium": { + "name": "高级版", + "description": "高级版功能介绍放这里" + }, + "enterprise": { + "name": "企业版", + "description": "企业版功能介绍放这里" + } + }, "NotFoundPage": { "title": "404", "message": "抱歉,您正在寻找的页面不存在", @@ -376,13 +407,13 @@ "comparator": { "title": "Comparator 组件" }, - "faqs": { - "title": "FAQs 组件" + "faq": { + "title": "FAQ 组件" }, "login": { "title": "Login 组件" }, - "sign-up": { + "signup": { "title": "Signup 组件" }, "forgot-password": { @@ -433,6 +464,7 @@ "avatar": { "dashboard": "工作台", "billing": "账单", + "credits": "积分", "settings": "设置" } }, @@ -458,14 +490,6 @@ "banReason": "封禁原因", "banExpires": "封禁到期时间" }, - "noResults": "没有结果", - "firstPage": "第一页", - "lastPage": "最后一页", - "nextPage": "下一页", - "previousPage": "上一页", - "rowsPerPage": "每页行数", - "page": "页", - "loading": "加载中...", "admin": "管理员", "user": "用户", "email": { @@ -547,6 +571,7 @@ "createCustomerPortalFailed": "打开Stripe客户界面失败" }, "price": "价格:", + "periodStartDate": "周期开始日期:", "nextBillingDate": "下次账单日期:", "trialEnds": "试用结束日期:", "freePlanMessage": "您当前使用的是功能有限的免费方案", @@ -555,7 +580,75 @@ "manageBilling": "管理账单", "upgradePlan": "升级方案", "retry": "重试", - "errorMessage": "获取数据失败" + "errorMessage": "获取数据失败", + "paymentSuccess": "支付成功" + }, + "credits": { + "title": "积分", + "description": "管理您的积分交易", + "balance": { + "title": "积分余额", + "description": "您的积分余额", + "credits": "积分", + "creditsDescription": "您有 {credits} 积分", + "creditsExpired": "积分已过期", + "creditsAdded": "积分已添加到您的账户", + "viewTransactions": "查看积分记录", + "retry": "重试", + "subscriptionCredits": "本月订阅获得 {credits} 积分", + "lifetimeCredits": "本月终身会员获得 {credits} 积分", + "expiringCredits": "{credits} 积分将在 {date} 过期" + }, + "packages": { + "title": "积分套餐", + "description": "购买积分以使用我们的更多服务", + "purchase": "购买", + "processing": "处理中...", + "popular": "热门", + "completePurchase": "请支付订单", + "failedToFetchCredits": "获取积分失败", + "failedToCreatePaymentIntent": "创建付款意向失败", + "failedToInitiatePayment": "发起付款失败", + "cancel": "取消", + "purchaseFailed": "购买积分失败", + "checkoutFailed": "创建支付会话失败", + "loading": "加载中...", + "pay": "支付" + }, + "transactions": { + "title": "积分记录", + "error": "获取积分交易记录失败", + "search": "搜索积分交易记录...", + "paymentIdCopied": "支付ID已复制到剪贴板", + "columns": { + "columns": "列", + "id": "ID", + "type": "类型", + "description": "描述", + "amount": "金额", + "remainingAmount": "剩余金额", + "paymentId": "支付编号", + "expirationDate": "过期日期", + "expirationDateProcessedAt": "过期处理时间", + "createdAt": "创建时间", + "updatedAt": "更新时间" + }, + "types": { + "MONTHLY_REFRESH": "每月赠送", + "REGISTER_GIFT": "注册赠送", + "PURCHASE": "购买积分", + "USAGE": "使用积分", + "EXPIRE": "过期积分", + "SUBSCRIPTION_RENEWAL": "订阅月度积分", + "LIFETIME_MONTHLY": "终身月度积分" + }, + "detailViewer": { + "title": "积分交易详情", + "close": "关闭" + }, + "expired": "已过期", + "never": "永不过期" + } }, "notification": { "title": "通知", diff --git a/package.json b/package.json index e2bec56..059cdce 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "fumadocs-core": "^15.5.3", "fumadocs-mdx": "^11.6.8", "fumadocs-ui": "^15.5.3", + "inngest": "^3.40.1", "input-otp": "^1.4.2", "lucide-react": "^0.483.0", "motion": "^12.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ffdacb..9609895 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,6 +224,9 @@ importers: fumadocs-ui: specifier: ^15.5.3 version: 15.5.3(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tailwindcss@4.0.14) + inngest: + specifier: ^3.40.1 + version: 3.40.1(express@5.0.1)(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.8.3) input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1055,6 +1058,9 @@ packages: cpu: [x64] os: [win32] + '@bufbuild/protobuf@2.6.0': + resolution: {integrity: sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==} + '@cloudflare/kv-asset-handler@0.4.0': resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} engines: {node: '>=18.0.0'} @@ -2229,6 +2235,15 @@ packages: '@formatjs/intl-localematcher@0.6.1': resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} + '@grpc/grpc-js@1.13.4': + resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -2354,10 +2369,16 @@ packages: cpu: [x64] os: [win32] + '@inngest/ai@0.1.5': + resolution: {integrity: sha512-Nj+Ee/O3EYmPIw+2eGryRh+b2TcqaZyL52RaO1/Cz707R/HrJVVDx8uRQo93gSeN4lMlaOluNrdleyM5M5wcQA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jpwilliams/waitgroup@2.1.1': + resolution: {integrity: sha512-0CxRhNfkvFCTLZBKGvKxY2FYtYW1yWhO2McLqBL0X5UWvYjIf9suH8anKW/DNutl369A75Ewyoh2iJMwBZ2tRg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -2382,6 +2403,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} @@ -2582,10 +2606,458 @@ packages: '@openpanel/web@1.0.1': resolution: {integrity: sha512-cVZ7Kr9SicczJ/RDIfEtZs8+1iGDzwkabVA/j3NqSl8VSucsC8m1+LVbjmCDzCJNnK4yVn6tEcc9PJRi2rtllw==} + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/auto-instrumentations-node@0.56.1': + resolution: {integrity: sha512-4cK0+unfkXRRbQQg2r9K3ki8JlE0j9Iw8+4DZEkChShAnmviiE+/JMgHGvK+VVcLrSlgV6BBHv4+ZTLukQwhkA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.57.2': + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2': + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.57.2': + resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.57.2': + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2': + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@1.30.1': + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.50.3': + resolution: {integrity: sha512-kotm/mRvSWUauudxcylc5YCDei+G/r+jnOH6q5S99aPLQ/Ms8D2yonMIxEJUILIPlthEmwLYxkw3ualWzMjm/A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.49.1': + resolution: {integrity: sha512-Vbj4BYeV/1K4Pbbfk+gQ8gwYL0w+tBeUwG88cOxnF7CLPO1XnskGV8Q3Gzut2Ah/6Dg17dBtlzEqL3UiFP2Z6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.45.1': + resolution: {integrity: sha512-T9POV9ccS41UjpsjLrJ4i0m8LfplBiN3dMeH9XZ2btiDrjoaWtDrst6tNb1avetBjkeshOuBp1EWKP22EVSr0g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1': + resolution: {integrity: sha512-RqnP0rK2hcKK1AKcmYvedLiL6G5TvFGiSUt2vI9wN0cCBdTt9Y9+wxxY19KoGxq7e9T/aHow6P5SUhCVI1sHvQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.14.1': + resolution: {integrity: sha512-ybO+tmH85pDO0ywTskmrMtZcccKyQr7Eb7wHy1keR2HFfx46SzZbjHo1AuGAX//Hook3gjM7+w211gJ2bwKe1Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.43.1': + resolution: {integrity: sha512-e/tMZYU1nc+k+J3259CQtqVZIPsPRSLNoAQbGEmSKrjLEY/KJSbpBZ17lu4dFVBzqoF1cZYIZxn9WPQxy4V9ng==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.44.2': + resolution: {integrity: sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.57.2': + resolution: {integrity: sha512-TR6YQA67cLSZzdxbf2SrbADJy2Y8eBW1+9mF15P0VK2MYcpdoUSmQTF1oMkBwa3B9NwqDFA2fq7wYTTutFQqaQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.43.1': + resolution: {integrity: sha512-rK5YWC22gmsLp2aEbaPk5F+9r6BFFZuc9GTnW/ErrWpz2XNHUgeFInoPDg4t+Trs8OttIfn8XwkfFkSKqhxanw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.44.1': + resolution: {integrity: sha512-4TXaqJK27QXoMqrt4+hcQ6rKFd8B6V4JfrTJKnqBmWR1cbaqd/uwyl9yxhNH1JEkyo8GaBfdpBC4ZE4FuUhPmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.43.1': + resolution: {integrity: sha512-TaMqP6tVx9/SxlY81dHlSyP5bWJIKq+K7vKfk4naB/LX4LBePPY3++1s0edpzH+RfwN+tEGVW9zTb9ci0up/lQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.46.1': + resolution: {integrity: sha512-HB8gD/9CNAKlTV+mdZehnFC4tLUtQ7e+729oGq88e4WipxzZxmMYuRwZ2vzOA9/APtq+MRkERJ9PcoDqSIjZ+g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.46.1': + resolution: {integrity: sha512-AN7OvlGlXmlvsgbLHs6dS1bggp6Fcki+GxgYZdSrb/DB692TyfjR7sVILaCe0crnP66aJuXsg9cge3hptHs9UA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.45.1': + resolution: {integrity: sha512-Zd6Go9iEa+0zcoA2vDka9r/plYKaT3BhD3ESIy4JNIzFWXeQBGbH3zZxQIsz0jbNTMEtonlymU7eTLeaGWiApA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.44.1': + resolution: {integrity: sha512-l4T/S7ByjpY5TCUPeDe1GPns02/5BpR0jroSMexyH3ZnXJt9PtYqx1IKAlOjaFEGEOQF2tGDsMi4PY5l+fSniQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.46.1': + resolution: {integrity: sha512-9AsCVUAHOqvfe2RM/2I0DsDnx2ihw1d5jIN4+Bly1YPFTJIbk4+bXjAkr9+X6PUfhiV5urQHZkiYYPU1Q4yzPA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.44.1': + resolution: {integrity: sha512-iexblTsT3fP0hHUz/M1mWr+Ylg3bsYN2En/jvKXZtboW3Qkvt17HrQJYTF9leVIkXAfN97QxAcTE99YGbQW7vQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2': + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagation-utils@0.30.16': + resolution: {integrity: sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1': + resolution: {integrity: sha512-9l0FVP3F4+Z6ax27vMzkmhZdNtxAbDqEfy7rduzya3xFLaRiJSvOpw6cru6Edl5LwO+WvgNui+VzHa9ViE8wCg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@1.12.0': + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.6.1': + resolution: {integrity: sha512-Djr31QCExVfWViaf9cGJnH+bUInD72p0GEfgDGgjCAztyvyji6WJvKjs6qmkpPN+Ig6KLk0ho2VgzT5Kdl4L2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.6.1': + resolution: {integrity: sha512-o4sLzx149DQXDmVa8pgjBDEEKOj9SuQnkSLbjUVOpQNnn10v0WNR6wLwh30mFsK26xOJ6SpqZBGKZiT7i5MjlA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.33.1': + resolution: {integrity: sha512-/aZJXI1rU6Eus04ih2vU0hxXAibXXMzH1WlDZ8bXcTJmhwmTY8cP392+6l7cWeMnTQOibBUz8UKV3nhcCBAefw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-node@0.57.2': + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@orama/orama@3.1.4': resolution: {integrity: sha512-7tTuIdkzgRscJ7sGHVsoK9GtXSpwbfrj3WYnuSu/SepXHhshYiQaOeXc/aeLh4MfgIre6tEs/caIop8wrhMi3g==} engines: {node: '>= 16.0.0'} @@ -2688,6 +3160,36 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -4813,9 +5315,18 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/aws-lambda@8.10.147': + resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} + + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cors@2.8.17': resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} @@ -4867,18 +5378,33 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + '@types/node@20.19.0': resolution: {integrity: sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==} + '@types/node@22.16.3': + resolution: {integrity: sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + '@types/pg@8.11.11': resolution: {integrity: sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==} '@types/pg@8.11.6': resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/react-dom@19.0.3': resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} peerDependencies: @@ -4887,6 +5413,12 @@ packages: '@types/react@19.0.9': resolution: {integrity: sha512-FedNTYgmMwSZmD1Sru/W1gJKuiYCN/3SuBkmZkcxX+FpO5zL76B22A9YNfAKg4HQO3Neh/30AiynP6BELdU0qQ==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -4964,6 +5496,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4998,6 +5535,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -5113,6 +5654,9 @@ packages: caniuse-lite@1.0.30001699: resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} + canonicalize@1.0.8: + resolution: {integrity: sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==} + canvas-confetti@1.9.3: resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} @@ -5143,6 +5687,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -5157,6 +5704,10 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -5238,6 +5789,9 @@ packages: cross-domain-utils@2.0.38: resolution: {integrity: sha512-zZfi3+2EIR9l4chrEiXI2xFleyacsJf8YMLR1eJ0Veb5FTMXeJ3DpxDjZkto2FhL/g717WSELqbptNSo85UJDw==} + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5728,6 +6282,9 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -5823,6 +6380,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.2.7: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} @@ -5904,6 +6465,9 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -5967,12 +6531,51 @@ packages: engines: {node: '>=16.x'} hasBin: true + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inngest@3.40.1: + resolution: {integrity: sha512-SC9Ly28i8NI+WymttE8Jk41L9r/wHXWOnlQoy7e7yoQyZI+R2C4S77DpFwzgEaqGT/H8puc1VDli84RoaffXBg==} + engines: {node: '>=14'} + peerDependencies: + '@sveltejs/kit': '>=1.27.3' + '@vercel/node': '>=2.15.9' + aws-lambda: '>=1.0.7' + express: '>=4.19.2' + fastify: '>=4.21.0' + h3: '>=1.8.1' + hono: '>=4.2.7' + koa: '>=2.14.2' + next: '>=12.0.0' + typescript: '>=4.7.2' + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/node': + optional: true + aws-lambda: + optional: true + express: + optional: true + fastify: + optional: true + h3: + optional: true + hono: + optional: true + koa: + optional: true + next: + optional: true + typescript: + optional: true + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -5999,6 +6602,10 @@ packages: is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -6080,6 +6687,9 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -6182,6 +6792,9 @@ packages: resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} engines: {node: '>= 12.0.0'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -6192,6 +6805,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -6444,6 +7060,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.0.1: resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} engines: {node: 20 || >=22} @@ -6475,6 +7094,9 @@ packages: mnemonist@0.38.3: resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + motion-dom@12.4.5: resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} @@ -6695,6 +7317,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -6744,6 +7369,9 @@ packages: pg-protocol@1.7.1: resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==} + pg-protocol@1.9.5: + resolution: {integrity: sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -6858,6 +7486,10 @@ packages: property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7081,6 +7713,14 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + resend@4.4.1: resolution: {integrity: sha512-FR22bzMW3VfoyZSBc8ScGo8ShrMWHmWB0G3FrispzWCnYSEEK5M7pyRvZtInKmM/09lsJETKc2q66mX+dXPSmg==} engines: {node: '>=18'} @@ -7088,6 +7728,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -7141,6 +7786,10 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + serialize-error-cjs@0.1.4: + resolution: {integrity: sha512-6a6dNqipzbCPlTFgztfNP2oG+IGcflMe/01zSzGrQcxGMKbIjOemBBD85pH92klWaJavAUWxAh9Z0aU28zxW6A==} + deprecated: Rolling release, please update to 0.2.0 + serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -7166,6 +7815,9 @@ packages: shiki@3.6.0: resolution: {integrity: sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -7264,6 +7916,10 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7310,6 +7966,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + swiper@11.2.5: resolution: {integrity: sha512-nG0kbIyBfeE2BPFt9nPUX03qUBF75o6+enzjIT/DfCmbh8ORlwhc4eZz1+4H/yseAgb3H+OoEYzmb64i0tYNnQ==} engines: {node: '>= 4.7.0'} @@ -7340,6 +8000,12 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + temporal-polyfill@0.2.5: + resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==} + + temporal-spec@0.2.4: + resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} + terser@5.16.9: resolution: {integrity: sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg==} engines: {node: '>=10'} @@ -7610,6 +8276,10 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -7618,6 +8288,14 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} @@ -7638,6 +8316,9 @@ packages: zod@3.22.3: resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.22.5: + resolution: {integrity: sha512-HqnGsCdVZ2xc0qWPLdO25WnseXThh0kEYKIdV5F/hTHO75hNZFp8thxSeHhiPrHZKrFTo1SOgkAj9po5bexZlw==} + zod@3.25.64: resolution: {integrity: sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==} @@ -9197,6 +9878,8 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true + '@bufbuild/protobuf@2.6.0': {} + '@cloudflare/kv-asset-handler@0.4.0': dependencies: mime: 3.0.0 @@ -9881,6 +10564,18 @@ snapshots: dependencies: tslib: 2.8.1 + '@grpc/grpc-js@1.13.4': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.3 + yargs: 17.7.2 + '@hexagon/base64@1.1.28': {} '@hookform/resolvers@4.1.0(react-hook-form@7.54.2(react@19.0.0))': @@ -9963,6 +10658,11 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@inngest/ai@0.1.5': + dependencies: + '@types/node': 22.16.3 + typescript: 5.8.3 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -9972,6 +10672,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jpwilliams/waitgroup@2.1.1': {} + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -9999,6 +10701,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} + '@levischuck/tiny-cbor@0.2.11': {} '@marsidev/react-turnstile@1.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': @@ -10193,8 +10897,678 @@ snapshots: dependencies: '@openpanel/sdk': 1.0.0 + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.9.0': {} + '@opentelemetry/auto-instrumentations-node@0.56.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.50.3(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.49.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.14.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.44.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.50.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/aws-lambda': 8.10.147 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.49.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/propagation-utils': 0.30.16(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.14.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.44.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.1 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.3 + + '@opentelemetry/propagation-utils@0.30.16(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-azure@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-container@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.1 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.36.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@orama/orama@3.1.4': {} '@orama/orama@3.1.7': {} @@ -10280,6 +11654,29 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.0': {} '@radix-ui/number@1.1.1': {} @@ -12834,8 +14231,18 @@ snapshots: dependencies: '@types/estree': 1.0.6 + '@types/aws-lambda@8.10.147': {} + + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 20.19.0 + '@types/canvas-confetti@1.9.0': {} + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.0 + '@types/cors@2.8.17': dependencies: '@types/node': 20.19.0 @@ -12886,12 +14293,28 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/memcached@2.2.10': + dependencies: + '@types/node': 20.19.0 + '@types/ms@2.1.0': {} + '@types/mysql@2.15.26': + dependencies: + '@types/node': 20.19.0 + '@types/node@20.19.0': dependencies: undici-types: 6.21.0 + '@types/node@22.16.3': + dependencies: + undici-types: 6.21.0 + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.11.11 + '@types/pg@8.11.11': dependencies: '@types/node': 20.19.0 @@ -12905,6 +14328,12 @@ snapshots: pg-types: 4.0.2 optional: true + '@types/pg@8.6.1': + dependencies: + '@types/node': 20.19.0 + pg-protocol: 1.9.5 + pg-types: 2.2.0 + '@types/react-dom@19.0.3(@types/react@19.0.9)': dependencies: '@types/react': 19.0.9 @@ -12913,6 +14342,12 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/shimmer@1.2.0': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 20.19.0 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -12950,6 +14385,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: acorn: 8.14.0 @@ -12974,6 +14413,8 @@ snapshots: ansi-colors@4.1.3: {} + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -13107,6 +14548,8 @@ snapshots: caniuse-lite@1.0.30001699: {} + canonicalize@1.0.8: {} + canvas-confetti@1.9.3: {} ccount@2.0.1: {} @@ -13130,6 +14573,8 @@ snapshots: dependencies: readdirp: 4.1.2 + cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -13142,6 +14587,12 @@ snapshots: client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} clsx@2.1.1: {} @@ -13215,6 +14666,12 @@ snapshots: dependencies: zalgo-promise: 1.0.48 + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -13816,6 +15273,8 @@ snapshots: dependencies: fd-package-json: 2.0.0 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -13936,6 +15395,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.2.7: dependencies: call-bind-apply-helpers: 1.0.2 @@ -14035,6 +15496,11 @@ snapshots: has-symbols@1.1.0: {} + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -14187,10 +15653,49 @@ snapshots: image-size@2.0.2: {} + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + inherits@2.0.4: {} inline-style-parser@0.2.4: {} + inngest@3.40.1(express@5.0.1)(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.8.3): + dependencies: + '@bufbuild/protobuf': 2.6.0 + '@inngest/ai': 0.1.5 + '@jpwilliams/waitgroup': 2.1.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/auto-instrumentations-node': 0.56.1(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@types/debug': 4.1.12 + canonicalize: 1.0.8 + chalk: 4.1.2 + cross-fetch: 4.1.0 + debug: 4.4.0 + hash.js: 1.1.7 + json-stringify-safe: 5.0.1 + ms: 2.1.3 + serialize-error-cjs: 0.1.4 + strip-ansi: 5.2.0 + temporal-polyfill: 0.2.5 + zod: 3.22.5 + optionalDependencies: + express: 5.0.1 + next: 15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + typescript: 5.8.3 + transitivePeerDependencies: + - encoding + - supports-color + input-otp@1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -14217,6 +15722,10 @@ snapshots: is-arrayish@0.3.2: optional: true + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-decimal@2.0.1: {} is-extglob@2.1.1: {} @@ -14273,6 +15782,8 @@ snapshots: json-schema@0.4.0: {} + json-stringify-safe@5.0.1: {} + json5@2.2.3: {} jsondiffpatch@0.6.0: @@ -14365,6 +15876,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.29.2 lightningcss-win32-x64-msvc: 1.29.2 + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -14374,6 +15887,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -14884,6 +16399,8 @@ snapshots: - bufferutil - utf-8-validate + minimalistic-assert@1.0.1: {} + minimatch@10.0.1: dependencies: brace-expansion: 2.0.1 @@ -14908,6 +16425,8 @@ snapshots: dependencies: obliterator: 1.6.1 + module-details-from-path@1.0.4: {} + motion-dom@12.4.5: dependencies: motion-utils: 12.0.0 @@ -15118,6 +16637,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -15159,6 +16680,8 @@ snapshots: pg-protocol@1.7.1: {} + pg-protocol@1.9.5: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -15268,6 +16791,21 @@ snapshots: property-information@7.0.0: {} + protobufjs@7.5.3: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.19.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -15625,6 +17163,16 @@ snapshots: transitivePeerDependencies: - supports-color + require-directory@2.1.1: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.0 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + resend@4.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@react-email/render': 1.0.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -15634,6 +17182,12 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -15677,8 +17231,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: - optional: true + semver@7.7.1: {} send@1.2.0: dependencies: @@ -15696,6 +17249,8 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error-cjs@0.1.4: {} + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 @@ -15762,6 +17317,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -15883,6 +17440,10 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -15921,6 +17482,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + swiper@11.2.5: {} swr@2.3.2(react@19.0.0): @@ -15943,6 +17506,12 @@ snapshots: tapable@2.2.1: {} + temporal-polyfill@0.2.5: + dependencies: + temporal-spec: 0.2.4 + + temporal-spec@0.2.4: {} + terser@5.16.9: dependencies: '@jridgewell/source-map': 0.3.6 @@ -16215,10 +17784,24 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yaml@2.7.0: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + youch@3.3.4: dependencies: cookie: 0.7.2 @@ -16237,6 +17820,8 @@ snapshots: zod@3.22.3: {} + zod@3.22.5: {} + zod@3.25.64: {} zustand@5.0.3(@types/react@19.0.9)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)): diff --git a/src/actions/consume-credits.ts b/src/actions/consume-credits.ts new file mode 100644 index 0000000..b9a7210 --- /dev/null +++ b/src/actions/consume-credits.ts @@ -0,0 +1,43 @@ +'use server'; + +import { consumeCredits } from '@/credits/credits'; +import { getSession } from '@/lib/server'; +import { createSafeActionClient } from 'next-safe-action'; +import { z } from 'zod'; + +const actionClient = createSafeActionClient(); + +// consume credits schema +const consumeSchema = z.object({ + amount: z.number().min(1), + description: z.string().optional(), +}); + +/** + * Consume credits + */ +export const consumeCreditsAction = actionClient + .schema(consumeSchema) + .action(async ({ parsedInput }) => { + const session = await getSession(); + if (!session) { + console.warn('unauthorized request to consume credits'); + return { success: false, error: 'Unauthorized' }; + } + + try { + await consumeCredits({ + userId: session.user.id, + amount: parsedInput.amount, + description: + parsedInput.description || `Consume credits: ${parsedInput.amount}`, + }); + return { success: true }; + } catch (error) { + console.error('consume credits error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Something went wrong', + }; + } + }); diff --git a/src/actions/create-checkout-session.ts b/src/actions/create-checkout-session.ts index f5b6afb..4c5fd62 100644 --- a/src/actions/create-checkout-session.ts +++ b/src/actions/create-checkout-session.ts @@ -64,7 +64,7 @@ export const createCheckoutAction = actionClient if (!plan) { return { success: false, - error: 'Plan not found', + error: 'Price plan not found', }; } @@ -87,7 +87,7 @@ export const createCheckoutAction = actionClient // Create the checkout session with localized URLs const successUrl = getUrlWithLocale( - '/settings/billing?session_id={CHECKOUT_SESSION_ID}', + `${Routes.SettingsBilling}?session_id={CHECKOUT_SESSION_ID}`, locale ); const cancelUrl = getUrlWithLocale(Routes.Pricing, locale); diff --git a/src/actions/create-credit-checkout-session.ts b/src/actions/create-credit-checkout-session.ts new file mode 100644 index 0000000..d648f2f --- /dev/null +++ b/src/actions/create-credit-checkout-session.ts @@ -0,0 +1,124 @@ +'use server'; + +import { websiteConfig } from '@/config/website'; +import { getCreditPackageById } from '@/credits/server'; +import { getSession } from '@/lib/server'; +import { getUrlWithLocale } from '@/lib/urls/urls'; +import { createCreditCheckout } from '@/payment'; +import type { CreateCreditCheckoutParams } from '@/payment/types'; +import { Routes } from '@/routes'; +import { getLocale } from 'next-intl/server'; +import { createSafeActionClient } from 'next-safe-action'; +import { cookies } from 'next/headers'; +import { z } from 'zod'; + +// Create a safe action client +const actionClient = createSafeActionClient(); + +// Credit checkout schema for validation +// metadata is optional, and may contain referral information if you need +const creditCheckoutSchema = z.object({ + userId: z.string().min(1, { message: 'User ID is required' }), + packageId: z.string().min(1, { message: 'Package ID is required' }), + priceId: z.string().min(1, { message: 'Price ID is required' }), + metadata: z.record(z.string()).optional(), +}); + +/** + * Create a checkout session for a credit package + */ +export const createCreditCheckoutSession = actionClient + .schema(creditCheckoutSchema) + .action(async ({ parsedInput }) => { + const { userId, packageId, priceId, metadata } = parsedInput; + + // Get the current user session for authorization + const session = await getSession(); + if (!session) { + console.warn( + `unauthorized request to create credit checkout session for user ${userId}` + ); + return { + success: false, + error: 'Unauthorized', + }; + } + + // Only allow users to create their own checkout session + if (session.user.id !== userId) { + console.warn( + `current user ${session.user.id} is not authorized to create credit checkout session for user ${userId}` + ); + return { + success: false, + error: 'Not authorized to do this action', + }; + } + + try { + // Get the current locale from the request + const locale = await getLocale(); + + // Find the credit package + const creditPackage = getCreditPackageById(packageId); + if (!creditPackage) { + return { + success: false, + error: 'Credit package not found', + }; + } + + // Add metadata to identify this as a credit purchase + const customMetadata: Record = { + ...metadata, + type: 'credit_purchase', + packageId, + credits: creditPackage.credits.toString(), + userId: session.user.id, + userName: session.user.name, + }; + + // https://datafa.st/docs/stripe-checkout-api + // if datafast analytics is enabled, add the revenue attribution to the metadata + if (websiteConfig.features.enableDatafastRevenueTrack) { + const cookieStore = await cookies(); + customMetadata.datafast_visitor_id = + cookieStore.get('datafast_visitor_id')?.value ?? ''; + customMetadata.datafast_session_id = + cookieStore.get('datafast_session_id')?.value ?? ''; + } + + // Create checkout session with credit-specific URLs + const successUrl = getUrlWithLocale( + `${Routes.SettingsBilling}?credits_session_id={CHECKOUT_SESSION_ID}`, + locale + ); + const cancelUrl = getUrlWithLocale(Routes.SettingsBilling, locale); + + const params: CreateCreditCheckoutParams = { + packageId, + priceId, + customerEmail: session.user.email, + metadata: customMetadata, + successUrl, + cancelUrl, + locale, + }; + + const result = await createCreditCheckout(params); + // console.log('create credit checkout session result:', result); + return { + success: true, + data: result, + }; + } catch (error) { + console.error('Create credit checkout session error:', error); + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Failed to create checkout session', + }; + } + }); diff --git a/src/actions/get-credit-balance.ts b/src/actions/get-credit-balance.ts new file mode 100644 index 0000000..1e0b6ba --- /dev/null +++ b/src/actions/get-credit-balance.ts @@ -0,0 +1,21 @@ +'use server'; + +import { getUserCredits } from '@/credits/credits'; +import { getSession } from '@/lib/server'; +import { createSafeActionClient } from 'next-safe-action'; + +const actionClient = createSafeActionClient(); + +/** + * Get current user's credits + */ +export const getCreditBalanceAction = actionClient.action(async () => { + const session = await getSession(); + if (!session) { + console.warn('unauthorized request to get credit balance'); + return { success: false, error: 'Unauthorized' }; + } + + const credits = await getUserCredits(session.user.id); + return { success: true, credits }; +}); diff --git a/src/actions/get-credit-stats.ts b/src/actions/get-credit-stats.ts new file mode 100644 index 0000000..d97cc94 --- /dev/null +++ b/src/actions/get-credit-stats.ts @@ -0,0 +1,110 @@ +'use server'; + +import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; +import { getDb } from '@/db'; +import { creditTransaction } from '@/db/schema'; +import { getSession } from '@/lib/server'; +import { addDays } from 'date-fns'; +import { and, eq, gte, isNotNull, lte, sql, sum } from 'drizzle-orm'; +import { createSafeActionClient } from 'next-safe-action'; + +const CREDITS_EXPIRATION_DAYS = 31; +const CREDITS_MONTHLY_DAYS = 31; + +// Create a safe action client +const actionClient = createSafeActionClient(); + +/** + * Get credit statistics for a user + */ +export const getCreditStatsAction = actionClient.action(async () => { + try { + const session = await getSession(); + if (!session) { + console.warn('unauthorized request to get credit stats'); + return { + success: false, + error: 'Unauthorized', + }; + } + + const db = await getDb(); + const userId = session.user.id; + + // Get credits expiring in the next CREDITS_EXPIRATION_DAYS days + const expirationDaysFromNow = addDays(new Date(), CREDITS_EXPIRATION_DAYS); + const expiringCredits = await db + .select({ + amount: sum(creditTransaction.remainingAmount), + earliestExpiration: sql`MIN(${creditTransaction.expirationDate})`, + }) + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + isNotNull(creditTransaction.expirationDate), + isNotNull(creditTransaction.remainingAmount), + gte(creditTransaction.remainingAmount, 1), + lte(creditTransaction.expirationDate, expirationDaysFromNow), + gte(creditTransaction.expirationDate, new Date()) + ) + ); + + // Get credits from subscription renewals (recent CREDITS_MONTHLY_DAYS days) + const monthlyRefreshDaysAgo = addDays(new Date(), -CREDITS_MONTHLY_DAYS); + const subscriptionCredits = await db + .select({ + amount: sum(creditTransaction.amount), + }) + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + eq( + creditTransaction.type, + CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL + ), + gte(creditTransaction.createdAt, monthlyRefreshDaysAgo) + ) + ); + + // Get credits from monthly lifetime distribution (recent CREDITS_MONTHLY_DAYS days) + const lifetimeCredits = await db + .select({ + amount: sum(creditTransaction.amount), + }) + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY), + gte(creditTransaction.createdAt, monthlyRefreshDaysAgo) + ) + ); + + return { + success: true, + data: { + expiringCredits: { + amount: Number(expiringCredits[0]?.amount) || 0, + earliestExpiration: expiringCredits[0]?.earliestExpiration || null, + }, + subscriptionCredits: { + amount: Number(subscriptionCredits[0]?.amount) || 0, + }, + lifetimeCredits: { + amount: Number(lifetimeCredits[0]?.amount) || 0, + }, + }, + }; + } catch (error) { + console.error('get credit stats error:', error); + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Failed to fetch credit statistics', + }; + } +}); diff --git a/src/actions/get-credit-transactions.ts b/src/actions/get-credit-transactions.ts new file mode 100644 index 0000000..ded99f0 --- /dev/null +++ b/src/actions/get-credit-transactions.ts @@ -0,0 +1,124 @@ +'use server'; + +import { getDb } from '@/db'; +import { creditTransaction } from '@/db/schema'; +import { getSession } from '@/lib/server'; +import { and, asc, desc, eq, ilike, or, sql } from 'drizzle-orm'; +import { createSafeActionClient } from 'next-safe-action'; +import { z } from 'zod'; + +// Create a safe action client +const actionClient = createSafeActionClient(); + +// Define the schema for getCreditTransactions parameters +const getCreditTransactionsSchema = z.object({ + pageIndex: z.number().min(0).default(0), + pageSize: z.number().min(1).max(100).default(10), + search: z.string().optional().default(''), + sorting: z + .array( + z.object({ + id: z.string(), + desc: z.boolean(), + }) + ) + .optional() + .default([]), +}); + +// Define sort field mapping +const sortFieldMap = { + type: creditTransaction.type, + amount: creditTransaction.amount, + remainingAmount: creditTransaction.remainingAmount, + description: creditTransaction.description, + createdAt: creditTransaction.createdAt, + updatedAt: creditTransaction.updatedAt, + expirationDate: creditTransaction.expirationDate, + expirationDateProcessedAt: creditTransaction.expirationDateProcessedAt, + paymentId: creditTransaction.paymentId, +} as const; + +// Create a safe action for getting credit transactions +export const getCreditTransactionsAction = actionClient + .schema(getCreditTransactionsSchema) + .action(async ({ parsedInput }) => { + try { + const session = await getSession(); + if (!session) { + return { + success: false, + error: 'Unauthorized', + }; + } + const { pageIndex, pageSize, search, sorting } = parsedInput; + + // search by type, amount, paymentId, description, and restrict to current user + const where = search + ? and( + eq(creditTransaction.userId, session.user.id), + or( + ilike(creditTransaction.type, `%${search}%`), + ilike(creditTransaction.amount, `%${search}%`), + ilike(creditTransaction.remainingAmount, `%${search}%`), + ilike(creditTransaction.paymentId, `%${search}%`), + ilike(creditTransaction.description, `%${search}%`) + ) + ) + : eq(creditTransaction.userId, session.user.id); + + const offset = pageIndex * pageSize; + + // Get the sort configuration + const sortConfig = sorting[0]; + const sortField = sortConfig?.id + ? sortFieldMap[sortConfig.id as keyof typeof sortFieldMap] + : creditTransaction.createdAt; + const sortDirection = sortConfig?.desc ? desc : asc; + + const db = await getDb(); + const [items, [{ count }]] = await Promise.all([ + db + .select({ + id: creditTransaction.id, + userId: creditTransaction.userId, + type: creditTransaction.type, + description: creditTransaction.description, + amount: creditTransaction.amount, + remainingAmount: creditTransaction.remainingAmount, + paymentId: creditTransaction.paymentId, + expirationDate: creditTransaction.expirationDate, + expirationDateProcessedAt: + creditTransaction.expirationDateProcessedAt, + createdAt: creditTransaction.createdAt, + updatedAt: creditTransaction.updatedAt, + }) + .from(creditTransaction) + .where(where) + .orderBy(sortDirection(sortField)) + .limit(pageSize) + .offset(offset), + db + .select({ count: sql`count(*)` }) + .from(creditTransaction) + .where(where), + ]); + + return { + success: true, + data: { + items, + total: Number(count), + }, + }; + } catch (error) { + console.error('get credit transactions error:', error); + return { + success: false, + error: + error instanceof Error + ? error.message + : 'Failed to fetch credit transactions', + }; + } + }); diff --git a/src/actions/get-users.ts b/src/actions/get-users.ts index f01702a..afd68c7 100644 --- a/src/actions/get-users.ts +++ b/src/actions/get-users.ts @@ -44,8 +44,13 @@ export const getUsersAction = actionClient try { const { pageIndex, pageSize, search, sorting } = parsedInput; + // search by name, email, and customerId const where = search - ? or(ilike(user.name, `%${search}%`), ilike(user.email, `%${search}%`)) + ? or( + ilike(user.name, `%${search}%`), + ilike(user.email, `%${search}%`), + ilike(user.customerId, `%${search}%`) + ) : undefined; const offset = pageIndex * pageSize; diff --git a/src/ai/image/components/QualityModeToggle.tsx b/src/ai/image/components/QualityModeToggle.tsx index ce44cd0..d9abde0 100644 --- a/src/ai/image/components/QualityModeToggle.tsx +++ b/src/ai/image/components/QualityModeToggle.tsx @@ -1,10 +1,10 @@ -"use client"; +'use client'; -import { Zap, Sparkles } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { useToast } from "@/hooks/use-toast"; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/hooks/use-toast'; +import { Sparkles, Zap } from 'lucide-react'; -export type QualityMode = "performance" | "quality"; +export type QualityMode = 'performance' | 'quality'; interface QualityModeToggleProps { value: QualityMode; @@ -25,9 +25,9 @@ export function QualityModeToggle({ variant="secondary" disabled={disabled} onClick={() => { - onValueChange("performance"); + onValueChange('performance'); toast({ - description: "Switching to faster models for quicker generation", + description: 'Switching to faster models for quicker generation', duration: 2000, }); }} @@ -39,10 +39,10 @@ export function QualityModeToggle({ variant="secondary" disabled={disabled} onClick={() => { - onValueChange("quality"); + onValueChange('quality'); toast({ description: - "Switching to higher quality models for better results", + 'Switching to higher quality models for better results', duration: 2000, }); }} diff --git a/src/ai/image/components/Stopwatch.tsx b/src/ai/image/components/Stopwatch.tsx index f4a1a82..e567796 100644 --- a/src/ai/image/components/Stopwatch.tsx +++ b/src/ai/image/components/Stopwatch.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState } from 'react'; export function Stopwatch({ startTime }: { startTime: number }) { const [elapsed, setElapsed] = useState(0); @@ -12,6 +12,8 @@ export function Stopwatch({ startTime }: { startTime: number }) { }, [startTime]); return ( -
{(elapsed / 1000).toFixed(1)}s
+
+ {(elapsed / 1000).toFixed(1)}s +
); } diff --git a/src/ai/image/lib/image-helpers.ts b/src/ai/image/lib/image-helpers.ts index 677ef69..dde39b5 100644 --- a/src/ai/image/lib/image-helpers.ts +++ b/src/ai/image/lib/image-helpers.ts @@ -1,5 +1,5 @@ export const imageHelpers = { - base64ToBlob: (base64Data: string, type = "image/png"): Blob => { + base64ToBlob: (base64Data: string, type = 'image/png'): Blob => { const byteString = atob(base64Data); const arrayBuffer = new ArrayBuffer(byteString.length); const uint8Array = new Uint8Array(arrayBuffer); @@ -13,7 +13,7 @@ export const imageHelpers = { generateImageFileName: (provider: string): string => { const uniqueId = Math.random().toString(36).substring(2, 8); - return `${provider}-${uniqueId}`.replace(/[^a-z0-9-]/gi, ""); + return `${provider}-${uniqueId}`.replace(/[^a-z0-9-]/gi, ''); }, shareOrDownload: async ( @@ -22,7 +22,7 @@ export const imageHelpers = { ): Promise => { const fileName = imageHelpers.generateImageFileName(provider); const blob = imageHelpers.base64ToBlob(imageData); - const file = new File([blob], `${fileName}.png`, { type: "image/png" }); + const file = new File([blob], `${fileName}.png`, { type: 'image/png' }); try { if (navigator.share) { @@ -31,13 +31,13 @@ export const imageHelpers = { title: `Image generated by ${provider}`, }); } else { - throw new Error("Share API not available"); + throw new Error('Share API not available'); } } catch (error) { // Fall back to download for any error (including share cancellation) - console.error("Error sharing/downloading:", error); + console.error('Error sharing/downloading:', error); const blobUrl = URL.createObjectURL(blob); - const link = document.createElement("a"); + const link = document.createElement('a'); link.href = blobUrl; link.download = `${fileName}.png`; document.body.appendChild(link); @@ -48,6 +48,6 @@ export const imageHelpers = { }, formatModelId: (modelId: string): string => { - return modelId.split("/").pop() || modelId; + return modelId.split('/').pop() || modelId; }, }; diff --git a/src/ai/text/components/consume-credit-card.tsx b/src/ai/text/components/consume-credit-card.tsx new file mode 100644 index 0000000..478bf76 --- /dev/null +++ b/src/ai/text/components/consume-credit-card.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { CreditsBalanceButton } from '@/components/layout/credits-balance-button'; +import { Button } from '@/components/ui/button'; +import { useCredits } from '@/hooks/use-credits'; +import { CoinsIcon } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; + +const CONSUME_CREDITS = 50; + +export function ConsumeCreditCard() { + const { consumeCredits, hasEnoughCredits, isLoading } = useCredits(); + const [loading, setLoading] = useState(false); + + const handleConsume = async () => { + if (!hasEnoughCredits(CONSUME_CREDITS)) { + toast.error('Insufficient credits, please buy more credits.'); + return; + } + setLoading(true); + const success = await consumeCredits( + CONSUME_CREDITS, + `AI Text Credit Consumption (${CONSUME_CREDITS} credits)` + ); + setLoading(false); + if (success) { + toast.success(`${CONSUME_CREDITS} credits have been consumed.`); + } else { + toast.error('Failed to consume credits, please try again later.'); + } + }; + + return ( +
+
+ +
+ +
+ ); +} diff --git a/src/app/[locale]/(marketing)/ai/text/page.tsx b/src/app/[locale]/(marketing)/ai/text/page.tsx index 95982bb..5702bfc 100644 --- a/src/app/[locale]/(marketing)/ai/text/page.tsx +++ b/src/app/[locale]/(marketing)/ai/text/page.tsx @@ -1,3 +1,4 @@ +import { ConsumeCreditCard } from '@/ai/text/components/consume-credit-card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { constructMetadata } from '@/lib/metadata'; import { getUrlWithLocale } from '@/lib/urls/urls'; @@ -28,7 +29,7 @@ export default async function AITextPage() {
{/* about section */}
-
+
{/* avatar and name */}
@@ -48,6 +49,9 @@ export default async function AITextPage() {
+ + {/* simulate consume credits */} +
diff --git a/src/app/[locale]/(protected)/settings/billing/layout.tsx b/src/app/[locale]/(protected)/settings/billing/layout.tsx index af239d4..5b85ac6 100644 --- a/src/app/[locale]/(protected)/settings/billing/layout.tsx +++ b/src/app/[locale]/(protected)/settings/billing/layout.tsx @@ -26,7 +26,7 @@ export default async function BillingLayout({ children }: BillingLayoutProps) {
-
+

{t('billing.title')} diff --git a/src/app/[locale]/(protected)/settings/billing/page.tsx b/src/app/[locale]/(protected)/settings/billing/page.tsx index 2b061d8..dddba6b 100644 --- a/src/app/[locale]/(protected)/settings/billing/page.tsx +++ b/src/app/[locale]/(protected)/settings/billing/page.tsx @@ -1,5 +1,23 @@ import BillingCard from '@/components/settings/billing/billing-card'; +import CreditsBalanceCard from '@/components/settings/billing/credits-balance-card'; +import { CreditPackages } from '@/components/settings/credits/credit-packages'; +import { websiteConfig } from '@/config/website'; export default function BillingPage() { - return ; + return ( +
+ {/* Billing and Credits Balance Cards */} +
+ + {websiteConfig.credits.enableCredits && } +
+ + {/* Credit Packages */} + {websiteConfig.credits.enableCredits && ( +
+ +
+ )} +
+ ); } diff --git a/src/app/[locale]/(protected)/settings/credits/layout.tsx b/src/app/[locale]/(protected)/settings/credits/layout.tsx new file mode 100644 index 0000000..5c1b6bc --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/layout.tsx @@ -0,0 +1,46 @@ +import { DashboardHeader } from '@/components/dashboard/dashboard-header'; +import { getTranslations } from 'next-intl/server'; + +interface CreditsLayoutProps { + children: React.ReactNode; +} + +export default async function CreditsLayout({ children }: CreditsLayoutProps) { + const t = await getTranslations('Dashboard.settings'); + + const breadcrumbs = [ + { + label: t('title'), + isCurrentPage: false, + }, + { + label: t('credits.title'), + isCurrentPage: true, + }, + ]; + + return ( + <> + + +
+
+
+
+
+

+ {t('credits.title')} +

+

+ {t('credits.description')} +

+
+ + {children} +
+
+
+
+ + ); +} diff --git a/src/app/[locale]/(protected)/settings/credits/loading.tsx b/src/app/[locale]/(protected)/settings/credits/loading.tsx new file mode 100644 index 0000000..ebfad58 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/loading.tsx @@ -0,0 +1,5 @@ +import { Loader2Icon } from 'lucide-react'; + +export default function Loading() { + return ; +} diff --git a/src/app/[locale]/(protected)/settings/credits/page.tsx b/src/app/[locale]/(protected)/settings/credits/page.tsx new file mode 100644 index 0000000..fcbbab9 --- /dev/null +++ b/src/app/[locale]/(protected)/settings/credits/page.tsx @@ -0,0 +1,16 @@ +import { CreditTransactionsPageClient } from '@/components/settings/credits/credit-transactions-page'; +import { websiteConfig } from '@/config/website'; +import { Routes } from '@/routes'; +import { redirect } from 'next/navigation'; + +/** + * Credits page, show credit transactions + */ +export default function CreditsPage() { + // If credits are disabled, redirect to billing page + if (!websiteConfig.credits.enableCredits) { + redirect(Routes.SettingsBilling); + } + + return ; +} diff --git a/src/app/[locale]/(protected)/settings/notifications/layout.tsx b/src/app/[locale]/(protected)/settings/notifications/layout.tsx index e480a3a..740dfad 100644 --- a/src/app/[locale]/(protected)/settings/notifications/layout.tsx +++ b/src/app/[locale]/(protected)/settings/notifications/layout.tsx @@ -28,7 +28,7 @@ export default async function NotificationsLayout({
-
+

{t('notification.title')} diff --git a/src/app/[locale]/(protected)/settings/notifications/page.tsx b/src/app/[locale]/(protected)/settings/notifications/page.tsx index 18d2ddb..faaff59 100644 --- a/src/app/[locale]/(protected)/settings/notifications/page.tsx +++ b/src/app/[locale]/(protected)/settings/notifications/page.tsx @@ -2,8 +2,10 @@ import { NewsletterFormCard } from '@/components/settings/notification/newslette export default function NotificationPage() { return ( -
- +
+
+ +
); } diff --git a/src/app/[locale]/(protected)/settings/profile/layout.tsx b/src/app/[locale]/(protected)/settings/profile/layout.tsx index c807a00..bf2e737 100644 --- a/src/app/[locale]/(protected)/settings/profile/layout.tsx +++ b/src/app/[locale]/(protected)/settings/profile/layout.tsx @@ -26,7 +26,7 @@ export default async function ProfileLayout({ children }: ProfileLayoutProps) {
-
+

{t('profile.title')} diff --git a/src/app/[locale]/(protected)/settings/profile/page.tsx b/src/app/[locale]/(protected)/settings/profile/page.tsx index f6915df..9fafa5f 100644 --- a/src/app/[locale]/(protected)/settings/profile/page.tsx +++ b/src/app/[locale]/(protected)/settings/profile/page.tsx @@ -4,8 +4,12 @@ import { UpdateNameCard } from '@/components/settings/profile/update-name-card'; export default function ProfilePage() { return (
- - +
+ +
+
+ +
); } diff --git a/src/app/[locale]/(protected)/settings/security/layout.tsx b/src/app/[locale]/(protected)/settings/security/layout.tsx index d42ac2e..c35eb87 100644 --- a/src/app/[locale]/(protected)/settings/security/layout.tsx +++ b/src/app/[locale]/(protected)/settings/security/layout.tsx @@ -28,7 +28,7 @@ export default async function SecurityLayout({
-
+

{t('security.title')} diff --git a/src/app/[locale]/(protected)/settings/security/page.tsx b/src/app/[locale]/(protected)/settings/security/page.tsx index 7574f3c..bab5416 100644 --- a/src/app/[locale]/(protected)/settings/security/page.tsx +++ b/src/app/[locale]/(protected)/settings/security/page.tsx @@ -4,8 +4,12 @@ import { PasswordCardWrapper } from '@/components/settings/security/password-car export default function SecurityPage() { return (
- - +
+ +
+
+ +
); } diff --git a/src/app/[locale]/providers.tsx b/src/app/[locale]/providers.tsx index 82192f0..40f290e 100644 --- a/src/app/[locale]/providers.tsx +++ b/src/app/[locale]/providers.tsx @@ -4,6 +4,7 @@ import { ActiveThemeProvider } from '@/components/layout/active-theme-provider'; import { PaymentProvider } from '@/components/layout/payment-provider'; import { TooltipProvider } from '@/components/ui/tooltip'; import { websiteConfig } from '@/config/website'; +import { CreditsProvider } from '@/providers/credits-provider'; import type { Translations } from 'fumadocs-ui/i18n'; import { RootProvider } from 'fumadocs-ui/provider'; import { useTranslations } from 'next-intl'; @@ -25,6 +26,7 @@ interface ProvidersProps { * - RootProvider: Provides the root provider for Fumadocs UI. * - TooltipProvider: Provides the tooltip to the app. * - PaymentProvider: Provides the payment state to the app. + * - CreditsProvider: Provides the credits state to the app. */ export function Providers({ children, locale }: ProvidersProps) { const theme = useTheme(); @@ -61,7 +63,9 @@ export function Providers({ children, locale }: ProvidersProps) { - {children} + + {children} + diff --git a/src/app/api/hello/route.ts b/src/app/api/hello/route.ts new file mode 100644 index 0000000..557479c --- /dev/null +++ b/src/app/api/hello/route.ts @@ -0,0 +1,20 @@ +import { inngest } from '@/inngest/client'; +import { NextResponse } from 'next/server'; + +// Opt out of caching; every request should send a new event +export const dynamic = 'force-dynamic'; + +// Create a simple async Next.js API route handler +export async function GET() { + console.log('Send event to Inngest start'); + // Send your event payload to Inngest + await inngest.send({ + name: 'test/hello.world', + data: { + email: 'testUser@example.com', + }, + }); + + console.log('Send event to Inngest end'); + return NextResponse.json({ message: 'Event sent!' }); +} diff --git a/src/app/api/inngest/route.ts b/src/app/api/inngest/route.ts new file mode 100644 index 0000000..607e9d5 --- /dev/null +++ b/src/app/api/inngest/route.ts @@ -0,0 +1,19 @@ +import { serve } from 'inngest/next'; +import { inngest } from '../../../inngest/client'; +import { distributeCreditsDaily, helloWorld } from '../../../inngest/functions'; + +/** + * Inngest route + * + * https://www.inngest.com/docs/getting-started/nextjs-quick-start + * + * Next.js Edge Functions hosted on Vercel can also stream responses back to Inngest, + * giving you a much higher request timeout of 15 minutes (up from 10 seconds on the Vercel Hobby plan!). + * To enable this, set your runtime to "edge" (see Quickstart for Using Edge Functions | Vercel Docs) + * and add the streaming: "allow" option to your serve handler: + * https://www.inngest.com/docs/learn/serving-inngest-functions#framework-next-js + */ +export const { GET, POST, PUT } = serve({ + client: inngest, + functions: [helloWorld, distributeCreditsDaily], +}); diff --git a/src/components/admin/user-detail-viewer.tsx b/src/components/admin/user-detail-viewer.tsx index f10cc0e..392ccf9 100644 --- a/src/components/admin/user-detail-viewer.tsx +++ b/src/components/admin/user-detail-viewer.tsx @@ -24,6 +24,7 @@ import { useIsMobile } from '@/hooks/use-mobile'; import { authClient } from '@/lib/auth-client'; import type { User } from '@/lib/auth-types'; import { formatDate } from '@/lib/formatter'; +import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls'; import { cn } from '@/lib/utils'; import { useUsersStore } from '@/stores/users-store'; import { @@ -149,7 +150,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) { />
{user.name} - {user.email} + {/* {user.email} */}

@@ -188,12 +189,51 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
- {/* information */} -
- {t('joined')}: {formatDate(user.createdAt)} + {/* email */} + {user.email && ( +
+ + {t('columns.email')}: + + { + navigator.clipboard.writeText(user.email!); + toast.success(t('emailCopied')); + }} + > + {user.email} + +
+ )} + + {/* customerId */} + {user.customerId && ( +
+ + {t('columns.customerId')}: + + + {user.customerId} + +
+ )} +
+ + {/* Timestamps */} +
+
+ {t('joined')}: + {formatDate(user.createdAt)}
-
- {t('updated')}: {formatDate(user.updatedAt)} +
+ {t('updated')}: + {formatDate(user.updatedAt)}
diff --git a/src/components/admin/users-page.tsx b/src/components/admin/users-page.tsx index ca49b4b..23fb7d3 100644 --- a/src/components/admin/users-page.tsx +++ b/src/components/admin/users-page.tsx @@ -6,7 +6,7 @@ import type { User } from '@/lib/auth-types'; import { useUsersStore } from '@/stores/users-store'; import type { SortingState } from '@tanstack/react-table'; import { useTranslations } from 'next-intl'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; export function UsersPageClient() { @@ -17,42 +17,44 @@ export function UsersPageClient() { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); - const [sorting, setSorting] = useState([]); + const [sorting, setSorting] = useState([ + { id: 'createdAt', desc: true }, + ]); const refreshTrigger = useUsersStore((state) => state.refreshTrigger); - useEffect(() => { - const fetchUsers = async () => { - try { - setLoading(true); - const result = await getUsersAction({ - pageIndex, - pageSize, - search, - sorting, - }); + const fetchUsers = useCallback(async () => { + try { + setLoading(true); + const result = await getUsersAction({ + pageIndex, + pageSize, + search, + sorting, + }); - if (result?.data?.success) { - setData(result.data.data?.items || []); - setTotal(result.data.data?.total || 0); - } else { - const errorMessage = result?.data?.error || t('error'); - toast.error(errorMessage); - setData([]); - setTotal(0); - } - } catch (error) { - console.error('Failed to fetch users:', error); - toast.error(t('error')); + if (result?.data?.success) { + setData(result.data.data?.items || []); + setTotal(result.data.data?.total || 0); + } else { + const errorMessage = result?.data?.error || t('error'); + toast.error(errorMessage); setData([]); setTotal(0); - } finally { - setLoading(false); } - }; - - fetchUsers(); + } catch (error) { + console.error('Failed to fetch users:', error); + toast.error(t('error')); + setData([]); + setTotal(0); + } finally { + setLoading(false); + } }, [pageIndex, pageSize, search, sorting, refreshTrigger]); + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + return ( <> ({ title, className, }: DataTableColumnHeaderProps) { + const tTable = useTranslations('Common.table'); if (!column.getCanSort()) { return
{title}
; } + const isSorted = column.getIsSorted(); // 'asc' | 'desc' | false + return (
- + + + + + + { + if (value === 'asc') column.toggleSorting(false); + else if (value === 'desc') column.toggleSorting(true); + }} + > + + + {tTable('ascending')} + + + + + {tTable('descending')} + + + + +
); } @@ -115,7 +144,10 @@ export function UsersTable({ onSortingChange, }: UsersTableProps) { const t = useTranslations('Dashboard.admin.users'); - const [sorting, setSorting] = useState([]); + const tTable = useTranslations('Common.table'); + const [sorting, setSorting] = useState([ + { id: 'createdAt', desc: true }, + ]); const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); @@ -145,6 +177,8 @@ export function UsersTable({ const user = row.original; return ; }, + minSize: 120, + size: 140, }, { accessorKey: 'email', @@ -173,6 +207,8 @@ export function UsersTable({
); }, + minSize: 180, + size: 200, }, { accessorKey: 'role', @@ -193,6 +229,8 @@ export function UsersTable({
); }, + minSize: 100, + size: 120, }, { accessorKey: 'createdAt', @@ -207,6 +245,8 @@ export function UsersTable({
); }, + minSize: 140, + size: 160, }, { accessorKey: 'customerId', @@ -235,6 +275,8 @@ export function UsersTable({
); }, + minSize: 120, + size: 140, }, { accessorKey: 'banned', @@ -256,6 +298,8 @@ export function UsersTable({

); }, + minSize: 100, + size: 120, }, { accessorKey: 'banReason', @@ -270,6 +314,8 @@ export function UsersTable({
); }, + minSize: 120, + size: 140, }, { accessorKey: 'banExpires', @@ -287,6 +333,8 @@ export function UsersTable({
); }, + minSize: 140, + size: 160, }, ]; @@ -395,16 +443,7 @@ export function UsersTable({ ))} - {loading ? ( - - - {t('loading')} - - - ) : table.getRowModel().rows?.length ? ( + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - {t('noResults')} + {loading ? tTable('loading') : tTable('noResults')} )} @@ -440,7 +479,7 @@ export function UsersTable({
{ + onSearch(event.target.value); + onPageChange(0); + }} + className="max-w-sm" + /> +
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {t( + columnIdToTranslationKey[ + column.id as keyof typeof columnIdToTranslationKey + ] || 'columns.columns' + )} + + ); + })} + + +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {loading ? tTable('loading') : tTable('noResults')} + + + )} + +
+
+ +
+
+ {total > 0 && {tTable('totalRecords', { count: total })}} +
+
+
+ + +
+
+ {tTable('page')} {pageIndex + 1} {' / '} + {Math.max(1, Math.ceil(total / pageSize))} +
+
+ + + + +
+
+
+
+ ); +} diff --git a/src/components/settings/notification/newsletter-form-card.tsx b/src/components/settings/notification/newsletter-form-card.tsx index cb1bd9b..43387b5 100644 --- a/src/components/settings/notification/newsletter-form-card.tsx +++ b/src/components/settings/notification/newsletter-form-card.tsx @@ -165,12 +165,7 @@ export function NewsletterFormCard({ className }: NewsletterFormCardProps) { }; return ( - + {t('newsletter.title')} diff --git a/src/components/settings/profile/update-avatar-card.tsx b/src/components/settings/profile/update-avatar-card.tsx index 8b4d514..2da97f4 100644 --- a/src/components/settings/profile/update-avatar-card.tsx +++ b/src/components/settings/profile/update-avatar-card.tsx @@ -128,7 +128,7 @@ export function UpdateAvatarCard({ className }: UpdateAvatarCardProps) { return ( diff --git a/src/components/settings/profile/update-name-card.tsx b/src/components/settings/profile/update-name-card.tsx index 5421872..b4d3abf 100644 --- a/src/components/settings/profile/update-name-card.tsx +++ b/src/components/settings/profile/update-name-card.tsx @@ -110,7 +110,7 @@ export function UpdateNameCard({ className }: UpdateNameCardProps) { return ( diff --git a/src/components/settings/security/delete-account-card.tsx b/src/components/settings/security/delete-account-card.tsx index c5bdf2c..0bc6edf 100644 --- a/src/components/settings/security/delete-account-card.tsx +++ b/src/components/settings/security/delete-account-card.tsx @@ -79,7 +79,7 @@ export function DeleteAccountCard() { return ( diff --git a/src/components/settings/security/password-card-wrapper.tsx b/src/components/settings/security/password-card-wrapper.tsx index 4949c96..497de96 100644 --- a/src/components/settings/security/password-card-wrapper.tsx +++ b/src/components/settings/security/password-card-wrapper.tsx @@ -80,11 +80,7 @@ export function PasswordCardWrapper() { function PasswordSkeletonCard() { const t = useTranslations('Dashboard.settings.security.updatePassword'); return ( - + {t('title')} {t('description')} diff --git a/src/components/settings/security/reset-password-card.tsx b/src/components/settings/security/reset-password-card.tsx index 274d09f..69d621d 100644 --- a/src/components/settings/security/reset-password-card.tsx +++ b/src/components/settings/security/reset-password-card.tsx @@ -55,7 +55,7 @@ export function ResetPasswordCard({ className }: ResetPasswordCardProps) { return ( diff --git a/src/components/settings/security/update-password-card.tsx b/src/components/settings/security/update-password-card.tsx index 54979d9..5b4588f 100644 --- a/src/components/settings/security/update-password-card.tsx +++ b/src/components/settings/security/update-password-card.tsx @@ -114,7 +114,7 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) { return ( diff --git a/src/config/credits-config.tsx b/src/config/credits-config.tsx new file mode 100644 index 0000000..da9d2df --- /dev/null +++ b/src/config/credits-config.tsx @@ -0,0 +1,58 @@ +'use client'; + +import type { CreditPackage } from '@/credits/types'; +import { useTranslations } from 'next-intl'; +import { websiteConfig } from './website'; + +/** + * Get credit packages with translations for client components + * + * NOTICE: This function should only be used in client components. + * If you need to get the credit packages in server components, use getAllCreditPackages instead. + * Use this function when showing the credit packages to the user. + * + * docs: + * https://mksaas.com/docs/config/credits + * + * @returns The credit packages with translated content + */ +export function getCreditPackages(): Record { + const t = useTranslations('CreditPackages'); + const creditConfig = websiteConfig.credits; + const packages: Record = {}; + + // Add translated content to each plan + if (creditConfig.packages.basic) { + packages.basic = { + ...creditConfig.packages.basic, + name: t('basic.name'), + description: t('basic.description'), + }; + } + + if (creditConfig.packages.standard) { + packages.standard = { + ...creditConfig.packages.standard, + name: t('standard.name'), + description: t('standard.description'), + }; + } + + if (creditConfig.packages.premium) { + packages.premium = { + ...creditConfig.packages.premium, + name: t('premium.name'), + description: t('premium.description'), + }; + } + + if (creditConfig.packages.enterprise) { + packages.enterprise = { + ...creditConfig.packages.enterprise, + name: t('enterprise.name'), + description: t('enterprise.description'), + }; + } + + return packages; +} diff --git a/src/config/navbar-config.tsx b/src/config/navbar-config.tsx index 19b665b..3bd9e85 100644 --- a/src/config/navbar-config.tsx +++ b/src/config/navbar-config.tsx @@ -72,13 +72,13 @@ export function getNavbarLinks(): NestedMenuItem[] { { title: t('ai.title'), items: [ - // { - // title: t('ai.items.text.title'), - // description: t('ai.items.text.description'), - // icon: , - // href: Routes.AIText, - // external: false, - // }, + { + title: t('ai.items.text.title'), + description: t('ai.items.text.description'), + icon: , + href: Routes.AIText, + external: false, + }, { title: t('ai.items.image.title'), description: t('ai.items.image.description'), diff --git a/src/config/sidebar-config.tsx b/src/config/sidebar-config.tsx index 2aa6936..316aaa6 100644 --- a/src/config/sidebar-config.tsx +++ b/src/config/sidebar-config.tsx @@ -5,6 +5,7 @@ import type { NestedMenuItem } from '@/types'; import { BellIcon, CircleUserRoundIcon, + CoinsIcon, CreditCardIcon, LayoutDashboardIcon, LockKeyholeIcon, @@ -13,6 +14,7 @@ import { UsersRoundIcon, } from 'lucide-react'; import { useTranslations } from 'next-intl'; +import { websiteConfig } from './website'; /** * Get sidebar config with translations @@ -66,6 +68,16 @@ export function getSidebarLinks(): NestedMenuItem[] { href: Routes.SettingsBilling, external: false, }, + ...(websiteConfig.credits.enableCredits + ? [ + { + title: t('settings.credits.title'), + icon: , + href: Routes.SettingsCredits, + external: false, + }, + ] + : []), { title: t('settings.security.title'), icon: , diff --git a/src/config/website.tsx b/src/config/website.tsx index 30f8235..e9f9231 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -90,6 +90,11 @@ export const websiteConfig: WebsiteConfig = { prices: [], isFree: true, isLifetime: false, + credits: { + enable: true, + amount: 50, + expireDays: 30, + }, }, pro: { id: 'pro', @@ -111,7 +116,12 @@ export const websiteConfig: WebsiteConfig = { ], isFree: false, isLifetime: false, - recommended: true, + popular: true, + credits: { + enable: true, + amount: 1000, + expireDays: 30, + }, }, lifetime: { id: 'lifetime', @@ -126,6 +136,70 @@ export const websiteConfig: WebsiteConfig = { ], isFree: false, isLifetime: true, + credits: { + enable: true, + amount: 1000, + expireDays: 30, + }, + }, + }, + }, + credits: { + enableCredits: true, + enableForFreePlan: false, + registerGiftCredits: { + enable: true, + credits: 50, + expireDays: 30, + }, + packages: { + basic: { + id: 'basic', + popular: false, + credits: 100, + expireDays: 30, + price: { + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_BASIC!, + amount: 990, + currency: 'USD', + allowPromotionCode: true, + }, + }, + standard: { + id: 'standard', + popular: true, + credits: 200, + expireDays: 30, + price: { + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_STANDARD!, + amount: 1490, + currency: 'USD', + allowPromotionCode: true, + }, + }, + premium: { + id: 'premium', + popular: false, + credits: 500, + expireDays: 30, + price: { + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_PREMIUM!, + amount: 3990, + currency: 'USD', + allowPromotionCode: true, + }, + }, + enterprise: { + id: 'enterprise', + popular: false, + credits: 1000, + expireDays: 30, + price: { + priceId: process.env.NEXT_PUBLIC_STRIPE_PRICE_CREDITS_ENTERPRISE!, + amount: 6990, + currency: 'USD', + allowPromotionCode: true, + }, }, }, }, diff --git a/src/credits/client.ts b/src/credits/client.ts new file mode 100644 index 0000000..d828662 --- /dev/null +++ b/src/credits/client.ts @@ -0,0 +1,21 @@ +import { getCreditPackages } from '@/config/credits-config'; +import type { CreditPackage } from './types'; + +/** + * Get credit packages, used in client components + * @returns Credit packages + */ +export function getCreditPackagesInClient(): CreditPackage[] { + return Object.values(getCreditPackages()); +} + +/** + * Get credit package by id, used in client components + * @param id - Credit package id + * @returns Credit package + */ +export function getCreditPackageByIdInClient( + id: string +): CreditPackage | undefined { + return getCreditPackagesInClient().find((pkg) => pkg.id === id); +} diff --git a/src/credits/credits.ts b/src/credits/credits.ts new file mode 100644 index 0000000..8edd58f --- /dev/null +++ b/src/credits/credits.ts @@ -0,0 +1,623 @@ +import { randomUUID } from 'crypto'; +import { websiteConfig } from '@/config/website'; +import { getDb } from '@/db'; +import { creditTransaction, payment, user, userCredit } from '@/db/schema'; +import { findPlanByPriceId } from '@/lib/price-plan'; +import { addDays, isAfter } from 'date-fns'; +import { and, asc, desc, eq, gt, isNull, not, or } from 'drizzle-orm'; +import { CREDIT_TRANSACTION_TYPE } from './types'; + +/** + * Get user's current credit balance + * @param userId - User ID + * @returns User's current credit balance + */ +export async function getUserCredits(userId: string): Promise { + const db = await getDb(); + const record = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + return record[0]?.currentCredits || 0; +} + +/** + * Update user's current credit balance + * @param userId - User ID + * @param credits - New credit balance + */ +export async function updateUserCredits(userId: string, credits: number) { + const db = await getDb(); + await db + .update(userCredit) + .set({ currentCredits: credits, updatedAt: new Date() }) + .where(eq(userCredit.userId, userId)); +} + +/** + * Update user's last refresh time + * @param userId - User ID + * @param date - Last refresh time + */ +export async function updateUserLastRefreshAt(userId: string, date: Date) { + const db = await getDb(); + await db + .update(userCredit) + .set({ lastRefreshAt: date, updatedAt: new Date() }) + .where(eq(userCredit.userId, userId)); +} + +/** + * Write a credit transaction record + * @param params - Credit transaction parameters + */ +export async function saveCreditTransaction({ + userId, + type, + amount, + description, + paymentId, + expirationDate, +}: { + userId: string; + type: string; + amount: number; + description: string; + paymentId?: string; + expirationDate?: Date; +}) { + if (!userId || !type || !description) { + console.error( + 'saveCreditTransaction, invalid params', + userId, + type, + description + ); + throw new Error('saveCreditTransaction, invalid params'); + } + if (!Number.isFinite(amount) || amount === 0) { + console.error('saveCreditTransaction, invalid amount', userId, amount); + throw new Error('saveCreditTransaction, invalid amount'); + } + const db = await getDb(); + await db.insert(creditTransaction).values({ + id: randomUUID(), + userId, + type, + amount, + // remaining amount is the same as amount for earn transactions + // remaining amount is null for spend transactions + remainingAmount: amount > 0 ? amount : null, + description, + paymentId, + expirationDate, + createdAt: new Date(), + updatedAt: new Date(), + }); +} + +/** + * Add credits (registration, monthly, purchase, etc.) + * @param params - Credit creation parameters + */ +export async function addCredits({ + userId, + amount, + type, + description, + paymentId, + expireDays, +}: { + userId: string; + amount: number; + type: string; + description: string; + paymentId?: string; + expireDays?: number; +}) { + if (!userId || !type || !description) { + console.error('addCredits, invalid params', userId, type, description); + throw new Error('Invalid params'); + } + if (!Number.isFinite(amount) || amount <= 0) { + console.error('addCredits, invalid amount', userId, amount); + throw new Error('Invalid amount'); + } + if ( + expireDays !== undefined && + (!Number.isFinite(expireDays) || expireDays <= 0) + ) { + console.error('addCredits, invalid expire days', userId, expireDays); + throw new Error('Invalid expire days'); + } + // Process expired credits first + await processExpiredCredits(userId); + // Update user credit balance + const db = await getDb(); + const current = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + // const newBalance = (current[0]?.currentCredits || 0) + amount; + if (current.length > 0) { + const newBalance = (current[0]?.currentCredits || 0) + amount; + console.log('addCredits, update user credit', userId, newBalance); + await db + .update(userCredit) + .set({ + currentCredits: newBalance, + // lastRefreshAt: new Date(), // NOTE: we can not update this field here + updatedAt: new Date(), + }) + .where(eq(userCredit.userId, userId)); + } else { + const newBalance = amount; + console.log('addCredits, insert user credit', userId, newBalance); + await db.insert(userCredit).values({ + id: randomUUID(), + userId, + currentCredits: newBalance, + // lastRefreshAt: new Date(), // NOTE: we can not update this field here + createdAt: new Date(), + updatedAt: new Date(), + }); + } + // Write credit transaction record + await saveCreditTransaction({ + userId, + type, + amount, + description, + paymentId, + expirationDate: expireDays ? addDays(new Date(), expireDays) : undefined, + }); +} + +export async function hasEnoughCredits({ + userId, + requiredCredits, +}: { + userId: string; + requiredCredits: number; +}) { + const balance = await getUserCredits(userId); + return balance >= requiredCredits; +} + +/** + * Consume credits (FIFO, by expiration) + * @param params - Credit consumption parameters + */ +export async function consumeCredits({ + userId, + amount, + description, +}: { + userId: string; + amount: number; + description: string; +}) { + if (!userId || !description) { + console.error('consumeCredits, invalid params', userId, description); + throw new Error('Invalid params'); + } + if (!Number.isFinite(amount) || amount <= 0) { + console.error('consumeCredits, invalid amount', userId, amount); + throw new Error('Invalid amount'); + } + // Process expired credits first + await processExpiredCredits(userId); + // Check balance + if (!(await hasEnoughCredits({ userId, requiredCredits: amount }))) { + console.error( + `consumeCredits, insufficient credits for user ${userId}, required: ${amount}` + ); + throw new Error('Insufficient credits'); + } + // FIFO consumption: consume from the earliest unexpired credits first + const db = await getDb(); + const now = new Date(); + const transactions = await db + .select() + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + // Exclude usage and expire records (these are consumption/expiration logs) + not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.USAGE)), + not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.EXPIRE)), + // Only include transactions with remaining amount > 0 + gt(creditTransaction.remainingAmount, 0), + // Only include unexpired credits (either no expiration date or not yet expired) + or( + isNull(creditTransaction.expirationDate), + gt(creditTransaction.expirationDate, now) + ) + ) + ) + .orderBy( + asc(creditTransaction.expirationDate), + asc(creditTransaction.createdAt) + ); + // Consume credits + let remainingToDeduct = amount; + for (const transaction of transactions) { + if (remainingToDeduct <= 0) break; + const remainingAmount = transaction.remainingAmount || 0; + if (remainingAmount <= 0) continue; + // credits to consume at most in this transaction + const deductFromThis = Math.min(remainingAmount, remainingToDeduct); + await db + .update(creditTransaction) + .set({ + remainingAmount: remainingAmount - deductFromThis, + updatedAt: new Date(), + }) + .where(eq(creditTransaction.id, transaction.id)); + remainingToDeduct -= deductFromThis; + } + // Update balance + const current = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + const newBalance = (current[0]?.currentCredits || 0) - amount; + await db + .update(userCredit) + .set({ currentCredits: newBalance, updatedAt: new Date() }) + .where(eq(userCredit.userId, userId)); + // Write usage record + await saveCreditTransaction({ + userId, + type: CREDIT_TRANSACTION_TYPE.USAGE, + amount: -amount, + description, + }); +} + +/** + * Process expired credits + * @param userId - User ID + */ +export async function processExpiredCredits(userId: string) { + const now = new Date(); + // Get all credit transactions that can expire (have expirationDate and not yet processed) + const db = await getDb(); + const transactions = await db + .select() + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + // Exclude usage and expire records (these are consumption/expiration logs) + not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.USAGE)), + not(eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.EXPIRE)), + // Only include transactions with expirationDate set + not(isNull(creditTransaction.expirationDate)), + // Only include transactions not yet processed for expiration + isNull(creditTransaction.expirationDateProcessedAt), + // Only include transactions with remaining amount > 0 + gt(creditTransaction.remainingAmount, 0) + ) + ); + let expiredTotal = 0; + // Process expired credit transactions + for (const transaction of transactions) { + if ( + transaction.expirationDate && + isAfter(now, transaction.expirationDate) && + !transaction.expirationDateProcessedAt + ) { + const remain = transaction.remainingAmount || 0; + if (remain > 0) { + expiredTotal += remain; + await db + .update(creditTransaction) + .set({ + remainingAmount: 0, + expirationDateProcessedAt: now, + updatedAt: now, + }) + .where(eq(creditTransaction.id, transaction.id)); + } + } + } + if (expiredTotal > 0) { + // Deduct expired credits from balance + const current = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + const newBalance = Math.max( + 0, + (current[0]?.currentCredits || 0) - expiredTotal + ); + await db + .update(userCredit) + .set({ currentCredits: newBalance, updatedAt: now }) + .where(eq(userCredit.userId, userId)); + // Write expire record + await saveCreditTransaction({ + userId, + type: CREDIT_TRANSACTION_TYPE.EXPIRE, + amount: -expiredTotal, + description: `Expire credits: ${expiredTotal}`, + }); + + console.log( + `processExpiredCredits, ${expiredTotal} credits expired for user ${userId}` + ); + } +} + +/** + * Add register gift credits + * @param userId - User ID + */ +export async function addRegisterGiftCredits(userId: string) { + if (!websiteConfig.credits.registerGiftCredits.enable) { + console.log('addRegisterGiftCredits, disabled'); + return; + } + // Check if user has already received register gift credits + const db = await getDb(); + const record = await db + .select() + .from(creditTransaction) + .where( + and( + eq(creditTransaction.userId, userId), + eq(creditTransaction.type, CREDIT_TRANSACTION_TYPE.REGISTER_GIFT) + ) + ) + .limit(1); + // add register gift credits if user has not received them yet + if (record.length === 0) { + const credits = websiteConfig.credits.registerGiftCredits.credits; + const expireDays = websiteConfig.credits.registerGiftCredits.expireDays; + await addCredits({ + userId, + amount: credits, + type: CREDIT_TRANSACTION_TYPE.REGISTER_GIFT, + description: `Register gift credits: ${credits}`, + expireDays, + }); + + console.log( + `addRegisterGiftCredits, ${credits} credits for user ${userId}` + ); + } +} + +/** + * Add free monthly credits + * @param userId - User ID + */ +export async function addMonthlyFreeCredits(userId: string) { + const freePlan = Object.values(websiteConfig.price.plans).find( + (plan) => plan.isFree && !plan.disabled + ); + if (!freePlan) { + console.log('addMonthlyFreeCredits, no free plan found'); + return; + } + if ( + freePlan.disabled || + !freePlan.credits?.enable || + !freePlan.credits?.amount + ) { + console.log( + 'addMonthlyFreeCredits, plan disabled or credits disabled', + freePlan.id + ); + return; + } + // Check last refresh time + const db = await getDb(); + const record = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + const now = new Date(); + let canAdd = false; + // never added credits before + if (!record[0]?.lastRefreshAt) { + canAdd = true; + } else { + const last = new Date(record[0].lastRefreshAt); + // different month or year means new month + canAdd = + now.getMonth() !== last.getMonth() || + now.getFullYear() !== last.getFullYear(); + } + // add credits if it's a new month + if (canAdd) { + const credits = freePlan.credits.amount; + const expireDays = freePlan.credits.expireDays; + await addCredits({ + userId, + amount: credits, + type: CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH, + description: `Free monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`, + expireDays, + }); + + console.log( + `addMonthlyFreeCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}` + ); + } +} + +/** + * Add subscription renewal credits + * @param userId - User ID + * @param priceId - Price ID + */ +export async function addSubscriptionCredits(userId: string, priceId: string) { + const pricePlan = findPlanByPriceId(priceId); + if ( + !pricePlan || + pricePlan.isFree || + !pricePlan.credits || + !pricePlan.credits.enable + ) { + console.log( + `addSubscriptionRenewalCredits, no credits configured for plan ${priceId}` + ); + return; + } + + const credits = pricePlan.credits.amount; + const expireDays = pricePlan.credits.expireDays; + const now = new Date(); + + await addCredits({ + userId, + amount: credits, + type: CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL, + description: `Subscription renewal credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`, + expireDays, + }); + + console.log( + `addSubscriptionRenewalCredits, ${credits} credits for user ${userId}, priceId: ${priceId}` + ); +} + +/** + * Add lifetime monthly credits + * @param userId - User ID + */ +export async function addLifetimeMonthlyCredits(userId: string) { + const lifetimePlan = Object.values(websiteConfig.price.plans).find( + (plan) => plan.isLifetime && !plan.disabled + ); + if ( + !lifetimePlan || + lifetimePlan.disabled || + !lifetimePlan.credits || + !lifetimePlan.credits.enable + ) { + console.log( + 'addLifetimeMonthlyCredits, plan disabled or credits disabled', + lifetimePlan?.id + ); + return; + } + + // Check last refresh time to avoid duplicate monthly credits + const db = await getDb(); + const record = await db + .select() + .from(userCredit) + .where(eq(userCredit.userId, userId)) + .limit(1); + + const now = new Date(); + let canAdd = false; + + // Check if user has never received lifetime credits or it's a new month + if (!record[0]?.lastRefreshAt) { + canAdd = true; + } else { + const last = new Date(record[0].lastRefreshAt); + // different month or year means new month + canAdd = + now.getMonth() !== last.getMonth() || + now.getFullYear() !== last.getFullYear(); + } + + // Add credits if it's a new month + if (canAdd) { + const credits = lifetimePlan.credits.amount; + const expireDays = lifetimePlan.credits.expireDays; + + await addCredits({ + userId, + amount: credits, + type: CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY, + description: `Lifetime monthly credits: ${credits} for ${now.getFullYear()}-${now.getMonth() + 1}`, + expireDays, + }); + + // Update last refresh time for lifetime credits + await updateUserLastRefreshAt(userId, now); + + console.log( + `addLifetimeMonthlyCredits, ${credits} credits for user ${userId}, date: ${now.getFullYear()}-${now.getMonth() + 1}` + ); + } +} + +/** + * Distribute credits to all users based on their plan type + * This function is designed to be called by a cron job + */ +export async function distributeCreditsToAllUsers() { + console.log('distributing credits to all users start'); + + const db = await getDb(); + + // Get all users with their current active payments/subscriptions + const users = await db + .select({ + userId: user.id, + email: user.email, + name: user.name, + }) + .from(user) + .where(eq(user.banned, false)); // Only active users + console.log('distributing credits to all users, users count:', users.length); + + let processedCount = 0; + let errorCount = 0; + + for (const userRecord of users) { + try { + // Get user's current active subscription/payment + const activePayments = await db + .select() + .from(payment) + .where( + and( + eq(payment.userId, userRecord.userId), + or(eq(payment.status, 'active'), eq(payment.status, 'trialing')) + ) + ) + .orderBy(desc(payment.createdAt)); + + if (activePayments.length > 0) { + // User has active subscription - check what type + const activePayment = activePayments[0]; + const pricePlan = findPlanByPriceId(activePayment.priceId); + + if (pricePlan?.isLifetime) { + // Lifetime user - add monthly credits + await addLifetimeMonthlyCredits(userRecord.userId); + } + // Note: Subscription renewals are handled by Stripe webhooks, not here + } else { + // User has no active subscription - add free monthly credits if enabled + await addMonthlyFreeCredits(userRecord.userId); + } + + processedCount++; + } catch (error) { + console.error( + `distributing credits to all users error, user: ${userRecord.userId}, error:`, + error + ); + errorCount++; + } + } + + console.log( + `distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}` + ); + return { processedCount, errorCount }; +} diff --git a/src/credits/server.ts b/src/credits/server.ts new file mode 100644 index 0000000..f812f84 --- /dev/null +++ b/src/credits/server.ts @@ -0,0 +1,19 @@ +import { websiteConfig } from '@/config/website'; +import type { CreditPackage } from './types'; + +/** + * Get all credit packages, can be used in server or client components + * @returns Credit packages + */ +export function getAllCreditPackages(): CreditPackage[] { + return Object.values(websiteConfig.credits.packages); +} + +/** + * Get credit package by id, can be used in server or client components + * @param id - Credit package id + * @returns Credit package + */ +export function getCreditPackageById(id: string): CreditPackage | undefined { + return getAllCreditPackages().find((pkg) => pkg.id === id); +} diff --git a/src/credits/types.ts b/src/credits/types.ts new file mode 100644 index 0000000..c79a1ba --- /dev/null +++ b/src/credits/types.ts @@ -0,0 +1,36 @@ +/** + * Credit transaction type enum + */ +export enum CREDIT_TRANSACTION_TYPE { + MONTHLY_REFRESH = 'MONTHLY_REFRESH', // Credits earned by monthly refresh (free users) + REGISTER_GIFT = 'REGISTER_GIFT', // Credits earned by register gift + PURCHASE_PACKAGE = 'PURCHASE_PACKAGE', // Credits earned by purchase package + SUBSCRIPTION_RENEWAL = 'SUBSCRIPTION_RENEWAL', // Credits earned by subscription renewal + LIFETIME_MONTHLY = 'LIFETIME_MONTHLY', // Credits earned by lifetime plan monthly distribution + USAGE = 'USAGE', // Credits spent by usage + EXPIRE = 'EXPIRE', // Credits expired +} + +/** + * Credit package price + */ +export interface CreditPackagePrice { + priceId: string; // Stripe price ID (not product id) + amount: number; // Price amount in currency units (dollars, euros, etc.) + currency: string; // Currency code (e.g., USD) + allowPromotionCode?: boolean; // Whether to allow promotion code for this price +} + +/** + * Credit package + */ +export interface CreditPackage { + id: string; // Unique identifier for the package + credits: number; // Number of credits in the package + price: CreditPackagePrice; // Price of the package + popular: boolean; // Whether the package is popular + name?: string; // Display name of the package + description?: string; // Description of the package + expireDays?: number; // Number of days to expire the credits, undefined means no expire + disabled?: boolean; // Whether the package is disabled in the UI +} diff --git a/src/db/migrations/0001_woozy_jigsaw.sql b/src/db/migrations/0001_woozy_jigsaw.sql new file mode 100644 index 0000000..dd997f8 --- /dev/null +++ b/src/db/migrations/0001_woozy_jigsaw.sql @@ -0,0 +1,25 @@ +CREATE TABLE "credit_transaction" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "type" text NOT NULL, + "description" text, + "amount" integer NOT NULL, + "remaining_amount" integer, + "payment_id" text, + "expiration_date" timestamp, + "expiration_date_processed_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_credit" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "current_credits" integer DEFAULT 0 NOT NULL, + "last_refresh_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "credit_transaction" ADD CONSTRAINT "credit_transaction_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_credit" ADD CONSTRAINT "user_credit_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..83c624e --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,635 @@ +{ + "id": "6ed4f085-66bb-42c4-a708-2e5d86438ca2", + "prevId": "7ecbd97a-94eb-4a46-996e-dbff727fc0c7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_transaction": { + "name": "credit_transaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "remaining_amount": { + "name": "remaining_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "payment_id": { + "name": "payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expiration_date_processed_at": { + "name": "expiration_date_processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "credit_transaction_user_id_user_id_fk": { + "name": "credit_transaction_user_id_user_id_fk", + "tableFrom": "credit_transaction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment": { + "name": "payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "payment_user_id_user_id_fk": { + "name": "payment_user_id_user_id_fk", + "tableFrom": "payment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_credit": { + "name": "user_credit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_credits": { + "name": "current_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_credit_user_id_user_id_fk": { + "name": "user_credit_user_id_user_id_fk", + "tableFrom": "user_credit", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 578b9e1..8f5787e 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1744304844165, "tag": "0000_fine_sir_ram", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1751214200582, + "tag": "0001_woozy_jigsaw", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index e8afeb3..0f05063 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; export const user = pgTable("user", { id: text("id").primaryKey(), @@ -69,3 +69,26 @@ export const payment = pgTable("payment", { createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }); + +export const userCredit = pgTable("user_credit", { + id: text("id").primaryKey(), + userId: text("user_id").notNull().references(() => user.id, { onDelete: 'cascade' }), + currentCredits: integer("current_credits").notNull().default(0), + lastRefreshAt: timestamp("last_refresh_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); + +export const creditTransaction = pgTable("credit_transaction", { + id: text("id").primaryKey(), + userId: text("user_id").notNull().references(() => user.id, { onDelete: 'cascade' }), + type: text("type").notNull(), + description: text("description"), + amount: integer("amount").notNull(), + remainingAmount: integer("remaining_amount"), + paymentId: text("payment_id"), + expirationDate: timestamp("expiration_date"), + expirationDateProcessedAt: timestamp("expiration_date_processed_at"), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), +}); diff --git a/src/hooks/use-credits.ts b/src/hooks/use-credits.ts new file mode 100644 index 0000000..f3837c6 --- /dev/null +++ b/src/hooks/use-credits.ts @@ -0,0 +1,52 @@ +import { authClient } from '@/lib/auth-client'; +import { useCreditsStore } from '@/stores/credits-store'; +import { useCallback, useEffect } from 'react'; + +/** + * Hook for accessing and managing credits state + * + * This hook provides access to the credits state and methods to manage it. + * It also automatically fetches credits information when the user changes. + */ +export function useCredits() { + const { + balance, + isLoading, + error, + fetchCredits: fetchCreditsFromStore, + consumeCredits, + } = useCreditsStore(); + + const { data: session } = authClient.useSession(); + + const fetchCredits = useCallback( + (force = false) => { + const currentUser = session?.user; + if (currentUser) { + fetchCreditsFromStore(currentUser, force); + } + }, + [session?.user, fetchCreditsFromStore] + ); + + useEffect(() => { + const currentUser = session?.user; + if (currentUser) { + fetchCreditsFromStore(currentUser); + } + }, [session?.user, fetchCreditsFromStore]); + + return { + // State + balance, + isLoading, + error, + + // Methods + fetchCredits, + consumeCredits, + + // Helper methods + hasEnoughCredits: (amount: number) => balance >= amount, + }; +} diff --git a/src/hooks/use-payment.ts b/src/hooks/use-payment.ts index f88dd48..259c020 100644 --- a/src/hooks/use-payment.ts +++ b/src/hooks/use-payment.ts @@ -1,6 +1,6 @@ import { authClient } from '@/lib/auth-client'; import { usePaymentStore } from '@/stores/payment-store'; -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; /** * Hook for accessing and managing payment state @@ -9,31 +9,41 @@ import { useEffect } from 'react'; * It also automatically fetches payment information when the user changes. */ export function usePayment() { - const { currentPlan, subscription, isLoading, error, fetchPayment } = - usePaymentStore(); - - const { data: session } = authClient.useSession(); - - useEffect(() => { - const currentUser = session?.user; - // Fetch payment data whenever the user session changes - if (currentUser) { - console.log('fetching payment info for user', currentUser.id); - fetchPayment(currentUser); - } - }, [session, fetchPayment]); - - return { + const { currentPlan, subscription, isLoading, error, - refetch: () => { + fetchPayment: fetchPaymentFromStore, + } = usePaymentStore(); + + const { data: session } = authClient.useSession(); + + const fetchPayment = useCallback( + (force = false) => { const currentUser = session?.user; if (currentUser) { - console.log('refetching payment info for user', currentUser.id); - fetchPayment(currentUser); + fetchPaymentFromStore(currentUser, force); } }, + [session?.user, fetchPaymentFromStore] + ); + + useEffect(() => { + const currentUser = session?.user; + if (currentUser) { + fetchPaymentFromStore(currentUser); + } + }, [session?.user, fetchPaymentFromStore]); + + return { + // State + currentPlan, + subscription, + isLoading, + error, + + // Methods + fetchPayment, }; } diff --git a/src/inngest/client.ts b/src/inngest/client.ts new file mode 100644 index 0000000..6164102 --- /dev/null +++ b/src/inngest/client.ts @@ -0,0 +1,8 @@ +import { Inngest } from 'inngest'; + +/** + * Create a client to send and receive events + * + * https://www.inngest.com/docs/getting-started/nextjs-quick-start + */ +export const inngest = new Inngest({ id: 'mksaas-template' }); diff --git a/src/inngest/functions.ts b/src/inngest/functions.ts new file mode 100644 index 0000000..c8dabe3 --- /dev/null +++ b/src/inngest/functions.ts @@ -0,0 +1,46 @@ +import { distributeCreditsToAllUsers } from '@/credits/credits'; +import { inngest } from './client'; + +/** + * Distribute credits to all users daily + * + * https://www.inngest.com/docs/guides/scheduled-functions + */ +export const distributeCreditsDaily = inngest.createFunction( + { id: 'distribute-credits-daily' }, + { cron: 'TZ=Asia/Shanghai 0 1 * * *' }, + async ({ step }) => { + // You should use step.run for any async or long-running logic. + // This allows Inngest to track, retry, and visualize each step in your workflow. + await step.run('distribute-credits-to-all-users', async () => { + console.log('distributing credits to all users start'); + const { processedCount, errorCount } = + await distributeCreditsToAllUsers(); + console.log( + `distributing credits to all users end, processed: ${processedCount}, errors: ${errorCount}` + ); + return { + message: `credits distributed, processed: ${processedCount}, errors: ${errorCount}`, + processedCount, + errorCount, + }; + }); + // you can add new steps here, for example, send email to admin + } +); + +/** + * Hello World function, for testing inngest + * + * https://www.inngest.com/docs/guides/scheduled-functions + */ +export const helloWorld = inngest.createFunction( + { id: 'hello-world' }, + { event: 'test/hello.world' }, + async ({ event, step }) => { + console.log('Hello World function start'); + await step.sleep('wait-a-moment', '1s'); + console.log('Hello World function end'); + return { message: `Hello ${event.data.email}!` }; + } +); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index bd4c3c7..c034285 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,14 +1,19 @@ import { websiteConfig } from '@/config/website'; +import { + addMonthlyFreeCredits, + addRegisterGiftCredits, +} from '@/credits/credits'; import { getDb } from '@/db/index'; import { defaultMessages } from '@/i18n/messages'; import { LOCALE_COOKIE_NAME, routing } from '@/i18n/routing'; import { sendEmail } from '@/mail'; import { subscribe } from '@/newsletter'; -import { betterAuth } from 'better-auth'; +import { type User, betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { admin } from 'better-auth/plugins'; import { parse as parseCookies } from 'cookie'; import type { Locale } from 'next-intl'; +import { getAllPricePlans } from './price-plan'; import { getBaseUrl, getUrlWithLocaleInCallbackUrl } from './urls/urls'; /** @@ -114,21 +119,7 @@ export const auth = betterAuth({ user: { create: { after: async (user) => { - // Auto subscribe user to newsletter after sign up if enabled in website config - if (user.email && websiteConfig.newsletter.autoSubscribeAfterSignUp) { - try { - const subscribed = await subscribe(user.email); - if (!subscribed) { - console.error( - `Failed to subscribe user ${user.email} to newsletter` - ); - } else { - console.log(`User ${user.email} subscribed to newsletter`); - } - } catch (error) { - console.error('Newsletter subscription error:', error); - } - } + await onCreateUser(user); }, }, }, @@ -164,3 +155,59 @@ export function getLocaleFromRequest(request?: Request): Locale { const cookies = parseCookies(request?.headers.get('cookie') ?? ''); return (cookies[LOCALE_COOKIE_NAME] as Locale) ?? routing.defaultLocale; } + +/** + * On create user hook + * + * @param user - The user to create + */ +async function onCreateUser(user: User) { + // Auto subscribe user to newsletter after sign up if enabled in website config + if (user.email && websiteConfig.newsletter.autoSubscribeAfterSignUp) { + try { + const subscribed = await subscribe(user.email); + if (!subscribed) { + console.error(`Failed to subscribe user ${user.email} to newsletter`); + } else { + console.log(`User ${user.email} subscribed to newsletter`); + } + } catch (error) { + console.error('Newsletter subscription error:', error); + } + } + + // Add register gift credits to the user if enabled in website config + if ( + websiteConfig.credits.registerGiftCredits.enable && + websiteConfig.credits.registerGiftCredits.credits > 0 + ) { + try { + await addRegisterGiftCredits(user.id); + const credits = websiteConfig.credits.registerGiftCredits.credits; + console.log( + `added register gift credits for user ${user.id}, credits: ${credits}` + ); + } catch (error) { + console.error('Register gift credits error:', error); + } + } + + // Add free monthly credits to the user if enabled in website config + const pricePlans = await getAllPricePlans(); + const freePlan = pricePlans.find((plan) => plan.isFree); + if ( + freePlan?.credits?.enable && + freePlan?.credits?.amount && + freePlan?.credits?.amount > 0 + ) { + try { + await addMonthlyFreeCredits(user.id); + const credits = freePlan.credits.amount; + console.log( + `added free monthly credits for user ${user.id}, credits: ${credits}` + ); + } catch (error) { + console.error('Free monthly credits error:', error); + } + } +} diff --git a/src/payment/index.ts b/src/payment/index.ts index 0a3d09f..ce0f0bc 100644 --- a/src/payment/index.ts +++ b/src/payment/index.ts @@ -3,6 +3,7 @@ import { StripeProvider } from './provider/stripe'; import type { CheckoutResult, CreateCheckoutParams, + CreateCreditCheckoutParams, CreatePortalParams, PaymentProvider, PortalResult, @@ -56,6 +57,18 @@ export const createCheckout = async ( return provider.createCheckout(params); }; +/** + * Create a checkout session for a credit package + * @param params Parameters for creating the checkout session + * @returns Checkout result + */ +export const createCreditCheckout = async ( + params: CreateCreditCheckoutParams +): Promise => { + const provider = getPaymentProvider(); + return provider.createCreditCheckout(params); +}; + /** * Create a customer portal session * @param params Parameters for creating the portal diff --git a/src/payment/provider/stripe.ts b/src/payment/provider/stripe.ts index 3485c92..ee8b736 100644 --- a/src/payment/provider/stripe.ts +++ b/src/payment/provider/stripe.ts @@ -1,6 +1,14 @@ import { randomUUID } from 'crypto'; +import { websiteConfig } from '@/config/website'; +import { + addCredits, + addLifetimeMonthlyCredits, + addSubscriptionCredits, +} from '@/credits/credits'; +import { getCreditPackageById } from '@/credits/server'; +import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; import { getDb } from '@/db'; -import { payment, session, user } from '@/db/schema'; +import { payment, user } from '@/db/schema'; import { findPlanByPlanId, findPlanByPriceId, @@ -12,6 +20,7 @@ import { Stripe } from 'stripe'; import { type CheckoutResult, type CreateCheckoutParams, + type CreateCreditCheckoutParams, type CreatePortalParams, type PaymentProvider, type PaymentStatus, @@ -84,9 +93,7 @@ export class StripeProvider implements PaymentProvider { // user does not exist, update user with customer id // in case you deleted user in database, but forgot to delete customer in Stripe if (!userId) { - console.log( - `User ${email} does not exist, update with customer id ${customerId}` - ); + console.log('User does not exist, update with customer id (hidden)'); await this.updateUserWithCustomerId(customerId, email); } return customerId; @@ -131,9 +138,9 @@ export class StripeProvider implements PaymentProvider { .returning({ id: user.id }); if (result.length > 0) { - console.log(`Updated user ${email} with customer ID ${customerId}`); + console.log('Updated user with customer ID (hidden)'); } else { - console.log(`No user found with email ${email}`); + console.log('No user found with given email'); } } catch (error) { console.error('Update user with customer ID error:', error); @@ -161,7 +168,7 @@ export class StripeProvider implements PaymentProvider { if (result.length > 0) { return result[0].id; } - console.warn(`No user found with customerId ${customerId}`); + console.warn('No user found with given customerId'); return undefined; } catch (error) { @@ -285,6 +292,104 @@ export class StripeProvider implements PaymentProvider { } } + /** + * Create a checkout session for a plan + * @param params Parameters for creating the checkout session + * @returns Checkout result + */ + public async createCreditCheckout( + params: CreateCreditCheckoutParams + ): Promise { + const { + packageId, + priceId, + customerEmail, + successUrl, + cancelUrl, + metadata, + locale, + } = params; + + try { + // Get credit package + const creditPackage = getCreditPackageById(packageId); + if (!creditPackage) { + throw new Error(`Credit package with ID ${packageId} not found`); + } + + // Get priceId from credit package + const priceId = creditPackage.price.priceId; + if (!priceId) { + throw new Error(`Price ID not found for credit package ${packageId}`); + } + + // Get userName from metadata if available + const userName = metadata?.userName; + + // Create or get customer + const customerId = await this.createOrGetCustomer( + customerEmail, + userName + ); + + // Add planId and priceId to metadata, so we can get it in the webhook event + const customMetadata = { + ...metadata, + packageId, + priceId, + }; + + // Set up the line items + const lineItems = [ + { + price: priceId, + quantity: 1, + }, + ]; + + // Create checkout session parameters + const checkoutParams: Stripe.Checkout.SessionCreateParams = { + line_items: lineItems, + mode: 'payment', + success_url: successUrl ?? '', + cancel_url: cancelUrl ?? '', + metadata: customMetadata, + allow_promotion_codes: creditPackage.price.allowPromotionCode ?? false, + }; + + // Add customer to checkout session + checkoutParams.customer = customerId; + + // Add locale if provided + if (locale) { + checkoutParams.locale = this.mapLocaleToStripeLocale( + locale + ) as Stripe.Checkout.SessionCreateParams.Locale; + } + + // Add payment intent data for one-time payments + checkoutParams.payment_intent_data = { + metadata: customMetadata, + }; + // Automatically create an invoice for the one-time payment + checkoutParams.invoice_creation = { + enabled: true, + }; + + // Create the checkout session + const session = + await this.stripe.checkout.sessions.create(checkoutParams); + + return { + url: session.url!, + id: session.id, + }; + } catch (error) { + console.error('Create credit checkout session error:', error); + throw new Error('Failed to create credit checkout session'); + } + } + /** * Create a customer portal session * @param params Parameters for creating the portal @@ -400,7 +505,11 @@ export class StripeProvider implements PaymentProvider { // Only process one-time payments (likely for lifetime plan) if (session.mode === 'payment') { - await this.onOnetimePayment(session); + if (session.metadata?.type === 'credit_purchase') { + await this.onCreditPurchase(session); + } else { + await this.onOnetimePayment(session); + } } } } @@ -417,26 +526,20 @@ export class StripeProvider implements PaymentProvider { private async onCreateSubscription( stripeSubscription: Stripe.Subscription ): Promise { - console.log( - `>> Create payment record for Stripe subscription ${stripeSubscription.id}` - ); + console.log('>> Create payment record for Stripe subscription'); const customerId = stripeSubscription.customer as string; // get priceId from subscription items (this is always available) const priceId = stripeSubscription.items.data[0]?.price.id; if (!priceId) { - console.warn( - `<< No priceId found for subscription ${stripeSubscription.id}` - ); + console.warn('No priceId found for subscription'); return; } // get userId from metadata, we add it in the createCheckout session const userId = stripeSubscription.metadata.userId; if (!userId) { - console.warn( - `<< No userId found for subscription ${stripeSubscription.id}` - ); + console.warn('No userId found for subscription'); return; } @@ -476,13 +579,18 @@ export class StripeProvider implements PaymentProvider { .returning({ id: payment.id }); if (result.length > 0) { - console.log( - `<< Created new payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}` - ); + console.log('<< Created new payment record for Stripe subscription'); } else { - console.warn( - `<< No payment record created for Stripe subscription ${stripeSubscription.id}` - ); + console.warn('<< No payment record created for Stripe subscription'); + } + + // Conditionally handle credits after subscription creation + if (websiteConfig.credits?.enableCredits) { + // Add subscription renewal credits if plan config enables credits + const pricePlan = findPlanByPriceId(priceId); + if (pricePlan?.credits?.enable) { + await addSubscriptionCredits(userId, priceId); + } } } @@ -493,19 +601,43 @@ export class StripeProvider implements PaymentProvider { private async onUpdateSubscription( stripeSubscription: Stripe.Subscription ): Promise { - console.log( - `>> Update payment record for Stripe subscription ${stripeSubscription.id}` - ); + console.log('>> Update payment record for Stripe subscription'); // get priceId from subscription items (this is always available) const priceId = stripeSubscription.items.data[0]?.price.id; if (!priceId) { - console.warn( - `<< No priceId found for subscription ${stripeSubscription.id}` - ); + console.warn('No priceId found for subscription'); return; } + // Get current payment record to check for period changes (indicating renewal) + const db = await getDb(); + const payments = await db + .select({ + userId: payment.userId, + periodStart: payment.periodStart, + periodEnd: payment.periodEnd, + }) + .from(payment) + .where(eq(payment.subscriptionId, stripeSubscription.id)) + .limit(1); + + // get new period start and end + const newPeriodStart = stripeSubscription.current_period_start + ? new Date(stripeSubscription.current_period_start * 1000) + : undefined; + const newPeriodEnd = stripeSubscription.current_period_end + ? new Date(stripeSubscription.current_period_end * 1000) + : undefined; + + // Check if this is a renewal (period has changed and subscription is active) + const isRenewal = + payments.length > 0 && + stripeSubscription.status === 'active' && + payments[0].periodStart && + newPeriodStart && + payments[0].periodStart.getTime() !== newPeriodStart.getTime(); + // update fields const updateFields: any = { priceId: priceId, @@ -513,12 +645,8 @@ export class StripeProvider implements PaymentProvider { status: this.mapSubscriptionStatusToPaymentStatus( stripeSubscription.status ), - periodStart: stripeSubscription.current_period_start - ? new Date(stripeSubscription.current_period_start * 1000) - : undefined, - periodEnd: stripeSubscription.current_period_end - ? new Date(stripeSubscription.current_period_end * 1000) - : undefined, + periodStart: newPeriodStart, + periodEnd: newPeriodEnd, cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end, trialStart: stripeSubscription.trial_start ? new Date(stripeSubscription.trial_start * 1000) @@ -529,7 +657,6 @@ export class StripeProvider implements PaymentProvider { updatedAt: new Date(), }; - const db = await getDb(); const result = await db .update(payment) .set(updateFields) @@ -537,13 +664,32 @@ export class StripeProvider implements PaymentProvider { .returning({ id: payment.id }); if (result.length > 0) { - console.log( - `<< Updated payment record ${result[0].id} for Stripe subscription ${stripeSubscription.id}` - ); + console.log('<< Updated payment record for Stripe subscription'); + + // Add credits for subscription renewal + const currentPayment = payments[0]; + if ( + isRenewal && + currentPayment.userId && + websiteConfig.credits?.enableCredits + ) { + // Add subscription renewal credits if plan config enables credits + const pricePlan = findPlanByPriceId(priceId); + if (pricePlan?.credits?.enable) { + try { + await addSubscriptionCredits(currentPayment.userId, priceId); + console.log('<< Added renewal credits for user'); + } catch (error) { + console.error('<< Failed to add renewal credits for user:', error); + } + } + } else { + console.log( + '<< No renewal credits added for user, isRenewal: ' + isRenewal + ); + } } else { - console.warn( - `<< No payment record found for Stripe subscription ${stripeSubscription.id}` - ); + console.warn('<< No payment record found for Stripe subscription'); } } @@ -554,9 +700,7 @@ export class StripeProvider implements PaymentProvider { private async onDeleteSubscription( stripeSubscription: Stripe.Subscription ): Promise { - console.log( - `>> Mark payment record for Stripe subscription ${stripeSubscription.id} as canceled` - ); + console.log('>> Mark payment record for Stripe subscription as canceled'); const db = await getDb(); const result = await db .update(payment) @@ -570,12 +714,10 @@ export class StripeProvider implements PaymentProvider { .returning({ id: payment.id }); if (result.length > 0) { - console.log( - `<< Marked payment record for subscription ${stripeSubscription.id} as canceled` - ); + console.log('<< Marked payment record for subscription as canceled'); } else { console.warn( - `<< No payment record found to cancel for Stripe subscription ${stripeSubscription.id}` + '<< No payment record found to cancel for Stripe subscription' ); } } @@ -588,12 +730,12 @@ export class StripeProvider implements PaymentProvider { session: Stripe.Checkout.Session ): Promise { const customerId = session.customer as string; - console.log(`>> Handle onetime payment for customer ${customerId}`); + console.log('>> Handle onetime payment for customer'); // get userId from session metadata, we add it in the createCheckout session const userId = session.metadata?.userId; if (!userId) { - console.warn(`<< No userId found for checkout session ${session.id}`); + console.warn('No userId found for checkout session'); return; } @@ -601,41 +743,112 @@ export class StripeProvider implements PaymentProvider { // const priceId = session.line_items?.data[0]?.price?.id; const priceId = session.metadata?.priceId; if (!priceId) { - console.warn(`<< No priceId found for checkout session ${session.id}`); + console.warn('No priceId found for checkout session'); return; } - // Create a one-time payment record - const now = new Date(); - const db = await getDb(); - const result = await db - .insert(payment) - .values({ - id: randomUUID(), - priceId: priceId, - type: PaymentTypes.ONE_TIME, - userId: userId, - customerId: customerId, - status: 'completed', // One-time payments are always completed - periodStart: now, - createdAt: now, - updatedAt: now, - }) - .returning({ id: payment.id }); + try { + // Create a one-time payment record + const now = new Date(); + const db = await getDb(); + const result = await db + .insert(payment) + .values({ + id: randomUUID(), + priceId: priceId, + type: PaymentTypes.ONE_TIME, + userId: userId, + customerId: customerId, + status: 'completed', // One-time payments are always completed + periodStart: now, + createdAt: now, + updatedAt: now, + }) + .returning({ id: payment.id }); - if (result.length === 0) { - console.warn( - `<< Failed to create one-time payment record for user ${userId}` - ); + if (result.length === 0) { + console.warn('<< Failed to create one-time payment record for user'); + return; + } + console.log('Created one-time payment record for user'); + + // Conditionally handle credits after one-time payment + if (websiteConfig.credits?.enableCredits) { + // If the plan is lifetime and credits are enabled, add lifetime monthly credits if needed + const lifetimePlan = Object.values( + websiteConfig.price?.plans || {} + ).find( + (plan) => plan.isLifetime && !plan.disabled && plan.credits?.enable + ); + if (lifetimePlan?.prices?.some((p) => p.priceId === priceId)) { + await addLifetimeMonthlyCredits(userId); + } + } + + // Send notification + const amount = session.amount_total ? session.amount_total / 100 : 0; + await sendNotification(session.id, customerId, userId, amount); + } catch (error) { + console.error('onOnetimePayment error for session: ' + session.id, error); + throw error; + } + } + + /** + * Handle credit purchase + * @param session Stripe checkout session + */ + private async onCreditPurchase( + session: Stripe.Checkout.Session + ): Promise { + const customerId = session.customer as string; + console.log('>> Handle credit purchase for customer'); + + // get userId from session metadata, we add it in the createCheckout session + const userId = session.metadata?.userId; + if (!userId) { + console.warn('No userId found for checkout session'); return; } - console.log( - `<< Created one-time payment record for user ${userId}, price: ${priceId}` - ); - // Send notification - const amount = session.amount_total ? session.amount_total / 100 : 0; - await sendNotification(session.id, customerId, userId, amount); + // get packageId from session metadata + const packageId = session.metadata?.packageId; + if (!packageId) { + console.warn('No packageId found for checkout session'); + return; + } + + // get credits from session metadata + const credits = session.metadata?.credits; + if (!credits) { + console.warn('No credits found for checkout session'); + return; + } + + // get credit package + const creditPackage = getCreditPackageById(packageId); + if (!creditPackage) { + console.warn('Credit package ' + packageId + ' not found'); + return; + } + + try { + // add credits to user account + const amount = session.amount_total ? session.amount_total / 100 : 0; + await addCredits({ + userId, + amount: Number.parseInt(credits), + type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE, + description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`, + paymentId: session.id, + expireDays: creditPackage.expireDays, + }); + + console.log('Added ' + credits + ' credits to user'); + } catch (error) { + console.error('onCreditPurchase error for session: ' + session.id, error); + throw error; + } } /** diff --git a/src/payment/types.ts b/src/payment/types.ts index 7b5bd00..ae087fe 100644 --- a/src/payment/types.ts +++ b/src/payment/types.ts @@ -50,6 +50,15 @@ export interface Price { disabled?: boolean; // Whether to disable this price in UI } +/** + * Credits configuration for a plan + */ +export interface Credits { + enable: boolean; // Whether to enable credits for this plan + amount: number; // Number of credits provided per month + expireDays?: number; // Number of days until credits expire, undefined means no expiration +} + /** * Price plan definition * @@ -70,8 +79,9 @@ export interface PricePlan { prices: Price[]; // Available prices for this plan isFree: boolean; // Whether this is a free plan isLifetime: boolean; // Whether this is a lifetime plan - recommended?: boolean; // Whether to mark this plan as recommended in UI + popular?: boolean; // Whether to mark this plan as popular in UI disabled?: boolean; // Whether to disable this plan in UI + credits?: Credits; // Credits configuration for this plan } /** @@ -128,6 +138,19 @@ export interface CreateCheckoutParams { locale?: Locale; } +/** + * Parameters for creating a credit checkout session + */ +export interface CreateCreditCheckoutParams { + packageId: string; + priceId: string; + customerEmail: string; + successUrl?: string; + cancelUrl?: string; + metadata?: Record; + locale?: Locale; +} + /** * Result of creating a checkout session */ @@ -168,6 +191,11 @@ export interface PaymentProvider { */ createCheckout(params: CreateCheckoutParams): Promise; + /** + * Create a credit checkout session + */ + createCreditCheckout(params: CreateCreditCheckoutParams): Promise; + /** * Create a customer portal session */ diff --git a/src/providers/credits-provider.tsx b/src/providers/credits-provider.tsx new file mode 100644 index 0000000..32ec079 --- /dev/null +++ b/src/providers/credits-provider.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { authClient } from '@/lib/auth-client'; +import { useCreditsStore } from '@/stores/credits-store'; +import { useEffect } from 'react'; + +/** + * Credits Provider Component + * + * This component initializes the credits store when the user is authenticated + * and handles cleanup when the user logs out. + */ +export function CreditsProvider({ children }: { children: React.ReactNode }) { + const { fetchCredits } = useCreditsStore(); + const { data: session } = authClient.useSession(); + + useEffect(() => { + if (session?.user) { + fetchCredits(session.user); + } + }, [session?.user, fetchCredits]); + + return <>{children}; +} diff --git a/src/routes.ts b/src/routes.ts index 1f1adf2..cab64b1 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -33,6 +33,7 @@ export enum Routes { AdminUsers = '/admin/users', SettingsProfile = '/settings/profile', SettingsBilling = '/settings/billing', + SettingsCredits = '/settings/credits', SettingsSecurity = '/settings/security', SettingsNotifications = '/settings/notifications', @@ -76,6 +77,7 @@ export const protectedRoutes = [ Routes.AdminUsers, Routes.SettingsProfile, Routes.SettingsBilling, + Routes.SettingsCredits, Routes.SettingsSecurity, Routes.SettingsNotifications, ]; diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts new file mode 100644 index 0000000..038482d --- /dev/null +++ b/src/stores/credits-store.ts @@ -0,0 +1,162 @@ +import { consumeCreditsAction } from '@/actions/consume-credits'; +import { getCreditBalanceAction } from '@/actions/get-credit-balance'; +import type { Session } from '@/lib/auth-types'; +import { create } from 'zustand'; + +// Cache duration: 2 minutes (optimized for better UX) +const CACHE_DURATION = 2 * 60 * 1000; + +/** + * Credits state interface + */ +export interface CreditsState { + // Current credit balance + balance: number; + // Loading state + isLoading: boolean; + // Error state + error: string | null; + // Last fetch timestamp to avoid frequent requests + lastFetchTime: number | null; + + // Actions + fetchCredits: ( + user: Session['user'] | null | undefined, + force?: boolean + ) => Promise; + consumeCredits: (amount: number, description: string) => Promise; +} + +/** + * Credits store using Zustand + * Manages the user's credit balance globally with caching and optimistic updates + */ +export const useCreditsStore = create((set, get) => ({ + // Initial state + balance: 0, + isLoading: false, + error: null, + lastFetchTime: null, + + /** + * Fetch credit balance for the current user with optional cache bypass + * @param user Current user from auth session + * @param force Whether to force refresh and ignore cache + */ + fetchCredits: async (user, force = false) => { + // Skip if already loading + if (get().isLoading) return; + + // Skip if no user is provided + if (!user) { + set({ + balance: 0, + error: null, + lastFetchTime: null, + }); + return; + } + + // Check if we have recent data (within cache duration) unless force refresh + if (!force) { + const { lastFetchTime } = get(); + const now = Date.now(); + if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) { + return; // Use cached data + } + } + + console.log(`fetchCredits, ${force ? 'force fetch' : 'fetch'} credits`); + set({ + isLoading: true, + error: null, + // Clear cache if force refresh + lastFetchTime: force ? null : get().lastFetchTime, + }); + + try { + const result = await getCreditBalanceAction(); + if (result?.data?.success) { + const newBalance = result.data.credits || 0; + console.log('fetchCredits, set new balance', newBalance); + set({ + balance: newBalance, + isLoading: false, + error: null, + lastFetchTime: Date.now(), + }); + } else { + console.warn('fetchCredits, failed to fetch credit balance', result); + set({ + error: result?.data?.error || 'Failed to fetch credit balance', + isLoading: false, + }); + } + } catch (error) { + console.error('fetchCredits, error:', error); + set({ + error: 'Failed to fetch credit balance', + isLoading: false, + }); + } + }, + + /** + * Consume credits with optimistic updates + * @param amount Amount of credits to consume + * @param description Description for the transaction + * @returns Promise Success status + */ + consumeCredits: async (amount: number, description: string) => { + const { balance } = get(); + + // Check if we have enough credits + if (balance < amount) { + console.log('consumeCredits, insufficient credits', balance, amount); + set({ + error: 'Insufficient credits', + }); + return false; + } + + // Optimistically update the balance + set({ + balance: balance - amount, + error: null, + isLoading: true, + }); + + try { + const result = await consumeCreditsAction({ + amount, + description, + }); + + if (result?.data?.success) { + set({ + isLoading: false, + error: null, + }); + return true; + } + + // Revert optimistic update on failure + console.warn('consumeCredits, reverting optimistic update'); + set({ + balance: balance, // Revert to original balance + error: result?.data?.error || 'Failed to consume credits', + isLoading: false, + }); + return false; + } catch (error) { + console.error('consumeCredits, error:', error); + // Revert optimistic update on error + set({ + balance: balance, // Revert to original balance + error: 'Failed to consume credits', + isLoading: false, + }); + return false; + } + }, +})); diff --git a/src/stores/payment-store.ts b/src/stores/payment-store.ts index f2b11c6..ba8edd8 100644 --- a/src/stores/payment-store.ts +++ b/src/stores/payment-store.ts @@ -5,6 +5,9 @@ import { getAllPricePlans } from '@/lib/price-plan'; import type { PricePlan, Subscription } from '@/payment/types'; import { create } from 'zustand'; +// Cache duration: 2 minutes (optimized for better UX) +const CACHE_DURATION = 2 * 60 * 1000; + /** * Payment state interface */ @@ -17,9 +20,14 @@ export interface PaymentState { isLoading: boolean; // Error state error: string | null; + // Last fetch timestamp to avoid frequent requests + lastFetchTime: number | null; // Actions - fetchPayment: (user: Session['user'] | null | undefined) => Promise; + fetchPayment: ( + user: Session['user'] | null | undefined, + force?: boolean + ) => Promise; resetState: () => void; } @@ -33,12 +41,13 @@ export const usePaymentStore = create((set, get) => ({ subscription: null, isLoading: false, error: null, + lastFetchTime: null, /** * Fetch payment and subscription data for the current user * @param user Current user from auth session */ - fetchPayment: async (user) => { + fetchPayment: async (user, force = false) => { // Skip if already loading if (get().isLoading) return; @@ -48,10 +57,21 @@ export const usePaymentStore = create((set, get) => ({ currentPlan: null, subscription: null, error: null, + lastFetchTime: null, }); return; } + // Check if we have recent data (within cache duration) unless force refresh + if (!force) { + const { lastFetchTime } = get(); + const now = Date.now(); + if (lastFetchTime && now - lastFetchTime < CACHE_DURATION) { + console.log('fetchPayment, use cached data'); + return; // Use cached data + } + } + // Fetch subscription data set({ isLoading: true, error: null }); @@ -66,30 +86,26 @@ export const usePaymentStore = create((set, get) => ({ const result = await getLifetimeStatusAction({ userId: user.id }); if (result?.data?.success) { isLifetimeMember = result.data.isLifetimeMember || false; - console.log('get lifetime status result', result); + console.log('fetchPayment, lifetime status', isLifetimeMember); } else { - console.warn('get lifetime status failed', result?.data?.error); - // set({ - // error: result?.data?.error || 'Failed to fetch payment data', - // isLoading: false - // }); + console.warn( + 'fetchPayment, lifetime status error', + result?.data?.error + ); } } catch (error) { - console.error('get lifetime status error:', error); - // set({ - // error: 'Failed to fetch payment data', - // isLoading: false - // }); + console.error('fetchPayment, lifetime status error:', error); } // If lifetime member, set the lifetime plan if (isLifetimeMember) { - console.log('set lifetime plan for user', user.id); + console.log('fetchPayment, set lifetime plan'); set({ currentPlan: lifetimePlan || null, subscription: null, isLoading: false, error: null, + lastFetchTime: Date.now(), }); return; } @@ -108,34 +124,29 @@ export const usePaymentStore = create((set, get) => ({ (price) => price.priceId === activeSubscription.priceId ) ) || null; - console.log( - 'subscription found, setting plan for user', - user.id, - plan?.id - ); + console.log('fetchPayment, subscription found, set pro plan'); set({ currentPlan: plan, subscription: activeSubscription, isLoading: false, error: null, + lastFetchTime: Date.now(), }); } else { // No subscription found - set to free plan - console.log( - 'no subscription found, setting free plan for user', - user.id - ); + console.log('fetchPayment, no subscription found, set free plan'); set({ currentPlan: freePlan || null, subscription: null, isLoading: false, error: null, + lastFetchTime: Date.now(), }); } } else { // Failed to fetch subscription console.error( - 'fetch subscription for user failed', + 'fetchPayment, subscription for user failed', result?.data?.error ); set({ @@ -144,7 +155,7 @@ export const usePaymentStore = create((set, get) => ({ }); } } catch (error) { - console.error('fetch payment data error:', error); + console.error('fetchPayment, error:', error); set({ error: 'Failed to fetch payment data', isLoading: false, @@ -163,6 +174,7 @@ export const usePaymentStore = create((set, get) => ({ subscription: null, isLoading: false, error: null, + lastFetchTime: null, }); }, })); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index f1dd73a..dc291f7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; import type { PricePlan } from '@/payment/types'; +import type { CreditPackage } from '@/credits/types'; /** * website config, without translations @@ -17,6 +18,7 @@ export type WebsiteConfig = { storage: StorageConfig; payment: PaymentConfig; price: PriceConfig; + credits: CreditsConfig; }; /** @@ -148,6 +150,20 @@ export interface PriceConfig { plans: Record; // Plans indexed by ID } +/** + * Credits configuration + */ +export interface CreditsConfig { + enableCredits: boolean; // Whether to enable credits + enableForFreePlan: boolean; // Whether to enable purchase credits for free plan users + registerGiftCredits: { + enable: boolean; // Whether to enable register gift credits + credits: number; // The number of credits to give to the user + expireDays?: number; // The number of days to expire the credits, undefined means no expire + }; + packages: Record; // Packages indexed by ID +} + /** * menu item, used for navbar links, sidebar links, footer links */