add member onboarding
This commit is contained in:
parent
cffc74caa4
commit
d3d2d5069e
|
@ -50,7 +50,7 @@ export default function InviteModal({ onClose }: Props) {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
signIn("invite", {
|
signIn("invite", {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
callbackUrl: "/",
|
callbackUrl: "/member-onboarding",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,10 @@ export default function AuthRedirect({ children }: Props) {
|
||||||
const isUnauthenticated = status === "unauthenticated";
|
const isUnauthenticated = status === "unauthenticated";
|
||||||
const isPublicPage = router.pathname.startsWith("/public");
|
const isPublicPage = router.pathname.startsWith("/public");
|
||||||
const hasInactiveSubscription =
|
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
|
// There are better ways of doing this... but this one works for now
|
||||||
const routes = [
|
const routes = [
|
||||||
|
@ -50,6 +53,8 @@ export default function AuthRedirect({ children }: Props) {
|
||||||
} else {
|
} else {
|
||||||
if (isLoggedIn && hasInactiveSubscription) {
|
if (isLoggedIn && hasInactiveSubscription) {
|
||||||
redirectTo("/subscribe");
|
redirectTo("/subscribe");
|
||||||
|
} else if (isLoggedIn && !user.name && user.parentSubscriptionId) {
|
||||||
|
redirectTo("/member-onboarding");
|
||||||
} else if (
|
} else if (
|
||||||
isLoggedIn &&
|
isLoggedIn &&
|
||||||
!routes.some((e) => router.pathname.startsWith(e.path) && e.isProtected)
|
!routes.some((e) => router.pathname.startsWith(e.path) && e.isProtected)
|
||||||
|
|
|
@ -12,6 +12,11 @@ export default async function getUserById(userId: number) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: {
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,7 +27,8 @@ export default async function getUserById(userId: number) {
|
||||||
(usernames) => usernames.username
|
(usernames) => usernames.username
|
||||||
);
|
);
|
||||||
|
|
||||||
const { password, subscriptions, ...lessSensitiveInfo } = user;
|
const { password, subscriptions, parentSubscription, ...lessSensitiveInfo } =
|
||||||
|
user;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
...lessSensitiveInfo,
|
...lessSensitiveInfo,
|
||||||
|
@ -30,6 +36,12 @@ export default async function getUserById(userId: number) {
|
||||||
subscription: {
|
subscription: {
|
||||||
active: subscriptions?.active,
|
active: subscriptions?.active,
|
||||||
},
|
},
|
||||||
|
parentSubscription: {
|
||||||
|
active: parentSubscription?.active,
|
||||||
|
user: {
|
||||||
|
email: parentSubscription?.user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return { response: data, status: 200 };
|
return { response: data, status: 200 };
|
||||||
|
|
|
@ -101,7 +101,6 @@ export default async function updateUserById(
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { email: true, password: true, name: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user && user.email && data.email && data.email !== user.email) {
|
if (user && user.email && data.email && data.email !== user.email) {
|
||||||
|
@ -170,8 +169,20 @@ export default async function updateUserById(
|
||||||
|
|
||||||
// Other settings / Apply changes
|
// 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 saltRounds = 10;
|
||||||
const newHashedPassword = bcrypt.hashSync(data.newPassword || "", saltRounds);
|
const newHashedPassword = bcrypt.hashSync(
|
||||||
|
data.newPassword || data.password || "",
|
||||||
|
saltRounds
|
||||||
|
);
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -198,18 +209,28 @@ export default async function updateUserById(
|
||||||
linksRouteTo: data.linksRouteTo,
|
linksRouteTo: data.linksRouteTo,
|
||||||
preventDuplicateLinks: data.preventDuplicateLinks,
|
preventDuplicateLinks: data.preventDuplicateLinks,
|
||||||
password:
|
password:
|
||||||
data.newPassword && data.newPassword !== ""
|
isInvited || (data.newPassword && data.newPassword !== "")
|
||||||
? newHashedPassword
|
? newHashedPassword
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
whitelistedUsers: true,
|
whitelistedUsers: true,
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: {
|
||||||
|
include: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { whitelistedUsers, password, subscriptions, ...userInfo } =
|
const {
|
||||||
updatedUser;
|
whitelistedUsers,
|
||||||
|
password,
|
||||||
|
subscriptions,
|
||||||
|
parentSubscription,
|
||||||
|
...userInfo
|
||||||
|
} = updatedUser;
|
||||||
|
|
||||||
// If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed
|
// If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed
|
||||||
const newWhitelistedUsernames: string[] = data.whitelistedUsers || [];
|
const newWhitelistedUsernames: string[] = data.whitelistedUsers || [];
|
||||||
|
@ -250,11 +271,19 @@ export default async function updateUserById(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: Omit<AccountSettings, "password"> = {
|
const response = {
|
||||||
...userInfo,
|
...userInfo,
|
||||||
whitelistedUsers: newWhitelistedUsernames,
|
whitelistedUsers: newWhitelistedUsernames,
|
||||||
image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
|
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 };
|
return { response, status: 200 };
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default async function paymentCheckout(
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,22 @@ import checkSubscriptionByEmail from "./checkSubscriptionByEmail";
|
||||||
|
|
||||||
interface UserIncludingSubscription extends User {
|
interface UserIncludingSubscription extends User {
|
||||||
subscriptions: Subscription | null;
|
subscriptions: Subscription | null;
|
||||||
|
parentSubscription: Subscription | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function verifySubscription(
|
export default async function verifySubscription(
|
||||||
user?: UserIncludingSubscription | null
|
user?: UserIncludingSubscription | null
|
||||||
) {
|
) {
|
||||||
if (!user || !user.subscriptions) {
|
if (!user || (!user.subscriptions && !user.parentSubscription)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.parentSubscription?.active) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!user.subscriptions.active ||
|
!user.subscriptions?.active ||
|
||||||
new Date() > user.subscriptions.currentPeriodEnd
|
new Date() > user.subscriptions.currentPeriodEnd
|
||||||
) {
|
) {
|
||||||
const subscription = await checkSubscriptionByEmail(user.email as string);
|
const subscription = await checkSubscriptionByEmail(user.email as string);
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default async function verifyByCredentials({
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default async function verifyUser({
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1393,6 +1393,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
subscriptions: true,
|
subscriptions: true,
|
||||||
|
parentSubscription: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
subscriptions: true,
|
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();
|
const { data: user = {} } = useUser();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("user", user);
|
||||||
if (
|
if (
|
||||||
session.status === "authenticated" &&
|
session.status === "authenticated" &&
|
||||||
user.id &&
|
user.id &&
|
||||||
user?.subscription?.active
|
(user?.subscription?.active || user.parentSubscription?.active)
|
||||||
) {
|
)
|
||||||
router.push("/dashboard");
|
router.push("/dashboard");
|
||||||
}
|
|
||||||
}, [session.status, user]);
|
}, [session.status, user]);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
|
|
|
@ -403,5 +403,8 @@
|
||||||
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
|
"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.",
|
"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",
|
"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