eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs(readme): add missing newline for webhook URL instructions and format code

Sam Sauer 6b3ba289 916940d7

+221 -71
+1
README.md
··· 107 107 1. Deploy your application and copy the webhook URL (e.g., `https://your-domain.com/api/webhooks`). 108 108 109 109 2. In your Stripe dashboard, go to Developers → Webhooks and add an endpoint: 110 + 110 111 - URL: `https://your-domain.com/api/webhooks` 111 112 - Events to listen to: 112 113 - `checkout.session.completed`
+3 -1
app/actions/auth.ts
··· 16 16 email: data.email, 17 17 password: data.password, 18 18 options: { 19 - emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/auth/callback`, 19 + emailRedirectTo: `${ 20 + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" 21 + }/auth/callback`, 20 22 }, 21 23 }); 22 24
+48 -15
app/actions/subscription.ts
··· 48 48 49 49 // Find active or trialing subscription 50 50 const activeSubscription = subscriptions.data.find( 51 - (sub) => (sub.status === "active" || sub.status === "trialing") && !sub.cancel_at_period_end 51 + (sub) => 52 + (sub.status === "active" || sub.status === "trialing") && 53 + !sub.cancel_at_period_end 52 54 ); 53 55 54 56 return activeSubscription || null; ··· 66 68 67 69 return { 68 70 subscribed: !!subscription, 69 - subscription: subscription ? { 70 - status: subscription.status, 71 - cancel_at_period_end: subscription.cancel_at_period_end, 72 - current_period_end: new Date(subscription.current_period_end * 1000).toISOString(), 73 - current_period_start: new Date(subscription.current_period_start * 1000).toISOString(), 74 - } : null, 71 + subscription: subscription 72 + ? { 73 + status: subscription.status, 74 + cancel_at_period_end: subscription.cancel_at_period_end, 75 + current_period_end: new Date( 76 + subscription.current_period_end * 1000 77 + ).toISOString(), 78 + current_period_start: new Date( 79 + subscription.current_period_start * 1000 80 + ).toISOString(), 81 + } 82 + : null, 75 83 }; 76 84 } 77 85 78 86 /** 79 87 * Verify active subscription for protected routes (always checks Stripe) 80 88 */ 81 - export async function verifyActiveSubscription(): Promise<{ active: boolean; subscription: Stripe.Subscription | null }> { 89 + export async function verifyActiveSubscription(): Promise<{ 90 + active: boolean; 91 + subscription: Stripe.Subscription | null; 92 + }> { 82 93 const subscription = await getActiveSubscription(); 83 94 84 95 return { ··· 122 133 const headersList = await headers(); 123 134 const originHeader = headersList.get("origin"); 124 135 const hostHeader = headersList.get("host"); 125 - const origin = originHeader || `https://${hostHeader}` || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; 136 + const origin = 137 + originHeader || 138 + `https://${hostHeader}` || 139 + process.env.NEXT_PUBLIC_APP_URL || 140 + "http://localhost:3000"; 126 141 127 142 const checkoutSession = await stripe.checkout.sessions.create({ 128 143 customer: customerId, ··· 164 179 console.error("Error canceling subscription:", error); 165 180 return { 166 181 success: false, 167 - error: error instanceof Error ? error.message : "Failed to cancel subscription", 182 + error: 183 + error instanceof Error 184 + ? error.message 185 + : "Failed to cancel subscription", 168 186 }; 169 187 } 170 188 } ··· 187 205 }); 188 206 189 207 const cancelingSubscription = subscriptions.data.find( 190 - (sub) => sub.cancel_at_period_end === true && (sub.status === "active" || sub.status === "trialing") 208 + (sub) => 209 + sub.cancel_at_period_end === true && 210 + (sub.status === "active" || sub.status === "trialing") 191 211 ); 192 212 193 213 if (!cancelingSubscription) { 194 - return { success: false, error: "No subscription scheduled for cancellation found" }; 214 + return { 215 + success: false, 216 + error: "No subscription scheduled for cancellation found", 217 + }; 195 218 } 196 219 197 220 await stripe.subscriptions.update(cancelingSubscription.id, { ··· 203 226 console.error("Error resuming subscription:", error); 204 227 return { 205 228 success: false, 206 - error: error instanceof Error ? error.message : "Failed to resume subscription", 229 + error: 230 + error instanceof Error 231 + ? error.message 232 + : "Failed to resume subscription", 207 233 }; 208 234 } 209 235 } ··· 221 247 const headersList = await headers(); 222 248 const originHeader = headersList.get("origin"); 223 249 const hostHeader = headersList.get("host"); 224 - const origin = originHeader || `https://${hostHeader}` || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; 250 + const origin = 251 + originHeader || 252 + `https://${hostHeader}` || 253 + process.env.NEXT_PUBLIC_APP_URL || 254 + "http://localhost:3000"; 225 255 226 256 try { 227 257 const session = await stripe.billingPortal.sessions.create({ ··· 234 264 console.error("Error creating billing portal session:", error); 235 265 return { 236 266 success: false, 237 - error: error instanceof Error ? error.message : "Failed to create billing portal session", 267 + error: 268 + error instanceof Error 269 + ? error.message 270 + : "Failed to create billing portal session", 238 271 }; 239 272 } 240 273 }
+5 -5
app/api/server/[endpoint]/route.ts
··· 12 12 } = await supabase.auth.getUser(); 13 13 14 14 if (!user) { 15 - return NextResponse.json( 16 - { message: "Unauthorized" }, 17 - { status: 401 } 18 - ); 15 + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 19 16 } 20 17 21 18 // Always verify subscription status directly from Stripe (source of truth) ··· 56 53 } catch (error) { 57 54 console.error("Error making server call:", error); 58 55 return NextResponse.json( 59 - { message: "Failed to make server call", error: error instanceof Error ? error.message : "Unknown error" }, 56 + { 57 + message: "Failed to make server call", 58 + error: error instanceof Error ? error.message : "Unknown error", 59 + }, 60 60 { status: 500 } 61 61 ); 62 62 }
+6 -5
app/api/webhooks/route.ts
··· 12 12 event = stripe.webhooks.constructEvent( 13 13 await (await req.blob()).text(), 14 14 req.headers.get("stripe-signature") as string, 15 - process.env.STRIPE_WEBHOOK_SECRET as string, 15 + process.env.STRIPE_WEBHOOK_SECRET as string 16 16 ); 17 17 } catch (err) { 18 18 const errorMessage = err instanceof Error ? err.message : "Unknown error"; 19 19 console.log(`❌ Webhook Error: ${errorMessage}`); 20 20 return NextResponse.json( 21 21 { message: `Webhook Error: ${errorMessage}` }, 22 - { status: 400 }, 22 + { status: 400 } 23 23 ); 24 24 } 25 25 ··· 33 33 34 34 if (session.mode === "subscription" && session.customer) { 35 35 const userId = session.metadata?.user_id; 36 - const customerId = typeof session.customer === "string" 37 - ? session.customer 38 - : session.customer.id; 36 + const customerId = 37 + typeof session.customer === "string" 38 + ? session.customer 39 + : session.customer.id; 39 40 40 41 if (userId && customerId) { 41 42 // Only store user_id -> stripe_customer_id mapping (minimal)
+92 -22
app/dashboard/dashboard-client.tsx
··· 1 1 "use client"; 2 2 3 3 import { useState } from "react"; 4 - import { createSubscriptionCheckout, cancelSubscription, resumeSubscription, createBillingPortalSession } from "@/actions/subscription"; 4 + import { 5 + createSubscriptionCheckout, 6 + cancelSubscription, 7 + resumeSubscription, 8 + createBillingPortalSession, 9 + } from "@/actions/subscription"; 5 10 6 11 interface DashboardClientProps { 7 12 subscribed: boolean; ··· 9 14 priceId: string; 10 15 } 11 16 12 - export default function DashboardClient({ subscribed, subscription, priceId }: DashboardClientProps) { 17 + export default function DashboardClient({ 18 + subscribed, 19 + subscription, 20 + priceId, 21 + }: DashboardClientProps) { 13 22 const [loading, setLoading] = useState(false); 14 23 const [actionLoading, setActionLoading] = useState<string | null>(null); 15 24 16 25 const handleSubscribe = async () => { 17 26 if (!priceId) { 18 - alert("Stripe price ID not configured. Please set NEXT_PUBLIC_STRIPE_PRICE_ID in your environment variables."); 27 + alert( 28 + "Stripe price ID not configured. Please set NEXT_PUBLIC_STRIPE_PRICE_ID in your environment variables." 29 + ); 19 30 return; 20 31 } 21 32 ··· 48 59 alert(`Success: ${JSON.stringify(data, null, 2)}`); 49 60 } catch (error) { 50 61 console.error("Error making server call:", error); 51 - alert(`Error: ${error instanceof Error ? error.message : "Unknown error"}`); 62 + alert( 63 + `Error: ${error instanceof Error ? error.message : "Unknown error"}` 64 + ); 52 65 } 53 66 }; 54 67 55 68 // Show subscription management 56 69 const hasSubscription = !!subscription; 57 - const isCanceled = subscription?.status === "canceled" || subscription?.status === "past_due"; 70 + const isCanceled = 71 + subscription?.status === "canceled" || subscription?.status === "past_due"; 58 72 59 73 if (!hasSubscription) { 60 74 return ( 61 - <div style={{ marginTop: "32px", padding: "24px", border: "1px solid #ccc", borderRadius: "8px" }}> 75 + <div 76 + style={{ 77 + marginTop: "32px", 78 + padding: "24px", 79 + border: "1px solid #ccc", 80 + borderRadius: "8px", 81 + }} 82 + > 62 83 <h2>Subscribe to Access</h2> 63 84 <p>You need an active subscription to access the server features.</p> 64 85 <button ··· 84 105 85 106 if (isCanceled) { 86 107 return ( 87 - <div style={{ marginTop: "32px", padding: "24px", border: "1px solid #ccc", borderRadius: "8px", backgroundColor: "#f5f5f5" }}> 108 + <div 109 + style={{ 110 + marginTop: "32px", 111 + padding: "24px", 112 + border: "1px solid #ccc", 113 + borderRadius: "8px", 114 + backgroundColor: "#f5f5f5", 115 + }} 116 + > 88 117 <h2 style={{ color: "#666", marginTop: 0 }}>Subscription Canceled</h2> 89 - <p>Your subscription has been canceled. Subscribe again to regain access.</p> 118 + <p> 119 + Your subscription has been canceled. Subscribe again to regain access. 120 + </p> 90 121 <button 91 122 onClick={handleSubscribe} 92 123 disabled={loading} ··· 109 140 } 110 141 111 142 const handleCancel = async () => { 112 - if (!confirm("Are you sure you want to cancel your subscription? You'll have access until the end of your billing period.")) { 143 + if ( 144 + !confirm( 145 + "Are you sure you want to cancel your subscription? You'll have access until the end of your billing period." 146 + ) 147 + ) { 113 148 return; 114 149 } 115 150 ··· 117 152 try { 118 153 const result = await cancelSubscription(); 119 154 if (result.success) { 120 - alert("Subscription canceled. You'll have access until the end of your billing period."); 155 + alert( 156 + "Subscription canceled. You'll have access until the end of your billing period." 157 + ); 121 158 window.location.reload(); 122 159 } else { 123 160 alert(`Error: ${result.error}`); ··· 171 208 172 209 return ( 173 210 <div style={{ marginTop: "32px" }}> 174 - <div style={{ padding: "24px", border: `1px solid ${statusColor}`, borderRadius: "8px", backgroundColor: statusBg, marginBottom: "24px" }}> 211 + <div 212 + style={{ 213 + padding: "24px", 214 + border: `1px solid ${statusColor}`, 215 + borderRadius: "8px", 216 + backgroundColor: statusBg, 217 + marginBottom: "24px", 218 + }} 219 + > 175 220 <h2 style={{ color: statusColor, marginTop: 0 }}> 176 221 {isCanceling ? "⚠️ Subscription Canceling" : "✓ Active Subscription"} 177 222 </h2> ··· 182 227 </p> 183 228 {subscription.current_period_end && ( 184 229 <p> 185 - <strong> 186 - {isCanceling ? "Access until:" : "Renews:"} 187 - </strong> {new Date(subscription.current_period_end).toLocaleDateString()} 230 + <strong>{isCanceling ? "Access until:" : "Renews:"}</strong>{" "} 231 + {new Date(subscription.current_period_end).toLocaleDateString()} 188 232 </p> 189 233 )} 190 234 {isCanceling && ( ··· 195 239 </div> 196 240 )} 197 241 198 - <div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "16px" }}> 242 + <div 243 + style={{ 244 + display: "flex", 245 + flexDirection: "column", 246 + gap: "8px", 247 + marginTop: "16px", 248 + }} 249 + > 199 250 <button 200 251 onClick={handleManageBilling} 201 252 disabled={actionLoading !== null} ··· 210 261 opacity: actionLoading !== null ? 0.6 : 1, 211 262 }} 212 263 > 213 - {actionLoading === "billing" ? "Loading..." : "Manage Payment Method"} 264 + {actionLoading === "billing" 265 + ? "Loading..." 266 + : "Manage Payment Method"} 214 267 </button> 215 268 216 269 {isCanceling ? ( ··· 228 281 opacity: actionLoading !== null ? 0.6 : 1, 229 282 }} 230 283 > 231 - {actionLoading === "resume" ? "Loading..." : "Resume Subscription"} 284 + {actionLoading === "resume" 285 + ? "Loading..." 286 + : "Resume Subscription"} 232 287 </button> 233 288 ) : ( 234 289 <button ··· 245 300 opacity: actionLoading !== null ? 0.6 : 1, 246 301 }} 247 302 > 248 - {actionLoading === "cancel" ? "Loading..." : "Cancel Subscription"} 303 + {actionLoading === "cancel" 304 + ? "Loading..." 305 + : "Cancel Subscription"} 249 306 </button> 250 307 )} 251 308 </div> 252 309 </div> 253 310 254 - <div style={{ padding: "24px", border: "1px solid #ccc", borderRadius: "8px" }}> 311 + <div 312 + style={{ 313 + padding: "24px", 314 + border: "1px solid #ccc", 315 + borderRadius: "8px", 316 + }} 317 + > 255 318 <h2>Server Actions</h2> 256 319 <p>You have access to the following server endpoints:</p> 257 - 258 - <div style={{ display: "flex", flexDirection: "column", gap: "12px", marginTop: "16px" }}> 320 + 321 + <div 322 + style={{ 323 + display: "flex", 324 + flexDirection: "column", 325 + gap: "12px", 326 + marginTop: "16px", 327 + }} 328 + > 259 329 <button 260 330 onClick={() => handleServerCall("action1")} 261 331 style={{ ··· 270 340 > 271 341 Call Server Action 1 272 342 </button> 273 - 343 + 274 344 <button 275 345 onClick={() => handleServerCall("action2")} 276 346 style={{
+3 -3
app/dashboard/page.tsx
··· 20 20 <main className="page-container"> 21 21 <h1>Dashboard</h1> 22 22 <p>Welcome, {user.email}!</p> 23 - 24 - <DashboardClient 25 - subscribed={subscribed} 23 + 24 + <DashboardClient 25 + subscribed={subscribed} 26 26 subscription={subscription} 27 27 priceId={process.env.NEXT_PUBLIC_STRIPE_PRICE_ID || ""} 28 28 />
+31 -12
app/layout.tsx
··· 40 40 <div className="container"> 41 41 <header> 42 42 <div className="header-content"> 43 - <Link href="/" style={{ textDecoration: "none", color: "inherit" }}> 43 + <Link 44 + href="/" 45 + style={{ textDecoration: "none", color: "inherit" }} 46 + > 44 47 <h1>eny.space</h1> 45 48 </Link> 46 - <nav style={{ display: "flex", flexDirection: "column", gap: "12px", marginTop: "24px" }}> 49 + <nav 50 + style={{ 51 + display: "flex", 52 + flexDirection: "column", 53 + gap: "12px", 54 + marginTop: "24px", 55 + }} 56 + > 47 57 {user ? ( 48 58 <> 49 - <Link href="/dashboard" style={{ color: "var(--h2-color)" }}>Dashboard</Link> 59 + <Link 60 + href="/dashboard" 61 + style={{ color: "var(--h2-color)" }} 62 + > 63 + Dashboard 64 + </Link> 50 65 <form action={signOut} style={{ margin: 0 }}> 51 - <button 52 - type="submit" 53 - style={{ 54 - background: "none", 55 - border: "none", 56 - cursor: "pointer", 66 + <button 67 + type="submit" 68 + style={{ 69 + background: "none", 70 + border: "none", 71 + cursor: "pointer", 57 72 color: "var(--h2-color)", 58 73 padding: 0, 59 74 textAlign: "left", 60 - font: "inherit" 75 + font: "inherit", 61 76 }} 62 77 > 63 78 Sign Out ··· 66 81 </> 67 82 ) : ( 68 83 <> 69 - <Link href="/login" style={{ color: "var(--h2-color)" }}>Login</Link> 70 - <Link href="/signup" style={{ color: "var(--h2-color)" }}>Sign Up</Link> 84 + <Link href="/login" style={{ color: "var(--h2-color)" }}> 85 + Login 86 + </Link> 87 + <Link href="/signup" style={{ color: "var(--h2-color)" }}> 88 + Sign Up 89 + </Link> 71 90 </> 72 91 )} 73 92 </nav>
+16 -4
app/login/page.tsx
··· 3 3 4 4 export default function LoginPage() { 5 5 return ( 6 - <main className="page-container" style={{ maxWidth: "400px", margin: "0 auto" }}> 6 + <main 7 + className="page-container" 8 + style={{ maxWidth: "400px", margin: "0 auto" }} 9 + > 7 10 <h1>Login</h1> 8 - <form action={signIn} style={{ display: "flex", flexDirection: "column", gap: "16px" }}> 11 + <form 12 + action={signIn} 13 + style={{ display: "flex", flexDirection: "column", gap: "16px" }} 14 + > 9 15 <div> 10 - <label htmlFor="email" style={{ display: "block", marginBottom: "8px" }}> 16 + <label 17 + htmlFor="email" 18 + style={{ display: "block", marginBottom: "8px" }} 19 + > 11 20 Email 12 21 </label> 13 22 <input ··· 24 33 /> 25 34 </div> 26 35 <div> 27 - <label htmlFor="password" style={{ display: "block", marginBottom: "8px" }}> 36 + <label 37 + htmlFor="password" 38 + style={{ display: "block", marginBottom: "8px" }} 39 + > 28 40 Password 29 41 </label> 30 42 <input
+16 -4
app/signup/page.tsx
··· 3 3 4 4 export default function SignUpPage() { 5 5 return ( 6 - <main className="page-container" style={{ maxWidth: "400px", margin: "0 auto" }}> 6 + <main 7 + className="page-container" 8 + style={{ maxWidth: "400px", margin: "0 auto" }} 9 + > 7 10 <h1>Sign Up</h1> 8 - <form action={signUp} style={{ display: "flex", flexDirection: "column", gap: "16px" }}> 11 + <form 12 + action={signUp} 13 + style={{ display: "flex", flexDirection: "column", gap: "16px" }} 14 + > 9 15 <div> 10 - <label htmlFor="email" style={{ display: "block", marginBottom: "8px" }}> 16 + <label 17 + htmlFor="email" 18 + style={{ display: "block", marginBottom: "8px" }} 19 + > 11 20 Email 12 21 </label> 13 22 <input ··· 24 33 /> 25 34 </div> 26 35 <div> 27 - <label htmlFor="password" style={{ display: "block", marginBottom: "8px" }}> 36 + <label 37 + htmlFor="password" 38 + style={{ display: "block", marginBottom: "8px" }} 39 + > 28 40 Password 29 41 </label> 30 42 <input