-
Notifications
You must be signed in to change notification settings - Fork 0
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: detect and upgrade users with missing metadata #18
Changes from all commits
c158f3d
07fc896
43168e1
763d6ac
b6d12ff
20bb5c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,6 +99,7 @@ | |
"posttest", | ||
"pptxgen", | ||
"pptxgenjs", | ||
"Preloadable", | ||
"PSED", | ||
"PSHE", | ||
"psql", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,128 +1,46 @@ | ||
"use client"; | ||
|
||
import { useState } from "react"; | ||
import { useEffect, useRef } from "react"; | ||
|
||
import { useUser } from "@clerk/nextjs"; | ||
import logger from "@oakai/logger/browser"; | ||
import { | ||
OakBox, | ||
OakFlex, | ||
OakHeading, | ||
OakLink, | ||
OakP, | ||
OakPrimaryButton, | ||
OakSpan, | ||
} from "@oaknational/oak-components"; | ||
import Link from "next/link"; | ||
import { useReloadSession } from "hooks/useReloadSession"; | ||
|
||
import Button from "@/components/Button"; | ||
import CheckBox from "@/components/CheckBox"; | ||
import SignUpSignInLayout from "@/components/SignUpSignInLayout"; | ||
import TermsContent from "@/components/TermsContent"; | ||
import { AcceptTermsForm } from "@/components/Onboarding/AcceptTermsForm"; | ||
import { LegacyUpgradeNotice } from "@/components/Onboarding/LegacyUpgradeNotice"; | ||
import { trpc } from "@/utils/trpc"; | ||
|
||
export const OnBoarding = () => { | ||
const [dropDownOpen, setDropDownOpen] = useState(true); | ||
const [termsAcceptedLocal, setTermsAcceptedLocal] = useState(false); | ||
const [privacyAcceptedLocal, setPrivacyAcceptedLocal] = useState(false); | ||
const acceptTerms = trpc.auth.acceptTerms.useMutation(); | ||
|
||
const handleAcceptTermsOfUse = async () => { | ||
try { | ||
const response = await acceptTerms.mutateAsync({ | ||
termsOfUse: new Date(), | ||
privacyPolicy: privacyAcceptedLocal ? new Date() : false, | ||
}); | ||
|
||
if (!response?.acceptedTermsOfUse) { | ||
throw new Error("Could not accept terms of use"); | ||
const { user } = useUser(); | ||
const reloadSession = useReloadSession(); | ||
const setDemoStatus = trpc.auth.setDemoStatus.useMutation(); | ||
|
||
const userHasAlreadyAcceptedTerms = | ||
user?.publicMetadata?.["labs"]?.["isOnboarded"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a TS complaint that the keys might not exist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. It's a slightly hokey way to wait for |
||
|
||
// Edge case: Legacy users have already accepted terms but don't have a demo status | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry being a bit slow here -- where's the condition that they don't have a demo status? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah just seen -- user won't land on this page otherwise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The case is any user who signed up before started capturing the demo status. Unless we can find a sensible representation of their region (maybe posthog?), our only option is to wait until they come back online to set it We're soon going to have users landing on labs from OWA who don't have all of our metadata, so this concept of upgrading metadata will likely come in useful anyway |
||
const isHandlingLegacyCase = useRef(false); | ||
useEffect(() => { | ||
async function handleDemoStatusSet() { | ||
if (userHasAlreadyAcceptedTerms && !isHandlingLegacyCase.current) { | ||
isHandlingLegacyCase.current = true; | ||
logger.debug("User has already accepted terms"); | ||
await setDemoStatus.mutateAsync(); | ||
logger.debug("Demo status set successfully"); | ||
await reloadSession(); | ||
logger.debug("Session token refreshed successfully. Redirecting"); | ||
window.location.href = "/?reason=metadata-upgraded"; | ||
} | ||
|
||
logger.debug("Terms of use accepted successfully."); | ||
window.location.href = "/"; | ||
} catch (error) { | ||
logger.error(error, "An error occurred while accepting terms of use"); | ||
} | ||
}; | ||
|
||
return ( | ||
<SignUpSignInLayout loaded={true}> | ||
<OakBox | ||
$mh="auto" | ||
$borderRadius="border-radius-m" | ||
$background="white" | ||
$pa="inner-padding-xl2" | ||
$maxWidth={"all-spacing-22"} | ||
> | ||
<OakHeading $font="heading-6" tag="h1"> | ||
This product is experimental and uses AI | ||
</OakHeading> | ||
<OakBox $mt="space-between-s"> | ||
<OakP> | ||
We have worked to ensure that our tools are as high quality and as | ||
safe as possible but we cannot guarantee accuracy. Please use with | ||
caution. | ||
</OakP> | ||
</OakBox> | ||
handleDemoStatusSet(); | ||
}, [userHasAlreadyAcceptedTerms, setDemoStatus, reloadSession]); | ||
|
||
<OakBox $pt="inner-padding-m" $mv="space-between-s"> | ||
<CheckBox | ||
label="Accept" | ||
setValue={setPrivacyAcceptedLocal} | ||
size="base" | ||
> | ||
<OakSpan> | ||
Keep me updated with latest Oak AI experiments, resources and | ||
other helpful content by email. You can unsubscribe at any time. | ||
See our{" "} | ||
<OakLink element={Link} href="/legal/privacy"> | ||
privacy policy | ||
</OakLink> | ||
. | ||
</OakSpan> | ||
</CheckBox> | ||
</OakBox> | ||
if (userHasAlreadyAcceptedTerms) { | ||
return <LegacyUpgradeNotice />; | ||
} | ||
|
||
{termsAcceptedLocal ? ( | ||
<OakFlex $flexDirection="column" $gap="all-spacing-7"> | ||
<p> | ||
Terms accepted, if the page does not reload please refresh and | ||
navigate to home. | ||
</p> | ||
</OakFlex> | ||
) : ( | ||
<OakFlex | ||
$flexDirection="row" | ||
$justifyContent="between" | ||
$gap="all-spacing-4" | ||
$width="100%" | ||
$alignItems="center" | ||
$mt="space-between-l" | ||
> | ||
<Button | ||
variant="text-link" | ||
onClick={() => setDropDownOpen(!dropDownOpen)} | ||
icon={dropDownOpen ? "chevron-down" : "chevron-up"} | ||
> | ||
See terms | ||
</Button> | ||
<OakPrimaryButton | ||
onClick={() => { | ||
handleAcceptTermsOfUse(); | ||
setTermsAcceptedLocal(true); | ||
}} | ||
> | ||
I understand | ||
</OakPrimaryButton> | ||
</OakFlex> | ||
)} | ||
{!dropDownOpen && ( | ||
<OakBox> | ||
<TermsContent /> | ||
</OakBox> | ||
)} | ||
</OakBox> | ||
</SignUpSignInLayout> | ||
); | ||
// For the typical new user, show the accept terms form | ||
return <AcceptTermsForm />; | ||
}; | ||
|
||
export default OnBoarding; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"use client"; | ||
|
||
import { useState } from "react"; | ||
|
||
import { useUser } from "@clerk/nextjs"; | ||
import logger from "@oakai/logger/browser"; | ||
import { | ||
OakBox, | ||
OakFlex, | ||
OakHeading, | ||
OakLink, | ||
OakP, | ||
OakPrimaryButton, | ||
OakSpan, | ||
} from "@oaknational/oak-components"; | ||
import { useReloadSession } from "hooks/useReloadSession"; | ||
import Link from "next/link"; | ||
|
||
import Button from "@/components/Button"; | ||
import CheckBox from "@/components/CheckBox"; | ||
import SignUpSignInLayout from "@/components/SignUpSignInLayout"; | ||
import TermsContent from "@/components/TermsContent"; | ||
import { trpc } from "@/utils/trpc"; | ||
|
||
export const AcceptTermsForm = () => { | ||
const [dropDownOpen, setDropDownOpen] = useState(true); | ||
const { isLoaded } = useUser(); | ||
const reloadSession = useReloadSession(); | ||
|
||
const [termsAcceptedLocal, setTermsAcceptedLocal] = useState(false); | ||
const [privacyAcceptedLocal, setPrivacyAcceptedLocal] = useState(false); | ||
const setDemoStatus = trpc.auth.setDemoStatus.useMutation(); | ||
const acceptTerms = trpc.auth.acceptTerms.useMutation({}); | ||
|
||
const handleAcceptTermsOfUse = async () => { | ||
try { | ||
await setDemoStatus.mutateAsync(); | ||
logger.debug("Demo status set successfully"); | ||
|
||
const now = new Date(); | ||
const response = await acceptTerms.mutateAsync({ | ||
termsOfUse: now, | ||
privacyPolicy: privacyAcceptedLocal ? now : false, | ||
}); | ||
|
||
if (!response?.acceptedTermsOfUse) { | ||
throw new Error("Could not accept terms of use"); | ||
} | ||
logger.debug("Terms of use accepted successfully."); | ||
|
||
await reloadSession(); | ||
logger.debug("Session token refreshed successfully. Redirecting"); | ||
|
||
window.location.href = "/?reason=onboarded"; | ||
} catch (error) { | ||
logger.error(error, "An error occurred while accepting terms of use"); | ||
} | ||
}; | ||
|
||
return ( | ||
<SignUpSignInLayout loaded={isLoaded}> | ||
<OakBox | ||
$mh="auto" | ||
$borderRadius="border-radius-m" | ||
$background="white" | ||
$pa="inner-padding-xl2" | ||
$maxWidth={"all-spacing-22"} | ||
> | ||
<OakHeading $font="heading-6" tag="h1"> | ||
This product is experimental and uses AI | ||
</OakHeading> | ||
<OakBox $mt="space-between-s"> | ||
<OakP> | ||
We have worked to ensure that our tools are as high quality and as | ||
safe as possible but we cannot guarantee accuracy. Please use with | ||
caution. | ||
</OakP> | ||
</OakBox> | ||
|
||
<OakBox $pt="inner-padding-m" $mv="space-between-s"> | ||
<CheckBox | ||
label="Accept" | ||
setValue={setPrivacyAcceptedLocal} | ||
size="base" | ||
> | ||
<OakSpan> | ||
Keep me updated with latest Oak AI experiments, resources and | ||
other helpful content by email. You can unsubscribe at any time. | ||
See our{" "} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The language here shifts from "me" - the user to "our" - Oak. Should this be "the"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't new copy, but you are right that the grammar isn't quite right The previous sentence has already switched to "You", so I don't think it's as bad as it looks at first glance That said, "Keep me updated with latest Oak AI experiments" seems to be missing a "the"? |
||
<OakLink element={Link} href="/legal/privacy"> | ||
privacy policy | ||
</OakLink> | ||
. | ||
</OakSpan> | ||
</CheckBox> | ||
</OakBox> | ||
|
||
{termsAcceptedLocal ? ( | ||
<OakFlex $flexDirection="column" $gap="all-spacing-7"> | ||
<p> | ||
Terms accepted. If the page does not reload please refresh and | ||
navigate to home. | ||
</p> | ||
</OakFlex> | ||
) : ( | ||
<OakFlex | ||
$flexDirection="row" | ||
$justifyContent="between" | ||
$gap="all-spacing-4" | ||
$width="100%" | ||
$alignItems="center" | ||
$mt="space-between-l" | ||
> | ||
<Button | ||
variant="text-link" | ||
onClick={() => setDropDownOpen(!dropDownOpen)} | ||
icon={dropDownOpen ? "chevron-down" : "chevron-up"} | ||
> | ||
See terms | ||
</Button> | ||
<OakPrimaryButton | ||
onClick={() => { | ||
handleAcceptTermsOfUse(); | ||
setTermsAcceptedLocal(true); | ||
}} | ||
> | ||
I understand | ||
</OakPrimaryButton> | ||
</OakFlex> | ||
)} | ||
{!dropDownOpen && ( | ||
<OakBox> | ||
<TermsContent /> | ||
</OakBox> | ||
)} | ||
</OakBox> | ||
</SignUpSignInLayout> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"use client"; | ||
|
||
import { OakBox, OakFlex, OakHeading } from "@oaknational/oak-components"; | ||
|
||
import LoadingWheel from "@/components/LoadingWheel"; | ||
import SignUpSignInLayout from "@/components/SignUpSignInLayout"; | ||
|
||
export const LegacyUpgradeNotice = () => { | ||
return ( | ||
<SignUpSignInLayout loaded> | ||
<OakBox | ||
$mh="auto" | ||
$ml="space-between-l" | ||
$borderRadius="border-radius-m" | ||
$background="white" | ||
$pa="inner-padding-xl2" | ||
$maxWidth={"all-spacing-22"} | ||
> | ||
<OakHeading $font="heading-6" tag="h1"> | ||
Preparing your account | ||
</OakHeading> | ||
<OakFlex $mt="space-between-s" $width={"100%"} $justifyContent="center"> | ||
<LoadingWheel /> | ||
</OakFlex> | ||
</OakBox> | ||
</SignUpSignInLayout> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to be publicMetadata.labs?.isDemoUser or is this safe at this point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type of
isDemoStatusSet
isSo that's already asserted here!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my concern is that we might be trusting what is coming through here, and it might be correct in the types, but I'm not sure we validate the actual data from Clerk?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean that isDemoStatusSet could be accidentally sanitising incorrect data with a correct type?
Here's the definition: