mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
add back onboarding
This commit is contained in:
parent
093949f447
commit
e405fb80f6
12 changed files with 96 additions and 53 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,16 +45,18 @@ const WithAuth = function ({ children }: PropsWithChildren) {
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
return (
|
return (
|
||||||
<ModalManager>
|
<OnboardingGuard>
|
||||||
<UserAccountContextProvider>
|
<ModalManager>
|
||||||
<AccountContextProvider>
|
<UserAccountContextProvider>
|
||||||
{children}
|
<AccountContextProvider>
|
||||||
|
{children}
|
||||||
|
|
||||||
{/* Add, edit, delete connections and manual accounts */}
|
{/* Add, edit, delete connections and manual accounts */}
|
||||||
<AccountsManager />
|
<AccountsManager />
|
||||||
</AccountContextProvider>
|
</AccountContextProvider>
|
||||||
</UserAccountContextProvider>
|
</UserAccountContextProvider>
|
||||||
</ModalManager>
|
</ModalManager>
|
||||||
|
</OnboardingGuard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -84,20 +85,18 @@ export default function App({
|
||||||
<Meta />
|
<Meta />
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<AuthProvider>
|
<SessionProvider>
|
||||||
<SessionProvider>
|
<AxiosProvider>
|
||||||
<AxiosProvider>
|
<>
|
||||||
<>
|
<APM />
|
||||||
<APM />
|
{Page.isPublic === true ? (
|
||||||
{Page.isPublic === true ? (
|
getLayout(<Page {...pageProps} />)
|
||||||
getLayout(<Page {...pageProps} />)
|
) : (
|
||||||
) : (
|
<WithAuth>{getLayout(<Page {...pageProps} />)}</WithAuth>
|
||||||
<WithAuth>{getLayout(<Page {...pageProps} />)}</WithAuth>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
</AxiosProvider>
|
||||||
</AxiosProvider>
|
</SessionProvider>
|
||||||
</SessionProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</LogProvider>
|
</LogProvider>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useQueryParam } from '@maybe-finance/client/shared'
|
||||||
import {
|
import {
|
||||||
AccountSidebar,
|
AccountSidebar,
|
||||||
BillingPreferences,
|
BillingPreferences,
|
||||||
GeneralPreferences,
|
|
||||||
SecurityPreferences,
|
SecurityPreferences,
|
||||||
UserDetails,
|
UserDetails,
|
||||||
WithSidebarLayout,
|
WithSidebarLayout,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue