Merge pull request #14 from blockworks-foundation/onboarding-tours

Onboarding tours
This commit is contained in:
tjshipe 2022-10-04 13:24:45 -04:00 committed by GitHub
commit f21af8f497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 629 additions and 367 deletions

View File

@ -8,12 +8,7 @@ import BottomBar from './mobile/BottomBar'
import BounceLoader from './shared/BounceLoader'
import TopBar from './TopBar'
import useLocalStorageState from '../hooks/useLocalStorageState'
import {
IS_ONBOARDED_KEY,
ONBOARDING_TOUR_KEY,
SIDEBAR_COLLAPSE_KEY,
} from '../utils/constants'
import OnboardingTour from './OnboardingTour'
import { SIDEBAR_COLLAPSE_KEY } from '../utils/constants'
const sideBarAnimationDuration = 500
@ -24,8 +19,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
SIDEBAR_COLLAPSE_KEY,
false
)
const [showOnboardingTour] = useLocalStorageState(ONBOARDING_TOUR_KEY, false)
const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
const { width } = useViewport()
useEffect(() => {
@ -94,9 +87,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
</div>
</div>
</div>
{showOnboardingTour && isOnboarded && connected ? (
<OnboardingTour />
) : null}
</>
)
}

View File

@ -50,7 +50,7 @@ const MangoAccountsList = ({
}
return (
<div id="step-one">
<div id="account-step-two">
<Popover>
{({ open }) => (
<>

View File

@ -1,180 +0,0 @@
import { XMarkIcon } from '@heroicons/react/20/solid'
import { useRouter } from 'next/router'
import {
CardinalOrientation,
MaskOptions,
Walktour,
WalktourLogic,
} from 'walktour'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { ONBOARDING_TOUR_KEY } from '../utils/constants'
const OnboardingTour = () => {
const [, setShowOnboardingTour] = useLocalStorageState(ONBOARDING_TOUR_KEY)
const router = useRouter()
const renderTooltip = (tourLogic: WalktourLogic | undefined) => {
const { title, description } = tourLogic!.stepContent
const { next, prev, close, allSteps, stepIndex } = tourLogic!
const handleClose = () => {
setShowOnboardingTour(false)
close()
}
return (
<div className="relative w-72 rounded-lg bg-gradient-to-b from-gradient-start via-gradient-mid to-gradient-end p-4">
<button
onClick={handleClose}
className={`absolute right-4 top-4 z-50 text-th-bkg-3 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XMarkIcon className={`h-6 w-6`} />
</button>
<h3 className="text-th-bkg-1">{title}</h3>
<p className="text-sm text-th-bkg-1">{description}</p>
<div className="mt-4 flex items-center justify-between">
{stepIndex !== 0 ? (
<button
className="default-transition h-8 rounded-md border border-th-bkg-1 px-3 font-bold text-th-bkg-1 focus:outline-none md:hover:border-th-bkg-3 md:hover:text-th-bkg-3"
onClick={() => prev()}
>
Back
</button>
) : (
<div className="h-8 w-[58.25px]" />
)}
<div className="flex space-x-2">
{allSteps.map((s, i) => (
<div
className={`h-1 w-1 rounded-full ${
i === stepIndex ? 'bg-th-primary' : 'bg-[rgba(0,0,0,0.2)]'
}`}
key={s.title}
/>
))}
</div>
{stepIndex !== allSteps.length - 1 ? (
<button
className="default-transition h-8 rounded-md bg-th-bkg-1 px-3 font-bold text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-3"
onClick={() => next()}
>
Next
</button>
) : (
<button
className="default-transition h-8 rounded-md bg-th-bkg-1 px-3 font-bold text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-3"
onClick={handleClose}
>
Finish
</button>
)}
</div>
</div>
)
}
const steps = [
{
selector: '#step-one',
title: 'Your Accounts',
description:
'Switch between accounts and create new ones. Use multiple accounts to trade isolated margin and protect your capital from liquidation.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#step-two',
title: 'Account Value',
description:
'The value of your assets (deposits) minus the value of your liabilities (borrows).',
orientationPreferences: [CardinalOrientation.EASTNORTH],
movingTarget: true,
customNextFunc: (tourLogic: WalktourLogic) => {
router.push('/')
setTimeout(() => tourLogic.next(), 1000)
},
},
{
selector: '#step-three',
title: 'Health',
description:
'If your account health reaches 0% your account will be liquidated. You can increase the health of your account by making a deposit.',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#step-four',
title: 'Free Collateral',
description:
"The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals.",
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#step-five',
title: 'Total Interest Value',
description:
'The value of interest earned (deposits) minus interest paid (borrows).',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#step-six',
title: 'Health Check',
description:
'Check the health of your account from any screen in the app. A green heart represents good health, orange okay and red poor.',
orientationPreferences: [CardinalOrientation.EASTSOUTH],
movingTarget: true,
customNextFunc: (tourLogic: WalktourLogic) => {
router.push('/swap')
setTimeout(() => tourLogic.next(), 1000)
},
},
{
selector: '#step-seven',
title: 'Swap',
description:
"You choose the quote token of your trades. This means you can easily trade tokens on their relative strength vs. another token. Let's say your thesis is BTC will see diminishing returns relative to SOL. You can sell BTC and buy SOL. Now you are long SOL/BTC",
orientationPreferences: [CardinalOrientation.CENTER],
customPrevFunc: (tourLogic: WalktourLogic) => {
router.push('/swap')
tourLogic.prev()
},
},
{
selector: '#step-eight',
title: 'Trade Settings',
description:
'Edit your slippage settings and toggle margin on and off. When margin is off your trades will be limited by your balance for each token.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
{
selector: '#step-nine',
title: 'Token to Sell',
description:
'Select the token you want to sell. If your sell size is above your token balance a loan will be opened to cover the shortfall.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
{
selector: '#step-ten',
title: 'Health Impact',
description:
'Projects the health of your account before you make a trade.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
]
return (
<Walktour
customTooltipRenderer={renderTooltip}
steps={steps}
updateInterval={200}
disableCloseOnClick
/>
)
}
export default OnboardingTour

View File

@ -20,26 +20,16 @@ import MangoAccountSummary from './account/MangoAccountSummary'
import Tooltip from './shared/Tooltip'
import { HealthType } from '@blockworks-foundation/mango-v4'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from '../hooks/useLocalStorageState'
import { ONBOARDING_TOUR_KEY } from '../utils/constants'
import mangoStore from '@store/mangoStore'
import HealthHeart from './account/HealthHeart'
const SideNav = ({ collapsed }: { collapsed: boolean }) => {
const [, setShowOnboardingTour] = useLocalStorageState(ONBOARDING_TOUR_KEY)
const { t } = useTranslation('common')
const { connected } = useWallet()
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const router = useRouter()
const { pathname } = router
const handleTakeTour = () => {
if (pathname !== '/') {
router.push('/')
}
setShowOnboardingTour(true)
}
return (
<div
className={`transition-all duration-300 ${
@ -144,15 +134,6 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
isExternal
showTooltip={false}
/>
{connected ? (
<button
className="default-transition mt-1 flex items-center px-4 text-th-fgd-2 md:hover:text-th-primary"
onClick={handleTakeTour}
>
<InformationCircleIcon className="mr-3 h-5 w-5" />
<span className="text-base">Take UI Tour</span>
</button>
) : null}
</ExpandableMenuItem>
</div>
</div>

View File

@ -105,21 +105,17 @@ const TokenList = () => {
</Tooltip>
</div>
</th>
<th>
<div className="flex justify-end">
<Tooltip content="The sum of interest earned and interest paid for each token.">
<span className="tooltip-underline">
{t('interest-earned-paid')}
</span>
</Tooltip>
</div>
<th className="text-right" id="account-step-eight">
<Tooltip content="The sum of interest earned and interest paid for each token.">
<span className="tooltip-underline">
{t('interest-earned-paid')}
</span>
</Tooltip>
</th>
<th>
<div className="flex justify-end">
<Tooltip content="The interest rates (per year) for depositing (green/left) and borrowing (red/right)">
<span className="tooltip-underline">{t('rates')}</span>
</Tooltip>
</div>
<th className="text-right" id="account-step-nine">
<Tooltip content="The interest rates (per year) for depositing (green/left) and borrowing (red/right).">
<span className="tooltip-underline">{t('rates')}</span>
</Tooltip>
</th>
<th className="text-right">{t('price')}</th>
<th className="hidden text-right lg:block"></th>
@ -127,7 +123,7 @@ const TokenList = () => {
</tr>
</thead>
<tbody>
{banks.map(({ key, value }) => {
{banks.map(({ key, value }, i) => {
const bank = value[0]
const oraclePrice = bank.uiPrice
@ -256,7 +252,10 @@ const TokenList = () => {
</div>
</td>
<td>
<div className="flex justify-end space-x-2">
<div
className="flex justify-end space-x-2"
id={i === 0 ? 'account-step-ten' : ''}
>
<ActionsMenu bank={bank} mangoAccount={mangoAccount} />
</div>
</td>

View File

@ -1,8 +1,6 @@
import {
HealthType,
I80F48,
toUiDecimalsForQuote,
ZERO_I80F48,
} from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -13,7 +11,6 @@ import WithdrawModal from '../modals/WithdrawModal'
import mangoStore, { PerformanceDataItem } from '@store/mangoStore'
import { formatDecimal, formatFixedDecimals } from '../../utils/numbers'
import FlipNumbers from 'react-flip-numbers'
import { DownTriangle, UpTriangle } from '../shared/DirectionTriangles'
import SimpleAreaChart from '../shared/SimpleAreaChart'
import { COLORS } from '../../styles/colors'
import { useTheme } from 'next-themes'
@ -31,6 +28,10 @@ import { breakpoints } from '../../utils/theme'
import useMangoAccount from '../shared/useMangoAccount'
import PercentageChange from '../shared/PercentageChange'
import Tooltip from '@components/shared/Tooltip'
import { IS_ONBOARDED_KEY } from 'utils/constants'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from 'hooks/useLocalStorageState'
import AccountOnboardingTour from '@components/tours/AccountOnboardingTour'
export async function getStaticProps({ locale }: { locale: string }) {
return {
@ -46,7 +47,8 @@ export async function getStaticProps({ locale }: { locale: string }) {
const AccountPage = () => {
const { t } = useTranslation('common')
const { mangoAccount, lastUpdatedAt } = useMangoAccount()
const { connected } = useWallet()
const { mangoAccount } = useMangoAccount()
const actions = mangoStore((s) => s.actions)
const loadPerformanceData = mangoStore(
(s) => s.mangoAccount.stats.performance.loading
@ -69,6 +71,8 @@ const AccountPage = () => {
const { theme } = useTheme()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const tourSettings = mangoStore((s) => s.settings.tours)
const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
useEffect(() => {
if (mangoAccount) {
@ -151,7 +155,7 @@ const AccountPage = () => {
<>
<div className="flex flex-wrap items-center justify-between border-b-0 border-th-bkg-3 px-6 pt-3 pb-0 md:border-b md:pb-3">
<div className="flex items-center space-x-6">
<div id="step-two">
<div id="account-step-three">
<Tooltip
maxWidth="20rem"
placement="bottom-start"
@ -243,7 +247,7 @@ const AccountPage = () => {
</div>
<div className="grid grid-cols-4 border-b border-th-bkg-3">
<div className="col-span-4 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-1 md:col-span-2 md:border-l md:border-t-0 lg:col-span-1">
<div id="step-three">
<div id="account-step-four">
<Tooltip
maxWidth="20rem"
placement="bottom-start"
@ -286,7 +290,7 @@ const AccountPage = () => {
</div>
</div>
<div className="col-span-4 flex border-t border-th-bkg-3 py-3 pl-6 md:col-span-1 md:col-span-2 md:border-l md:border-t-0 lg:col-span-1">
<div id="step-four">
<div id="account-step-five">
<Tooltip
content="The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals."
maxWidth="20rem"
@ -317,7 +321,7 @@ const AccountPage = () => {
onClick={() => handleChartToShow('pnl')}
>
<div className="flex items-center justify-between">
<div>
<div id="account-step-six">
<Tooltip
content="The amount your account has made or lost."
placement="bottom-start"
@ -342,7 +346,7 @@ const AccountPage = () => {
onClick={() => handleChartToShow('cumulative-interest-value')}
>
<div className="flex items-center justify-between">
<div id="step-five">
<div id="account-step-seven">
<Tooltip
content="The value of interest earned (deposits) minus interest paid (borrows)."
maxWidth="20rem"
@ -375,6 +379,9 @@ const AccountPage = () => {
onClose={() => setShowWithdrawModal(false)}
/>
) : null}
{!tourSettings?.account_tour_seen && isOnboarded && connected ? (
<AccountOnboardingTour />
) : null}
</>
) : (
<div className="p-6">

View File

@ -12,7 +12,7 @@ const HealthHeart = ({
return (
<svg
id="step-six"
id="account-step-eleven"
xmlns="http://www.w3.org/2000/svg"
className={
health

View File

@ -130,7 +130,7 @@ const UserSetupModal = ({ isOpen, onClose }: ModalProps) => {
})
await actions.reloadMangoAccount()
setShowSetupStep(4)
onClose()
setSubmitDeposit(false)
} catch (e: any) {
notify({
@ -379,7 +379,7 @@ const UserSetupModal = ({ isOpen, onClose }: ModalProps) => {
</Button>
<LinkButton
className="flex w-full justify-center"
onClick={() => setShowSetupStep(4)}
onClick={onClose}
>
<span className="default-transition text-th-fgd-4 underline md:hover:text-th-fgd-3 md:hover:no-underline">
Skip for now
@ -506,7 +506,7 @@ const UserSetupModal = ({ isOpen, onClose }: ModalProps) => {
Deposit
</div>
</Button>
<LinkButton onClick={() => setShowSetupStep(4)}>
<LinkButton onClick={onClose}>
<span className="default-transition text-th-fgd-4 underline md:hover:text-th-fgd-3 md:hover:no-underline">
Skip for now
</span>

View File

@ -237,7 +237,7 @@ const SwapForm = () => {
</EnterBottomExitBottom>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base text-th-fgd-2">{t('swap')}</h2>
<div id="step-eight">
<div id="swap-step-one">
<IconButton
className="text-th-fgd-2"
hideBg
@ -248,7 +248,10 @@ const SwapForm = () => {
</IconButton>
</div>
</div>
<div id="step-nine" className="mb-2 flex items-center justify-between">
<div
id="swap-step-two"
className="mb-2 flex items-center justify-between"
>
<p className="text-th-fgd-3">{t('swap:from')}</p>
<MaxSwapAmount
useMargin={useMargin}
@ -303,7 +306,7 @@ const SwapForm = () => {
</button>
</div>
<p className="mb-2 text-th-fgd-3">{t('swap:to')}</p>
<div className="mb-3 grid grid-cols-2">
<div id="swap-step-three" className="mb-3 grid grid-cols-2">
<div className="col-span-1 rounded-lg rounded-r-none border border-r-0 border-th-bkg-4 bg-th-bkg-1">
<TokenSelect
tokenSymbol={outputTokenInfo?.symbol || OUTPUT_TOKEN_DEFAULT}
@ -348,7 +351,7 @@ const SwapForm = () => {
/>
</div>
<div
id="step-ten"
id="swap-step-four"
className={`border-t border-th-bkg-3 px-6 transition-all ${
showHealthImpact ? 'max-h-40 py-4 ' : 'h-0'
}`}

View File

@ -2,26 +2,35 @@ import Swap from './SwapForm'
import SwapTokenChart from './SwapTokenChart'
import mangoStore from '@store/mangoStore'
import AccountTabs from '../account/AccountTabs'
import SwapOnboardingTour from '@components/tours/SwapOnboardingTour'
import { useWallet } from '@solana/wallet-adapter-react'
const SwapPage = () => {
const inputTokenInfo = mangoStore((s) => s.swap.inputTokenInfo)
const outputTokenInfo = mangoStore((s) => s.swap.outputTokenInfo)
const { connected } = useWallet()
const tourSettings = mangoStore((s) => s.settings.tours)
return (
<div className="grid grid-cols-12">
<div className="col-span-12 border-th-bkg-3 md:col-span-6 md:border-b lg:col-span-8">
<SwapTokenChart
inputTokenId={inputTokenInfo?.extensions?.coingeckoId}
outputTokenId={outputTokenInfo?.extensions?.coingeckoId}
/>
<>
<div className="grid grid-cols-12">
<div className="col-span-12 border-th-bkg-3 md:col-span-6 md:border-b lg:col-span-8">
<SwapTokenChart
inputTokenId={inputTokenInfo?.extensions?.coingeckoId}
outputTokenId={outputTokenInfo?.extensions?.coingeckoId}
/>
</div>
<div className="col-span-12 mt-2 space-y-6 border-th-bkg-3 md:col-span-6 md:mt-0 md:border-b lg:col-span-4">
<Swap />
</div>
<div className="col-span-12">
<AccountTabs />
</div>
</div>
<div className="col-span-12 mt-2 space-y-6 border-th-bkg-3 md:col-span-6 md:mt-0 md:border-b lg:col-span-4">
<Swap />
</div>
<div className="col-span-12">
<AccountTabs />
</div>
</div>
{!tourSettings?.swap_tour_seen && connected ? (
<SwapOnboardingTour />
) : null}
</>
)
}

View File

@ -0,0 +1,119 @@
import { CardinalOrientation, Walktour, WalktourLogic } from 'walktour'
import CustomTooltip from './CustomTooltip'
const AccountOnboardingTour = () => {
const renderTooltip = (tourLogic: WalktourLogic | undefined) => {
return (
<CustomTooltip hasSeenKey="account_tour_seen" tourLogic={tourLogic!} />
)
}
const steps = [
{
selector: '#account-step-zero',
title: 'Your Account Dashboard',
description:
"Here you'll find the important information related to your account. Let us show you around. Click close to skip the tour at any time.",
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-one',
title: 'Profile Menu',
description:
"If you haven't chosen a profile name yet, you'll see your assigned one here. You can edit it and change your profile picture from this menu.",
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-two',
title: 'Your Accounts',
description:
'Switch between accounts and create new ones. Use multiple accounts to trade isolated margin and protect your capital from liquidation.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-three',
title: 'Account Value',
description:
'The value of your assets (deposits) minus the value of your liabilities (borrows).',
orientationPreferences: [CardinalOrientation.EASTNORTH],
movingTarget: true,
},
{
selector: '#account-step-four',
title: 'Health',
description:
'If your account health reaches 0% your account will be liquidated. You can increase the health of your account by making a deposit.',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#account-step-five',
title: 'Free Collateral',
description:
"The amount of capital you have to trade or borrow against. When your free collateral reaches $0 you won't be able to make withdrawals.",
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#account-step-six',
title: 'PnL (Profit and Loss)',
description: 'The amount your account has made or lost.',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#account-step-seven',
title: 'Total Interest Value',
description:
'The value of interest earned (deposits) minus interest paid (borrows).',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#account-step-eight',
title: 'Interest Earned',
description:
'The sum of interest earned and interest paid for each token.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-nine',
title: 'Rates',
description:
'The interest rates (per year) for depositing (green/left) and borrowing (red/right).',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-ten',
title: 'Token Actions',
description:
'Deposit, withdraw, borrow, buy and sell buttons for each token.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#account-step-eleven',
title: 'Health Check',
description:
'Check the health of your account from any screen in the app. A green heart represents good health, orange okay and red poor.',
orientationPreferences: [CardinalOrientation.EASTSOUTH],
movingTarget: true,
},
]
return (
<Walktour
customTooltipRenderer={renderTooltip}
steps={steps}
updateInterval={200}
disableCloseOnClick
/>
)
}
export default AccountOnboardingTour

View File

@ -0,0 +1,115 @@
import Loading from '@components/shared/Loading'
import { XMarkIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import mangoStore from '@store/mangoStore'
import { useState } from 'react'
import { WalktourLogic } from 'walktour'
const CustomTooltip = ({
customOnClose,
hasSeenKey,
tourLogic,
}: {
customOnClose?: () => void
hasSeenKey: 'account_tour_seen' | 'swap_tour_seen' | 'trade_tour_seen'
tourLogic: WalktourLogic | undefined
}) => {
const { title, description } = tourLogic!.stepContent
const { next, prev, close, allSteps, stepIndex } = tourLogic!
const { publicKey } = useWallet()
const actions = mangoStore((s) => s.actions)
const tourSettings = mangoStore((s) => s.settings.tours)
const [loading, setLoading] = useState(false)
const onClose = async () => {
if (!publicKey || !tourSettings) return
setLoading(true)
try {
const settings = {
...tourSettings,
}
settings[hasSeenKey] = true
const message = JSON.stringify(settings)
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: message,
}
const response = await fetch(
'https://mango-transaction-log.herokuapp.com/v4/user-data/settings-unsigned',
requestOptions
)
if (response.status === 200) {
await actions.fetchTourSettings(publicKey.toString())
}
} catch (e) {
console.log(e)
} finally {
if (customOnClose) {
customOnClose()
}
setLoading(false)
close()
}
}
return (
<div className="relative w-72 rounded-lg bg-gradient-to-b from-gradient-start via-gradient-mid to-gradient-end p-4">
{!loading ? (
<>
<button
onClick={onClose}
className={`absolute right-4 top-4 z-50 text-th-bkg-3 focus:outline-none md:right-2 md:top-2 md:hover:text-th-primary`}
>
<XMarkIcon className={`h-5 w-5`} />
</button>
<h3 className="text-th-bkg-1">{title}</h3>
<p className="text-sm text-th-bkg-1">{description}</p>
<div className="mt-4 flex items-center justify-between">
{stepIndex !== 0 ? (
<button
className="default-transition h-8 rounded-md border border-th-bkg-1 px-3 font-bold text-th-bkg-1 focus:outline-none md:hover:border-th-bkg-3 md:hover:text-th-bkg-3"
onClick={() => prev()}
>
Back
</button>
) : (
<div className="h-8 w-[58.25px]" />
)}
<div className="flex space-x-1.5">
{allSteps.map((s, i) => (
<div
className={`h-1 w-1 rounded-full ${
i === stepIndex ? 'bg-th-primary' : 'bg-[rgba(0,0,0,0.2)]'
}`}
key={s.title}
/>
))}
</div>
{stepIndex !== allSteps.length - 1 ? (
<button
className="default-transition h-8 rounded-md bg-th-bkg-1 px-3 font-bold text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-3"
onClick={() => next()}
>
Next
</button>
) : (
<button
className="default-transition h-8 rounded-md bg-th-bkg-1 px-3 font-bold text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-3"
onClick={onClose}
>
Finish
</button>
)}
</div>
</>
) : (
<div className="flex h-full items-center justify-center py-12">
<Loading />
</div>
)}
</div>
)
}
export default CustomTooltip

View File

@ -0,0 +1,61 @@
import { CardinalOrientation, Walktour, WalktourLogic } from 'walktour'
import CustomTooltip from './CustomTooltip'
const SwapOnboardingTour = () => {
const renderTooltip = (tourLogic: WalktourLogic | undefined) => {
return <CustomTooltip tourLogic={tourLogic!} hasSeenKey="swap_tour_seen" />
}
const steps = [
{
selector: '#swap-step-zero',
title: "We've Juiced Swap",
description:
"The swap you know and love + leverage. Swap lets you trade tokens on their relative strength. Let's say your thesis is BTC will see diminishing returns relative to SOL. You can sell BTC and buy SOL. Now you are long SOL/BTC",
orientationPreferences: [CardinalOrientation.CENTER],
},
{
selector: '#swap-step-one',
title: 'Swap Settings',
description:
'Edit your slippage settings and toggle margin on and off. When margin is off your swaps will be limited by your balance for each token.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
{
selector: '#swap-step-two',
title: 'From Token',
description:
'Select the token you want to swap from (sell). If you have margin on and your size is above your token balance a loan will be opened to cover the shortfall. Check the borrow rate before making a margin swap.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
{
selector: '#swap-step-three',
title: 'To Token',
description:
"The token you'll receive in your Mango Account after making a swap. You can think of this token as the one you're buying/longing.",
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
{
selector: '#swap-step-four',
title: 'Health Impact',
description:
'Projects the health of your account before you make a swap. The first value is your current account health and the second, your projected account health.',
orientationPreferences: [CardinalOrientation.WESTNORTH],
movingTarget: true,
},
]
return (
<Walktour
customTooltipRenderer={renderTooltip}
steps={steps}
updateInterval={200}
disableCloseOnClick
/>
)
}
export default SwapOnboardingTour

View File

@ -0,0 +1,108 @@
import { CardinalOrientation, Walktour, WalktourLogic } from 'walktour'
import CustomTooltip from './CustomTooltip'
const TradeOnboardingTour = () => {
const renderTooltip = (tourLogic: WalktourLogic | undefined) => {
return <CustomTooltip tourLogic={tourLogic!} hasSeenKey="trade_tour_seen" />
}
const steps = [
{
selector: '#trade-step-zero',
title: 'Trade 100s of Tokens...',
description:
'A refined interface without listing limits. The tokens you want to trade are now on Mango and no longer only quoted in USDC.',
orientationPreferences: [CardinalOrientation.CENTER],
},
{
selector: '#trade-step-one',
title: 'Market Selector',
description: 'Chose the market you want to trade.',
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#trade-step-two',
title: 'Oracle Price',
description:
"The oracle price uses an average of price data from many sources. It's used to avoid price manipulation which could lead to liquidations.",
orientationPreferences: [CardinalOrientation.SOUTHWEST],
movingTarget: true,
},
{
selector: '#trade-step-three',
title: 'Toggle Orderbook',
description:
'Use these buttons if you only want to see one side of the orderbook. Looking to bid/buy? Toggle off the buy orders to only see the sells and vice versa.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-four',
title: 'Orderbook Grouping',
description:
'Adjust the price intervals to change how orders are grouped. Small intervals will show more small orders in the book',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-five',
title: 'Recent Trades',
description:
'Shows the most recent orders for a market across all accounts.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-six',
title: 'Post Only',
description:
"An order condition that will only allow your order to enter the orderbook as a maker order. If the condition can't be met the order will be cancelled.",
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-seven',
title: 'Immediate or Cancel (IoC)',
description:
'An order condition that attempts to execute all or part of an order immediately and then cancels any unfilled portion.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-eight',
title: 'Margin',
description:
"When margin is on you can trade with more size than your token balance. Using margin increases your risk of loss. If you're not an experienced trader, use it with caution.",
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-nine',
title: 'Spread',
description:
'The difference between the prices quoted for an immediate sell (ask) and an immediate buy (bid). Or, in other words, the difference between the lowest sell price and the highest buy price.',
orientationPreferences: [CardinalOrientation.SOUTHEAST],
movingTarget: true,
},
{
selector: '#trade-step-ten',
title: 'Unsettled Balance',
description:
'When a limit order is filled, the funds are placed in your unsettled balances. When you have an unsettled balance you\'ll see a "Settle All" button above this table. Use it to move the funds to your account balance.',
orientationPreferences: [CardinalOrientation.NORTHEAST],
movingTarget: true,
},
]
return (
<Walktour
customTooltipRenderer={renderTooltip}
steps={steps}
updateInterval={200}
disableCloseOnClick
/>
)
}
export default TradeOnboardingTour

View File

@ -49,7 +49,10 @@ const MarketSelectDropdown = () => {
return (
<Popover>
{({ close, open }) => (
<div className="relative flex flex-col overflow-visible">
<div
className="relative flex flex-col overflow-visible"
id="trade-step-one"
>
<Popover.Button className="default-transition flex w-full items-center justify-between hover:text-th-primary">
<MarketLogos baseURI={baseLogoURI} quoteURI={quoteLogoURI} />
<div className="text-xl font-bold text-th-fgd-1 md:text-base">
@ -160,7 +163,7 @@ const AdvancedMarketHeader = () => {
<MarketSelectDropdown />
</div>
</div>
<div className="ml-6 flex-col">
<div id="trade-step-two" className="ml-6 flex-col">
<div className="text-xs text-th-fgd-4">{t('trade:oracle-price')}</div>
<OraclePrice />
</div>

View File

@ -370,7 +370,7 @@ const AdvancedTradeForm = () => {
<div className="flex flex-wrap px-5">
{tradeForm.tradeType === 'Limit' ? (
<div className="flex">
<div className="mr-4 mt-4">
<div className="mr-4 mt-4" id="trade-step-six">
<Tooltip
className="hidden md:block"
delay={250}
@ -385,7 +385,7 @@ const AdvancedTradeForm = () => {
</Checkbox>
</Tooltip>
</div>
<div className="mr-4 mt-4">
<div className="mr-4 mt-4" id="trade-step-seven">
<Tooltip
className="hidden md:block"
delay={250}
@ -404,7 +404,7 @@ const AdvancedTradeForm = () => {
</div>
</div>
) : null}
<div className="mt-4">
<div className="mt-4" id="trade-step-eight">
<Tooltip
delay={250}
placement="left"

View File

@ -380,7 +380,7 @@ const Orderbook = () => {
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-th-bkg-3 px-4 py-2">
<div className="flex items-center space-x-2">
<div id="trade-step-three" className="flex items-center space-x-2">
<Tooltip
content={showBuys ? t('trade:hide-bids') : t('trade:show-bids')}
placement="top"
@ -411,13 +411,15 @@ const Orderbook = () => {
</Tooltip>
</div>
{serum3MarketExternal ? (
<Tooltip content={t('trade:grouping')} placement="top">
<GroupSize
tickSize={serum3MarketExternal.tickSize}
onChange={onGroupSizeChange}
value={grouping}
/>
</Tooltip>
<div id="trade-step-four">
<Tooltip content={t('trade:grouping')} placement="top">
<GroupSize
tickSize={serum3MarketExternal.tickSize}
onChange={onGroupSizeChange}
value={grouping}
/>
</Tooltip>
</div>
) : null}
</div>
<div className="grid grid-cols-2 px-4 pt-2 pb-1 text-xxs text-th-fgd-4">
@ -458,7 +460,10 @@ const Orderbook = () => {
})
: null}
{showBuys && showSells ? (
<div className="my-2 grid grid-cols-2 border-y border-th-bkg-3 py-2 px-4 text-xs text-th-fgd-4">
<div
className="my-2 grid grid-cols-2 border-y border-th-bkg-3 py-2 px-4 text-xs text-th-fgd-4"
id="trade-step-nine"
>
<div className="col-span-1 flex justify-between">
<div className="text-xxs">{t('trade:spread')}</div>
<div className="font-mono">

View File

@ -12,6 +12,8 @@ import AdvancedTradeForm from './AdvancedTradeForm'
import TradeInfoTabs from './TradeInfoTabs'
import MobileTradeAdvancedPage from './MobileTradeAdvancedPage'
import OrderbookAndTrades from './OrderbookAndTrades'
import { useWallet } from '@solana/wallet-adapter-react'
import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
const TradingViewChart = dynamic(() => import('./TradingViewChart'), {
ssr: false,
@ -49,6 +51,8 @@ const TradeAdvancedPage = () => {
const [currentBreakpoint, setCurrentBreakpoint] = useState<string>()
const { uiLocked } = mangoStore((s) => s.settings)
const showMobileView = width <= breakpoints.md
const tourSettings = mangoStore((s) => s.settings.tours)
const { connected } = useWallet()
const defaultLayouts: ReactGridLayout.Layouts = useMemo(() => {
const topnavbarHeight = 67
@ -162,52 +166,62 @@ const TradeAdvancedPage = () => {
return showMobileView ? (
<MobileTradeAdvancedPage />
) : (
<ResponsiveGridLayout
// layouts={savedLayouts ? savedLayouts : defaultLayouts}
layouts={defaultLayouts}
breakpoints={gridBreakpoints}
cols={{
xxxl: totalCols,
xxl: totalCols,
xl: totalCols,
lg: totalCols,
md: totalCols,
sm: totalCols,
}}
rowHeight={1}
isDraggable={!uiLocked}
isResizable={!uiLocked}
onBreakpointChange={(newBreakpoint) => onBreakpointChange(newBreakpoint)}
onLayoutChange={(layout, layouts) => onLayoutChange(layouts)}
measureBeforeMount
containerPadding={[0, 0]}
margin={[0, 0]}
useCSSTransforms
>
<div key="market-header" className="z-10">
<AdvancedMarketHeader />
</div>
<div key="tv-chart" className="h-full border border-x-0 border-th-bkg-3">
<div className={`relative h-full overflow-auto`}>
<TradingViewChart />
<>
<ResponsiveGridLayout
// layouts={savedLayouts ? savedLayouts : defaultLayouts}
layouts={defaultLayouts}
breakpoints={gridBreakpoints}
cols={{
xxxl: totalCols,
xxl: totalCols,
xl: totalCols,
lg: totalCols,
md: totalCols,
sm: totalCols,
}}
rowHeight={1}
isDraggable={!uiLocked}
isResizable={!uiLocked}
onBreakpointChange={(newBreakpoint) =>
onBreakpointChange(newBreakpoint)
}
onLayoutChange={(layout, layouts) => onLayoutChange(layouts)}
measureBeforeMount
containerPadding={[0, 0]}
margin={[0, 0]}
useCSSTransforms
>
<div key="market-header" className="z-10">
<AdvancedMarketHeader />
</div>
</div>
<div key="balances">
<TradeInfoTabs />
</div>
<div
key="trade-form"
className="border border-t-0 border-r-0 border-th-bkg-3 md:border-b lg:border-b-0"
>
<AdvancedTradeForm />
</div>
<div
key="orderbook"
className="border border-y-0 border-r-0 border-th-bkg-3"
>
<OrderbookAndTrades />
</div>
</ResponsiveGridLayout>
<div
key="tv-chart"
className="h-full border border-x-0 border-th-bkg-3"
>
<div className={`relative h-full overflow-auto`}>
<TradingViewChart />
</div>
</div>
<div key="balances">
<TradeInfoTabs />
</div>
<div
key="trade-form"
className="border border-t-0 border-r-0 border-th-bkg-3 md:border-b lg:border-b-0"
>
<AdvancedTradeForm />
</div>
<div
key="orderbook"
className="border border-y-0 border-r-0 border-th-bkg-3"
>
<OrderbookAndTrades />
</div>
</ResponsiveGridLayout>
{!tourSettings?.trade_tour_seen && connected ? (
<TradeOnboardingTour />
) : null}
</>
)
}

View File

@ -46,7 +46,7 @@ const onConnectFetchAccountData = async (wallet: Wallet) => {
if (!wallet) return
const actions = mangoStore.getState().actions
await actions.fetchMangoAccounts(wallet.adapter as unknown as AnchorWallet)
actions.fetchProfilePicture(wallet.adapter as unknown as AnchorWallet)
actions.fetchTourSettings(wallet.adapter.publicKey?.toString() as string)
actions.fetchWalletTokens(wallet.adapter as unknown as AnchorWallet)
}

View File

@ -54,27 +54,29 @@ const ConnectedMenu = () => {
{({ open }) => (
<div className="relative">
<Menu.Button
className={`default-transition flex h-16 ${
className={`default-transition h-16 ${
!isMobile ? 'w-48 border-l border-th-bkg-3 px-3' : ''
} items-center hover:bg-th-bkg-2 focus:outline-none`}
} hover:bg-th-bkg-2 focus:outline-none`}
>
<ProfileImage
imageSize="40"
placeholderSize="24"
isOwnerProfile
/>
{!loadProfileDetails && !isMobile ? (
<div className="ml-2.5 w-32 text-left">
<p className="text-xs text-th-fgd-3">
{wallet_pk
? abbreviateAddress(new PublicKey(wallet_pk))
: ''}
</p>
<p className="truncate pr-2 text-sm font-bold capitalize text-th-fgd-1">
{profile_name}
</p>
</div>
) : null}
<div className="flex items-center" id="account-step-one">
<ProfileImage
imageSize="40"
placeholderSize="24"
isOwnerProfile
/>
{!loadProfileDetails && !isMobile ? (
<div className="ml-2.5 w-32 text-left">
<p className="text-xs text-th-fgd-3">
{wallet_pk
? abbreviateAddress(new PublicKey(wallet_pk))
: ''}
</p>
<p className="truncate pr-2 text-sm font-bold capitalize text-th-fgd-1">
{profile_name}
</p>
</div>
) : null}
</div>
</Menu.Button>
<Transition
appear={true}

View File

@ -4,7 +4,6 @@ import create from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { AnchorProvider, Wallet, web3 } from '@project-serum/anchor'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp'
import { TOKEN_LIST_URL } from '@jup-ag/core'
import { OpenOrders, Order } from '@project-serum/serum/lib/market'
import { Wallet as WalletAdapter } from '@solana/wallet-adapter-react'
@ -101,6 +100,27 @@ interface ProfileDetails {
wallet_pk: string
}
interface TourSettings {
account_tour_seen: boolean
swap_tour_seen: boolean
trade_tour_seen: boolean
wallet_pk: string
}
// const defaultUserSettings = {
// account_tour_seen: false,
// default_language: 'English',
// default_market: 'SOL-Perp',
// orderbook_animation: false,
// rpc_endpoint: 'Triton (RPC Pool)',
// rpc_node_url: null,
// spot_margin: false,
// swap_tour_seen: false,
// theme: 'Mango',
// trade_tour_seen: false,
// wallet_pk: '',
// }
export type MangoStore = {
coingeckoPrices: {
data: any[]
@ -140,6 +160,11 @@ export type MangoStore = {
}
serumMarkets: Serum3Market[]
serumOrders: Order[] | undefined
settings: {
loading: boolean
tours: TourSettings
uiLocked: boolean
}
swap: {
inputBank: Bank | undefined
outputBank: Bank | undefined
@ -149,9 +174,6 @@ export type MangoStore = {
slippage: number
}
set: (x: (x: MangoStore) => void) => void
settings: {
uiLocked: boolean
}
tradeForm: {
side: 'buy' | 'sell'
price: string
@ -162,8 +184,6 @@ export type MangoStore = {
ioc: boolean
}
wallet: {
loadProfilePic: boolean
profilePic: ProfilePicture | undefined
tokens: TokenAccount[]
nfts: {
data: NFT[] | []
@ -183,8 +203,8 @@ export type MangoStore = {
fetchMangoAccounts: (wallet: Wallet) => Promise<void>
fetchNfts: (connection: Connection, walletPk: PublicKey) => void
fetchSerumOpenOrders: (ma?: MangoAccount) => Promise<void>
fetchProfilePicture: (wallet: Wallet) => void
fetchProfileDetails: (walletPk: string) => void
fetchTourSettings: (walletPk: string) => void
fetchTradeHistory: (mangoAccountPk: string) => Promise<void>
fetchWalletTokens: (wallet: Wallet) => Promise<void>
connectMangoClientWithWallet: (wallet: WalletAdapter) => Promise<void>
@ -238,6 +258,13 @@ const mangoStore = create<MangoStore>()(
serumOrders: undefined,
set: (fn) => _set(produce(fn)),
settings: {
loading: false,
tours: {
account_tour_seen: true,
swap_tour_seen: true,
trade_tour_seen: true,
wallet_pk: '',
},
uiLocked: true,
},
tradeForm: {
@ -258,8 +285,6 @@ const mangoStore = create<MangoStore>()(
slippage: 0.5,
},
wallet: {
loadProfilePic: true,
profilePic: undefined,
tokens: [],
nfts: {
data: [],
@ -674,27 +699,6 @@ const mangoStore = create<MangoStore>()(
console.error('Error fetching group', e)
}
},
async fetchProfilePicture(wallet: Wallet) {
const set = get().set
const walletPk = wallet?.publicKey
const connection = get().connection
if (!walletPk) return
try {
const result = await getProfilePicture(connection, walletPk)
set((state) => {
state.wallet.profilePic = result
state.wallet.loadProfilePic = false
})
} catch (e) {
console.error('Could not get profile picture', e)
set((state) => {
state.wallet.loadProfilePic = false
})
}
},
async fetchProfileDetails(walletPk: string) {
const set = get().set
set((state) => {
@ -711,12 +715,34 @@ const mangoStore = create<MangoStore>()(
})
} catch (e) {
notify({ type: 'error', title: 'Failed to load profile details' })
console.error(e)
console.log(e)
set((state) => {
state.profile.loadDetails = false
})
}
},
async fetchTourSettings(walletPk: string) {
const set = get().set
set((state) => {
state.settings.loading = true
})
try {
const response = await fetch(
`https://mango-transaction-log.herokuapp.com/v4/user-data/settings-unsigned?wallet-pk=${walletPk}`
)
const data = await response.json()
set((state) => {
state.settings.tours = data
state.settings.loading = false
})
} catch (e) {
notify({ type: 'error', title: 'Failed to load profile details' })
console.error(e)
set((state) => {
state.settings.loading = false
})
}
},
},
}
})