fix #582: send user back to onboarding after adding integration (#635)

* fix #582: send user back to onboarding after adding integration if incomplete

* use more accurate, descriptive typings

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Femi Odugbesan 2021-10-08 08:10:57 -05:00 committed by GitHub
parent 2c9b301b77
commit 015b7c18af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 55 deletions

View file

@ -1,3 +1,15 @@
import { User } from "@prisma/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString();
export const ONBOARDING_NEXT_REDIRECT = {
redirect: {
permanent: false,
destination: "/getting-started",
},
} as const;
export const shouldShowOnboarding = (user: Pick<User, "createdDate" | "completedOnboarding">) => {
return !user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT);
};

View file

@ -8,9 +8,8 @@ import {
PlusIcon, PlusIcon,
UsersIcon, UsersIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client"; import { SchedulingType, Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client"; import { GetServerSidePropsContext } from "next";
import dayjs from "dayjs";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
@ -23,7 +22,7 @@ import { getSession } from "@lib/auth";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils"; import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started"; import { shouldShowOnboarding, ONBOARDING_NEXT_REDIRECT } from "@lib/getting-started";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { useToggleQuery } from "@lib/hooks/useToggleQuery"; import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import createEventType from "@lib/mutations/event-types/create-event-type"; import createEventType from "@lib/mutations/event-types/create-event-type";
@ -145,7 +144,7 @@ const EventTypesPage = (props: PageProps) => {
"hover:bg-neutral-50 flex justify-between items-center ", "hover:bg-neutral-50 flex justify-between items-center ",
type.$disabled && "pointer-events-none" type.$disabled && "pointer-events-none"
)}> )}>
<div className="flex items-center w-full justify-between px-4 py-4 sm:px-6 hover:bg-neutral-50"> <div className="flex items-center justify-between w-full px-4 py-4 sm:px-6 hover:bg-neutral-50">
<Link href={"/event-types/" + type.id}> <Link href={"/event-types/" + type.id}>
<a className="flex-grow text-sm truncate"> <a className="flex-grow text-sm truncate">
<div> <div>
@ -224,7 +223,7 @@ const EventTypesPage = (props: PageProps) => {
leaveTo="transform opacity-0 scale-95"> leaveTo="transform opacity-0 scale-95">
<Menu.Items <Menu.Items
static static
className="absolute z-10 right-0 w-56 mt-2 origin-top-right bg-white divide-y rounded-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none divide-neutral-100"> className="absolute right-0 z-10 w-56 mt-2 origin-top-right bg-white divide-y rounded-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none divide-neutral-100">
<div className="py-1"> <div className="py-1">
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
@ -558,7 +557,7 @@ const CreateNewEventDialog = ({
); );
}; };
export async function getServerSideProps(context) { export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getSession(context); const session = await getSession(context);
const locale = await getOrSetUserLocaleFromHeaders(context.req); const locale = await getOrSetUserLocaleFromHeaders(context.req);
@ -647,13 +646,10 @@ export async function getServerSideProps(context) {
}; };
} }
if (!user.completedOnboarding && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT)) { if (
return { shouldShowOnboarding({ completedOnboarding: user.completedOnboarding, createdDate: user.createdDate })
redirect: { ) {
permanent: false, return ONBOARDING_NEXT_REDIRECT;
destination: "/getting-started",
},
};
} }
// backwards compatibility, TMP: // backwards compatibility, TMP:

View file

