diff --git a/components/ModalContent/InviteModal.tsx b/components/ModalContent/InviteModal.tsx index 0af71d5..7f4ee09 100644 --- a/components/ModalContent/InviteModal.tsx +++ b/components/ModalContent/InviteModal.tsx @@ -50,7 +50,7 @@ export default function InviteModal({ onClose }: Props) { onSettled: () => { signIn("invite", { email: form.email, - callbackUrl: "/", + callbackUrl: "/member-onboarding", redirect: false, }); }, diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 2ec9899..ad1d0be 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -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) diff --git a/lib/api/controllers/users/userId/getUserById.ts b/lib/api/controllers/users/userId/getUserById.ts index 8c6faa8..f704155 100644 --- a/lib/api/controllers/users/userId/getUserById.ts +++ b/lib/api/controllers/users/userId/getUserById.ts @@ -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 }; diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts index 7c92585..49e5c4c 100644 --- a/lib/api/controllers/users/userId/updateUserById.ts +++ b/lib/api/controllers/users/userId/updateUserById.ts @@ -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 = { + 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 }; diff --git a/lib/api/paymentCheckout.ts b/lib/api/paymentCheckout.ts index 0a89ea6..f81b61f 100644 --- a/lib/api/paymentCheckout.ts +++ b/lib/api/paymentCheckout.ts @@ -17,6 +17,7 @@ export default async function paymentCheckout( }, include: { subscriptions: true, + parentSubscription: true, }, }); diff --git a/lib/api/stripe/verifySubscription.ts b/lib/api/stripe/verifySubscription.ts index d4b756d..70f0ba6 100644 --- a/lib/api/stripe/verifySubscription.ts +++ b/lib/api/stripe/verifySubscription.ts @@ -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); diff --git a/lib/api/verifyByCredentials.ts b/lib/api/verifyByCredentials.ts index 48baf5b..ccf406a 100644 --- a/lib/api/verifyByCredentials.ts +++ b/lib/api/verifyByCredentials.ts @@ -33,6 +33,7 @@ export default async function verifyByCredentials({ }, include: { subscriptions: true, + parentSubscription: true, }, }); diff --git a/lib/api/verifyUser.ts b/lib/api/verifyUser.ts index 66017e4..cf4947a 100644 --- a/lib/api/verifyUser.ts +++ b/lib/api/verifyUser.ts @@ -30,6 +30,7 @@ export default async function verifyUser({ }, include: { subscriptions: true, + parentSubscription: true, }, }); diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index cd9f585..cc40dae 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -1393,6 +1393,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { }, include: { subscriptions: true, + parentSubscription: true, }, }); diff --git a/pages/api/v1/users/[id].ts b/pages/api/v1/users/[id].ts index e37fe41..161338d 100644 --- a/pages/api/v1/users/[id].ts +++ b/pages/api/v1/users/[id].ts @@ -41,6 +41,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) { }, include: { subscriptions: true, + parentSubscription: true, }, }); diff --git a/pages/member-onboarding.tsx b/pages/member-onboarding.tsx new file mode 100644 index 0000000..6d69a7c --- /dev/null +++ b/pages/member-onboarding.tsx @@ -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({ + 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) { + 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 ( + +
+
+

+ {t("finalize_profile")} +

+ +
+ +

+ {t("invitation_desc", { + owner: user?.parentSubscription?.user?.email, + })} +

+ +
+

+ {t("display_name")} +

+ setForm({ ...form, name: e.target.value })} + /> +
+ +
+

+ {t("new_password")} +

+ setForm({ ...form, password: e.target.value })} + /> +
+ + {process.env.NEXT_PUBLIC_STRIPE && ( +
+

+ , + , + ]} + /> +

+
+ )} + + +
+
+
+ ); +} + +export { getServerSideProps }; diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index f4f6844..20e9599 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -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() { diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 93f772c..b493c75 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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." } \ No newline at end of file