Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eight-yaks-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Added email display in account settings page.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ node_modules

# Local files
*.local

.env.local
Copy link
Member

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!

4 changes: 2 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The 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<
Expand Down
7 changes: 7 additions & 0 deletions convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export const setName = userMutation({
},
});

export const setEmail = userMutation({
args: { email: v.optional(v.string()) },
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update users.test.ts with a test for this new mutation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const ParamsSchema = z.object({
email: z.string().email(),
});

Should I implement this from Convex Auth docs with zod library or implement a regex?

Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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) => {
Expand Down
112 changes: 112 additions & 0 deletions src/components/settings/EditEmailSetting/EditEmailSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [error, setError] = useState<string>();
const [error, setError] = useState('');

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title="Email address"
description="This is the email we’ll use to contact you."
title="Edit email address"
description="What email would you like to use for Namesake?"

/>
<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>
);
};
1 change: 1 addition & 0 deletions src/components/settings/EditEmailSetting/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./EditEmailSetting";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file can be renamed to index.ts (no x)

3 changes: 2 additions & 1 deletion src/components/settings/EditNameSetting/EditNameSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for adding this?

value={name}
onChange={(value) => {
setName(value);
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./DeleteAccountSetting";
export * from "./EditBirthplaceSetting";
export * from "./EditNameSetting";
export * from "./EditEmailSetting";
export * from "./EditMinorSetting";
export * from "./EditResidenceSetting";
export * from "./EditThemeSetting";
Expand Down
2 changes: 2 additions & 0 deletions src/routes/_authenticated/settings/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PageHeader } from "@/components/app";
import {
DeleteAccountSetting,
EditBirthplaceSetting,
EditEmailSetting,
EditMinorSetting,
EditNameSetting,
EditResidenceSetting,
Expand Down Expand Up @@ -30,6 +31,7 @@ function SettingsAccountRoute() {
<>
<SettingsGroup title="Personal Information">
<EditNameSetting user={user} />
<EditEmailSetting user={user} />
<EditMinorSetting user={user} />
<EditResidenceSetting user={user} />
<EditBirthplaceSetting user={user} />
Expand Down
Loading