1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-08 23:15:24 +02:00

add back onboarding

This commit is contained in:
Tyler Myracle 2024-01-12 23:46:54 -06:00
parent 093949f447
commit e405fb80f6
12 changed files with 96 additions and 53 deletions

View file

@ -1,19 +1,19 @@
import { useAuth0 } from '@auth0/auth0-react'
import { useEffect } from 'react' import { useEffect } from 'react'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { useSession } from 'next-auth/react'
export default function APM() { export default function APM() {
const { user } = useAuth0() const { data: session } = useSession()
// Identify Sentry user // Identify Sentry user
useEffect(() => { useEffect(() => {
if (user) { if (session && session.user) {
Sentry.setUser({ Sentry.setUser({
id: user.sub, id: session.user['sub'] ?? undefined,
email: user.email, email: session.user['https://maybe.co'] ?? undefined,
}) })
} }
}, [user]) }, [session])
return null return null
} }

View file

@ -8,9 +8,8 @@ import {
ErrorFallback, ErrorFallback,
LogProvider, LogProvider,
UserAccountContextProvider, UserAccountContextProvider,
AuthProvider,
} from '@maybe-finance/client/shared' } from '@maybe-finance/client/shared'
import { AccountsManager } from '@maybe-finance/client/features' import { AccountsManager, OnboardingGuard } from '@maybe-finance/client/features'
import { AccountContextProvider } from '@maybe-finance/client/shared' import { AccountContextProvider } from '@maybe-finance/client/shared'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing' import { BrowserTracing } from '@sentry/tracing'
@ -46,6 +45,7 @@ const WithAuth = function ({ children }: PropsWithChildren) {
if (session) { if (session) {
return ( return (
<OnboardingGuard>
<ModalManager> <ModalManager>
<UserAccountContextProvider> <UserAccountContextProvider>
<AccountContextProvider> <AccountContextProvider>
@ -56,6 +56,7 @@ const WithAuth = function ({ children }: PropsWithChildren) {
</AccountContextProvider> </AccountContextProvider>
</UserAccountContextProvider> </UserAccountContextProvider>
</ModalManager> </ModalManager>
</OnboardingGuard>
) )
} }
return null return null
@ -84,7 +85,6 @@ export default function App({
<Meta /> <Meta />
<Analytics /> <Analytics />
<QueryProvider> <QueryProvider>
<AuthProvider>
<SessionProvider> <SessionProvider>
<AxiosProvider> <AxiosProvider>
<> <>
@ -97,7 +97,6 @@ export default function App({
</> </>
</AxiosProvider> </AxiosProvider>
</SessionProvider> </SessionProvider>
</AuthProvider>
</QueryProvider> </QueryProvider>
</ErrorBoundary> </ErrorBoundary>
</LogProvider> </LogProvider>

View file

@ -3,7 +3,6 @@ import { useQueryParam } from '@maybe-finance/client/shared'
import { import {
AccountSidebar, AccountSidebar,
BillingPreferences, BillingPreferences,
GeneralPreferences,
SecurityPreferences, SecurityPreferences,
UserDetails, UserDetails,
WithSidebarLayout, WithSidebarLayout,

View file

@ -105,6 +105,15 @@ router.put(
}) })
) )
router.get(
'/auth-profile',
endpoint.create({
resolve: async ({ ctx }) => {
return ctx.userService.getAuthProfile(ctx.user!.id)
},
})
)
// TODO: Remove this endpoint // TODO: Remove this endpoint
router.get( router.get(
'/auth0-profile', '/auth0-profile',

View file

@ -1,16 +1,21 @@
import type { SharedType } from '@maybe-finance/shared' import type { SharedType } from '@maybe-finance/shared'
import { BrowserUtil, useAccountApi, useAccountContext } from '@maybe-finance/client/shared' import {
BrowserUtil,
useAccountApi,
useAccountContext,
useUserApi,
} from '@maybe-finance/client/shared'
import { Menu } from '@maybe-finance/design-system' import { Menu } from '@maybe-finance/design-system'
import { RiDeleteBin5Line, RiPencilLine, RiRefreshLine } from 'react-icons/ri' import { RiDeleteBin5Line, RiPencilLine, RiRefreshLine } from 'react-icons/ri'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useAuth0 } from '@auth0/auth0-react'
type Props = { type Props = {
account?: SharedType.AccountDetail account?: SharedType.AccountDetail
} }
export function AccountMenu({ account }: Props) { export function AccountMenu({ account }: Props) {
const { user } = useAuth0() const { useProfile } = useUserApi()
const user = useProfile()
const { editAccount, deleteAccount } = useAccountContext() const { editAccount, deleteAccount } = useAccountContext()
const { useSyncAccount } = useAccountApi() const { useSyncAccount } = useAccountApi()

View file

@ -22,9 +22,9 @@ import {
RiArrowRightSLine, RiArrowRightSLine,
} from 'react-icons/ri' } from 'react-icons/ri'
import { Button, Tooltip } from '@maybe-finance/design-system' import { Button, Tooltip } from '@maybe-finance/design-system'
import { useAuth0 } from '@auth0/auth0-react'
import { MenuPopover } from './MenuPopover' import { MenuPopover } from './MenuPopover'
import { SidebarOnboarding } from '../onboarding' import { SidebarOnboarding } from '../onboarding'
import { useSession } from 'next-auth/react'
export interface DesktopLayoutProps { export interface DesktopLayoutProps {
sidebar: React.ReactNode sidebar: React.ReactNode
@ -93,7 +93,8 @@ export function DesktopLayout({ children, sidebar }: DesktopLayoutProps) {
const [onboardingExpanded, setOnboardingExpanded] = useState(false) const [onboardingExpanded, setOnboardingExpanded] = useState(false)
const { popoutContents, close: closePopout } = usePopoutContext() const { popoutContents, close: closePopout } = usePopoutContext()
const { user } = useAuth0() const { data: session } = useSession()
const user = session!.user
const { useOnboarding, useUpdateOnboarding } = useUserApi() const { useOnboarding, useUpdateOnboarding } = useUserApi()
const onboarding = useOnboarding('sidebar') const onboarding = useOnboarding('sidebar')
const updateOnboarding = useUpdateOnboarding() const updateOnboarding = useUpdateOnboarding()
@ -268,8 +269,8 @@ export function DesktopLayout({ children, sidebar }: DesktopLayoutProps) {
)} )}
</AnimatePresence> </AnimatePresence>
} }
name={user?.name} name={user?.name ?? ''}
email={user?.email} email={user?.email ?? ''}
> >
{sidebar} {sidebar}
</DefaultContent> </DefaultContent>

View file

@ -237,7 +237,7 @@ export function SidebarOnboarding({ onClose, onHide }: Props) {
const description = getDescriptionComponent(step.key) const description = getDescriptionComponent(step.key)
return ( return (
<Disclosure> <Disclosure key={idx}>
{({ open }) => ( {({ open }) => (
<div <div
className={classNames( className={classNames(

View file

@ -8,14 +8,14 @@ import { useUserApi } from '@maybe-finance/client/shared'
import type { StepProps } from '../StepProps' import type { StepProps } from '../StepProps'
export function EmailVerification({ title, onNext }: StepProps) { export function EmailVerification({ title, onNext }: StepProps) {
const { useAuth0Profile, useResendEmailVerification } = useUserApi() const { useAuthProfile, useResendEmailVerification } = useUserApi()
const emailVerified = useRef(false) const emailVerified = useRef(false)
const profile = useAuth0Profile({ const profile = useAuthProfile({
refetchInterval: emailVerified.current ? false : 5_000, refetchInterval: emailVerified.current ? false : 5_000,
onSuccess: (data) => { onSuccess: (data) => {
if (data.email_verified) { if (data.emailVerified) {
emailVerified.current = true emailVerified.current = true
} }
}, },
@ -70,7 +70,7 @@ export function EmailVerification({ title, onNext }: StepProps) {
'linear-gradient(180deg, rgba(35, 36, 40, 0.2) 0%, rgba(68, 71, 76, 0.2) 100%)', 'linear-gradient(180deg, rgba(35, 36, 40, 0.2) 0%, rgba(68, 71, 76, 0.2) 100%)',
}} }}
> >
{profile.data?.email_verified ? ( {profile.data?.emailVerified ? (
<RiMailCheckLine className="w-6 h-6" /> <RiMailCheckLine className="w-6 h-6" />
) : ( ) : (
<RiMailSendLine className="w-6 h-6" /> <RiMailSendLine className="w-6 h-6" />
@ -78,10 +78,10 @@ export function EmailVerification({ title, onNext }: StepProps) {
</div> </div>
</div> </div>
<h3 className="mt-12 text-center"> <h3 className="mt-12 text-center">
{profile.data?.email_verified ? 'Email verified' : title} {profile.data?.emailVerified ? 'Email verified' : title}
</h3> </h3>
<div className="text-base text-center"> <div className="text-base text-center">
{profile.data?.email_verified ? ( {profile.data?.emailVerified ? (
<p className="mt-4 text-gray-50"> <p className="mt-4 text-gray-50">
You have successfully verified{' '} You have successfully verified{' '}
<span className="text-gray-25">{profile.data?.email ?? 'your email'}</span> <span className="text-gray-25">{profile.data?.email ?? 'your email'}</span>
@ -130,7 +130,7 @@ export function EmailVerification({ title, onNext }: StepProps) {
</> </>
)} )}
</div> </div>
{profile.data?.email_verified && ( {profile.data?.emailVerified && (
<Button className="mt-5" fullWidth onClick={onNext}> <Button className="mt-5" fullWidth onClick={onNext}>
Continue setup <RiArrowRightLine className="ml-2 w-5 h-5" /> Continue setup <RiArrowRightLine className="ml-2 w-5 h-5" />
</Button> </Button>

View file

@ -65,6 +65,11 @@ const UserApi = (
return data return data
}, },
async getAuthProfile() {
const { data } = await axios.get<SharedType.AuthUser>('/users/auth-profile')
return data
},
async getAuth0Profile() { async getAuth0Profile() {
const { data } = await axios.get<SharedType.Auth0Profile>('/users/auth0-profile') const { data } = await axios.get<SharedType.Auth0Profile>('/users/auth0-profile')
return data return data
@ -160,10 +165,10 @@ const UserApi = (
return data return data
}, },
async resendEmailVerification(auth0Id?: string) { async resendEmailVerification(authId?: string) {
const { data } = await axios.post<{ success: boolean }>( const { data } = await axios.post<{ success: boolean }>(
'/users/resend-verification-email', '/users/resend-verification-email',
{ auth0Id } { authId }
) )
return data return data
@ -288,6 +293,17 @@ export function useUserApi() {
...options, ...options,
}) })
const useAuthProfile = (
options?: Omit<
UseQueryOptions<SharedType.AuthUser, unknown, SharedType.AuthUser, any[]>,
'queryKey' | 'queryFn'
>
) =>
useQuery(['auth-profile'], api.getAuthProfile, {
staleTime: staleTimes.user,
...options,
})
const useAuth0Profile = ( const useAuth0Profile = (
options?: Omit<UseQueryOptions<SharedType.Auth0Profile>, 'queryKey' | 'queryFn'> options?: Omit<UseQueryOptions<SharedType.Auth0Profile>, 'queryKey' | 'queryFn'>
) => useQuery(['users', 'auth0-profile'], api.getAuth0Profile, options) ) => useQuery(['users', 'auth0-profile'], api.getAuth0Profile, options)
@ -403,6 +419,7 @@ export function useUserApi() {
useCurrentNetWorth, useCurrentNetWorth,
useProfile, useProfile,
useUpdateProfile, useUpdateProfile,
useAuthProfile,
useAuth0Profile, useAuth0Profile,
useUpdateAuth0Profile, useUpdateAuth0Profile,
useSubscription, useSubscription,

View file

@ -1,7 +1,7 @@
import type { Dispatch, MouseEventHandler, PropsWithChildren, SetStateAction } from 'react' import type { Dispatch, MouseEventHandler, PropsWithChildren, SetStateAction } from 'react'
import type { IconType } from 'react-icons' import type { IconType } from 'react-icons'
import type { PopperProps } from 'react-popper' import type { PopperProps } from 'react-popper'
import React, { createContext, useContext, useState, useEffect } from 'react' import React, { createContext, useContext, useState, useEffect, useRef } from 'react'
import { Listbox as HeadlessListbox } from '@headlessui/react' import { Listbox as HeadlessListbox } from '@headlessui/react'
import classNames from 'classnames' import classNames from 'classnames'
import { RiArrowDownSFill, RiCheckFill } from 'react-icons/ri' import { RiArrowDownSFill, RiCheckFill } from 'react-icons/ri'
@ -166,6 +166,8 @@ function Options({
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null) const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const isOpenRef = useRef(false)
const { styles, attributes, update } = usePopper(referenceElement, popperElement, { const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
placement, placement,
modifiers: [ modifiers: [
@ -180,6 +182,7 @@ function Options({
useEffect(() => { useEffect(() => {
if (isOpen && update) update() if (isOpen && update) update()
if (isOpenRef.current !== isOpen) setIsOpen(isOpenRef.current)
}, [isOpen, update]) }, [isOpen, update])
return ( return (
@ -198,7 +201,7 @@ function Options({
{...rest} {...rest}
> >
{({ open }) => { {({ open }) => {
setIsOpen(open) isOpenRef.current = open
return children return children
}} }}
</HeadlessListbox.Options> </HeadlessListbox.Options>

View file

@ -46,6 +46,8 @@ function Items({
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null) const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const isOpenRef = useRef(false)
const { styles, attributes, update } = usePopper(referenceElement?.current, popperElement, { const { styles, attributes, update } = usePopper(referenceElement?.current, popperElement, {
placement, placement,
modifiers: [ modifiers: [
@ -60,6 +62,7 @@ function Items({
useEffect(() => { useEffect(() => {
if (isOpen && update) update() if (isOpen && update) update()
if (isOpenRef.current !== isOpen) setIsOpen(isOpenRef.current)
}, [isOpen, update]) }, [isOpen, update])
return ( return (
@ -75,7 +78,7 @@ function Items({
{...rest} {...rest}
> >
{(renderProps) => { {(renderProps) => {
setIsOpen(renderProps.open) isOpenRef.current = renderProps.open
return typeof children === 'function' ? children(renderProps) : children return typeof children === 'function' ? children(renderProps) : children
}} }}
</HeadlessMenu.Items> </HeadlessMenu.Items>

View file

@ -65,6 +65,13 @@ export class UserService implements IUserService {
}) })
} }
async getAuthProfile(id: User['id']): Promise<SharedType.AuthUser> {
const user = await this.get(id)
return this.prisma.authUser.findUniqueOrThrow({
where: { id: user.authId },
})
}
// TODO: Update this to use new Auth // TODO: Update this to use new Auth
async getAuth0Profile(user: User): Promise<SharedType.Auth0Profile> { async getAuth0Profile(user: User): Promise<SharedType.Auth0Profile> {
if (!user.email) throw new Error('No email found for user') if (!user.email) throw new Error('No email found for user')
@ -371,7 +378,7 @@ export class UserService implements IUserService {
.setTitle((_) => "Before we start, let's verify your email") .setTitle((_) => "Before we start, let's verify your email")
.addToGroup('setup') .addToGroup('setup')
.completeIf((user) => user.emailVerified) .completeIf((user) => user.emailVerified)
.excludeIf((user) => user.isAppleIdentity) // Auth0 auto-verifies Apple identities. .excludeIf((user) => user.isAppleIdentity || true) // TODO: Needs email service to send, skip for now
onboarding onboarding
.addStep('firstAccount') .addStep('firstAccount')