shipd 0.1.3 → 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.
Files changed (115) hide show
  1. package/base-package/app/globals.css +126 -0
  2. package/base-package/app/layout.tsx +53 -0
  3. package/base-package/app/page.tsx +15 -0
  4. package/base-package/base.config.json +57 -0
  5. package/base-package/components/ui/avatar.tsx +53 -0
  6. package/base-package/components/ui/badge.tsx +46 -0
  7. package/base-package/components/ui/button.tsx +59 -0
  8. package/base-package/components/ui/card.tsx +92 -0
  9. package/base-package/components/ui/chart.tsx +353 -0
  10. package/base-package/components/ui/checkbox.tsx +32 -0
  11. package/base-package/components/ui/dialog.tsx +135 -0
  12. package/base-package/components/ui/dropdown-menu.tsx +257 -0
  13. package/base-package/components/ui/form.tsx +167 -0
  14. package/base-package/components/ui/input.tsx +21 -0
  15. package/base-package/components/ui/label.tsx +24 -0
  16. package/base-package/components/ui/progress.tsx +31 -0
  17. package/base-package/components/ui/resizable.tsx +56 -0
  18. package/base-package/components/ui/select.tsx +185 -0
  19. package/base-package/components/ui/separator.tsx +28 -0
  20. package/base-package/components/ui/sheet.tsx +139 -0
  21. package/base-package/components/ui/skeleton.tsx +13 -0
  22. package/base-package/components/ui/sonner.tsx +25 -0
  23. package/base-package/components/ui/switch.tsx +31 -0
  24. package/base-package/components/ui/tabs.tsx +66 -0
  25. package/base-package/components/ui/textarea.tsx +18 -0
  26. package/base-package/components/ui/toggle-group.tsx +73 -0
  27. package/base-package/components/ui/toggle.tsx +47 -0
  28. package/base-package/components/ui/tooltip.tsx +61 -0
  29. package/base-package/components.json +21 -0
  30. package/base-package/eslint.config.mjs +16 -0
  31. package/base-package/lib/utils.ts +6 -0
  32. package/base-package/middleware.ts +12 -0
  33. package/base-package/next.config.ts +27 -0
  34. package/base-package/package.json +49 -0
  35. package/base-package/postcss.config.mjs +5 -0
  36. package/base-package/public/favicon.svg +4 -0
  37. package/base-package/tailwind.config.ts +89 -0
  38. package/base-package/tsconfig.json +27 -0
  39. package/dist/index.js +1858 -956
  40. package/features/ai-chat/README.md +258 -0
  41. package/features/ai-chat/app/api/chat/route.ts +16 -0
  42. package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
  43. package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
  44. package/features/ai-chat/feature.config.json +22 -0
  45. package/features/analytics/README.md +308 -0
  46. package/features/analytics/feature.config.json +20 -0
  47. package/features/analytics/lib/posthog.ts +36 -0
  48. package/features/auth/README.md +336 -0
  49. package/features/auth/app/api/auth/[...all]/route.ts +4 -0
  50. package/features/auth/app/dashboard/layout.tsx +15 -0
  51. package/features/auth/app/dashboard/page.tsx +140 -0
  52. package/features/auth/app/sign-in/page.tsx +228 -0
  53. package/features/auth/app/sign-up/page.tsx +243 -0
  54. package/features/auth/auth-schema.ts +47 -0
  55. package/features/auth/components/auth/setup-instructions.tsx +123 -0
  56. package/features/auth/feature.config.json +33 -0
  57. package/features/auth/lib/auth-client.ts +8 -0
  58. package/features/auth/lib/auth.ts +295 -0
  59. package/features/auth/lib/email-stub.ts +55 -0
  60. package/features/auth/lib/email.ts +47 -0
  61. package/features/auth/middleware.patch.ts +43 -0
  62. package/features/database/README.md +256 -0
  63. package/features/database/db/drizzle.ts +48 -0
  64. package/features/database/db/schema.ts +21 -0
  65. package/features/database/drizzle.config.ts +13 -0
  66. package/features/database/feature.config.json +30 -0
  67. package/features/email/README.md +282 -0
  68. package/features/email/emails/components/layout.tsx +181 -0
  69. package/features/email/emails/password-reset.tsx +67 -0
  70. package/features/email/emails/payment-failed.tsx +167 -0
  71. package/features/email/emails/subscription-confirmation.tsx +129 -0
  72. package/features/email/emails/welcome.tsx +100 -0
  73. package/features/email/feature.config.json +22 -0
  74. package/features/email/lib/email.ts +118 -0
  75. package/features/file-upload/README.md +271 -0
  76. package/features/file-upload/app/api/upload-image/route.ts +64 -0
  77. package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
  78. package/features/file-upload/feature.config.json +23 -0
  79. package/features/file-upload/lib/upload-image.ts +28 -0
  80. package/features/marketing-landing/README.md +266 -0
  81. package/features/marketing-landing/app/page.tsx +25 -0
  82. package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
  83. package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
  84. package/features/marketing-landing/components/homepage/footer.tsx +53 -0
  85. package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
  86. package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
  87. package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
  88. package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
  89. package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
  90. package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
  91. package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
  92. package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
  93. package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
  94. package/features/marketing-landing/components/logos/Polar.tsx +7 -0
  95. package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
  96. package/features/marketing-landing/components/logos/index.ts +6 -0
  97. package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
  98. package/features/marketing-landing/feature.config.json +23 -0
  99. package/features/payments/README.md +306 -0
  100. package/features/payments/app/api/subscription/route.ts +25 -0
  101. package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  102. package/features/payments/app/dashboard/payment/page.tsx +126 -0
  103. package/features/payments/app/success/page.tsx +123 -0
  104. package/features/payments/feature.config.json +31 -0
  105. package/features/payments/lib/polar-products.ts +49 -0
  106. package/features/payments/lib/subscription.ts +148 -0
  107. package/features/payments/payments-schema.ts +30 -0
  108. package/features/seo/README.md +244 -0
  109. package/features/seo/app/blog/[slug]/page.tsx +314 -0
  110. package/features/seo/app/blog/page.tsx +107 -0
  111. package/features/seo/app/robots.txt +13 -0
  112. package/features/seo/app/sitemap.ts +70 -0
  113. package/features/seo/feature.config.json +19 -0
  114. package/features/seo/lib/seo-utils.ts +163 -0
  115. 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&apos;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
+