Account Management
Build comprehensive user account pages with profile management, subscription controls, billing history, and settings.
Account Management Overview
Account management provides users with full control over their profile, subscription, billing, and application settings in a clean, organized interface.
š¤ Profile Features
- ⢠Profile information editing
- ⢠Avatar/image upload
- ⢠Email verification status
- ⢠Account creation date
- ⢠Connected accounts
- ⢠Privacy settings
š³ Billing Features
- ⢠Current subscription details
- ⢠Usage tracking and limits
- ⢠Billing history
- ⢠Payment method management
- ⢠Plan upgrade/downgrade
- ⢠Cancellation handling
Account Page Implementation
Main Account Page
Complete account page with all user management features:
src/app/account/page.tsx
1import { auth } from "~/auth";2import { db } from "~/server/db";3import { redirect } from "next/navigation";4import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";5import { Badge } from "~/components/ui/badge";6import { Button } from "~/components/ui/button";7import { AccountMessages } from "~/components/AccountMessages";8import { SubscriptionManagement } from "~/components/SubscriptionManagement";9import { SUBSCRIPTION_PLANS } from "~/lib/subscriptions";1011export default async function AccountPage() {12 const session = await auth();1314 if (!session?.user) {15 redirect("/auth/signin");16 }1718 // Fetch user data and subscription19 const [subscription, user] = await Promise.all([20 db.subscription.findUnique({21 where: { userId: session.user.id },22 }),23 db.user.findUnique({24 where: { id: session.user.id },25 select: {26 id: true,27 name: true,28 email: true,29 image: true,30 emailVerified: true,31 createdAt: true,32 },33 }),34 ]);3536 const isActive = subscription?.status === "active";37 const isCanceling = subscription?.cancelAtPeriodEnd;38 const currentPlan = SUBSCRIPTION_PLANS[subscription?.type ?? "FREE"];3940 return (41 <div className="container mx-auto px-4 py-8 max-w-4xl">42 <div className="space-y-8">43 {/* Header */}44 <div>45 <h1 className="text-3xl font-bold">Account Settings</h1>46 <p className="text-muted-foreground">47 Manage your profile, subscription, and preferences48 </p>49 </div>5051 {/* Account Messages */}52 <AccountMessages />5354 {/* Profile Section */}55 <Card>56 <CardHeader>57 <CardTitle>Profile Information</CardTitle>58 </CardHeader>59 <CardContent className="space-y-6">60 <div className="flex items-center space-x-4">61 {user?.image ? (62 <img63 src={user.image}64 alt="Profile"65 className="h-16 w-16 rounded-full"66 />67 ) : (68 <div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center">69 <span className="text-2xl font-semibold">70 {user?.name?.charAt(0)?.toUpperCase() ?? "U"}71 </span>72 </div>73 )}74 <div>75 <h3 className="text-lg font-medium">{user?.name ?? "User"}</h3>76 <p className="text-sm text-muted-foreground">{user?.email}</p>77 <div className="flex items-center gap-2 mt-1">78 {user?.emailVerified ? (79 <Badge variant="secondary" className="text-xs">80 ā Verified81 </Badge>82 ) : (83 <Badge variant="destructive" className="text-xs">84 Unverified85 </Badge>86 )}87 <span className="text-xs text-muted-foreground">88 Member since {new Date(user?.createdAt ?? "").toLocaleDateString()}89 </span>90 </div>91 </div>92 </div>9394 <div className="flex gap-3">95 <Button variant="outline">Edit Profile</Button>96 <Button variant="outline">Change Password</Button>97 </div>98 </CardContent>99 </Card>100101 {/* Subscription Section */}102 <Card>103 <CardHeader>104 <CardTitle className="flex items-center justify-between">105 <span>Subscription</span>106 <Badge variant={isActive ? "default" : "secondary"}>107 {currentPlan.name}108 </Badge>109 </CardTitle>110 </CardHeader>111 <CardContent className="space-y-6">112 {/* Current Plan Info */}113 <div className="grid gap-4 md:grid-cols-2">114 <div>115 <h4 className="font-medium mb-2">Current Plan</h4>116 <p className="text-2xl font-bold">{currentPlan.name}</p>117 <p className="text-sm text-muted-foreground">118 {currentPlan.description}119 </p>120 </div>121 <div>122 <h4 className="font-medium mb-2">Usage This Month</h4>123 <div className="space-y-2">124 <div className="flex justify-between">125 <span className="text-sm">Projects</span>126 <span className="text-sm font-medium">127 {subscription?.usageCount ?? 0} / {subscription?.usageLimit === -1 ? "ā" : subscription?.usageLimit}128 </span>129 </div>130 <div className="w-full bg-muted rounded-full h-2">131 <div132 className="bg-primary h-2 rounded-full"133 style={{134 width: `${Math.min(135 ((subscription?.usageCount ?? 0) / (subscription?.usageLimit || 1)) * 100,136 100137 )}%`138 }}139 />140 </div>141 </div>142 </div>143 </div>144145 {/* Subscription Status */}146 {isActive && (147 <div className="space-y-3">148 {isCanceling ? (149 <div className="p-4 rounded-lg bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800">150 <div className="flex items-center gap-2">151 <span className="text-yellow-600 dark:text-yellow-400">ā ļø</span>152 <div>153 <p className="font-medium text-yellow-800 dark:text-yellow-200">154 Subscription Ending155 </p>156 <p className="text-sm text-yellow-700 dark:text-yellow-300">157 Your subscription will end on{" "}158 {subscription?.stripeCurrentPeriodEnd159 ? new Date(subscription.stripeCurrentPeriodEnd).toLocaleDateString()160 : "the next billing date"161 }162 </p>163 </div>164 </div>165 </div>166 ) : (167 <div className="p-4 rounded-lg bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800">168 <div className="flex items-center gap-2">169 <span className="text-green-600 dark:text-green-400">ā </span>170 <div>171 <p className="font-medium text-green-800 dark:text-green-200">172 Active Subscription173 </p>174 <p className="text-sm text-green-700 dark:text-green-300">175 Next billing: {subscription?.stripeCurrentPeriodEnd176 ? new Date(subscription.stripeCurrentPeriodEnd).toLocaleDateString()177 : "Unknown"178 }179 </p>180 </div>181 </div>182 </div>183 )}184 </div>185 )}186187 {/* Subscription Management */}188 <SubscriptionManagement subscription={subscription} />189 </CardContent>190 </Card>191192 {/* Billing History */}193 <Card>194 <CardHeader>195 <CardTitle>Billing History</CardTitle>196 </CardHeader>197 <CardContent>198 <div className="space-y-4">199 <p className="text-sm text-muted-foreground">200 Access your complete billing history and download invoices201 </p>202 <Button variant="outline">203 View Billing Portal204 </Button>205 </div>206 </CardContent>207 </Card>208209 {/* Danger Zone */}210 <Card className="border-destructive/20">211 <CardHeader>212 <CardTitle className="text-destructive">Danger Zone</CardTitle>213 </CardHeader>214 <CardContent className="space-y-4">215 <div className="space-y-3">216 <div>217 <h4 className="font-medium">Delete Account</h4>218 <p className="text-sm text-muted-foreground">219 Permanently delete your account and all associated data. This action cannot be undone.220 </p>221 </div>222 <Button variant="destructive" size="sm">223 Delete Account224 </Button>225 </div>226 </CardContent>227 </Card>228 </div>229 </div>230 );231}
Account Messages Component
Display important account-related messages and notifications:
src/components/AccountMessages.tsx
1"use client";23import { useSearchParams } from "next/navigation";4import { Alert, AlertDescription } from "~/components/ui/alert";5import { CheckCircle, XCircle, Info } from "lucide-react";67export function AccountMessages() {8 const searchParams = useSearchParams();9 const message = searchParams.get("message");10 const error = searchParams.get("error");11 const sessionId = searchParams.get("session_id");1213 // Success messages14 if (sessionId) {15 return (16 <Alert className="border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950 dark:text-green-200">17 <CheckCircle className="h-4 w-4" />18 <AlertDescription>19 š Welcome to the Pro plan! Your subscription is now active and you have access to all premium features.20 </AlertDescription>21 </Alert>22 );23 }2425 if (message === "subscription_updated") {26 return (27 <Alert className="border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200">28 <Info className="h-4 w-4" />29 <AlertDescription>30 Your subscription has been successfully updated.31 </AlertDescription>32 </Alert>33 );34 }3536 if (message === "subscription_canceled") {37 return (38 <Alert className="border-yellow-200 bg-yellow-50 text-yellow-800 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-200">39 <Info className="h-4 w-4" />40 <AlertDescription>41 Your subscription has been canceled. You'll continue to have access until the end of your billing period.42 </AlertDescription>43 </Alert>44 );45 }4647 // Error messages48 if (error === "checkout_canceled") {49 return (50 <Alert className="border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">51 <XCircle className="h-4 w-4" />52 <AlertDescription>53 Checkout was canceled. No charges were made to your account.54 </AlertDescription>55 </Alert>56 );57 }5859 if (error === "subscription_error") {60 return (61 <Alert className="border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">62 <XCircle className="h-4 w-4" />63 <AlertDescription>64 There was an error processing your subscription. Please try again or contact support.65 </AlertDescription>66 </Alert>67 );68 }6970 return null;71}
Subscription Management
SubscriptionManagement Component
Handle subscription changes, upgrades, and cancellations:
src/components/SubscriptionManagement.tsx
1"use client";23import { useState } from "react";4import { Button } from "~/components/ui/button";5import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";6import { Badge } from "~/components/ui/badge";7import { Loader2, ExternalLink } from "lucide-react";8import { createCheckoutSession, createBillingPortalSession } from "~/server/actions/stripe";9import { SUBSCRIPTION_PLANS } from "~/lib/subscriptions";10import type { Subscription } from "@prisma/client";1112interface SubscriptionManagementProps {13 subscription: Subscription | null;14}1516export function SubscriptionManagement({ subscription }: SubscriptionManagementProps) {17 const [loading, setLoading] = useState<string | null>(null);1819 const isActive = subscription?.status === "active";20 const isCanceling = subscription?.cancelAtPeriodEnd;21 const currentType = subscription?.type ?? "FREE";2223 const handleUpgrade = async (priceId: string, planName: string) => {24 setLoading(planName);25 try {26 const result = await createCheckoutSession(priceId);27 if (result.error) {28 console.error("Upgrade error:", result.error);29 return;30 }31 if (result.url) {32 window.location.href = result.url;33 }34 } catch (error) {35 console.error("Upgrade error:", error);36 } finally {37 setLoading(null);38 }39 };4041 const handleBillingPortal = async () => {42 setLoading("portal");43 try {44 const result = await createBillingPortalSession();45 if (result.error) {46 console.error("Portal error:", result.error);47 return;48 }49 if (result.url) {50 window.open(result.url, "_blank");51 }52 } catch (error) {53 console.error("Portal error:", error);54 } finally {55 setLoading(null);56 }57 };5859 return (60 <div className="space-y-6">61 {/* Current Plan Status */}62 <div className="flex items-center justify-between">63 <div>64 <h3 className="font-medium">Subscription Actions</h3>65 <p className="text-sm text-muted-foreground">66 Manage your subscription and billing67 </p>68 </div>69 {isActive && (70 <Badge variant={isCanceling ? "destructive" : "default"}>71 {isCanceling ? "Ending" : "Active"}72 </Badge>73 )}74 </div>7576 {/* Action Buttons */}77 <div className="grid gap-3 md:grid-cols-2">78 {/* Billing Portal */}79 {isActive && (80 <Button81 variant="outline"82 onClick={handleBillingPortal}83 disabled={loading === "portal"}84 className="flex items-center gap-2"85 >86 {loading === "portal" ? (87 <Loader2 className="h-4 w-4 animate-spin" />88 ) : (89 <ExternalLink className="h-4 w-4" />90 )}91 Manage Billing92 </Button>93 )}9495 {/* Upgrade Options */}96 {currentType === "FREE" && (97 <>98 <Button99 onClick={() => handleUpgrade(100 process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY!,101 "Monthly"102 )}103 disabled={loading === "Monthly"}104 >105 {loading === "Monthly" ? (106 <>107 <Loader2 className="mr-2 h-4 w-4 animate-spin" />108 Processing...109 </>110 ) : (111 "Upgrade to Pro Monthly"112 )}113 </Button>114 <Button115 variant="outline"116 onClick={() => handleUpgrade(117 process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY!,118 "Yearly"119 )}120 disabled={loading === "Yearly"}121 >122 {loading === "Yearly" ? (123 <>124 <Loader2 className="mr-2 h-4 w-4 animate-spin" />125 Processing...126 </>127 ) : (128 "Upgrade to Pro Yearly (Save 17%)"129 )}130 </Button>131 </>132 )}133134 {/* Plan Change Options */}135 {isActive && currentType === "MONTHLY" && (136 <Button137 variant="outline"138 onClick={() => handleUpgrade(139 process.env.NEXT_PUBLIC_STRIPE_PRICE_YEARLY!,140 "Switch to Yearly"141 )}142 disabled={loading === "Switch to Yearly"}143 >144 {loading === "Switch to Yearly" ? (145 <>146 <Loader2 className="mr-2 h-4 w-4 animate-spin" />147 Processing...148 </>149 ) : (150 "Switch to Yearly (Save 17%)"151 )}152 </Button>153 )}154155 {isActive && currentType === "YEARLY" && (156 <Button157 variant="outline"158 onClick={() => handleUpgrade(159 process.env.NEXT_PUBLIC_STRIPE_PRICE_MONTHLY!,160 "Switch to Monthly"161 )}162 disabled={loading === "Switch to Monthly"}163 >164 {loading === "Switch to Monthly" ? (165 <>166 <Loader2 className="mr-2 h-4 w-4 animate-spin" />167 Processing...168 </>169 ) : (170 "Switch to Monthly"171 )}172 </Button>173 )}174 </div>175176 {/* Plan Comparison */}177 {currentType === "FREE" && (178 <div className="rounded-lg border p-4 space-y-3">179 <h4 className="font-medium">Upgrade Benefits</h4>180 <div className="grid gap-2 text-sm">181 <div className="flex items-center gap-2">182 <span className="text-green-600">ā</span>183 <span>Unlimited projects</span>184 </div>185 <div className="flex items-center gap-2">186 <span className="text-green-600">ā</span>187 <span>Advanced analytics</span>188 </div>189 <div className="flex items-center gap-2">190 <span className="text-green-600">ā</span>191 <span>Priority support</span>192 </div>193 <div className="flex items-center gap-2">194 <span className="text-green-600">ā</span>195 <span>Team collaboration</span>196 </div>197 </div>198 </div>199 )}200201 {/* Cancellation Notice */}202 {isCanceling && (203 <div className="rounded-lg border border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950 p-4">204 <h4 className="font-medium text-yellow-800 dark:text-yellow-200 mb-2">205 Subscription Ending206 </h4>207 <p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">208 Your subscription will remain active until{" "}209 {subscription?.stripeCurrentPeriodEnd210 ? new Date(subscription.stripeCurrentPeriodEnd).toLocaleDateString()211 : "the end of your billing period"212 }.213 </p>214 <Button215 variant="outline"216 size="sm"217 onClick={handleBillingPortal}218 disabled={loading === "portal"}219 >220 Reactivate Subscription221 </Button>222 </div>223 )}224 </div>225 );226}
Billing Portal Integration
Server action for Stripe billing portal:
Billing portal server action
1"use server";23import { auth } from "~/auth";4import { stripe } from "~/server/lib/stripe";5import { db } from "~/server/db";6import { env } from "~/env";78export async function createBillingPortalSession() {9 const session = await auth();1011 if (!session?.user?.id) {12 return { error: "Not authenticated" };13 }1415 try {16 // Get user's subscription with Stripe customer ID17 const subscription = await db.subscription.findUnique({18 where: { userId: session.user.id },19 select: { stripeCustomerId: true },20 });2122 if (!subscription?.stripeCustomerId) {23 return { error: "No billing information found" };24 }2526 // Create billing portal session27 const portalSession = await stripe.billingPortal.sessions.create({28 customer: subscription.stripeCustomerId,29 return_url: `${env.NEXTAUTH_URL}/account`,30 });3132 return { url: portalSession.url };33 } catch (error) {34 console.error("Billing portal error:", error);35 return { error: "Failed to create billing portal session" };36 }37}
Profile Management
Profile Edit Form
Allow users to update their profile information:
Profile management
1"use client";23import { useState } from "react";4import { Button } from "~/components/ui/button";5import { Input } from "~/components/ui/input";6import { Label } from "~/components/ui/label";7import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";8import { updateProfile } from "~/server/actions/profile";9import { useRouter } from "next/navigation";1011interface ProfileFormProps {12 user: {13 id: string;14 name?: string | null;15 email?: string | null;16 image?: string | null;17 };18}1920export function ProfileForm({ user }: ProfileFormProps) {21 const [loading, setLoading] = useState(false);22 const [name, setName] = useState(user.name ?? "");23 const router = useRouter();2425 const handleSubmit = async (e: React.FormEvent) => {26 e.preventDefault();27 setLoading(true);2829 try {30 const result = await updateProfile({ name });3132 if (result.error) {33 console.error("Profile update error:", result.error);34 return;35 }3637 // Refresh the page to show updated data38 router.refresh();39 } catch (error) {40 console.error("Profile update error:", error);41 } finally {42 setLoading(false);43 }44 };4546 return (47 <Card>48 <CardHeader>49 <CardTitle>Edit Profile</CardTitle>50 </CardHeader>51 <CardContent>52 <form onSubmit={handleSubmit} className="space-y-4">53 <div className="space-y-2">54 <Label htmlFor="name">Name</Label>55 <Input56 id="name"57 type="text"58 value={name}59 onChange={(e) => setName(e.target.value)}60 placeholder="Enter your name"61 />62 </div>6364 <div className="space-y-2">65 <Label htmlFor="email">Email</Label>66 <Input67 id="email"68 type="email"69 value={user.email ?? ""}70 disabled71 className="bg-muted"72 />73 <p className="text-xs text-muted-foreground">74 Email cannot be changed. Contact support if needed.75 </p>76 </div>7778 <div className="flex gap-3">79 <Button type="submit" disabled={loading}>80 {loading ? "Saving..." : "Save Changes"}81 </Button>82 <Button type="button" variant="outline">83 Cancel84 </Button>85 </div>86 </form>87 </CardContent>88 </Card>89 );90}9192// Server action for profile updates93export async function updateProfile(data: { name: string }) {94 const session = await auth();9596 if (!session?.user?.id) {97 return { error: "Not authenticated" };98 }99100 try {101 await db.user.update({102 where: { id: session.user.id },103 data: { name: data.name.trim() || null },104 });105106 return { success: true };107 } catch (error) {108 console.error("Profile update error:", error);109 return { error: "Failed to update profile" };110 }111}
š” Account Management Best Practices
User Experience
- ⢠Clear subscription status indicators
- ⢠Easy access to billing portal
- ⢠Transparent usage tracking
- ⢠Helpful error messages
- ⢠Confirmation for destructive actions
Technical
- ⢠Validate data server-side
- ⢠Handle loading states properly
- ⢠Use secure session management
- ⢠Implement proper error handling
- ⢠Follow GDPR compliance
Account Management Complete!
Your account management system provides comprehensive user control. Next, learn about admin dashboard components.