@ -6,6 +6,7 @@ import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { ONBOARDING_NEXT_REDIRECT, shouldShowOnboarding } from "@lib/getting-started";
import AddAppleIntegration, { import AddAppleIntegration, {
ADD_APPLE_INTEGRATION_FORM_TITLE, ADD_APPLE_INTEGRATION_FORM_TITLE,
} from "@lib/integrations/Apple/components/AddAppleIntegration"; } from "@lib/integrations/Apple/components/AddAppleIntegration";
@ -138,8 +139,8 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
const ConnectNewAppDialog = () => ( const ConnectNewAppDialog = () => (
<Dialog> <Dialog>
<DialogTrigger className="py-2 px-4 mt-6 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> <DialogTrigger className="px-4 py-2 mt-6 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
<PlusIcon className="w-5 h-5 mr-1 inline" /> <PlusIcon className="inline w-5 h-5 mr-1" />
Connect a new App Connect a new App
</DialogTrigger> </DialogTrigger>
@ -152,14 +153,14 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
.map((integration) => { .map((integration) => {
return ( return (
<li key={integration.type} className="flex py-4"> <li key={integration.type} className="flex py-4">
<div className="w-1/12 mr-4 pt-2"> <div className="w-1/12 pt-2 mr-4">
<img className="h-8 w-8 mr-2" src={integration.imageSrc} alt={integration.title} /> <img className="w-8 h-8 mr-2" src={integration.imageSrc} alt={integration.title} />
</div> </div>
<div className="w-10/12"> <div className="w-10/12">
<h2 className="font-cal text-gray-800 font-medium">{integration.title}</h2> <h2 className="font-medium text-gray-800 font-cal">{integration.title}</h2>
<p className="text-gray-400 text-sm">{integration.description}</p> <p className="text-sm text-gray-400">{integration.description}</p>
</div> </div>
<div className="w-2/12 text-right pt-2"> <div className="w-2/12 pt-2 text-right">
<button <button
onClick={() => integrationHandler(integration.type)} onClick={() => integrationHandler(integration.type)}
className="font-medium text-neutral-900 hover:text-neutral-500"> className="font-medium text-neutral-900 hover:text-neutral-500">
@ -171,7 +172,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
})} })}
</ul> </ul>
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2"> <div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild> <DialogClose asChild>
<Button color="secondary">Cancel</Button> <Button color="secondary">Cancel</Button>
</DialogClose> </DialogClose>
@ -182,7 +183,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
const SelectCalendarDialog = () => ( const SelectCalendarDialog = () => (
<Dialog onOpenChange={(open) => !open && onCloseSelectCalendar()}> <Dialog onOpenChange={(open) => !open && onCloseSelectCalendar()}>
<DialogTrigger className="py-2 px-4 mt-6 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> <DialogTrigger className="px-4 py-2 mt-6 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
Select calendars Select calendars
</DialogTrigger> </DialogTrigger>
@ -192,20 +193,20 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
subtitle="If no entry is selected, all calendars will be checked" subtitle="If no entry is selected, all calendars will be checked"
/> />
<div className="my-4"> <div className="my-4">
<ul className="divide-y divide-gray-200 max-h-96 overflow-y-auto"> <ul className="overflow-y-auto divide-y divide-gray-200 max-h-96">
{selectableCalendars.map((calendar) => ( {selectableCalendars.map((calendar) => (
<li key={calendar.name} className="flex py-4"> <li key={calendar.name} className="flex py-4">
<div className="w-1/12 mr-4 pt-2"> <div className="w-1/12 pt-2 mr-4">
<img <img
className="h-8 w-8 mr-2" className="w-8 h-8 mr-2"
src={getCalendarIntegrationImage(calendar.integration)} src={getCalendarIntegrationImage(calendar.integration)}
alt={calendar.integration} alt={calendar.integration}
/> />
</div> </div>
<div className="w-10/12 pt-3"> <div className="w-10/12 pt-3">
<h2 className="text-gray-800 font-medium">{calendar.name}</h2> <h2 className="font-medium text-gray-800">{calendar.name}</h2>
</div> </div>
<div className="w-2/12 text-right pt-3"> <div className="w-2/12 pt-3 text-right">
<Switch <Switch
defaultChecked={calendar.selected} defaultChecked={calendar.selected}
onCheckedChange={calendarSelectionHandler(calendar)} onCheckedChange={calendarSelectionHandler(calendar)}
@ -215,7 +216,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
))} ))}
</ul> </ul>
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2"> <div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild> <DialogClose asChild>
<Button color="secondary">Confirm</Button> <Button color="secondary">Confirm</Button>
</DialogClose> </DialogClose>
@ -275,7 +276,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
/> />
<div className="my-4"> <div className="my-4">
{addCalDavError && ( {addCalDavError && (
<p className="text-red-700 text-sm"> <p className="text-sm text-red-700">
<span className="font-bold">Error: </span> <span className="font-bold">Error: </span>
{addCalDavError.message} {addCalDavError.message}
</p> </p>
@ -285,11 +286,11 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
onSubmit={handleAddCalDavIntegrationSaveButtonPress} onSubmit={handleAddCalDavIntegrationSaveButtonPress}
/> />
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2"> <div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button <Button
type="submit" type="submit"
form={ADD_CALDAV_INTEGRATION_FORM_TITLE} form={ADD_CALDAV_INTEGRATION_FORM_TITLE}
className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> className="flex justify-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
Save Save
</Button> </Button>
<DialogClose <DialogClose
@ -329,7 +330,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
/> />
<div className="my-4"> <div className="my-4">
{addAppleError && ( {addAppleError && (
<p className="text-red-700 text-sm"> <p className="text-sm text-red-700">
<span className="font-bold">Error: </span> <span className="font-bold">Error: </span>
{addAppleError.message} {addAppleError.message}
</p> </p>
@ -339,11 +340,11 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
onSubmit={handleAddAppleIntegrationSaveButtonPress} onSubmit={handleAddAppleIntegrationSaveButtonPress}
/> />
</div> </div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-2"> <div className="gap-2 mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button <button
type="submit" type="submit"
form={ADD_APPLE_INTEGRATION_FORM_TITLE} form={ADD_APPLE_INTEGRATION_FORM_TITLE}
className="flex justify-center py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900"> className="flex justify-center px-4 py-2 text-sm font-medium text-white border border-transparent rounded-sm shadow-sm bg-neutral-900 hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900">
Save Save
</button> </button>
<DialogClose <DialogClose
@ -366,7 +367,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
return ( return (
<div> <div>
<Shell heading="Integrations" subtitle="Connect your favourite apps." CTA={<ConnectNewAppDialog />}> <Shell heading="Integrations" subtitle="Connect your favourite apps." CTA={<ConnectNewAppDialog />}>
<div className="bg-white border border-gray-200 overflow-hidden rounded-sm mb-8"> <div className="mb-8 overflow-hidden bg-white border border-gray-200 rounded-sm">
{integrations.filter((ig) => ig.credential).length !== 0 ? ( {integrations.filter((ig) => ig.credential).length !== 0 ? (
<ul className="divide-y divide-gray-200"> <ul className="divide-y divide-gray-200">
{integrations {integrations
@ -376,13 +377,13 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
<Link href={"/integrations/" + ig.credential.id}> <Link href={"/integrations/" + ig.credential.id}>
<a className="block hover:bg-gray-50"> <a className="block hover:bg-gray-50">
<div className="flex items-center px-4 py-4 sm:px-6"> <div className="flex items-center px-4 py-4 sm:px-6">
<div className="min-w-0 flex-1 flex items-center"> <div className="flex items-center flex-1 min-w-0">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<img className="h-10 w-10 mr-2" src={ig.imageSrc} alt={ig.title} /> <img className="w-10 h-10 mr-2" src={ig.imageSrc} alt={ig.title} />
</div> </div>
<div className="min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-4"> <div className="flex-1 min-w-0 px-4 md:grid md:grid-cols-2 md:gap-4">
<div> <div>
<p className="text-sm font-medium text-neutral-900 truncate">{ig.title}</p> <p className="text-sm font-medium truncate text-neutral-900">{ig.title}</p>
<p className="flex items-center text-sm text-gray-500"> <p className="flex items-center text-sm text-gray-500">
{ig.type.endsWith("_calendar") && ( {ig.type.endsWith("_calendar") && (
<span className="truncate">Calendar Integration</span> <span className="truncate">Calendar Integration</span>
@ -394,13 +395,13 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
</div> </div>
<div className="hidden md:block"> <div className="hidden md:block">
{ig.credential.key && ( {ig.credential.key && (
<p className="mt-2 flex items-center text text-gray-500"> <p className="flex items-center mt-2 text-gray-500 text">
<CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" /> <CheckCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-green-400" />
Connected Connected
</p> </p>
)} )}
{!ig.credential.key && ( {!ig.credential.key && (
<p className="mt-3 flex items-center text text-gray-500"> <p className="flex items-center mt-3 text-gray-500 text">
<XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" /> <XCircleIcon className="flex-shrink-0 mr-1.5 h-5 w-5 text-yellow-400" />
Not connected Not connected
</p> </p>
@ -408,7 +409,7 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
</div> </div>
</div> </div>
<div> <div>
<ChevronRightIcon className="h-5 w-5 text-gray-400" /> <ChevronRightIcon className="w-5 h-5 text-gray-400" />
</div> </div>
</div> </div>
</div> </div>
@ -418,13 +419,13 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
))} ))}
</ul> </ul>
) : ( ) : (
<div className="bg-white shadow rounded-sm"> <div className="bg-white rounded-sm shadow">
<div className="flex"> <div className="flex">
<div className="py-9 pl-8"> <div className="pl-8 py-9">
<InformationCircleIcon className="text-neutral-900 w-16" /> <InformationCircleIcon className="w-16 text-neutral-900" />
</div> </div>
<div className="py-5 sm:p-6"> <div className="py-5 sm:p-6">
<h3 className="text-lg leading-6 font-medium text-gray-900"> <h3 className="text-lg font-medium leading-6 text-gray-900">
You don&apos;t have any apps connected. You don&apos;t have any apps connected.
</h3> </h3>
<div className="mt-2 text-sm text-gray-500"> <div className="mt-2 text-sm text-gray-500">
@ -438,10 +439,10 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
</div> </div>
)} )}
</div> </div>
<div className="bg-white border border-gray-200 rounded-sm mb-8"> <div className="mb-8 bg-white border border-gray-200 rounded-sm">
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<h3 className="font-cal text-lg leading-6 font-medium text-gray-900">Select calendars</h3> <h3 className="text-lg font-medium leading-6 text-gray-900 font-cal">Select calendars</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500"> <div className="max-w-xl mt-2 text-sm text-gray-500">
<p>Select which calendars are checked for availability to prevent double bookings.</p> <p>Select which calendars are checked for availability to prevent double bookings.</p>
</div> </div>
<SelectCalendarDialog /> <SelectCalendarDialog />
@ -449,8 +450,8 @@ export default function Home({ integrations }: inferSSRProps<typeof getServerSid
</div> </div>
<div className="border border-gray-200 rounded-sm"> <div className="border border-gray-200 rounded-sm">
<div className="px-4 py-5 sm:p-6"> <div className="px-4 py-5 sm:p-6">
<h3 className="font-cal text-lg leading-6 font-medium text-gray-900">Launch your own App</h3> <h3 className="text-lg font-medium leading-6 text-gray-900 font-cal">Launch your own App</h3>
<div className="mt-2 max-w-xl text-sm text-gray-500"> <div className="max-w-xl mt-2 text-sm text-gray-500">
<p>If you want to add your own App here, get in touch with us.</p> <p>If you want to add your own App here, get in touch with us.</p>
</div> </div>
<div className="mt-5"> <div className="mt-5">
@ -485,14 +486,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
key: true, key: true,
}, },
}, },
completedOnboarding: true,
createdDate: true,
}, },
}); });
if (!user) return { redirect: { permanent: false, destination: "/auth/login" } }; if (!user)
return {
redirect: { permanent: false, destination: "/auth/login" },
};
const { credentials } = user; if (
shouldShowOnboarding({ completedOnboarding: user.completedOnboarding, createdDate: user.createdDate })
) {
return ONBOARDING_NEXT_REDIRECT;
}
const integrations = getIntegrations(credentials); const integrations = getIntegrations(user.credentials);
return { return {
props: { session, integrations }, props: { session, integrations },