-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Allow modifying user email from settings #282
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"namesake": minor | ||
--- | ||
|
||
Added email display in account settings page. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,3 +42,5 @@ node_modules | |
|
||
# Local files | ||
*.local | ||
|
||
.env.local | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,8 +25,8 @@ import type * as seed from "../seed.js"; | |
import type * as topics from "../topics.js"; | ||
import type * as userFormData from "../userFormData.js"; | ||
import type * as userQuests from "../userQuests.js"; | ||
import type * as userSettings from "../userSettings.js"; | ||
import type * as users from "../users.js"; | ||
import type * as userSettings from "../userSettings.js"; | ||
import type * as validators from "../validators.js"; | ||
|
||
/** | ||
|
@@ -50,8 +50,8 @@ declare const fullApi: ApiFromModules<{ | |
topics: typeof topics; | ||
userFormData: typeof userFormData; | ||
userQuests: typeof userQuests; | ||
userSettings: typeof userSettings; | ||
users: typeof users; | ||
userSettings: typeof userSettings; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did the linter rearrange these? 🤔 |
||
validators: typeof validators; | ||
}>; | ||
export declare const api: FilterApi< | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,13 @@ export const setName = userMutation({ | |
}, | ||
}); | ||
|
||
export const setEmail = userMutation({ | ||
args: { email: v.optional(v.string()) }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should check to make sure the email address is valid here, and throw an error if an empty string or invalid email are submitted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const ParamsSchema = z.object({ Should I implement this from Convex Auth docs with zod library or implement a regex? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be great to add Zod so we can use it to validate other functions, too! Here's a helpful guide: https://stack.convex.dev/typescript-zod-function-validation We already have Convex helpers installed, but you'll need to install zod. |
||
handler: async (ctx, args) => { | ||
await ctx.db.patch(ctx.userId, { email: args.email }); | ||
}, | ||
}); | ||
Comment on lines
+53
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to add a verification flow for changing emails and during signup. We can address that separately from this PR, but I've created a ticket: #286 |
||
|
||
export const setResidence = userMutation({ | ||
args: { residence: jurisdiction }, | ||
handler: async (ctx, args) => { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,112 @@ | ||||||||||
import { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll want to write a unit test for this component! I haven't written many tests for settings components yet, but now's a good time to start. You can see a test for a similar modal-style form submission component in EditQuestTimeRequiredModal and EditQuestCostsModal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR adds tests for other settings components and is a great place to look! #290 |
||||||||||
Banner, | ||||||||||
Button, | ||||||||||
Form, | ||||||||||
Modal, | ||||||||||
ModalFooter, | ||||||||||
ModalHeader, | ||||||||||
TextField, | ||||||||||
} from "@/components/common"; | ||||||||||
import { api } from "@convex/_generated/api"; | ||||||||||
import type { Doc } from "@convex/_generated/dataModel"; | ||||||||||
import { useMutation } from "convex/react"; | ||||||||||
import { Pencil } from "lucide-react"; | ||||||||||
import { useState } from "react"; | ||||||||||
import { toast } from "sonner"; | ||||||||||
import { SettingsItem } from "../SettingsItem"; | ||||||||||
|
||||||||||
type EditEmailModalProps = { | ||||||||||
defaultEmail: string; | ||||||||||
isOpen: boolean; | ||||||||||
onOpenChange: (isOpen: boolean) => void; | ||||||||||
onSubmit: () => void; | ||||||||||
}; | ||||||||||
|
||||||||||
const EditEmailModal = ({ | ||||||||||
defaultEmail, | ||||||||||
isOpen, | ||||||||||
onOpenChange, | ||||||||||
onSubmit, | ||||||||||
}: EditEmailModalProps) => { | ||||||||||
const updateEmail = useMutation(api.users.setEmail); | ||||||||||
const [email, setEmail] = useState(defaultEmail); | ||||||||||
const [error, setError] = useState<string>(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nit! |
||||||||||
const [isSubmitting, setIsSubmitting] = useState(false); | ||||||||||
|
||||||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||||||||||
e.preventDefault(); | ||||||||||
setError(undefined); | ||||||||||
|
||||||||||
try { | ||||||||||
setIsSubmitting(true); | ||||||||||
await updateEmail({ email: email.trim() }); | ||||||||||
onSubmit(); | ||||||||||
toast.success("Email updated."); | ||||||||||
} catch (err) { | ||||||||||
setError("Failed to update email. Please try again."); | ||||||||||
} finally { | ||||||||||
setIsSubmitting(false); | ||||||||||
} | ||||||||||
}; | ||||||||||
|
||||||||||
return ( | ||||||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> | ||||||||||
<ModalHeader | ||||||||||
title="Email address" | ||||||||||
description="This is the email we’ll use to contact you." | ||||||||||
Comment on lines
+55
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
/> | ||||||||||
<Form onSubmit={handleSubmit} className="w-full"> | ||||||||||
{error && <Banner variant="danger">{error}</Banner>} | ||||||||||
<TextField | ||||||||||
label="Email" | ||||||||||
name="email" | ||||||||||
type="email" | ||||||||||
value={email} | ||||||||||
onChange={(value) => { | ||||||||||
setEmail(value); | ||||||||||
setError(undefined); | ||||||||||
}} | ||||||||||
className="w-full" | ||||||||||
isRequired | ||||||||||
/> | ||||||||||
<ModalFooter> | ||||||||||
<Button | ||||||||||
variant="secondary" | ||||||||||
isDisabled={isSubmitting} | ||||||||||
onPress={() => onOpenChange(false)} | ||||||||||
> | ||||||||||
Cancel | ||||||||||
</Button> | ||||||||||
<Button type="submit" variant="primary" isDisabled={isSubmitting}> | ||||||||||
Save | ||||||||||
</Button> | ||||||||||
</ModalFooter> | ||||||||||
</Form> | ||||||||||
</Modal> | ||||||||||
); | ||||||||||
}; | ||||||||||
|
||||||||||
type EditEmailSettingProps = { | ||||||||||
user: Doc<"users">; | ||||||||||
}; | ||||||||||
|
||||||||||
export const EditEmailSetting = ({ user }: EditEmailSettingProps) => { | ||||||||||
const [isEmailModalOpen, setIsEmailModalOpen] = useState(false); | ||||||||||
|
||||||||||
return ( | ||||||||||
<SettingsItem | ||||||||||
label="Email address" | ||||||||||
description="This is the email we’ll use to contact you." | ||||||||||
> | ||||||||||
<Button icon={Pencil} onPress={() => setIsEmailModalOpen(true)}> | ||||||||||
{user?.email ?? "Set email"} | ||||||||||
</Button> | ||||||||||
<EditEmailModal | ||||||||||
isOpen={isEmailModalOpen} | ||||||||||
onOpenChange={setIsEmailModalOpen} | ||||||||||
defaultEmail={user.email ?? ""} | ||||||||||
onSubmit={() => setIsEmailModalOpen(false)} | ||||||||||
/> | ||||||||||
</SettingsItem> | ||||||||||
); | ||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./EditEmailSetting"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file can be renamed to |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,8 +63,9 @@ const EditNameModal = ({ | |
<Form onSubmit={handleSubmit} className="w-full"> | ||
{error && <Banner variant="danger">{error}</Banner>} | ||
<TextField | ||
name="name" | ||
label="Name" | ||
name="name" | ||
type="text" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason for adding this? |
||
value={name} | ||
onChange={(value) => { | ||
setName(value); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.env.local
is already in.gitignore
, so you can leave the file as-is!