Settings
Account settings modal over the workspace. Opened from the gear, account flyout Settings / Get help, or /dashboard?settings={tab} (also recognized on /my-courses?settings=). buildSettingsHref always points at /dashboard?settings=…. After open, the query is stripped. Legacy /settings, /settings/profile, /settings/security, /settings/notifications, /settings/billing, /settings/help, /settings/danger redirect here. /settings/appearance and /settings/language redirect to profile.
PAGE: /dashboard?settings={tab}
Purpose: Every student account setting (identity, notifications, billing, security, delete, help).
Access: logged-in + onboarded workspace. GET /api/workspace/settings returns 401 if anonymous.
Entry points: Sidebar/header gear; flyout Settings; flyout Get help (help tab); security Manage account deletion →; marketing header gear → /dashboard?settings=profile.
SECTION: Modal chrome
Copy: Dialog title (sr-only) Settings. Nav aria-label Settings sections. Load skeleton aria-label Loading settings. Error Could not load settings. Please try again. Billing placeholder Loading settings...
Data shown: GET /api/workspace/settings → { settings, billing }
ELEMENT: Profile
Action: toggle
Destination: profile pane (settings=profile)
Writes to: none
Reads from: none
ELEMENT: Notifications Action: toggle Destination: notifications pane Writes to: none Reads from: none
ELEMENT: Plan & billing Action: toggle Destination: billing pane Writes to: none Reads from: none
ELEMENT: Security Action: toggle Destination: security pane Writes to: none Reads from: none
ELEMENT: Danger zone Action: toggle Destination: danger pane Writes to: none Reads from: none
ELEMENT: Help & contact Action: toggle Destination: help pane Writes to: none Reads from: none
SECTION: Profile
Copy: Profile Correct name, university, and optional programs here. Paid access stays on the courses you bought — not these labels.
Data shown: profiles.*, user_badges.*
Where you study
Copy: Where you study University matches AUB or LAU course offerings. Changing it does not change courses you already bought.
Copy: Which university? AUB and LAU use different course codes. This picks the matching offering.
Campus (optional, LAU only): Campus (optional) Beirut or Byblos does not change which Gradeful courses you can open.
ELEMENT: AUB / LAU
Action: toggle
Destination: none
Writes to: profiles.university on Save changes
Reads from: profiles.university
ELEMENT: Beirut / Byblos (optional)
Action: toggle
Destination: none
Writes to: profiles.campus on Save changes
Reads from: profiles.campus
Programs (optional)
Copy: Programs (optional) Major and minors are for later recommendations. They are not required to open a course.
ELEMENT: Major / Second major / Minors (typeaheads, not required)
Writes to: profiles.major, second_major, minors on Save changes
You
Copy: You How Gradeful greets you in the workspace. Labels Profile photo Full name Nickname {n}/20 Used in your workspace greeting. Letters, numbers, and underscores only. Account email This is your Gradeful login and receipts. It is not overwritten by an AUB or LAU address. Institution email Optional, and separate from login. Checkout may ask for your AUB or LAU address as a payment contact.
Data shown: profiles.full_name, nickname, avatar_url; Auth email
ELEMENT: Change photo
Action: open modal
Destination: crop dialog Crop your photo Drag to reposition and use the slider to zoom. We save a square crop.
Writes to: none until Save photo
Reads from: profiles.avatar_url
ELEMENT: Zoom (range 1–3) Action: toggle Destination: none Writes to: none Reads from: none
ELEMENT: Cancel Action: toggle Destination: closes crop Writes to: none Reads from: none
ELEMENT: Save photo / Uploading...
Action: submit
Destination: none
Writes to: Storage avatars/{userId}/avatar.jpg; profiles.avatar_url; profiles.updated_at
Reads from: cropped blob
Success: Profile photo updated
Errors: Upload a JPG, PNG, or WebP image. Photo must be 5MB or smaller. You must be signed in to change your photo. Choose a photo to upload. Your photo could not be uploaded. Please try again. Your photo uploaded but could not be saved. Please try again.
ELEMENT: Full name
Action: toggle
Destination: none
Writes to: profiles.full_name on Save changes (max 100)
Reads from: profiles.full_name
ELEMENT: Nickname
Action: toggle
Destination: none
Writes to: profiles.nickname (2–20, [a-zA-Z0-9_]+)
Reads from: profiles.nickname
Errors: Enter a nickname. Use at least 2 characters. Use at most 20 characters. Use letters, numbers, and underscores only.
ELEMENT: Email (disabled)
Action: none
Destination: none
Writes to: none
Reads from: auth.users.email
ELEMENT: Contact support
Action: external link
Destination: https://wa.me/96179057533
Writes to: none
Reads from: none
ELEMENT: Save changes / Saving...
Action: submit
Destination: toast + refresh
Writes to: profiles.full_name, profiles.nickname, profiles.updated_at
Reads from: form
Success: Profile updated
Error: Something went wrong
Showing up
Copy: Showing up Quiet milestones for coming back. Not trophies for never missing a day.
Cards: One week Showed up seven days in a row; Two weeks A fortnight of showing up; One month A month of making this a habit; Two months Sixty days of quiet consistency; One hundred days A home you kept returning to. Earned subtitle formatted MMM D, YYYY.
Data shown: user_badges.badge_type, user_badges.earned_at
ELEMENT: none (display only)
Action: none
Destination: none
Writes to: none
Reads from: user_badges
SECTION: Notifications
Copy: Notifications Choose what you hear about from us. Study reminders stay quiet and optional.
Data shown: profiles.notification_preferences JSON. Defaults: access_granted true, new_course true, announcements false, study_reminders true. Key reservation_updates exists in JSON (default true) with no toggle.
ELEMENT: Access granted emails
Action: toggle
Destination: none (saves immediately)
Writes to: profiles.notification_preferences.access_granted, profiles.updated_at
Reads from: same
Description: Get an email when your course access is activated.
ELEMENT: New course available
Action: toggle
Destination: none
Writes to: profiles.notification_preferences.new_course
Reads from: same
Description: Hear when a new course pack is added for your university.
ELEMENT: Platform announcements
Action: toggle
Destination: none
Writes to: profiles.notification_preferences.announcements
Reads from: same
Description: Occasional updates about Gradeful features and changes.
ELEMENT: Study reminders
Action: toggle
Destination: none
Writes to: profiles.notification_preferences.study_reminders
Reads from: same
Description: A gentle evening nudge if you haven't opened a pack yet. Never a guilt trip.
Toasts: Preferences updated Something went wrong
SECTION: Plan & billing
Copy: Plans & access Your active courses and payment history. Change plan anytime. Active access Payment history columns Date Plan Amount Status
Data shown: Active cards from user_access (plan_type, course_ids, expires_at, statuses paid/confirmed/pending, not expired) joined to courses.code. History from Whish orders for the signed-in user (plan, amount, payment_status, created_at). Plan names Focus / Balance / Full Load / Crunch else Course access. Status from commercialOrderStatus(). Courses covered: {codes | Selected courses | No courses selected} Expires {date | Not set} badge Expiring soon if ≤14 days. Empty access No active access yet. Empty history No payment history yet. Amount uses orders.amount.
ELEMENT: Upgrade or change plan
Action: navigate
Destination: /pricing
Writes to: none
Reads from: none
ELEMENT: Browse plans →
Action: navigate
Destination: /pricing
Writes to: none
Reads from: visible when no active access
SECTION: Security
Copy: Security Manage how you sign in to Gradeful loading Loading sign-in methods…
Data shown: Supabase Auth identities and session (last_sign_in_at / updated_at as last active)
Change password (email identity only)
Copy: Change password labels Current password New password Confirm new password
ELEMENT: Update password / Updating...
Action: submit
Destination: none
Writes to: auth.users password (signInWithPassword then updateUser)
Reads from: email prop + form
Toasts: New password must be at least 8 characters New passwords do not match Current password is incorrect Something went wrong Password updated
Phone sign-in (phone identity)
Copy: Phone sign-in You sign in with your phone number ending in ••••{lastFour}.
Data shown: Auth phone identity
ELEMENT: none (display)
Copy: Google You sign in with Google. status Connected / Not connected check aria-label Connected
ELEMENT: Connect / Working...
Action: submit
Destination: /auth/callback?next=/dashboard?settings=security
Writes to: Auth identity (linkIdentity Google)
Reads from: identities
Toast: Google could not be connected
ELEMENT: Disconnect / Working...
Action: submit
Destination: none
Writes to: Auth (unlinkIdentity)
Reads from: blocked if only one identity
Toasts: Google could not be disconnected Add another sign-in method before disconnecting Google Google disconnected
Add password backup (Google-only)
Copy: Add password backup Add a password so you can still sign in if Google is unavailable.
ELEMENT: Password / Confirm password / Add backup / Saving...
Action: submit
Destination: none
Writes to: Auth updateUser({ password })
Reads from: none
Toasts: Password must be at least 8 characters Passwords do not match We could not add a password backup. Try again. Password backup added
Add email and password backup (phone-only)
Copy: Add email and password backup Add an email and password so you have another way back in. After send: We sent a confirmation link to {email}. Confirm it to finish adding your backup sign-in method.
ELEMENT: Email / Password / Confirm password / Add backup / Saving...
Action: submit
Destination: none
Writes to: Auth email+password
Reads from: none
Toasts: Enter a valid email address. We could not add email and password backup. Try again. Check your email to confirm the backup sign-in method. plus password match/length toasts above
This device
Copy: This device Last active {Just now | Recently | formatted datetime}
ELEMENT: none (display)
Reads from: Auth last_sign_in_at / updated_at
ELEMENT: Manage account deletion →
Action: toggle
Destination: Danger zone tab (fallback /dashboard?settings=danger)
Writes to: none
Reads from: none
SECTION: Danger zone
Copy: Danger zone Account actions that cannot be undone
Sign out
Copy: Sign out End your session on this device.
ELEMENT: Sign out / Signing out...
Action: submit
Destination: /
Writes to: clears Auth session
Reads from: none
Delete your account
Copy: Delete your account This permanently removes your profile, access records, and account data. This cannot be undone. Dialog Delete your account? This action is permanent. Type DELETE below to confirm. Label Confirmation placeholder DELETE
ELEMENT: Delete account Action: open modal Destination: confirm dialog Writes to: none Reads from: none
ELEMENT: Cancel Action: toggle Destination: closes dialog Writes to: none Reads from: none
ELEMENT: Permanently delete / Deleting...
Action: submit
Destination: / on success; /login if unauthorized
Writes to: deletes Auth user (auth.admin.deleteUser); related rows cascade
Reads from: confirmation must equal DELETE
Errors: You must be signed in to delete your account. We couldn't delete your account right now. Please contact support.
SECTION: Help & contact
Copy: Help & contact Reach out or browse common answers Chat with us Usually responds within a day Request a missing course We prioritize by demand
ELEMENT: Open WhatsApp
Action: external link
Destination: https://wa.me/96179057533
Writes to: none
Reads from: none
ELEMENT: Request a course
Action: navigate
Destination: /courses#course-request
Writes to: none
Reads from: none
ELEMENT: Privacy Policy
Action: navigate
Destination: /privacy
Writes to: none
Reads from: none
ELEMENT: Terms of Service
Action: navigate
Destination: /terms
Writes to: none
Reads from: none
ELEMENT: Refund Policy
Action: navigate
Destination: /refunds
Writes to: none
Reads from: none
ELEMENT: Academic Integrity
Action: navigate
Destination: /academic-integrity
Writes to: none
Reads from: none
Not in the modal (removed half-features)
Dark mode and extra languages are not shipped. The orphaned Appearance and Language panes were removed so settings only shows working tabs. /settings/appearance and /settings/language still redirect to profile. profiles.theme_preference and profiles.language_preference remain in the schema for a later full feature — do not expose “coming soon” UI until then.