shipd 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base-package/app/globals.css +126 -0
- package/base-package/app/layout.tsx +53 -0
- package/base-package/app/page.tsx +15 -0
- package/base-package/base.config.json +57 -0
- package/base-package/components/ui/avatar.tsx +53 -0
- package/base-package/components/ui/badge.tsx +46 -0
- package/base-package/components/ui/button.tsx +59 -0
- package/base-package/components/ui/card.tsx +92 -0
- package/base-package/components/ui/chart.tsx +353 -0
- package/base-package/components/ui/checkbox.tsx +32 -0
- package/base-package/components/ui/dialog.tsx +135 -0
- package/base-package/components/ui/dropdown-menu.tsx +257 -0
- package/base-package/components/ui/form.tsx +167 -0
- package/base-package/components/ui/input.tsx +21 -0
- package/base-package/components/ui/label.tsx +24 -0
- package/base-package/components/ui/progress.tsx +31 -0
- package/base-package/components/ui/resizable.tsx +56 -0
- package/base-package/components/ui/select.tsx +185 -0
- package/base-package/components/ui/separator.tsx +28 -0
- package/base-package/components/ui/sheet.tsx +139 -0
- package/base-package/components/ui/skeleton.tsx +13 -0
- package/base-package/components/ui/sonner.tsx +25 -0
- package/base-package/components/ui/switch.tsx +31 -0
- package/base-package/components/ui/tabs.tsx +66 -0
- package/base-package/components/ui/textarea.tsx +18 -0
- package/base-package/components/ui/toggle-group.tsx +73 -0
- package/base-package/components/ui/toggle.tsx +47 -0
- package/base-package/components/ui/tooltip.tsx +61 -0
- package/base-package/components.json +21 -0
- package/base-package/eslint.config.mjs +16 -0
- package/base-package/lib/utils.ts +6 -0
- package/base-package/middleware.ts +12 -0
- package/base-package/next.config.ts +27 -0
- package/base-package/package.json +49 -0
- package/base-package/postcss.config.mjs +5 -0
- package/base-package/public/favicon.svg +4 -0
- package/base-package/tailwind.config.ts +89 -0
- package/base-package/tsconfig.json +27 -0
- package/dist/index.js +1862 -948
- package/features/ai-chat/README.md +258 -0
- package/features/ai-chat/app/api/chat/route.ts +16 -0
- package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
- package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
- package/features/ai-chat/feature.config.json +22 -0
- package/features/analytics/README.md +308 -0
- package/features/analytics/feature.config.json +20 -0
- package/features/analytics/lib/posthog.ts +36 -0
- package/features/auth/README.md +336 -0
- package/features/auth/app/api/auth/[...all]/route.ts +4 -0
- package/features/auth/app/dashboard/layout.tsx +15 -0
- package/features/auth/app/dashboard/page.tsx +140 -0
- package/features/auth/app/sign-in/page.tsx +228 -0
- package/features/auth/app/sign-up/page.tsx +243 -0
- package/features/auth/auth-schema.ts +47 -0
- package/features/auth/components/auth/setup-instructions.tsx +123 -0
- package/features/auth/feature.config.json +33 -0
- package/features/auth/lib/auth-client.ts +8 -0
- package/features/auth/lib/auth.ts +295 -0
- package/features/auth/lib/email-stub.ts +55 -0
- package/features/auth/lib/email.ts +47 -0
- package/features/auth/middleware.patch.ts +43 -0
- package/features/database/README.md +256 -0
- package/features/database/db/drizzle.ts +48 -0
- package/features/database/db/schema.ts +21 -0
- package/features/database/drizzle.config.ts +13 -0
- package/features/database/feature.config.json +30 -0
- package/features/email/README.md +282 -0
- package/features/email/emails/components/layout.tsx +181 -0
- package/features/email/emails/password-reset.tsx +67 -0
- package/features/email/emails/payment-failed.tsx +167 -0
- package/features/email/emails/subscription-confirmation.tsx +129 -0
- package/features/email/emails/welcome.tsx +100 -0
- package/features/email/feature.config.json +22 -0
- package/features/email/lib/email.ts +118 -0
- package/features/file-upload/README.md +271 -0
- package/features/file-upload/app/api/upload-image/route.ts +64 -0
- package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
- package/features/file-upload/feature.config.json +23 -0
- package/features/file-upload/lib/upload-image.ts +28 -0
- package/features/marketing-landing/README.md +266 -0
- package/features/marketing-landing/app/page.tsx +25 -0
- package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
- package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
- package/features/marketing-landing/components/homepage/footer.tsx +53 -0
- package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
- package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
- package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
- package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
- package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
- package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
- package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
- package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
- package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
- package/features/marketing-landing/components/logos/Polar.tsx +7 -0
- package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
- package/features/marketing-landing/components/logos/index.ts +6 -0
- package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
- package/features/marketing-landing/feature.config.json +23 -0
- package/features/payments/README.md +306 -0
- package/features/payments/app/api/subscription/route.ts +25 -0
- package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/features/payments/app/dashboard/payment/page.tsx +126 -0
- package/features/payments/app/success/page.tsx +123 -0
- package/features/payments/feature.config.json +31 -0
- package/features/payments/lib/polar-products.ts +49 -0
- package/features/payments/lib/subscription.ts +148 -0
- package/features/payments/payments-schema.ts +30 -0
- package/features/seo/README.md +244 -0
- package/features/seo/app/blog/[slug]/page.tsx +314 -0
- package/features/seo/app/blog/page.tsx +107 -0
- package/features/seo/app/robots.txt +13 -0
- package/features/seo/app/sitemap.ts +70 -0
- package/features/seo/feature.config.json +19 -0
- package/features/seo/lib/seo-utils.ts +163 -0
- package/package.json +3 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { ExternalLink } from "lucide-react";
|
|
4
|
+
import { authClient } from "@/lib/auth-client";
|
|
5
|
+
|
|
6
|
+
export default function ManageSubscription() {
|
|
7
|
+
return (
|
|
8
|
+
<Button
|
|
9
|
+
variant="outline"
|
|
10
|
+
onClick={async () => {
|
|
11
|
+
try {
|
|
12
|
+
await authClient.customer.portal();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Failed to open customer portal:", error);
|
|
15
|
+
}
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
<ExternalLink className="h-4 w-4 mr-2" />
|
|
19
|
+
Manage Subscription
|
|
20
|
+
</Button>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "@/components/ui/card";
|
|
9
|
+
import { getSubscriptionDetails } from "@/lib/subscription";
|
|
10
|
+
import Link from "next/link";
|
|
11
|
+
import ManageSubscription from "./_components/manage-subscription";
|
|
12
|
+
|
|
13
|
+
export default async function PaymentPage() {
|
|
14
|
+
const subscriptionDetails = await getSubscriptionDetails();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div>
|
|
18
|
+
<div className="p-6 space-y-4">
|
|
19
|
+
<div className="relative min-h-screen">
|
|
20
|
+
{!subscriptionDetails.hasSubscription ||
|
|
21
|
+
subscriptionDetails.subscription?.status !== "active" ? (
|
|
22
|
+
<>
|
|
23
|
+
<div className="absolute inset-0 z-10 rounded-lg flex items-center justify-center">
|
|
24
|
+
<div className="bg-white dark:bg-gray-900 p-8 rounded-lg shadow-lg text-center max-w-md">
|
|
25
|
+
<h3 className="text-xl font-semibold mb-2">
|
|
26
|
+
Subscription Required
|
|
27
|
+
</h3>
|
|
28
|
+
<p className="text-muted-foreground mb-4">
|
|
29
|
+
You need an active subscription to access payment management
|
|
30
|
+
features.
|
|
31
|
+
</p>
|
|
32
|
+
<Link href="/pricing">
|
|
33
|
+
<Button>Subscribe Now</Button>
|
|
34
|
+
</Link>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="blur-sm pointer-events-none">
|
|
38
|
+
<Card>
|
|
39
|
+
<CardHeader>
|
|
40
|
+
<CardTitle>Payment Management</CardTitle>
|
|
41
|
+
<CardDescription>
|
|
42
|
+
Manage your billing and payment methods
|
|
43
|
+
</CardDescription>
|
|
44
|
+
</CardHeader>
|
|
45
|
+
<CardContent className="space-y-4">
|
|
46
|
+
<div className="grid grid-cols-2 gap-4">
|
|
47
|
+
<div>
|
|
48
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
49
|
+
Current Plan
|
|
50
|
+
</p>
|
|
51
|
+
<p className="text-md">Pro Plan</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
55
|
+
Billing Status
|
|
56
|
+
</p>
|
|
57
|
+
<p className="text-md">Active</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
</div>
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
<Card>
|
|
66
|
+
<CardHeader>
|
|
67
|
+
<CardTitle>Subscription Details</CardTitle>
|
|
68
|
+
<CardDescription>
|
|
69
|
+
Your current subscription information
|
|
70
|
+
</CardDescription>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent className="space-y-4">
|
|
73
|
+
<div className="grid grid-cols-2 gap-4">
|
|
74
|
+
<div>
|
|
75
|
+
<p className="text-sm font-semibold text-muted-foreground">
|
|
76
|
+
Status
|
|
77
|
+
</p>
|
|
78
|
+
<p className="text-md capitalize">
|
|
79
|
+
{subscriptionDetails.subscription.status}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<p className="text-sm font-semibold text-muted-foreground">
|
|
84
|
+
Amount
|
|
85
|
+
</p>
|
|
86
|
+
<p className="text-md">
|
|
87
|
+
{subscriptionDetails.subscription.amount / 100}{" "}
|
|
88
|
+
{subscriptionDetails.subscription.currency.toUpperCase()}
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
<div>
|
|
92
|
+
<p className="text-sm font-semibold text-muted-foreground">
|
|
93
|
+
Billing Interval
|
|
94
|
+
</p>
|
|
95
|
+
<p className="text-md capitalize">
|
|
96
|
+
{subscriptionDetails.subscription.recurringInterval}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
<div>
|
|
100
|
+
<p className="text-sm font-semibold text-muted-foreground">
|
|
101
|
+
Current Period End
|
|
102
|
+
</p>
|
|
103
|
+
<p className="text-md">
|
|
104
|
+
{new Date(
|
|
105
|
+
subscriptionDetails.subscription.currentPeriodEnd,
|
|
106
|
+
).toLocaleDateString()}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
{subscriptionDetails.subscription.cancelAtPeriodEnd && (
|
|
111
|
+
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
112
|
+
<p className="text-sm text-yellow-800">
|
|
113
|
+
Your subscription will cancel at the end of the current
|
|
114
|
+
billing period.
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
<ManageSubscription />
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
5
|
+
import { CheckCircle2, Terminal, BookOpen, Zap } from "lucide-react";
|
|
6
|
+
import { useRouter } from "next/navigation";
|
|
7
|
+
import Link from "next/link";
|
|
8
|
+
|
|
9
|
+
export default function SuccessPage() {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="min-h-screen bg-black flex items-center justify-center p-6">
|
|
14
|
+
<Card className="bg-[#0a0a0a] border-[#2a2a2a] max-w-2xl w-full">
|
|
15
|
+
<CardContent className="p-12">
|
|
16
|
+
{/* Success Icon */}
|
|
17
|
+
<div className="w-20 h-20 bg-green-500/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
18
|
+
<CheckCircle2 className="w-12 h-12 text-green-400" />
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{/* Success Message */}
|
|
22
|
+
<div className="text-center mb-8">
|
|
23
|
+
<h1 className="text-3xl font-bold text-white mb-3">
|
|
24
|
+
Welcome! 🎉
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="text-gray-400 text-lg">
|
|
27
|
+
Your subscription is now active. Let's get you started building your SaaS.
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{/* Next Steps */}
|
|
32
|
+
<div className="space-y-6 mb-8">
|
|
33
|
+
<h2 className="text-xl font-semibold text-white">Next Steps</h2>
|
|
34
|
+
|
|
35
|
+
{/* Step 1 */}
|
|
36
|
+
<div className="flex gap-4">
|
|
37
|
+
<div className="w-8 h-8 rounded-full bg-[#ff5722] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
|
38
|
+
1
|
|
39
|
+
</div>
|
|
40
|
+
<div className="flex-1">
|
|
41
|
+
<h3 className="text-white font-semibold mb-1">Authenticate the CLI</h3>
|
|
42
|
+
<p className="text-gray-400 text-sm mb-2">
|
|
43
|
+
Run this command in your terminal to authenticate:
|
|
44
|
+
</p>
|
|
45
|
+
<div className="bg-black/70 p-3 rounded-lg border border-[#2a2a2a]">
|
|
46
|
+
<code className="text-[#ff5722] font-mono text-sm">
|
|
47
|
+
npx your-cli login
|
|
48
|
+
</code>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Step 2 */}
|
|
54
|
+
<div className="flex gap-4">
|
|
55
|
+
<div className="w-8 h-8 rounded-full bg-[#ff5722] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
|
56
|
+
2
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex-1">
|
|
59
|
+
<h3 className="text-white font-semibold mb-1">Create your first project</h3>
|
|
60
|
+
<p className="text-gray-400 text-sm mb-2">
|
|
61
|
+
Generate a complete SaaS application:
|
|
62
|
+
</p>
|
|
63
|
+
<div className="bg-black/70 p-3 rounded-lg border border-[#2a2a2a]">
|
|
64
|
+
<code className="text-[#ff5722] font-mono text-sm">
|
|
65
|
+
npx your-cli init my-app
|
|
66
|
+
</code>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Step 3 */}
|
|
72
|
+
<div className="flex gap-4">
|
|
73
|
+
<div className="w-8 h-8 rounded-full bg-[#ff5722] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
|
74
|
+
3
|
|
75
|
+
</div>
|
|
76
|
+
<div className="flex-1">
|
|
77
|
+
<h3 className="text-white font-semibold mb-1">Start building</h3>
|
|
78
|
+
<p className="text-gray-400 text-sm">
|
|
79
|
+
Your project includes authentication, billing, dashboard, and more - all ready to customize and deploy.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Action Buttons */}
|
|
86
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
87
|
+
<Button
|
|
88
|
+
onClick={() => router.push("/dashboard")}
|
|
89
|
+
className="w-full bg-[#ff5722] hover:bg-[#d84315]"
|
|
90
|
+
>
|
|
91
|
+
<Zap className="w-4 h-4 mr-2" />
|
|
92
|
+
Go to Dashboard
|
|
93
|
+
</Button>
|
|
94
|
+
<Link href="/dashboard/cli" className="w-full">
|
|
95
|
+
<Button variant="outline" className="w-full border-[#2a2a2a] hover:bg-[#1a1a1a]">
|
|
96
|
+
<Terminal className="w-4 h-4 mr-2" />
|
|
97
|
+
CLI Guide
|
|
98
|
+
</Button>
|
|
99
|
+
</Link>
|
|
100
|
+
<Link href="/docs" className="w-full">
|
|
101
|
+
<Button variant="outline" className="w-full border-[#2a2a2a] hover:bg-[#1a1a1a]">
|
|
102
|
+
<BookOpen className="w-4 h-4 mr-2" />
|
|
103
|
+
Documentation
|
|
104
|
+
</Button>
|
|
105
|
+
</Link>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Help Section */}
|
|
109
|
+
<div className="mt-8 pt-8 border-t border-[#2a2a2a] text-center">
|
|
110
|
+
<p className="text-gray-400 text-sm mb-2">
|
|
111
|
+
Need help getting started?
|
|
112
|
+
</p>
|
|
113
|
+
<Link href="mailto:support@yourdomain.com">
|
|
114
|
+
<Button variant="link" className="text-[#ff5722] hover:text-[#d84315]">
|
|
115
|
+
Contact Support
|
|
116
|
+
</Button>
|
|
117
|
+
</Link>
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payments",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Polar.sh subscription management and payment processing",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@polar-sh/sdk": "^0.42.1"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {},
|
|
9
|
+
"envVars": [
|
|
10
|
+
"POLAR_ACCESS_TOKEN",
|
|
11
|
+
"POLAR_SUCCESS_URL",
|
|
12
|
+
"POLAR_WEBHOOK_SECRET",
|
|
13
|
+
"NEXT_PUBLIC_STARTER_TIER",
|
|
14
|
+
"NEXT_PUBLIC_STARTER_SLUG",
|
|
15
|
+
"NEXT_PUBLIC_APP_URL"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"app/dashboard/payment/**/*",
|
|
19
|
+
"app/success/**/*",
|
|
20
|
+
"app/api/subscription/**/*",
|
|
21
|
+
"lib/subscription.ts",
|
|
22
|
+
"lib/polar-products.ts",
|
|
23
|
+
"payments-schema.ts"
|
|
24
|
+
],
|
|
25
|
+
"requires": [
|
|
26
|
+
"auth",
|
|
27
|
+
"database"
|
|
28
|
+
],
|
|
29
|
+
"conflicts": []
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Polar } from '@polar-sh/sdk';
|
|
2
|
+
|
|
3
|
+
const polarClient = new Polar({
|
|
4
|
+
accessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export interface ProductDetails {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string | null;
|
|
11
|
+
prices: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
amount: number;
|
|
14
|
+
currency: string;
|
|
15
|
+
recurring_interval: 'month' | 'year';
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch product details from Polar
|
|
21
|
+
*/
|
|
22
|
+
export async function getProductDetails(
|
|
23
|
+
productId: string
|
|
24
|
+
): Promise<ProductDetails | null> {
|
|
25
|
+
try {
|
|
26
|
+
const product = await polarClient.products.get({
|
|
27
|
+
id: productId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!product) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: product.id,
|
|
36
|
+
name: product.name,
|
|
37
|
+
description: product.description || null,
|
|
38
|
+
prices: (product.prices || []).map((price) => ({
|
|
39
|
+
id: price.id,
|
|
40
|
+
amount: price.priceAmount || 0,
|
|
41
|
+
currency: price.priceCurrency || 'usd',
|
|
42
|
+
recurring_interval: (price.recurringInterval as 'month' | 'year') || 'month',
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Error fetching product from Polar:', error);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { auth } from "@/lib/auth";
|
|
2
|
+
import { db } from "@/db/drizzle";
|
|
3
|
+
import { subscription } from "@/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { headers } from "next/headers";
|
|
6
|
+
|
|
7
|
+
export type SubscriptionDetails = {
|
|
8
|
+
id: string;
|
|
9
|
+
productId: string;
|
|
10
|
+
status: string;
|
|
11
|
+
amount: number;
|
|
12
|
+
currency: string;
|
|
13
|
+
recurringInterval: string;
|
|
14
|
+
currentPeriodStart: Date;
|
|
15
|
+
currentPeriodEnd: Date;
|
|
16
|
+
cancelAtPeriodEnd: boolean;
|
|
17
|
+
canceledAt: Date | null;
|
|
18
|
+
organizationId: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SubscriptionDetailsResult = {
|
|
22
|
+
hasSubscription: boolean;
|
|
23
|
+
subscription?: SubscriptionDetails;
|
|
24
|
+
error?: string;
|
|
25
|
+
errorType?: "CANCELED" | "EXPIRED" | "GENERAL";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export async function getSubscriptionDetails(): Promise<SubscriptionDetailsResult> {
|
|
29
|
+
try {
|
|
30
|
+
const session = await auth.api.getSession({
|
|
31
|
+
headers: await headers(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!session?.user?.id) {
|
|
35
|
+
return { hasSubscription: false };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const userSubscriptions = await db
|
|
39
|
+
.select()
|
|
40
|
+
.from(subscription)
|
|
41
|
+
.where(eq(subscription.userId, session.user.id));
|
|
42
|
+
|
|
43
|
+
if (!userSubscriptions.length) {
|
|
44
|
+
return { hasSubscription: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get the most recent active subscription
|
|
48
|
+
const activeSubscription = userSubscriptions
|
|
49
|
+
.filter((sub) => sub.status === "active")
|
|
50
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
51
|
+
|
|
52
|
+
if (!activeSubscription) {
|
|
53
|
+
// Check for canceled or expired subscriptions
|
|
54
|
+
const latestSubscription = userSubscriptions
|
|
55
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
56
|
+
|
|
57
|
+
if (latestSubscription) {
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const isExpired = new Date(latestSubscription.currentPeriodEnd) < now;
|
|
60
|
+
const isCanceled = latestSubscription.status === "canceled";
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
hasSubscription: true,
|
|
64
|
+
subscription: {
|
|
65
|
+
id: latestSubscription.id,
|
|
66
|
+
productId: latestSubscription.productId,
|
|
67
|
+
status: latestSubscription.status,
|
|
68
|
+
amount: latestSubscription.amount,
|
|
69
|
+
currency: latestSubscription.currency,
|
|
70
|
+
recurringInterval: latestSubscription.recurringInterval,
|
|
71
|
+
currentPeriodStart: latestSubscription.currentPeriodStart,
|
|
72
|
+
currentPeriodEnd: latestSubscription.currentPeriodEnd,
|
|
73
|
+
cancelAtPeriodEnd: latestSubscription.cancelAtPeriodEnd,
|
|
74
|
+
canceledAt: latestSubscription.canceledAt,
|
|
75
|
+
organizationId: null,
|
|
76
|
+
},
|
|
77
|
+
error: isCanceled ? "Subscription has been canceled" : isExpired ? "Subscription has expired" : "Subscription is not active",
|
|
78
|
+
errorType: isCanceled ? "CANCELED" : isExpired ? "EXPIRED" : "GENERAL",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { hasSubscription: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
hasSubscription: true,
|
|
87
|
+
subscription: {
|
|
88
|
+
id: activeSubscription.id,
|
|
89
|
+
productId: activeSubscription.productId,
|
|
90
|
+
status: activeSubscription.status,
|
|
91
|
+
amount: activeSubscription.amount,
|
|
92
|
+
currency: activeSubscription.currency,
|
|
93
|
+
recurringInterval: activeSubscription.recurringInterval,
|
|
94
|
+
currentPeriodStart: activeSubscription.currentPeriodStart,
|
|
95
|
+
currentPeriodEnd: activeSubscription.currentPeriodEnd,
|
|
96
|
+
cancelAtPeriodEnd: activeSubscription.cancelAtPeriodEnd,
|
|
97
|
+
canceledAt: activeSubscription.canceledAt,
|
|
98
|
+
organizationId: null,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error fetching subscription details:", error);
|
|
103
|
+
return {
|
|
104
|
+
hasSubscription: false,
|
|
105
|
+
error: "Failed to load subscription details",
|
|
106
|
+
errorType: "GENERAL",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Simple helper to check if user has an active subscription
|
|
112
|
+
export async function isUserSubscribed(): Promise<boolean> {
|
|
113
|
+
const result = await getSubscriptionDetails();
|
|
114
|
+
return result.hasSubscription && result.subscription?.status === "active";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Helper to check if user has access to a specific product/tier
|
|
118
|
+
export async function hasAccessToProduct(productId: string): Promise<boolean> {
|
|
119
|
+
const result = await getSubscriptionDetails();
|
|
120
|
+
return (
|
|
121
|
+
result.hasSubscription &&
|
|
122
|
+
result.subscription?.status === "active" &&
|
|
123
|
+
result.subscription?.productId === productId
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Helper to get user's current subscription status
|
|
128
|
+
export async function getUserSubscriptionStatus(): Promise<"active" | "canceled" | "expired" | "none"> {
|
|
129
|
+
const result = await getSubscriptionDetails();
|
|
130
|
+
|
|
131
|
+
if (!result.hasSubscription) {
|
|
132
|
+
return "none";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (result.subscription?.status === "active") {
|
|
136
|
+
return "active";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.errorType === "CANCELED") {
|
|
140
|
+
return "canceled";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (result.errorType === "EXPIRED") {
|
|
144
|
+
return "expired";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return "none";
|
|
148
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
// Subscription table for Polar webhook data
|
|
4
|
+
// Note: userId references user.id from db/schema.ts (user table is added by auth module)
|
|
5
|
+
export const subscription = pgTable("subscription", {
|
|
6
|
+
id: text("id").primaryKey(),
|
|
7
|
+
createdAt: timestamp("createdAt").notNull(),
|
|
8
|
+
modifiedAt: timestamp("modifiedAt"),
|
|
9
|
+
amount: integer("amount").notNull(),
|
|
10
|
+
currency: text("currency").notNull(),
|
|
11
|
+
recurringInterval: text("recurringInterval").notNull(),
|
|
12
|
+
status: text("status").notNull(),
|
|
13
|
+
currentPeriodStart: timestamp("currentPeriodStart").notNull(),
|
|
14
|
+
currentPeriodEnd: timestamp("currentPeriodEnd").notNull(),
|
|
15
|
+
cancelAtPeriodEnd: boolean("cancelAtPeriodEnd").notNull().default(false),
|
|
16
|
+
canceledAt: timestamp("canceledAt"),
|
|
17
|
+
startedAt: timestamp("startedAt").notNull(),
|
|
18
|
+
endsAt: timestamp("endsAt"),
|
|
19
|
+
endedAt: timestamp("endedAt"),
|
|
20
|
+
customerId: text("customerId").notNull(),
|
|
21
|
+
productId: text("productId").notNull(),
|
|
22
|
+
discountId: text("discountId"),
|
|
23
|
+
checkoutId: text("checkoutId").notNull(),
|
|
24
|
+
customerCancellationReason: text("customerCancellationReason"),
|
|
25
|
+
customerCancellationComment: text("customerCancellationComment"),
|
|
26
|
+
metadata: text("metadata"), // JSON string
|
|
27
|
+
customFieldData: text("customFieldData"), // JSON string
|
|
28
|
+
userId: text("userId"), // References user.id (user table from auth module)
|
|
29
|
+
});
|
|
30
|
+
|