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";
10
11export default async function AccountPage() {
12 const session = await auth();
13
14 if (!session?.user) {
15 redirect("/auth/signin");
16 }
17
18 // Fetch user data and subscription
19 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 ]);
35
36 const isActive = subscription?.status === "active";
37 const isCanceling = subscription?.cancelAtPeriodEnd;
38 const currentPlan = SUBSCRIPTION_PLANS[subscription?.type ?? "FREE"];
39
40 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 preferences
48 </p>
49 </div>
50
51 {/* Account Messages */}
52 <AccountMessages />
53
54 {/* 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 <img
63 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 āœ“ Verified
81 </Badge>
82 ) : (
83 <Badge variant="destructive" className="text-xs">
84 Unverified
85 </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>
93
94 <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>
100
101 {/* 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 <div
132 className="bg-primary h-2 rounded-full"
133 style={{
134 width: `${Math.min(
135 ((subscription?.usageCount ?? 0) / (subscription?.usageLimit || 1)) * 100,
136 100
137 )}%`
138 }}
139 />
140 </div>
141 </div>
142 </div>
143 </div>
144
145 {/* 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 Ending
155 </p>
156 <p className="text-sm text-yellow-700 dark:text-yellow-300">
157 Your subscription will end on{" "}
158 {subscription?.stripeCurrentPeriodEnd
159 ? 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 Subscription
173 </p>
174 <p className="text-sm text-green-700 dark:text-green-300">
175 Next billing: {subscription?.stripeCurrentPeriodEnd
176 ? new Date(subscription.stripeCurrentPeriodEnd).toLocaleDateString()
177 : "Unknown"
178 }
179 </p>
180 </div>
181 </div>
182 </div>
183 )}
184 </div>
185 )}
186
187 {/* Subscription Management */}
188 <SubscriptionManagement subscription={subscription} />
189 </CardContent>
190 </Card>
191
192 {/* 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 invoices
201 </p>
202 <Button variant="outline">
203 View Billing Portal
204 </Button>
205 </div>
206 </CardContent>
207 </Card>
208
209 {/* 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 Account
224 </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";
2
3import { useSearchParams } from "next/navigation";
4import { Alert, AlertDescription } from "~/components/ui/alert";
5import { CheckCircle, XCircle, Info } from "lucide-react";
6
7export 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");
12
13 // Success messages
14 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 }
24
25 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 }
35
36 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&apos;ll continue to have access until the end of your billing period.
42 </AlertDescription>
43 </Alert>
44 );
45 }
46
47 // Error messages
48 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 }
58
59 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 }
69
70 return null;
71}

Subscription Management

SubscriptionManagement Component

Handle subscription changes, upgrades, and cancellations:

src/components/SubscriptionManagement.tsx
1"use client";
2
3import { 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";
11
12interface SubscriptionManagementProps {
13 subscription: Subscription | null;
14}
15
16export function SubscriptionManagement({ subscription }: SubscriptionManagementProps) {
17 const [loading, setLoading] = useState<string | null>(null);
18
19 const isActive = subscription?.status === "active";
20 const isCanceling = subscription?.cancelAtPeriodEnd;
21 const currentType = subscription?.type ?? "FREE";
22
23 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 };
40
41 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 };
58
59 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 billing
67 </p>
68 </div>
69 {isActive && (
70 <Badge variant={isCanceling ? "destructive" : "default"}>
71 {isCanceling ? "Ending" : "Active"}
72 </Badge>
73 )}
74 </div>
75
76 {/* Action Buttons */}
77 <div className="grid gap-3 md:grid-cols-2">
78 {/* Billing Portal */}
79 {isActive && (
80 <Button
81 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 Billing
92 </Button>
93 )}
94
95 {/* Upgrade Options */}
96 {currentType === "FREE" && (
97 <>
98 <Button
99 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 <Button
115 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 )}
133
134 {/* Plan Change Options */}
135 {isActive && currentType === "MONTHLY" && (
136 <Button
137 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 )}
154
155 {isActive && currentType === "YEARLY" && (
156 <Button
157 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>
175
176 {/* 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 )}
200
201 {/* 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 Ending
206 </h4>
207 <p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
208 Your subscription will remain active until{" "}
209 {subscription?.stripeCurrentPeriodEnd
210 ? new Date(subscription.stripeCurrentPeriodEnd).toLocaleDateString()
211 : "the end of your billing period"
212 }.
213 </p>
214 <Button
215 variant="outline"
216 size="sm"
217 onClick={handleBillingPortal}
218 disabled={loading === "portal"}
219 >
220 Reactivate Subscription
221 </Button>
222 </div>
223 )}
224 </div>
225 );
226}
Billing Portal Integration

Server action for Stripe billing portal:

Billing portal server action
1"use server";
2
3import { auth } from "~/auth";
4import { stripe } from "~/server/lib/stripe";
5import { db } from "~/server/db";
6import { env } from "~/env";
7
8export async function createBillingPortalSession() {
9 const session = await auth();
10
11 if (!session?.user?.id) {
12 return { error: "Not authenticated" };
13 }
14
15 try {
16 // Get user's subscription with Stripe customer ID
17 const subscription = await db.subscription.findUnique({
18 where: { userId: session.user.id },
19 select: { stripeCustomerId: true },
20 });
21
22 if (!subscription?.stripeCustomerId) {
23 return { error: "No billing information found" };
24 }
25
26 // Create billing portal session
27 const portalSession = await stripe.billingPortal.sessions.create({
28 customer: subscription.stripeCustomerId,
29 return_url: `${env.NEXTAUTH_URL}/account`,
30 });
31
32 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";
2
3import { 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";
10
11interface ProfileFormProps {
12 user: {
13 id: string;
14 name?: string | null;
15 email?: string | null;
16 image?: string | null;
17 };
18}
19
20export function ProfileForm({ user }: ProfileFormProps) {
21 const [loading, setLoading] = useState(false);
22 const [name, setName] = useState(user.name ?? "");
23 const router = useRouter();
24
25 const handleSubmit = async (e: React.FormEvent) => {
26 e.preventDefault();
27 setLoading(true);
28
29 try {
30 const result = await updateProfile({ name });
31
32 if (result.error) {
33 console.error("Profile update error:", result.error);
34 return;
35 }
36
37 // Refresh the page to show updated data
38 router.refresh();
39 } catch (error) {
40 console.error("Profile update error:", error);
41 } finally {
42 setLoading(false);
43 }
44 };
45
46 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 <Input
56 id="name"
57 type="text"
58 value={name}
59 onChange={(e) => setName(e.target.value)}
60 placeholder="Enter your name"
61 />
62 </div>
63
64 <div className="space-y-2">
65 <Label htmlFor="email">Email</Label>
66 <Input
67 id="email"
68 type="email"
69 value={user.email ?? ""}
70 disabled
71 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>
77
78 <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 Cancel
84 </Button>
85 </div>
86 </form>
87 </CardContent>
88 </Card>
89 );
90}
91
92// Server action for profile updates
93export async function updateProfile(data: { name: string }) {
94 const session = await auth();
95
96 if (!session?.user?.id) {
97 return { error: "Not authenticated" };
98 }
99
100 try {
101 await db.user.update({
102 where: { id: session.user.id },
103 data: { name: data.name.trim() || null },
104 });
105
106 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.