add member onboarding
This commit is contained in:
parent
cffc74caa4
commit
d3d2d5069e
|
@ -50,7 +50,7 @@ export default function InviteModal({ onClose }: Props) {
|
|||
onSettled: () => {
|
||||
signIn("invite", {
|
||||
email: form.email,
|
||||
callbackUrl: "/",
|
||||
callbackUrl: "/member-onboarding",
|
||||
redirect: false,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -23,7 +23,10 @@ export default function AuthRedirect({ children }: Props) {
|
|||
const isUnauthenticated = status === "unauthenticated";
|
||||
const isPublicPage = router.pathname.startsWith("/public");
|
||||
const hasInactiveSubscription =
|
||||
user.id && !user.subscription?.active && stripeEnabled;
|
||||
user.id &&
|
||||
!user.subscription?.active &&
|
||||
!user.parentSubscription?.active &&
|
||||
stripeEnabled;
|
||||
|
||||
// There are better ways of doing this... but this one works for now
|
||||
const routes = [
|
||||
|
@ -50,6 +53,8 @@ export default function AuthRedirect({ children }: Props) {
|
|||
} else {
|
||||
if (isLoggedIn && hasInactiveSubscription) {
|
||||
redirectTo("/subscribe");
|
||||
} else if (isLoggedIn && !user.name && user.parentSubscriptionId) {
|
||||
redirectTo("/member-onboarding");
|
||||
} else if (
|
||||
isLoggedIn &&
|
||||
!routes.some((e) => router.pathname.startsWith(e.path) && e.isProtected)
|
||||
|
|
|
@ -12,6 +12,11 @@ export default async function getUserById(userId: number) {
|
|||
},
|
||||
},
|
||||
subscriptions: true,
|
||||
parentSubscription: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -22,7 +27,8 @@ export default async function getUserById(userId: number) {
|
|||
(usernames) => usernames.username
|
||||
);
|
||||
|
||||
const { password, subscriptions, ...lessSensitiveInfo } = user;
|
||||
const { password, subscriptions, parentSubscription, ...lessSensitiveInfo } =
|
||||
user;
|
||||
|
||||
const data = {
|
||||
...lessSensitiveInfo,
|
||||
|
@ -30,6 +36,12 @@ export default async function getUserById(userId: number) {
|
|||
subscription: {
|
||||
active: subscriptions?.active,
|
||||
},
|
||||
parentSubscription: {
|
||||
active: parentSubscription?.active,
|
||||
user: {
|
||||
email: parentSubscription?.user.email,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { response: data, status: 200 };
|
||||
|
|
|
@ -101,7 +101,6 @@ export default async function updateUserById(
|
|||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { email: true, password: true, name: true },
|
||||
});
|
||||
|
||||
if (user && user.email && data.email && data.email !== user.email) {
|
||||
|
@ -170,8 +169,20 @@ export default async function updateUserById(
|
|||
|
||||
// Other settings / Apply changes
|
||||
|
||||
const isInvited =
|
||||
user?.name === null && user.parentSubscriptionId && !user.password;
|
||||
|
||||
if (isInvited && data.password === "")
|
||||
return {
|
||||
response: "Password is required.",
|
||||
status: 400,
|
||||
};
|
||||
|
||||
const saltRounds = 10;
|
||||
const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds);
|
||||
const newHashedPassword = bcrypt.hashSync(
|
||||
data.newPassword || data.password || "",
|
||||
saltRounds
|
||||
);
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
|
@ -198,18 +209,28 @@ export default async function updateUserById(
|
|||
linksRouteTo: data.linksRouteTo,
|
||||
preventDuplicateLinks: data.preventDuplicateLinks,
|
||||
password:
|
||||
data.newPassword && data.newPassword !== ""
|
||||
isInvited || (data.newPassword && data.newPassword !== "")
|
||||
? newHashedPassword
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
whitelistedUsers: true,
|
||||
subscriptions: true,
|
||||
parentSubscription: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { whitelistedUsers, password, subscriptions, ...userInfo } =
|
||||
updatedUser;
|
||||
const {
|
||||
whitelistedUsers,
|
||||
password,
|
||||
subscriptions,
|
||||
parentSubscription,
|
||||
...userInfo
|
||||
} = updatedUser;
|
||||
|
||||
// If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed
|
||||
const newWhitelistedUsernames: string[] = data.whitelistedUsers || [];
|
||||
|
@ -250,11 +271,19 @@ export default async function updateUserById(
|
|||
});
|
||||
}
|
||||
|
||||
const response: Omit<AccountSettings, "password"> = {
|
||||
const response = {
|
||||
...userInfo,
|
||||
whitelistedUsers: newWhitelistedUsernames,
|
||||
image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
|
||||
subscription: { active: subscriptions?.active },
|
||||
subscription: {
|
||||
active: subscriptions?.active,
|
||||
},
|
||||
parentSubscription: {
|
||||
active: parentSubscription?.active,
|
||||
user: {
|
||||
email: parentSubscription?.user.email,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { response, status: 200 };
|
||||
|
|
|
@ -17,6 +17,7 @@ export default async function paymentCheckout(
|
|||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
parentSubscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -4,17 +4,22 @@ import checkSubscriptionByEmail from "./checkSubscriptionByEmail";
|
|||
|
||||
interface UserIncludingSubscription extends User {
|
||||
subscriptions: Subscription | null;
|
||||
parentSubscription: Subscription | null;
|
||||
}
|
||||
|
||||
export default async function verifySubscription(
|
||||
user?: UserIncludingSubscription | null
|
||||
) {
|
||||
if (!user || !user.subscriptions) {
|
||||
if (!user || (!user.subscriptions && !user.parentSubscription)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.parentSubscription?.active) {
|
||||
return user;
|
||||
}
|
||||
|
||||
if (
|
||||
!user.subscriptions.active ||
|
||||
!user.subscriptions?.active ||
|
||||
new Date() > user.subscriptions.currentPeriodEnd
|
||||
) {
|
||||
const subscription = await checkSubscriptionByEmail(user.email as string);
|
||||
|
|
|
@ -33,6 +33,7 @@ export default async function verifyByCredentials({
|
|||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
parentSubscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ export default async function verifyUser({
|
|||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
parentSubscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1393,6 +1393,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
parentSubscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
include: {
|
||||
subscriptions: true,
|
||||
parentSubscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
import Button from "@/components/ui/Button";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import CenteredForm from "@/layouts/CenteredForm";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
interface FormData {
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function MemberOnboarding() {
|
||||
const { t } = useTranslation();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [form, setForm] = useState<FormData>({
|
||||
password: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
const { data: user = {} } = useUser();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const { status } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
toast.success(t("accepted_invitation_please_fill"));
|
||||
}, []);
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
if (form.password !== "" && form.name !== "" && !submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("sending_password_recovery_link"));
|
||||
|
||||
await updateUser.mutateAsync(
|
||||
{
|
||||
...user,
|
||||
name: form.name,
|
||||
password: form.password,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
onSettled: (data, error) => {
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.success(t("settings_applied"));
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setSubmitLoader(false);
|
||||
} else {
|
||||
toast.error(t("please_fill_all_fields"));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredForm>
|
||||
<form onSubmit={submit}>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
{t("finalize_profile")}
|
||||
</p>
|
||||
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<p
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
}}
|
||||
>
|
||||
{t("invitation_desc", {
|
||||
owner: user?.parentSubscription?.user?.email,
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p className="text-sm w-fit font-semibold mb-1">
|
||||
{t("display_name")}
|
||||
</p>
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder="John Doe"
|
||||
value={form.name}
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm w-fit font-semibold mb-1">
|
||||
{t("new_password")}
|
||||
</p>
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{process.env.NEXT_PUBLIC_STRIPE && (
|
||||
<div className="text-xs text-neutral mb-3">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="sign_up_agreement"
|
||||
components={[
|
||||
<Link
|
||||
href="https://linkwarden.app/tos"
|
||||
className="font-semibold"
|
||||
data-testid="terms-of-service-link"
|
||||
key={0}
|
||||
/>,
|
||||
<Link
|
||||
href="https://linkwarden.app/privacy-policy"
|
||||
className="font-semibold"
|
||||
data-testid="privacy-policy-link"
|
||||
key={1}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
intent="accent"
|
||||
className="mt-2"
|
||||
size="full"
|
||||
loading={submitLoader}
|
||||
>
|
||||
{t("sign_up")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CenteredForm>
|
||||
);
|
||||
}
|
||||
|
||||
export { getServerSideProps };
|
|
@ -23,13 +23,13 @@ export default function Subscribe() {
|
|||
const { data: user = {} } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("user", user);
|
||||
if (
|
||||
session.status === "authenticated" &&
|
||||
user.id &&
|
||||
user?.subscription?.active
|
||||
) {
|
||||
(user?.subscription?.active || user.parentSubscription?.active)
|
||||
)
|
||||
router.push("/dashboard");
|
||||
}
|
||||
}, [session.status, user]);
|
||||
|
||||
async function submit() {
|
||||
|
|
|
@ -403,5 +403,8 @@
|
|||
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
|
||||
"invite_user_note": "Please note that once the invitation is accepted, an additional seat will be used and your account will automatically be billed for this addition.",
|
||||
"send_invitation": "Send Invitation",
|
||||
"learn_more": "Learn more"
|
||||
"learn_more": "Learn more",
|
||||
"finalize_profile": "Finalize Your Profile",
|
||||
"invitation_desc": "{{owner}} invited you to join Linkwarden. \nTo continue, please finish setting up your account.",
|
||||
"accepted_invitation_please_fill": "You've accepted the invitation to join Linkwarden. Please fill out the following to finalize your account."
|
||||
}
|
Ŝarĝante…
Reference in New Issue