diff --git a/portal/src/OnboardingSurveyLayout.module.css b/portal/src/OnboardingSurveyLayout.module.css new file mode 100644 index 0000000000..a3f4c632cd --- /dev/null +++ b/portal/src/OnboardingSurveyLayout.module.css @@ -0,0 +1,34 @@ +.body { + @apply min-h-screen; + @apply shrink-0; + @apply flex flex-col; + @apply relative; +} + +.logo { + @apply absolute; + @apply h-12; +} + +.content { + @apply self-center; + @apply flex flex-col; + @apply mt-[100px] mx-4 mb-4; +} + +.navigationDiv { + @apply self-center; + @apply flex flex-row justify-between items-center; + @apply gap-2.5; +} + +.SurveyTitle { + @apply whitespace-pre-line; + @apply text-center; + @apply mb-6; +} + +.SurveySubtitle { + @apply whitespace-pre-line; + @apply text-center; +} diff --git a/portal/src/OnboardingSurveyLayout.tsx b/portal/src/OnboardingSurveyLayout.tsx new file mode 100644 index 0000000000..06388cd945 --- /dev/null +++ b/portal/src/OnboardingSurveyLayout.tsx @@ -0,0 +1,118 @@ +import cn from "classnames"; +import React, { useContext, useMemo } from "react"; +import { useTheme, Text } from "@fluentui/react"; +import { Context } from "@oursky/react-messageformat"; +import styles from "./OnboardingSurveyLayout.module.css"; +import authgearLogoURL from "./images/authgear_logo_color.svg"; + +interface LogoProps {} + +const Logo: React.VFC = (_props: LogoProps) => { + const { renderToString } = useContext(Context); + const theme = useTheme(); + const logoStyles = useMemo(() => { + return { + fill: theme.semanticColors.bodyText, + }; + }, [theme]); + return ( + {renderToString("system.name")} + ); +}; + +export interface SurveyTitleProps { + children?: React.ReactNode; +} + +export function SurveyTitle(props: SurveyTitleProps): React.ReactElement { + const theme = useTheme(); + const overrideStyles = useMemo(() => { + return { + root: { + color: theme.semanticColors.bodyText, + }, + }; + }, [theme]); + return ( + + {props.children} + + ); +} + +export interface SurveySubtitleProps { + children?: React.ReactNode; +} + +export function SurveySubtitle(props: SurveySubtitleProps): React.ReactElement { + const theme = useTheme(); + const overrideStyles = useMemo(() => { + return { + root: { + color: theme.semanticColors.bodySubtext, + }, + }; + }, [theme]); + return ( + + {props.children} + + ); +} + +export interface SurveyLayoutProps { + title: string; + subtitle: string; + nextButton: React.ReactNode; + backButton?: React.ReactNode; + children?: React.ReactNode; + contentClassName?: string; +} + +export default function SurveyLayout( + props: SurveyLayoutProps +): React.ReactElement { + const { + title, + subtitle, + nextButton, + backButton, + children, + contentClassName, + } = props; + const theme = useTheme(); + const overrideBodyStyles = useMemo(() => { + return { + backgroundColor: theme.semanticColors.bodyStandoutBackground, + }; + }, [theme]); + return ( +
+ +
+ {title} + {subtitle} + {children} +
+ {backButton} + {nextButton} +
+
+
+ ); +} diff --git a/portal/src/OnboardingSurveyScreen.module.css b/portal/src/OnboardingSurveyScreen.module.css new file mode 100644 index 0000000000..8d32adaf43 --- /dev/null +++ b/portal/src/OnboardingSurveyScreen.module.css @@ -0,0 +1,55 @@ +.SingleChoiceButtonGroupVariantCentered { + @apply my-[72px]; + @apply flex flex-row flex-wrap items-center justify-center; + @apply gap-4; +} + +.SingleChoiceButtonGroupVariantLabeled { + @apply flex flex-row flex-wrap items-center; + @apply gap-4; + @apply w-full; +} + +.formBox { + @apply mt-[48px] mb-[72px]; + @apply grid; + @apply gap-8; +} + +.step1Content { + @apply max-w-[600px]; +} + +.step2Content { + @apply max-w-[420px]; +} + +.step3Content { + @apply max-w-[420px]; +} + +.step4Content { + @apply max-w-[760px]; +} + +.step4 { + @apply mt-[48px] mb-[72px]; +} + +.MultiChoiceButtonGroup { + @apply grid; + @apply gap-6 mobile:gap-3; + @apply grid-cols-2 mobile:grid-cols-1; + grid-auto-rows: 1fr; +} + +.CompoundChoiceButton { + max-width: 368px; + min-height: 118px; + height: 100%; + padding: 25px; +} + +.otherReasonInput { + @apply mt-8; +} diff --git a/portal/src/OnboardingSurveyScreen.tsx b/portal/src/OnboardingSurveyScreen.tsx new file mode 100644 index 0000000000..10e8788ac6 --- /dev/null +++ b/portal/src/OnboardingSurveyScreen.tsx @@ -0,0 +1,821 @@ +import cn from "classnames"; +import React, { + useContext, + useState, + useMemo, + useCallback, + useEffect, +} from "react"; +import { + Routes, + Navigate, + Route, + useNavigate, + NavigateFunction, +} from "react-router-dom"; +import { useTheme, Label, CompoundButton, IButtonProps } from "@fluentui/react"; +import PrimaryButton from "./PrimaryButton"; +import DefaultButton, { DefaultButtonProps } from "./DefaultButton"; +import { FormattedMessage, Context } from "@oursky/react-messageformat"; +import FormTextField from "./FormTextField"; +import PhoneTextField, { PhoneTextFieldValues } from "./PhoneTextField"; +import { FormProvider } from "./form"; +import SurveyLayout from "./OnboardingSurveyLayout"; +import styles from "./OnboardingSurveyScreen.module.css"; + +const buttonTranslationKeys = { + step1_roleChoiceGroup_Dev: "OnboardingSurveyScreen.step1.roleChoiceGroup.Dev", + step1_roleChoiceGroup_IT: "OnboardingSurveyScreen.step1.roleChoiceGroup.IT", + step1_roleChoiceGroup_PM: "OnboardingSurveyScreen.step1.roleChoiceGroup.PM", + step1_roleChoiceGroup_PD: "OnboardingSurveyScreen.step1.roleChoiceGroup.PD", + step1_roleChoiceGroup_Market: + "OnboardingSurveyScreen.step1.roleChoiceGroup.Market", + step1_roleChoiceGroup_Owner: + "OnboardingSurveyScreen.step1.roleChoiceGroup.Owner", + step1_roleChoiceGroup_Other: + "OnboardingSurveyScreen.step1.roleChoiceGroup.Other", + step2_teamOrIndividualChoiceGroup_Team: + "OnboardingSurveyScreen.step2.teamOrIndividualChoiceGroup.Team", + step2_teamOrIndividualChoiceGroup_Individual: + "OnboardingSurveyScreen.step2.teamOrIndividualChoiceGroup.Individual", + step3_team_companySizeChoiceGroup_label: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.label", + ["step3_team_companySizeChoiceGroup_1-49"]: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.1-49", + ["step3_team_companySizeChoiceGroup_50-99"]: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.50-99", + ["step3_team_companySizeChoiceGroup_100-499"]: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.100-499", + ["step3_team_companySizeChoiceGroup_500-1999"]: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.500-1999", + ["step3_team_companySizeChoiceGroup_2000+"]: + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.2000+", + step4_reasonChoiceGroup_Auth_title: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Auth.title", + step4_reasonChoiceGroup_Auth_subtitle: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Auth.subtitle", + step4_reasonChoiceGroup_SSO_title: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.SSO.title", + step4_reasonChoiceGroup_SSO_subtitle: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.SSO.subtitle", + step4_reasonChoiceGroup_Security_title: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Security.title", + step4_reasonChoiceGroup_Security_subtitle: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Security.subtitle", + step4_reasonChoiceGroup_Portal_title: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Portal.title", + step4_reasonChoiceGroup_Portal_subtitle: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Portal.subtitle", + step4_reasonChoiceGroup_Other_title: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Other.title", + step4_reasonChoiceGroup_Other_subtitle: + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Other.subtitle", +}; +const localStorageKey = "authgear-onboarding-survey"; + +function ChoiceButton(props: DefaultButtonProps) { + const theme = useTheme(); + const { checked } = props; + const styles = useMemo(() => { + return { + root: { + border: "none", + "--tw-ring-color": theme.semanticColors.variantBorder, + }, + rootChecked: { + "--tw-ring-color": theme.palette.themePrimary, + color: theme.palette.themePrimary, + backgroundColor: theme.semanticColors.buttonBackground, + }, + rootCheckedHovered: { + color: theme.palette.themePrimary, + }, + }; + }, [theme]); + return ( + + ); +} + +interface DefaultCompoundButtonProps + extends Omit { + text?: React.ReactNode; + secondaryText?: React.ReactNode; +} + +function CompoundChoiceButton(props: DefaultCompoundButtonProps) { + const theme = useTheme(); + const { checked } = props; + const overrideStyles = useMemo(() => { + return { + root: { + border: "none", + "--tw-ring-color": theme.semanticColors.variantBorder, + }, + rootChecked: { + "--tw-ring-color": theme.palette.themePrimary, + color: theme.palette.themePrimary, + backgroundColor: theme.semanticColors.buttonBackground, + }, + rootCheckedHovered: { + color: theme.palette.themePrimary, + }, + label: { + "margin-bottom": "10px", + "font-size": "medium", + }, + description: { + "line-height": "18px", + "font-size": "small", + }, + }; + }, [theme]); + return ( + // @ts-expect-error + + ); +} + +interface ChoiceButtonGroupProps { + prefix: string; + availableChoices: string[]; + selectedChoices: string[]; + setChoice: (newChoices: string[]) => void; +} + +function processSingleChoice( + availableChoices: string[], + oldSelectedChoices: string[], + newlySelectedChoice: string +): string[] { + if (!availableChoices.includes(newlySelectedChoice)) + return oldSelectedChoices; + else if (oldSelectedChoices.includes(newlySelectedChoice)) + return oldSelectedChoices.filter( + (choice) => choice !== newlySelectedChoice + ); + return [newlySelectedChoice]; +} + +function processMultiChoice( + availableChoices: string[], + oldSelectedChoices: string[], + newlySelectedChoice: string +): string[] { + if (!availableChoices.includes(newlySelectedChoice)) + return oldSelectedChoices; + else if (oldSelectedChoices.includes(newlySelectedChoice)) + return oldSelectedChoices.filter( + (choice) => choice !== newlySelectedChoice + ); + oldSelectedChoices.push(newlySelectedChoice); + return oldSelectedChoices; +} + +function SingleChoiceButtonGroupVariantCentered(props: ChoiceButtonGroupProps) { + const { prefix, availableChoices, selectedChoices, setChoice } = props; + const { renderToString } = useContext(Context); + const buttons = useMemo( + () => + availableChoices.map((choice) => { + const key = (prefix + + "_" + + choice) as keyof typeof buttonTranslationKeys; + return ( + { + setChoice( + structuredClone( + processSingleChoice(availableChoices, selectedChoices, choice) + ) + ); + }} + /> + ); + }), + [prefix, availableChoices, selectedChoices, setChoice, renderToString] + ); + return ( +
+ {buttons} +
+ ); +} + +function SingleChoiceButtonGroupVariantLabeled(props: ChoiceButtonGroupProps) { + const { prefix, availableChoices, selectedChoices, setChoice } = props; + const { renderToString } = useContext(Context); + const buttons = useMemo( + () => + availableChoices.map((choice) => { + const key = (prefix + + "_" + + choice) as keyof typeof buttonTranslationKeys; + return ( + { + setChoice( + structuredClone( + processSingleChoice(availableChoices, selectedChoices, choice) + ) + ); + }} + /> + ); + }), + [prefix, availableChoices, selectedChoices, setChoice, renderToString] + ); + const labelKey = (prefix + "_label") as keyof typeof buttonTranslationKeys; + return ( +
+ +
+ {buttons} +
+
+ ); +} + +function MultiChoiceButtonGroup(props: ChoiceButtonGroupProps) { + const { prefix, availableChoices, selectedChoices, setChoice } = props; + const { renderToString } = useContext(Context); + const buttons = useMemo( + () => + availableChoices.map((choice) => { + const titleKey = (prefix + + "_" + + choice + + "_title") as keyof typeof buttonTranslationKeys; + const subtitleKey = (prefix + + "_" + + choice + + "_subtitle") as keyof typeof buttonTranslationKeys; + return ( + { + setChoice( + structuredClone( + processMultiChoice(availableChoices, selectedChoices, choice) + ) + ); + }} + /> + ); + }), + [prefix, availableChoices, selectedChoices, setChoice, renderToString] + ); + return
{buttons}
; +} + +interface LocallyStoredData { + roleChoices?: string; + toriChoices?: string; + companyName?: string; + companySize?: string; + companyPhone?: PhoneTextFieldValues; + individualWebsite?: string; + individualPhone?: PhoneTextFieldValues; + reasonChoices?: string[]; + otherReason?: string; +} + +function getFromLocalStorage( + prop: keyof LocallyStoredData +): string | string[] | PhoneTextFieldValues | undefined { + const locallyStoredData = localStorage.getItem(localStorageKey); + let localJson: LocallyStoredData = {}; + if (locallyStoredData === null) return undefined; + localJson = JSON.parse(locallyStoredData); + return localJson[prop]; +} + +function setLocalStorage(prop: keyof LocallyStoredData, value: any): void { + const locallyStoredData = localStorage.getItem(localStorageKey); + let localJson: LocallyStoredData = {}; + if (locallyStoredData) localJson = JSON.parse(locallyStoredData); + localJson[prop] = value; + localStorage.setItem(localStorageKey, JSON.stringify(localJson)); +} + +function goToFirstUnfilled( + currentStep: number, + navigate: NavigateFunction +): void { + if (currentStep > 1 && getFromLocalStorage("roleChoices") === undefined) { + navigate("./../1"); + } + if (currentStep > 2 && getFromLocalStorage("toriChoices") === undefined) { + navigate("./../2"); + } + if ( + currentStep > 3 && + getFromLocalStorage("toriChoices") === "Team" && + (getFromLocalStorage("companyName") === undefined || + getFromLocalStorage("companySize") === undefined) + ) { + navigate("./../3"); + } +} + +interface StepProps {} + +function Step1(_props: StepProps) { + const prefix = "step1"; + const roleChoiceGroup = "roleChoiceGroup"; + const roleChoices = ["Dev", "IT", "PM", "PD", "Market", "Owner", "Other"]; + const roleChoicesFromLocalStorage = getFromLocalStorage("roleChoices"); + const defaultRoleChoicesState: string[] = ( + roleChoicesFromLocalStorage === undefined + ? [] + : [roleChoicesFromLocalStorage] + ) as string[]; + const [roleChoicesState, setRoleChoicesState] = useState( + defaultRoleChoicesState + ); + const empty = useMemo(() => { + return roleChoicesState.length === 0; + }, [roleChoicesState]); + const { renderToString } = useContext(Context); + const navigate = useNavigate(); + const onClickNext = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + setLocalStorage("roleChoices", roleChoicesState[0]); + navigate("./../2"); + }, + [navigate, roleChoicesState] + ); + return ( + } + disabled={empty} + /> + } + > + + + ); +} + +function Step2(_props: StepProps) { + const prefix = "step2"; + const toriChoiceGroup = "teamOrIndividualChoiceGroup"; + const toriChoices = ["Team", "Individual"]; + const toriChoicesFromLocalStorage = getFromLocalStorage("toriChoices"); + const defaultToriChoices: string[] = ( + toriChoicesFromLocalStorage === undefined + ? [] + : [toriChoicesFromLocalStorage] + ) as string[]; + const [toriChoicesState, setToriChoicesState] = useState(defaultToriChoices); + const empty = useMemo(() => { + return toriChoicesState.length === 0; + }, [toriChoicesState]); + const { renderToString } = useContext(Context); + const navigate = useNavigate(); + useEffect(() => goToFirstUnfilled(2, navigate), [navigate]); + const onClickNext = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + setLocalStorage("toriChoices", toriChoicesState[0]); + if (toriChoicesState.includes("Team")) navigate("./../3-team"); + if (toriChoicesState.includes("Individual")) + navigate("./../3-individual"); + }, + [navigate, toriChoicesState] + ); + const onClickBack = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + navigate("./../1"); + }, + [navigate] + ); + const theme = useTheme(); + const backButtonStyles = useMemo(() => { + return { + root: { + border: "none", + "background-color": theme.semanticColors.bodyStandoutBackground, + }, + }; + }, [theme]); + return ( + } + className={styles.nextButton} + disabled={empty} + /> + } + backButton={ + } + /> + } + > + + + ); +} + +function Step3Team(_props: StepProps) { + const prefix = "step3_team"; + const companyNameFromLocalStorage = getFromLocalStorage("companyName"); + const [companyName, setCompanyName] = useState( + companyNameFromLocalStorage === undefined + ? "" + : (companyNameFromLocalStorage as string) + ); + const companyPhoneFromLocalStorage = getFromLocalStorage("companyPhone"); + const defaultPhone: PhoneTextFieldValues = ( + companyPhoneFromLocalStorage === undefined + ? { rawInputValue: "" } + : companyPhoneFromLocalStorage + ) as PhoneTextFieldValues; + const [companyPhone, setCompanyPhone] = useState(defaultPhone); + const companySizeChoiceGroup = "companySizeChoiceGroup"; + const companySizeChoices = ["1-49", "50-99", "100-499", "500-1999", "2000+"]; + const defaultCompanySizeFromLocalStorage = getFromLocalStorage("companySize"); + const defaultCompanySize: string[] = ( + defaultCompanySizeFromLocalStorage === undefined + ? [] + : [defaultCompanySizeFromLocalStorage] + ) as string[]; + const [companySizeChoicesState, setCompanySizeChoicesState] = + useState(defaultCompanySize); + const companySizeEmpty = useMemo(() => { + return companySizeChoicesState.length === 0; + }, [companySizeChoicesState]); + const { renderToString } = useContext(Context); + const navigate = useNavigate(); + useEffect(() => { + goToFirstUnfilled(3, navigate); + if (getFromLocalStorage("toriChoices") === "Individual") + navigate("./../3-individual"); + }, [navigate]); + const onClickNext = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + setLocalStorage("companyName", companyName); + setLocalStorage("companySize", companySizeChoicesState[0]); + if (companyPhone.rawInputValue !== "") + setLocalStorage("companyPhone", companyPhone); + navigate("./../4"); + }, + [navigate, companyName, companySizeChoicesState, companyPhone] + ); + const onClickBack = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + navigate("./../2"); + }, + [navigate] + ); + const theme = useTheme(); + const inputStyles = useMemo(() => { + return { + fieldGroup: { + "border-color": theme.semanticColors.variantBorder, + }, + }; + }, [theme]); + const backButtonStyles = useMemo(() => { + return { + root: { + border: "none", + "background-color": theme.semanticColors.bodyStandoutBackground, + }, + }; + }, [theme]); + return ( + } + className={styles.nextButton} + disabled={companySizeEmpty || companyName === ""} + /> + } + backButton={ + } + /> + } + > +
+ + setCompanyName(v!)} + /> + + setCompanyPhone(v)} + /> + +
+
+ ); +} + +function Step3Individual(_props: StepProps) { + const individualWebsiteFromLocalStorage = + getFromLocalStorage("individualWebsite"); + const [individualWebsite, setIndividualWebsite] = useState( + individualWebsiteFromLocalStorage === undefined + ? "" + : (individualWebsiteFromLocalStorage as string) + ); + const individualPhoneFromLocalStorage = getFromLocalStorage("companyPhone"); + const defaultPhone: PhoneTextFieldValues = ( + individualPhoneFromLocalStorage === undefined + ? { rawInputValue: "" } + : individualPhoneFromLocalStorage + ) as PhoneTextFieldValues; + const [individualPhone, setIndividualPhone] = useState(defaultPhone); + const { renderToString } = useContext(Context); + const navigate = useNavigate(); + useEffect(() => { + goToFirstUnfilled(3, navigate); + if (getFromLocalStorage("toriChoices") === "Team") navigate("./../3-team"); + }, [navigate]); + const onClickNext = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + if (individualWebsite !== "") + setLocalStorage("individualWebsite", individualWebsite); + if (individualPhone.rawInputValue !== "") + setLocalStorage("individualPhone", individualPhone); + navigate("./../4"); + }, + [navigate, individualWebsite, individualPhone] + ); + const onClickBack = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + navigate("./../2"); + }, + [navigate] + ); + const theme = useTheme(); + const inputStyles = useMemo(() => { + return { + fieldGroup: { + "border-color": theme.semanticColors.variantBorder, + }, + }; + }, [theme]); + const backButtonStyles = useMemo(() => { + return { + root: { + border: "none", + "background-color": theme.semanticColors.bodyStandoutBackground, + }, + }; + }, [theme]); + return ( + } + className={styles.nextButton} + disabled={false} + /> + } + backButton={ + } + /> + } + > +
+ + setIndividualWebsite(v!)} + /> + setIndividualPhone(v)} + /> + +
+
+ ); +} + +function Step4(_props: StepProps) { + const prefix = "step4"; + const reasonChoiceGroup = "reasonChoiceGroup"; + const reasonChoices = ["Auth", "SSO", "Security", "Portal", "Other"]; + const reasonChoicesFromLocalStorage = getFromLocalStorage("reasonChoices"); + const defaultReasonChoices: string[] = ( + reasonChoicesFromLocalStorage === undefined + ? [] + : reasonChoicesFromLocalStorage + ) as string[]; + const [reasonChoicesState, setReasonChoicesState] = + useState(defaultReasonChoices); + const empty = useMemo(() => { + return reasonChoicesState.length === 0; + }, [reasonChoicesState]); + const { renderToString } = useContext(Context); + const otherReasonFromLocalStorage = getFromLocalStorage("otherReason"); + const [otherReason, setOtherReason] = useState( + otherReasonFromLocalStorage === undefined + ? "" + : (otherReasonFromLocalStorage as string) + ); + const navigate = useNavigate(); + useEffect(() => goToFirstUnfilled(4, navigate), [navigate]); + const onClickNext = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + setLocalStorage("reasonChoices", reasonChoicesState); + if (otherReason !== "") setLocalStorage("otherReason", otherReason); + localStorage.removeItem(localStorageKey); + navigate("./../../projects/create"); + }, + [navigate, reasonChoicesState, otherReason] + ); + const onClickBack = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + navigate( + getFromLocalStorage("toriChoices") === "Team" + ? "./../3-team" + : "./../3-individual" + ); + }, + [navigate] + ); + const theme = useTheme(); + const inputStyles = useMemo(() => { + return { + fieldGroup: { + "border-color": theme.semanticColors.variantBorder, + }, + }; + }, [theme]); + const backButtonStyles = useMemo(() => { + return { + root: { + border: "none", + "background-color": theme.semanticColors.bodyStandoutBackground, + }, + }; + }, [theme]); + return ( + + } + backButton={ + } + /> + } + > +
+ + {reasonChoicesState.includes("Other") ? ( + + setOtherReason(v!)} + /> + + ) : null} +
+
+ ); +} + +export const OnboardingSurveyScreen: React.VFC = + function OnboardingSurveyScreen() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + + ); + }; + +export default OnboardingSurveyScreen; diff --git a/portal/src/ReactApp.tsx b/portal/src/ReactApp.tsx index a7209bcaab..1e02b799f5 100644 --- a/portal/src/ReactApp.tsx +++ b/portal/src/ReactApp.tsx @@ -57,6 +57,9 @@ const AppsScreen = lazy(async () => import("./graphql/portal/AppsScreen")); const CreateProjectScreen = lazy( async () => import("./graphql/portal/CreateProjectScreen") ); +const OnboardingSurveyScreen = lazy( + async () => import("./OnboardingSurveyScreen") +); const ProjectWizardScreen = lazy( async () => import("./graphql/portal/ProjectWizardScreen") ); @@ -126,7 +129,20 @@ const ReactAppRoutes: React.VFC = function ReactAppRoutes() { } /> - + + + }> + + + + } + /> + + + + + + + + + + + + + + + + + diff --git a/portal/src/locale-data/en.json b/portal/src/locale-data/en.json index 8a769f64ee..dc29f9e09c 100644 --- a/portal/src/locale-data/en.json +++ b/portal/src/locale-data/en.json @@ -1598,6 +1598,48 @@ "ProjectWizardScreen.step3.q3.option.oob_otp_email": "Receive One-Time-Password (OTP) via Email", "ProjectWizardScreen.step3.q3.option.password": "Additional Password", + "OnboardingSurveyScreen.step1.title": "Welcome to Authgear!\nWhat is your role?", + "OnboardingSurveyScreen.step1.subtitle": "This helps us personalize your experience.", + "OnboardingSurveyScreen.step1.roleChoiceGroup.Dev": "Developer", + "OnboardingSurveyScreen.step1.roleChoiceGroup.IT": "IT", + "OnboardingSurveyScreen.step1.roleChoiceGroup.PM": "Product Manager / Project Manager", + "OnboardingSurveyScreen.step1.roleChoiceGroup.PD": "Product Designer", + "OnboardingSurveyScreen.step1.roleChoiceGroup.Market": "Marketing", + "OnboardingSurveyScreen.step1.roleChoiceGroup.Owner": "Business Owner", + "OnboardingSurveyScreen.step1.roleChoiceGroup.Other": "Other", + "OnboardingSurveyScreen.step2.title": "Who will be using Authgear?", + "OnboardingSurveyScreen.step2.subtitle": "Are you creating this account for a company or personal project?", + "OnboardingSurveyScreen.step2.teamOrIndividualChoiceGroup.Team": "My Team/Company", + "OnboardingSurveyScreen.step2.teamOrIndividualChoiceGroup.Individual": "Just me", + "OnboardingSurveyScreen.step3-team.title": "Tell us about your company", + "OnboardingSurveyScreen.step3-team.subtitle": "This helps us recommend the right solution to you.", + "OnboardingSurveyScreen.step3-team.companyName.label": "Company Name", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.label": "Company Size", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.1-49": "1-49", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.50-99": "50-99", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.100-499": "100-499", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.500-1999": "500-1999", + "OnboardingSurveyScreen.step3-team.companySizeChoiceGroup.2000+": "2000+", + "OnboardingSurveyScreen.step3-team.phone.label": "Phone (Optional)", + "OnboardingSurveyScreen.step3-individual.title": "Tell us about your project", + "OnboardingSurveyScreen.step3-individual.subtitle": "This helps us recommend the right solution to you.", + "OnboardingSurveyScreen.step3-individual.projectWebsite.label": "Project Website (Optional)", + "OnboardingSurveyScreen.step3-individual.phone.label": "Phone (Optional)", + "OnboardingSurveyScreen.step4.title": "What brings you to Authgear?", + "OnboardingSurveyScreen.step4.subtitle": "This helps us recommend the right solution for you.", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Auth.title": "Authentication", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Auth.subtitle": "I'm building a new software product and looking for auth.", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.SSO.title": "Enterprise SSO", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.SSO.subtitle": "I'm looking for a SSO solution for multiple apps in my company.", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Security.title": "Security", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Security.subtitle": "I want to enhance security by realtime security monitoring, brute-force attack prevention.", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Portal.title": "Management Portal", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Portal.subtitle": "I'm looking for an identity and user management solution for frontline staffs of my company.", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Other.title": "A different reason", + "OnboardingSurveyScreen.step4.reasonChoiceGroup.Other.subtitle": "Describe your reason", + "OnboardingSurveyScreen.step4.otherReason.label": "Describe your reason (Optional)", + "OnboardingSurveyScreen.step4.finish": "Finish Survey", + "GetStartedScreen.title": "Getting Started 🎉", "GetStartedScreen.description": "Here are a few steps to get you set up for success.", "GetStartedScreen.counter": "{remaining, plural, =0{Done! You are ready to go.} other{# remaining}}",