shipd 0.1.3 → 0.2.0

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 (116) 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/docs-template/README.md +74 -0
  41. package/features/ai-chat/README.md +316 -0
  42. package/features/ai-chat/app/api/chat/route.ts +16 -0
  43. package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
  44. package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
  45. package/features/ai-chat/feature.config.json +22 -0
  46. package/features/analytics/README.md +364 -0
  47. package/features/analytics/feature.config.json +20 -0
  48. package/features/analytics/lib/posthog.ts +36 -0
  49. package/features/auth/README.md +409 -0
  50. package/features/auth/app/api/auth/[...all]/route.ts +4 -0
  51. package/features/auth/app/dashboard/layout.tsx +15 -0
  52. package/features/auth/app/dashboard/page.tsx +140 -0
  53. package/features/auth/app/sign-in/page.tsx +228 -0
  54. package/features/auth/app/sign-up/page.tsx +243 -0
  55. package/features/auth/auth-schema.ts +47 -0
  56. package/features/auth/components/auth/setup-instructions.tsx +123 -0
  57. package/features/auth/feature.config.json +33 -0
  58. package/features/auth/lib/auth-client.ts +8 -0
  59. package/features/auth/lib/auth.ts +295 -0
  60. package/features/auth/lib/email-stub.ts +55 -0
  61. package/features/auth/lib/email.ts +47 -0
  62. package/features/auth/middleware.patch.ts +43 -0
  63. package/features/database/README.md +312 -0
  64. package/features/database/db/drizzle.ts +48 -0
  65. package/features/database/db/schema.ts +21 -0
  66. package/features/database/drizzle.config.ts +13 -0
  67. package/features/database/feature.config.json +30 -0
  68. package/features/email/README.md +341 -0
  69. package/features/email/emails/components/layout.tsx +181 -0
  70. package/features/email/emails/password-reset.tsx +67 -0
  71. package/features/email/emails/payment-failed.tsx +167 -0
  72. package/features/email/emails/subscription-confirmation.tsx +129 -0
  73. package/features/email/emails/welcome.tsx +100 -0
  74. package/features/email/feature.config.json +22 -0
  75. package/features/email/lib/email.ts +118 -0
  76. package/features/file-upload/README.md +329 -0
  77. package/features/file-upload/app/api/upload-image/route.ts +64 -0
  78. package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
  79. package/features/file-upload/feature.config.json +23 -0
  80. package/features/file-upload/lib/upload-image.ts +28 -0
  81. package/features/marketing-landing/README.md +333 -0
  82. package/features/marketing-landing/app/page.tsx +25 -0
  83. package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
  84. package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
  85. package/features/marketing-landing/components/homepage/footer.tsx +53 -0
  86. package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
  87. package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
  88. package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
  89. package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
  90. package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
  91. package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
  92. package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
  93. package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
  94. package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
  95. package/features/marketing-landing/components/logos/Polar.tsx +7 -0
  96. package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
  97. package/features/marketing-landing/components/logos/index.ts +6 -0
  98. package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
  99. package/features/marketing-landing/feature.config.json +23 -0
  100. package/features/payments/README.md +375 -0
  101. package/features/payments/app/api/subscription/route.ts +25 -0
  102. package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  103. package/features/payments/app/dashboard/payment/page.tsx +126 -0
  104. package/features/payments/app/success/page.tsx +123 -0
  105. package/features/payments/feature.config.json +31 -0
  106. package/features/payments/lib/polar-products.ts +49 -0
  107. package/features/payments/lib/subscription.ts +148 -0
  108. package/features/payments/payments-schema.ts +30 -0
  109. package/features/seo/README.md +302 -0
  110. package/features/seo/app/blog/[slug]/page.tsx +314 -0
  111. package/features/seo/app/blog/page.tsx +107 -0
  112. package/features/seo/app/robots.txt +13 -0
  113. package/features/seo/app/sitemap.ts +70 -0
  114. package/features/seo/feature.config.json +19 -0
  115. package/features/seo/lib/seo-utils.ts +163 -0
  116. package/package.json +3 -1
@@ -0,0 +1,295 @@
1
+ import { db } from "@/db/drizzle";
2
+ import { account, session, user, verification } from "@/db/schema";
3
+ import { eq } from "drizzle-orm";
4
+ // Subscription is optional (only if payments module is installed)
5
+ // We'll check for it at runtime in the webhook handler
6
+ import {
7
+ checkout,
8
+ polar,
9
+ portal,
10
+ usage,
11
+ webhooks,
12
+ } from "@polar-sh/better-auth";
13
+ import { Polar } from "@polar-sh/sdk";
14
+ import { betterAuth } from "better-auth";
15
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
16
+ import { nextCookies } from "better-auth/next-js";
17
+ import {
18
+ sendPasswordResetEmail,
19
+ sendSubscriptionConfirmationEmail,
20
+ sendPaymentFailedEmail,
21
+ } from "@/lib/email";
22
+
23
+ // Utility function to safely parse dates
24
+ function safeParseDate(value: string | Date | null | undefined): Date | null {
25
+ if (!value) return null;
26
+ if (value instanceof Date) return value;
27
+ return new Date(value);
28
+ }
29
+
30
+ // Check if Polar is configured
31
+ const isPolarConfigured = !!(
32
+ process.env.POLAR_ACCESS_TOKEN &&
33
+ process.env.POLAR_WEBHOOK_SECRET &&
34
+ process.env.NEXT_PUBLIC_STARTER_TIER &&
35
+ process.env.NEXT_PUBLIC_STARTER_SLUG
36
+ );
37
+
38
+ if (!isPolarConfigured && process.env.NODE_ENV === "development") {
39
+ console.warn(
40
+ "⚠️ Polar.sh not configured - subscription features disabled. Configure POLAR_* env vars to enable payments.",
41
+ );
42
+ }
43
+
44
+ const polarClient = isPolarConfigured
45
+ ? new Polar({
46
+ accessToken: process.env.POLAR_ACCESS_TOKEN,
47
+ })
48
+ : null;
49
+
50
+ export const auth = betterAuth({
51
+ trustedOrigins: [`${process.env.NEXT_PUBLIC_APP_URL}`],
52
+ allowedDevOrigins: [`${process.env.NEXT_PUBLIC_APP_URL}`],
53
+ cookieCache: {
54
+ enabled: true,
55
+ maxAge: 5 * 60, // Cache duration in seconds
56
+ },
57
+ database: drizzleAdapter(db, {
58
+ provider: "pg",
59
+ schema: {
60
+ user,
61
+ session,
62
+ account,
63
+ verification,
64
+ },
65
+ }),
66
+ emailAndPassword: {
67
+ enabled: true,
68
+ sendResetPassword: async ({ user, url }) => {
69
+ // Send password reset email
70
+ await sendPasswordResetEmail(user.email, url);
71
+ },
72
+ },
73
+ user: {
74
+ additionalFields: {
75
+ name: {
76
+ type: "string",
77
+ required: false,
78
+ },
79
+ },
80
+ },
81
+ socialProviders: {
82
+ google: {
83
+ clientId: process.env.GOOGLE_CLIENT_ID!,
84
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
85
+ },
86
+ },
87
+ // TODO: Implement welcome emails via a custom sign-up API route
88
+ // Better Auth hooks API may have changed - using webhook approach instead
89
+ plugins: [
90
+ ...(isPolarConfigured && polarClient
91
+ ? [
92
+ polar({
93
+ client: polarClient,
94
+ createCustomerOnSignUp: true,
95
+ use: [
96
+ checkout({
97
+ products: [
98
+ {
99
+ productId: process.env.NEXT_PUBLIC_STARTER_TIER!,
100
+ slug: process.env.NEXT_PUBLIC_STARTER_SLUG!,
101
+ },
102
+ ],
103
+ successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/${process.env.POLAR_SUCCESS_URL}`,
104
+ authenticatedUsersOnly: true,
105
+ }),
106
+ portal(),
107
+ usage(),
108
+ webhooks({
109
+ secret: process.env.POLAR_WEBHOOK_SECRET!,
110
+ onPayload: async ({ data, type }) => {
111
+ if (
112
+ type === "subscription.created" ||
113
+ type === "subscription.active" ||
114
+ type === "subscription.canceled" ||
115
+ type === "subscription.revoked" ||
116
+ type === "subscription.uncanceled" ||
117
+ type === "subscription.updated"
118
+ ) {
119
+ console.log("🎯 Processing subscription webhook:", type);
120
+ console.log("📦 Payload data:", JSON.stringify(data, null, 2));
121
+
122
+ try {
123
+ // STEP 1: Extract user ID from customer data
124
+ const userId = data.customer?.externalId;
125
+ // STEP 2: Build subscription data
126
+ const subscriptionData = {
127
+ id: data.id,
128
+ createdAt: new Date(data.createdAt),
129
+ modifiedAt: safeParseDate(data.modifiedAt),
130
+ amount: data.amount,
131
+ currency: data.currency,
132
+ recurringInterval: data.recurringInterval,
133
+ status: data.status,
134
+ currentPeriodStart:
135
+ safeParseDate(data.currentPeriodStart) || new Date(),
136
+ currentPeriodEnd:
137
+ safeParseDate(data.currentPeriodEnd) || new Date(),
138
+ cancelAtPeriodEnd: data.cancelAtPeriodEnd || false,
139
+ canceledAt: safeParseDate(data.canceledAt),
140
+ startedAt: safeParseDate(data.startedAt) || new Date(),
141
+ endsAt: safeParseDate(data.endsAt),
142
+ endedAt: safeParseDate(data.endedAt),
143
+ customerId: data.customerId,
144
+ productId: data.productId,
145
+ discountId: data.discountId || null,
146
+ checkoutId: data.checkoutId || "",
147
+ customerCancellationReason:
148
+ data.customerCancellationReason || null,
149
+ customerCancellationComment:
150
+ data.customerCancellationComment || null,
151
+ metadata: data.metadata
152
+ ? JSON.stringify(data.metadata)
153
+ : null,
154
+ customFieldData: data.customFieldData
155
+ ? JSON.stringify(data.customFieldData)
156
+ : null,
157
+ userId: userId as string | null,
158
+ };
159
+
160
+ console.log("💾 Final subscription data:", {
161
+ id: subscriptionData.id,
162
+ status: subscriptionData.status,
163
+ userId: subscriptionData.userId,
164
+ amount: subscriptionData.amount,
165
+ });
166
+
167
+ // STEP 3: Use Drizzle's onConflictDoUpdate for proper upsert
168
+ // Only if subscription table exists (payments module installed)
169
+ // Dynamically import subscription table
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ let subscriptionTable: any;
172
+ try {
173
+ const schemaModule = await import("@/db/schema");
174
+ if ('subscription' in schemaModule && schemaModule.subscription) {
175
+ subscriptionTable = schemaModule.subscription;
176
+ } else {
177
+ console.warn("⚠️ Subscription table not found. Install payments module to enable subscription webhooks.");
178
+ return;
179
+ }
180
+ } catch {
181
+ console.warn("⚠️ Subscription table not found. Install payments module to enable subscription webhooks.");
182
+ return;
183
+ }
184
+
185
+ await db
186
+ .insert(subscriptionTable)
187
+ .values(subscriptionData)
188
+ .onConflictDoUpdate({
189
+ target: subscriptionTable.id,
190
+ set: {
191
+ modifiedAt: subscriptionData.modifiedAt || new Date(),
192
+ amount: subscriptionData.amount,
193
+ currency: subscriptionData.currency,
194
+ recurringInterval: subscriptionData.recurringInterval,
195
+ status: subscriptionData.status,
196
+ currentPeriodStart: subscriptionData.currentPeriodStart,
197
+ currentPeriodEnd: subscriptionData.currentPeriodEnd,
198
+ cancelAtPeriodEnd: subscriptionData.cancelAtPeriodEnd,
199
+ canceledAt: subscriptionData.canceledAt,
200
+ startedAt: subscriptionData.startedAt,
201
+ endsAt: subscriptionData.endsAt,
202
+ endedAt: subscriptionData.endedAt,
203
+ customerId: subscriptionData.customerId,
204
+ productId: subscriptionData.productId,
205
+ discountId: subscriptionData.discountId,
206
+ checkoutId: subscriptionData.checkoutId,
207
+ customerCancellationReason:
208
+ subscriptionData.customerCancellationReason,
209
+ customerCancellationComment:
210
+ subscriptionData.customerCancellationComment,
211
+ metadata: subscriptionData.metadata,
212
+ customFieldData: subscriptionData.customFieldData,
213
+ userId: subscriptionData.userId,
214
+ },
215
+ });
216
+
217
+ console.log("✅ Upserted subscription:", data.id);
218
+
219
+ // Send subscription emails based on event type
220
+ try {
221
+ // Get user email from database
222
+ if (userId) {
223
+ const userRecord = await db
224
+ .select()
225
+ .from(user)
226
+ .where(eq(user.id, userId))
227
+ .limit(1);
228
+
229
+ if (userRecord[0]?.email) {
230
+ const userEmail = userRecord[0].email;
231
+
232
+ // Send confirmation email for new or reactivated subscriptions
233
+ if (
234
+ type === "subscription.created" ||
235
+ type === "subscription.active"
236
+ ) {
237
+ const amount = `$${(data.amount / 100).toFixed(2)}/${data.recurringInterval}`;
238
+ await sendSubscriptionConfirmationEmail(
239
+ userEmail,
240
+ "Premium", // TODO: Get actual plan name from product
241
+ amount
242
+ );
243
+ console.log("📧 Sent subscription confirmation email to:", userEmail);
244
+ }
245
+ }
246
+ }
247
+ } catch (emailError) {
248
+ console.error("📧 Failed to send subscription email:", emailError);
249
+ // Don't throw - email failures shouldn't block webhook processing
250
+ }
251
+ } catch (error) {
252
+ console.error(
253
+ "💥 Error processing subscription webhook:",
254
+ error,
255
+ );
256
+ // Don't throw - let webhook succeed to avoid retries
257
+ }
258
+ }
259
+
260
+ // Handle payment failed events
261
+ if (type === "subscription.payment_failed") {
262
+ try {
263
+ const userId = data.customer?.externalId;
264
+ if (userId) {
265
+ const userRecord = await db
266
+ .select()
267
+ .from(user)
268
+ .where(eq(user.id, userId))
269
+ .limit(1);
270
+
271
+ if (userRecord[0]?.email) {
272
+ const retryDate = new Date();
273
+ retryDate.setDate(retryDate.getDate() + 3); // 3 days from now
274
+
275
+ await sendPaymentFailedEmail(
276
+ userRecord[0].email,
277
+ "Premium", // TODO: Get actual plan name
278
+ retryDate.toLocaleDateString()
279
+ );
280
+ console.log("📧 Sent payment failed email to:", userRecord[0].email);
281
+ }
282
+ }
283
+ } catch (error) {
284
+ console.error("📧 Failed to send payment failed email:", error);
285
+ }
286
+ }
287
+ },
288
+ }),
289
+ ],
290
+ }),
291
+ ]
292
+ : []),
293
+ nextCookies(),
294
+ ],
295
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Email stub for auth module
3
+ *
4
+ * This provides minimal email functionality when the full email module is not installed.
5
+ * Email sending will be logged to console in development.
6
+ */
7
+
8
+ interface EmailOptions {
9
+ to: string;
10
+ subject: string;
11
+ url?: string;
12
+ user?: { email: string; name?: string };
13
+ }
14
+
15
+ /**
16
+ * Send password reset email (stub)
17
+ */
18
+ export async function sendPasswordResetEmail(
19
+ email: string,
20
+ url: string
21
+ ): Promise<void> {
22
+ if (process.env.NODE_ENV === 'development') {
23
+ console.log(`📧 [Email Stub] Password reset email would be sent to ${email}`);
24
+ console.log(` Reset URL: ${url}`);
25
+ console.log(` Install the email module for actual email sending.`);
26
+ }
27
+ // In production, this would fail silently or you should install the email module
28
+ }
29
+
30
+ /**
31
+ * Send subscription confirmation email (stub)
32
+ */
33
+ export async function sendSubscriptionConfirmationEmail(
34
+ email: string,
35
+ subscriptionDetails: any
36
+ ): Promise<void> {
37
+ if (process.env.NODE_ENV === 'development') {
38
+ console.log(`📧 [Email Stub] Subscription confirmation email would be sent to ${email}`);
39
+ console.log(` Install the email module for actual email sending.`);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Send payment failed email (stub)
45
+ */
46
+ export async function sendPaymentFailedEmail(
47
+ email: string,
48
+ errorDetails: any
49
+ ): Promise<void> {
50
+ if (process.env.NODE_ENV === 'development') {
51
+ console.log(`📧 [Email Stub] Payment failed email would be sent to ${email}`);
52
+ console.log(` Install the email module for actual email sending.`);
53
+ }
54
+ }
55
+
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Email functions for auth module
3
+ *
4
+ * This is a minimal email implementation that logs to console.
5
+ * Install the full email module (Resend) for production email sending.
6
+ */
7
+
8
+ /**
9
+ * Send password reset email
10
+ */
11
+ export async function sendPasswordResetEmail(
12
+ email: string,
13
+ url: string
14
+ ): Promise<void> {
15
+ if (process.env.NODE_ENV === 'development') {
16
+ console.log(`📧 [Email] Password reset email would be sent to ${email}`);
17
+ console.log(` Reset URL: ${url}`);
18
+ console.log(` Install the email module (Resend) for actual email sending.`);
19
+ }
20
+ // In production without email module, this fails silently
21
+ // Install the email module for production use
22
+ }
23
+
24
+ /**
25
+ * Send subscription confirmation email
26
+ */
27
+ export async function sendSubscriptionConfirmationEmail(
28
+ email: string
29
+ ): Promise<void> {
30
+ if (process.env.NODE_ENV === 'development') {
31
+ console.log(`📧 [Email] Subscription confirmation email would be sent to ${email}`);
32
+ console.log(` Install the email module (Resend) for actual email sending.`);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Send payment failed email
38
+ */
39
+ export async function sendPaymentFailedEmail(
40
+ email: string
41
+ ): Promise<void> {
42
+ if (process.env.NODE_ENV === 'development') {
43
+ console.log(`📧 [Email] Payment failed email would be sent to ${email}`);
44
+ console.log(` Install the email module (Resend) for actual email sending.`);
45
+ }
46
+ }
47
+
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Auth Middleware Patch
3
+ *
4
+ * This file contains the middleware logic for authentication.
5
+ * When the auth module is appended, this will be merged into the base middleware.ts
6
+ */
7
+
8
+ import { NextRequest, NextResponse } from "next/server";
9
+ import { getSessionCookie } from "better-auth/cookies";
10
+
11
+ /**
12
+ * Apply auth middleware logic
13
+ * This function should be called from the base middleware
14
+ */
15
+ export function applyAuthMiddleware(
16
+ request: NextRequest,
17
+ baseResponse: NextResponse
18
+ ): NextResponse | null {
19
+ const sessionCookie = getSessionCookie(request);
20
+ const { pathname } = request.nextUrl;
21
+
22
+ // Don't redirect from sign-in/sign-up pages to avoid redirect loops
23
+ if (["/sign-in", "/sign-up"].includes(pathname)) {
24
+ return null; // Let base middleware handle it
25
+ }
26
+
27
+ // Redirect unauthenticated users from protected routes
28
+ const protectedRoutes = ["/dashboard"]; // Can be customized
29
+ const isProtectedRoute = protectedRoutes.some(route => pathname.startsWith(route));
30
+
31
+ if (isProtectedRoute && !sessionCookie) {
32
+ const returnTo = encodeURIComponent(pathname);
33
+ return NextResponse.redirect(new URL(`/sign-in?returnTo=${returnTo}`, request.url));
34
+ }
35
+
36
+ return null; // Continue with base middleware
37
+ }
38
+
39
+ /**
40
+ * Middleware matcher configuration for auth
41
+ */
42
+ export const matcher = ["/dashboard/:path*", "/sign-in", "/sign-up"];
43
+