feat: good now? :)
This commit is contained in:
parent
18eef39275
commit
a9afee477e
|
@ -1,25 +1,18 @@
|
||||||
|
import { LoginIcon, PaperAirplaneIcon } from '@heroicons/react/outline'
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DocumentDuplicateIcon,
|
DocumentDuplicateIcon,
|
||||||
DownloadIcon,
|
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
KeyIcon,
|
|
||||||
LibraryIcon,
|
|
||||||
QrcodeIcon,
|
QrcodeIcon,
|
||||||
RssIcon,
|
RssIcon,
|
||||||
UploadIcon,
|
ShareIcon,
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { AES, enc } from 'crypto-js'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
|
|
||||||
import { checkBiometrics } from '../lib/biometrics'
|
import { useAccount } from '../lib/context/accountContext'
|
||||||
import computeWorkAsync from '../lib/computeWorkAsync'
|
|
||||||
import { useAccount, useAccounts } from '../lib/context/accountContext'
|
|
||||||
import { usePreferences } from '../lib/context/preferencesContext'
|
import { usePreferences } from '../lib/context/preferencesContext'
|
||||||
import { getEncryptedSeed } from '../lib/db/encryptedSeeds'
|
|
||||||
import decryptSeed from '../lib/decryptSeed'
|
|
||||||
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -34,15 +27,6 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
||||||
const account = useAccount()
|
const account = useAccount()
|
||||||
|
|
||||||
const [confirmCopyAddress, setConfirmCopyAddress] = useState(false)
|
const [confirmCopyAddress, setConfirmCopyAddress] = useState(false)
|
||||||
const [confirmCopySeed, setConfirmCopySeed] = useState(false)
|
|
||||||
|
|
||||||
const onCopySeed = async () => {
|
|
||||||
const seed = await decryptSeed('os')
|
|
||||||
navigator.clipboard.writeText(seed)
|
|
||||||
setConfirmCopySeed(true)
|
|
||||||
setTimeout(() => setConfirmCopySeed(false), 1500)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCopyAddress = () => {
|
const onCopyAddress = () => {
|
||||||
if (account !== undefined) {
|
if (account !== undefined) {
|
||||||
navigator.clipboard.writeText(account.address)
|
navigator.clipboard.writeText(account.address)
|
||||||
|
@ -51,6 +35,13 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onShare = () => {
|
||||||
|
navigator.share({
|
||||||
|
title: 'This is my nano address!',
|
||||||
|
text: account?.address,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const isWelcoming = useIsWelcoming()
|
const isWelcoming = useIsWelcoming()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -75,94 +66,66 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex gap-8 items-end',
|
'flex gap-10 items-end',
|
||||||
leftHanded ? 'flex-row-reverse' : 'flex-row'
|
leftHanded ? 'flex-row-reverse' : 'flex-row'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
{'share' in navigator ? (
|
||||||
className={clsx('flex gap-6 items-end', {
|
<button
|
||||||
'flex-row-reverse': leftHanded,
|
disabled={isWelcoming}
|
||||||
})}
|
className="p-1 h-16 w-7 rounded shadow-lg bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default"
|
||||||
>
|
onClick={onShare}
|
||||||
<div className="flex flex-col h-16 justify-between">
|
>
|
||||||
<button
|
<ShareIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||||
disabled={isWelcoming || confirmCopySeed}
|
</button>
|
||||||
className={clsx(
|
) : (
|
||||||
'p-1 h-7 rounded shadow-lg',
|
<button
|
||||||
confirmCopySeed
|
disabled={isWelcoming || confirmCopyAddress}
|
||||||
? 'bg-purple-50'
|
className={clsx(
|
||||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
'p-1 h-16 w-10 rounded shadow-lg',
|
||||||
)}
|
confirmCopyAddress
|
||||||
onClick={onCopySeed}
|
? 'bg-purple-50'
|
||||||
>
|
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
||||||
{confirmCopySeed ? (
|
)}
|
||||||
<CheckIcon className="h-full text-purple-500" />
|
onClick={onCopyAddress}
|
||||||
) : (
|
>
|
||||||
<KeyIcon className="h-full text-purple-50 dark:text-gray-900" />
|
{confirmCopyAddress ? (
|
||||||
)}
|
<CheckIcon className="text-purple-500" />
|
||||||
</button>
|
) : (
|
||||||
<button
|
<DocumentDuplicateIcon className="text-purple-50 dark:text-gray-900" />
|
||||||
disabled={isWelcoming}
|
)}
|
||||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg disabled:cursor-default"
|
</button>
|
||||||
>
|
)}
|
||||||
<LibraryIcon className="h-full text-purple-50 dark:text-gray-900" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col h-16 justify-between">
|
|
||||||
<button
|
|
||||||
disabled={isWelcoming || confirmCopyAddress}
|
|
||||||
className={clsx(
|
|
||||||
'p-1 h-7 rounded shadow-lg',
|
|
||||||
confirmCopyAddress
|
|
||||||
? 'bg-purple-50'
|
|
||||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500 disabled:cursor-default'
|
|
||||||
)}
|
|
||||||
onClick={onCopyAddress}
|
|
||||||
>
|
|
||||||
{confirmCopyAddress ? (
|
|
||||||
<CheckIcon className="h-full text-purple-500" />
|
|
||||||
) : (
|
|
||||||
<DocumentDuplicateIcon className="h-full text-purple-50 dark:text-gray-900" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
disabled={isWelcoming}
|
|
||||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg disabled:cursor-default"
|
|
||||||
>
|
|
||||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative h-16">
|
<div className="relative h-16">
|
||||||
<button
|
<button
|
||||||
disabled={isWelcoming}
|
disabled={isWelcoming}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'bg-purple-500 absolute top-0 h-16 w-7 rounded-r hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
'bg-purple-500 absolute top-0 px-1 h-16 w-10 rounded-r hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||||
leftHanded
|
leftHanded
|
||||||
? 'left-0 -translate-x-2/3 rounded-l'
|
? 'left-0 -translate-x-2/3 rounded-l'
|
||||||
: 'right-0 translate-x-2/3 rounded-r'
|
: 'right-0 translate-x-2/3 rounded-r'
|
||||||
)}
|
)}
|
||||||
onClick={() => push('/send/qr')}
|
onClick={() => push('/send/qr')}
|
||||||
>
|
>
|
||||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
<PaperAirplaneIcon className="h-full text-purple-50 dark:text-gray-900 w-full rotate-[30deg] translate-x-1" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="border-purple-500 border-t-2 border-b-2 py-1 px-3 h-16 shadow-lg">
|
<div className="border-purple-500 border-t-2 border-b-2 py-1 px-4 h-16 shadow-lg">
|
||||||
<QrcodeIcon className="h-full text-gray-900 dark:text-purple-100" />
|
<QrcodeIcon className="h-full text-gray-900 dark:text-purple-100" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={isWelcoming}
|
disabled={isWelcoming}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'bg-purple-500 absolute top-0 h-16 w-7 hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
'bg-purple-500 absolute top-0 px-1 h-16 w-10 hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default',
|
||||||
leftHanded
|
leftHanded
|
||||||
? 'right-0 translate-x-2/3 rounded-r'
|
? 'right-0 translate-x-2/3 rounded-r'
|
||||||
: 'left-0 -translate-x-2/3 rounded-l'
|
: 'left-0 -translate-x-2/3 rounded-l'
|
||||||
)}
|
)}
|
||||||
onClick={() => push('/receive/qr')}
|
onClick={() => push('/receive/qr')}
|
||||||
>
|
>
|
||||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
<LoginIcon className="h-full text-purple-50 dark:text-gray-900 w-full -rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -171,7 +134,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
||||||
disabled={isWelcoming}
|
disabled={isWelcoming}
|
||||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default"
|
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md disabled:cursor-default"
|
||||||
>
|
>
|
||||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
<PaperAirplaneIcon className="h-full text-purple-50 dark:text-gray-900 rotate-[30deg] translate-x-[2px]" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={isWelcoming}
|
disabled={isWelcoming}
|
||||||
|
|
|
@ -5,11 +5,9 @@ import { FC } from 'react'
|
||||||
|
|
||||||
import { usePreferences } from '../lib/context/preferencesContext'
|
import { usePreferences } from '../lib/context/preferencesContext'
|
||||||
import useListenToColorMedia from '../lib/hooks/useListenToColorMedia'
|
import useListenToColorMedia from '../lib/hooks/useListenToColorMedia'
|
||||||
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
|
||||||
import useSetupDb from '../lib/hooks/useSetupDb'
|
|
||||||
import Balance from './Balance'
|
import Balance from './Balance'
|
||||||
import BottomMenu from './BottomMenu'
|
import BottomMenu from './BottomMenu'
|
||||||
import PreferencesMenu from './PreferencesMenu'
|
import TopMenu from './TopMenu'
|
||||||
|
|
||||||
export interface Props {}
|
export interface Props {}
|
||||||
|
|
||||||
|
@ -41,7 +39,7 @@ const Layout: FC<Props> = ({ children }) => {
|
||||||
</h1>
|
</h1>
|
||||||
<LightningBoltIcon className="text-gray-900 dark:text-purple-100 h-4" />
|
<LightningBoltIcon className="text-gray-900 dark:text-purple-100 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<PreferencesMenu />
|
<TopMenu />
|
||||||
</header>
|
</header>
|
||||||
{pathname !== '/' ? (
|
{pathname !== '/' ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import {
|
|
||||||
CogIcon,
|
|
||||||
FingerPrintIcon,
|
|
||||||
HandIcon,
|
|
||||||
MoonIcon,
|
|
||||||
} from '@heroicons/react/solid'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { FC, useRef, useState } from 'react'
|
|
||||||
import colors from 'tailwindcss/colors'
|
|
||||||
|
|
||||||
import { usePreferences } from '../lib/context/preferencesContext'
|
|
||||||
import useClickAway from '../lib/hooks/useClickAway'
|
|
||||||
import useIsiOS from '../lib/hooks/useIsiOS'
|
|
||||||
|
|
||||||
export interface Props {}
|
|
||||||
|
|
||||||
const PreferencesMenu: FC<Props> = () => {
|
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
|
||||||
const toggleMenu = () => setShowMenu(prev => !prev)
|
|
||||||
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
|
||||||
useClickAway(menuRef, () => setShowMenu(false))
|
|
||||||
const {
|
|
||||||
preferences: { darkMode, biometricsAuth, leftHanded },
|
|
||||||
setPreference,
|
|
||||||
} = usePreferences()
|
|
||||||
|
|
||||||
const isiOS = useIsiOS()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative z-20 justify-center" ref={menuRef}>
|
|
||||||
<button
|
|
||||||
className={clsx(
|
|
||||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark text-purple-50:dark:text-gray-900'
|
|
||||||
)}
|
|
||||||
onClick={toggleMenu}
|
|
||||||
>
|
|
||||||
<CogIcon className="w-full" />
|
|
||||||
</button>
|
|
||||||
<ul
|
|
||||||
role="menu"
|
|
||||||
className={clsx(
|
|
||||||
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3',
|
|
||||||
showMenu ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isiOS && (
|
|
||||||
<li role="menuitem">
|
|
||||||
<button
|
|
||||||
disabled={!showMenu}
|
|
||||||
className={clsx(
|
|
||||||
'p-1 rounded transition-colors duration-100 w-full',
|
|
||||||
biometricsAuth
|
|
||||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
|
||||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
|
||||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setPreference('biometricsAuth', !biometricsAuth)
|
|
||||||
setShowMenu(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FingerPrintIcon className="h-full" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<li role="menuitem">
|
|
||||||
<button
|
|
||||||
disabled={!showMenu}
|
|
||||||
className={clsx(
|
|
||||||
'p-1 rounded transition-colors duration-100 w-full',
|
|
||||||
darkMode
|
|
||||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
|
||||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
|
||||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setPreference('darkMode', !darkMode)
|
|
||||||
setShowMenu(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MoonIcon className="h-full" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li role="menuitem">
|
|
||||||
<button
|
|
||||||
disabled={!showMenu}
|
|
||||||
className={clsx(
|
|
||||||
'p-1 rounded transition-colors duration-100 w-full',
|
|
||||||
leftHanded
|
|
||||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
|
||||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 dark:hover:text-purple-300 text-purple-50',
|
|
||||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setPreference('leftHanded', !leftHanded)
|
|
||||||
setShowMenu(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<HandIcon className="h-full" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PreferencesMenu
|
|
|
@ -1,16 +1,9 @@
|
||||||
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { tools } from 'nanocurrency-web'
|
import { tools } from 'nanocurrency-web'
|
||||||
import { FC, useCallback, useMemo } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
import { useAccount } from '../lib/context/accountContext'
|
|
||||||
import fetcher from '../lib/fetcher'
|
|
||||||
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
||||||
import {
|
|
||||||
AccountHistoryResponse,
|
|
||||||
AccountPendingResponse,
|
|
||||||
BlocksInfoResponse,
|
|
||||||
} from '../lib/types'
|
|
||||||
|
|
||||||
const rawToNanoDisplay = (raw: string) =>
|
const rawToNanoDisplay = (raw: string) =>
|
||||||
Number(tools.convert(raw, 'RAW', 'NANO').slice(0, 20))
|
Number(tools.convert(raw, 'RAW', 'NANO').slice(0, 20))
|
||||||
|
|
185
components/TopMenu.tsx
Normal file
185
components/TopMenu.tsx
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
import {
|
||||||
|
CogIcon,
|
||||||
|
FingerPrintIcon,
|
||||||
|
HandIcon,
|
||||||
|
KeyIcon,
|
||||||
|
LibraryIcon,
|
||||||
|
MoonIcon,
|
||||||
|
PlusIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from '@heroicons/react/solid'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { FC, useRef, useState } from 'react'
|
||||||
|
import colors from 'tailwindcss/colors'
|
||||||
|
|
||||||
|
import { usePreferences } from '../lib/context/preferencesContext'
|
||||||
|
import decryptSeed from '../lib/decryptSeed'
|
||||||
|
import useClickAway from '../lib/hooks/useClickAway'
|
||||||
|
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
||||||
|
import useIsiOS from '../lib/hooks/useIsiOS'
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
const TopMenu: FC<Props> = () => {
|
||||||
|
const [showPreferences, setShowPreferences] = useState(false)
|
||||||
|
const togglePreferences = () => setShowPreferences(prev => !prev)
|
||||||
|
|
||||||
|
const preferencesRef = useRef<HTMLDivElement>(null)
|
||||||
|
useClickAway(preferencesRef, () => setShowPreferences(false))
|
||||||
|
const {
|
||||||
|
preferences: { darkMode, biometricsAuth, leftHanded },
|
||||||
|
setPreference,
|
||||||
|
} = usePreferences()
|
||||||
|
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||||
|
const toggleAdvanced = () => setShowAdvanced(prev => !prev)
|
||||||
|
|
||||||
|
const advancedRef = useRef<HTMLDivElement>(null)
|
||||||
|
useClickAway(advancedRef, () => setShowAdvanced(false))
|
||||||
|
|
||||||
|
const isiOS = useIsiOS()
|
||||||
|
const isWelcoming = useIsWelcoming()
|
||||||
|
|
||||||
|
const onCopySeed = async () => {
|
||||||
|
const seed = await decryptSeed('os')
|
||||||
|
navigator.clipboard.writeText(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('flex gap-3 justify-center', {
|
||||||
|
'flex-row-reverse': leftHanded,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!isWelcoming && (
|
||||||
|
<div className="relative z-20" ref={advancedRef}>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark:hover:text-purple-50 dark:text-gray-900'
|
||||||
|
)}
|
||||||
|
onClick={toggleAdvanced}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-full" />
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
role="menu"
|
||||||
|
className={clsx(
|
||||||
|
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3 hover:text-white',
|
||||||
|
showAdvanced ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||||
|
)}
|
||||||
|
onClick={onCopySeed}
|
||||||
|
>
|
||||||
|
<KeyIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
// todo
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LibraryIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-50'
|
||||||
|
)}
|
||||||
|
onClick={onCopySeed}
|
||||||
|
>
|
||||||
|
<UsersIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative z-20" ref={preferencesRef}>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 hover:cursor-pointer hover:bg-purple-400 transition-colors dark:hover:text-purple-50 dark:text-gray-900'
|
||||||
|
)}
|
||||||
|
onClick={togglePreferences}
|
||||||
|
>
|
||||||
|
<CogIcon className="w-full" />
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
role="menu"
|
||||||
|
className={clsx(
|
||||||
|
'transition-all flex flex-col gap-2 absolute w-10 bg-purple-400 p-1 rounded mt-3',
|
||||||
|
showPreferences ? 'opacity-100' : 'opacity-0 -translate-y-2'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
boxShadow: `${colors.coolGray[900]} 0px 2px 15px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!isiOS && (
|
||||||
|
<li role="menuitem">
|
||||||
|
<button
|
||||||
|
disabled={!showPreferences}
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||||
|
biometricsAuth
|
||||||
|
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||||
|
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||||
|
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setPreference('biometricsAuth', !biometricsAuth)
|
||||||
|
setShowPreferences(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FingerPrintIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li role="menuitem">
|
||||||
|
<button
|
||||||
|
disabled={!showPreferences}
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||||
|
darkMode
|
||||||
|
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||||
|
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||||
|
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setPreference('darkMode', !darkMode)
|
||||||
|
setShowPreferences(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoonIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role="menuitem">
|
||||||
|
<button
|
||||||
|
disabled={!showPreferences}
|
||||||
|
className={clsx(
|
||||||
|
'p-1 rounded transition-colors duration-100 w-full dark:hover:text-purple-50',
|
||||||
|
leftHanded
|
||||||
|
? 'dark:text-purple-400 dark:bg-gray-900 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||||
|
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50',
|
||||||
|
showPreferences ? 'hover:cursor-pointer' : 'cursor-default'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setPreference('leftHanded', !leftHanded)
|
||||||
|
setShowPreferences(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HandIcon className="h-full" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopMenu
|
|
@ -15,7 +15,7 @@ export interface PreferenceContextValue {
|
||||||
preferences: PreferenceTypes
|
preferences: PreferenceTypes
|
||||||
setPreference: <P extends PreferenceName>(
|
setPreference: <P extends PreferenceName>(
|
||||||
preference: P,
|
preference: P,
|
||||||
value: PreferenceTypes[P]
|
value: Exclude<PreferenceTypes[P], undefined>
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,12 +53,21 @@ export const PreferencesProvider: FC = ({ children }) => {
|
||||||
}
|
}
|
||||||
fetchPreferencesFromIdb()
|
fetchPreferencesFromIdb()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const setDarkMode = useDarkMode()
|
||||||
|
|
||||||
const setPreference = useCallback(
|
const setPreference = useCallback(
|
||||||
<P extends PreferenceName>(name: P, value: PreferenceTypes[P]) => {
|
<P extends PreferenceName>(
|
||||||
|
name: P,
|
||||||
|
value: Exclude<PreferenceTypes[P], undefined>
|
||||||
|
) => {
|
||||||
setPreferences(prev => ({ ...prev, [name]: value }))
|
setPreferences(prev => ({ ...prev, [name]: value }))
|
||||||
putPreference(name, value)
|
putPreference(name, value)
|
||||||
|
|
||||||
|
// ? is there a better way to type this?
|
||||||
|
if (name === 'darkMode') setDarkMode(value as boolean)
|
||||||
},
|
},
|
||||||
[]
|
[setDarkMode]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
const fetcher = <T>(...args: Parameters<typeof fetch>) =>
|
const fetcher = <T>(
|
||||||
fetch(...args).then(res => {
|
input: Parameters<typeof fetch>[0],
|
||||||
|
init?: Omit<Exclude<Parameters<typeof fetch>[1], undefined>, 'body'> & {
|
||||||
|
method: 'POST' | 'PUT' | 'PATCH'
|
||||||
|
body?: any
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers:
|
||||||
|
(init?.method ?? 'GET') === 'GET'
|
||||||
|
? []
|
||||||
|
: [['Content-Type', 'application/json']],
|
||||||
|
...(init?.body !== undefined ? { body: JSON.stringify(init?.body) } : {}),
|
||||||
|
}).then(res => {
|
||||||
if (!res.ok) throw new Error() // todo improve this error
|
if (!res.ok) throw new Error() // todo improve this error
|
||||||
return res.json() as Promise<T>
|
return res.json() as Promise<T>
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import { useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
|
|
||||||
import { getPreference } from '../db/preferences'
|
import { getPreference } from '../db/preferences'
|
||||||
|
|
||||||
const useDarkMode = (darkMode?: boolean) => {
|
const useDarkMode = () => {
|
||||||
|
const setDarkMode = useCallback(async (darkMode: boolean) => {
|
||||||
|
const htmlClasses = document.querySelector('html')?.classList
|
||||||
|
if (darkMode) htmlClasses?.add('dark')
|
||||||
|
else htmlClasses?.remove('dark')
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setDarkModeClass = async () => {
|
const darkModeOnStarup = async () => {
|
||||||
const isDark =
|
const darkMode =
|
||||||
darkMode ??
|
|
||||||
(await getPreference('darkMode')) ??
|
(await getPreference('darkMode')) ??
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
const htmlClasses = document.querySelector('html')?.classList
|
setDarkMode(darkMode)
|
||||||
if (isDark) htmlClasses?.add('dark')
|
|
||||||
else htmlClasses?.remove('dark')
|
|
||||||
}
|
}
|
||||||
setDarkModeClass()
|
darkModeOnStarup()
|
||||||
}, [darkMode])
|
}, [setDarkMode])
|
||||||
|
|
||||||
|
return setDarkMode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useDarkMode
|
export default useDarkMode
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
const useImperativeRender = () => {
|
|
||||||
const [, setValue] = useState(0)
|
|
||||||
return () => setValue(value => value + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useImperativeRender
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import { useAccounts } from '../context/accountContext'
|
import { useAccounts } from '../context/accountContext'
|
||||||
|
import { ConfirmationMessage } from '../types'
|
||||||
|
|
||||||
const useListenToTxn = () => {
|
const useListenToTxn = (
|
||||||
|
onConfirmation: (confirmation: ConfirmationMessage) => void
|
||||||
|
) => {
|
||||||
const { accounts } = useAccounts()
|
const { accounts } = useAccounts()
|
||||||
const [mostRecentTxn, setMostRecentTxn] = useState<any | undefined>(undefined)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accounts !== undefined) {
|
if (accounts !== undefined) {
|
||||||
const ws = new WebSocket('wss://ws.mynano.ninja/')
|
const ws = new WebSocket('wss://ws.mynano.ninja/')
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
console.log('subscribed')
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
action: 'subscribe',
|
action: 'subscribe',
|
||||||
|
@ -21,16 +22,14 @@ const useListenToTxn = () => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
ws.addEventListener('message', ({ data }) => {
|
ws.addEventListener('message', ({ data }) => {
|
||||||
const parsed = JSON.parse(data)
|
const parsed = JSON.parse(data) as ConfirmationMessage
|
||||||
console.log(parsed)
|
onConfirmation(parsed)
|
||||||
setMostRecentTxn(parsed)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => ws.close()
|
return () => ws.close()
|
||||||
}
|
}
|
||||||
}, [accounts])
|
}, [accounts, onConfirmation])
|
||||||
return mostRecentTxn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useListenToTxn
|
export default useListenToTxn
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useCallback } from 'react'
|
||||||
|
|
||||||
import useSetup from './useSetup'
|
import useSetup from './useSetup'
|
||||||
|
|
||||||
const useSetupSw = (skip?: boolean) =>
|
const useSetupServiceWorker = (skip?: boolean) =>
|
||||||
useSetup(
|
useSetup(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
@ -16,4 +16,4 @@ const useSetupSw = (skip?: boolean) =>
|
||||||
skip
|
skip
|
||||||
)
|
)
|
||||||
|
|
||||||
export default useSetupSw
|
export default useSetupServiceWorker
|
42
lib/types.ts
42
lib/types.ts
|
@ -1,3 +1,6 @@
|
||||||
|
export type BooleanString = 'true' | 'false'
|
||||||
|
export type TransactionSubtype = 'send' | 'receive' | 'change'
|
||||||
|
|
||||||
export interface AccountHistoryResponse {
|
export interface AccountHistoryResponse {
|
||||||
account: string
|
account: string
|
||||||
history:
|
history:
|
||||||
|
@ -27,7 +30,7 @@ export interface BlocksInfoResponse {
|
||||||
balance: string
|
balance: string
|
||||||
height: string
|
height: string
|
||||||
local_timestamp: string
|
local_timestamp: string
|
||||||
confirmed: 'true' | 'false'
|
confirmed: BooleanString
|
||||||
contents: {
|
contents: {
|
||||||
type: 'state'
|
type: 'state'
|
||||||
account: string
|
account: string
|
||||||
|
@ -39,7 +42,7 @@ export interface BlocksInfoResponse {
|
||||||
signature: string
|
signature: string
|
||||||
work: string
|
work: string
|
||||||
}
|
}
|
||||||
subtype: 'send' | 'receive' | 'change'
|
subtype: TransactionSubtype
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,13 +58,13 @@ export type AccountInfoResponse =
|
||||||
account_version: string
|
account_version: string
|
||||||
confirmation_height: string
|
confirmation_height: string
|
||||||
confirmation_height_frontier: string
|
confirmation_height_frontier: string
|
||||||
confirmed_balance: '11999999999999999918751838129509869131'
|
confirmed_balance: string
|
||||||
confirmed_height: '22966'
|
confirmed_height: string
|
||||||
confirmed_frontier: '80A6745762493FA21A22718ABFA4F635656A707B48B3324198AC7F3938DE6D4F'
|
confirmed_frontier: string
|
||||||
representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
representative: string
|
||||||
confirmed_representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
confirmed_representative: string
|
||||||
}
|
}
|
||||||
| { error: 'Account not found' }
|
| { error: string }
|
||||||
|
|
||||||
export interface AccountInfoCache {
|
export interface AccountInfoCache {
|
||||||
address: string
|
address: string
|
||||||
|
@ -85,3 +88,26 @@ export type ProcessResponse =
|
||||||
hash: string
|
hash: string
|
||||||
}
|
}
|
||||||
| { error: string }
|
| { error: string }
|
||||||
|
|
||||||
|
export interface ConfirmationMessage {
|
||||||
|
topic: 'confirmation'
|
||||||
|
time: string
|
||||||
|
message: {
|
||||||
|
account: string
|
||||||
|
amount: string
|
||||||
|
hash: string
|
||||||
|
confirmation_type: string
|
||||||
|
block: {
|
||||||
|
type: 'state'
|
||||||
|
account: string
|
||||||
|
previous: string
|
||||||
|
representative: string
|
||||||
|
balance: string
|
||||||
|
link: string
|
||||||
|
link_as_account: string
|
||||||
|
signature: string
|
||||||
|
work: string
|
||||||
|
subtype: TransactionSubtype
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
15
lib/xno/fetchAccountHistory.ts
Normal file
15
lib/xno/fetchAccountHistory.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import fetcher from '../fetcher'
|
||||||
|
import { AccountHistoryResponse } from '../types'
|
||||||
|
|
||||||
|
const fetchAccountHistory = (address: string, count = 20, head = undefined) =>
|
||||||
|
fetcher('https://mynano.ninja/api/node', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
action: 'account_history',
|
||||||
|
account: address,
|
||||||
|
head,
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
}) as Promise<AccountHistoryResponse>
|
||||||
|
|
||||||
|
export default fetchAccountHistory
|
|
@ -4,13 +4,12 @@ import { AccountInfoResponse } from '../types'
|
||||||
const fetchAccountInfo = (address: string) =>
|
const fetchAccountInfo = (address: string) =>
|
||||||
fetcher('https://mynano.ninja/api/node', {
|
fetcher('https://mynano.ninja/api/node', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: [['Content-Type', 'application/json']],
|
body: {
|
||||||
body: JSON.stringify({
|
|
||||||
action: 'account_info',
|
action: 'account_info',
|
||||||
account: address,
|
account: address,
|
||||||
representative: 'true',
|
representative: 'true',
|
||||||
include_confirmed: 'true',
|
include_confirmed: 'true',
|
||||||
}),
|
},
|
||||||
}) as Promise<AccountInfoResponse>
|
}) as Promise<AccountInfoResponse>
|
||||||
|
|
||||||
export default fetchAccountInfo
|
export default fetchAccountInfo
|
||||||
|
|
27
lib/xno/fetchAccountReceivable.ts
Normal file
27
lib/xno/fetchAccountReceivable.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import fetcher from '../fetcher'
|
||||||
|
import { AccountHistoryResponse } from '../types'
|
||||||
|
|
||||||
|
const fetchAccountReceivable = (
|
||||||
|
address: string,
|
||||||
|
count = 20,
|
||||||
|
head = undefined,
|
||||||
|
version22 = false
|
||||||
|
) =>
|
||||||
|
fetcher('https://mynano.ninja/api/node', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
action: version22 ? 'account_pending' : 'account_receivable',
|
||||||
|
account: address,
|
||||||
|
head,
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
}) as Promise<AccountHistoryResponse>
|
||||||
|
|
||||||
|
// most nodes haven't upgraded yet https://docs.nano.org/commands/rpc-protocol/#accounts_pending
|
||||||
|
// this will be the future api for this function
|
||||||
|
const withVersionFallback = (address: string, count = 20, head = undefined) =>
|
||||||
|
fetchAccountReceivable(address, count, head).catch(() =>
|
||||||
|
fetchAccountReceivable(address, count, head, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default withVersionFallback
|
|
@ -6,9 +6,11 @@ import Layout from '../components/Layout'
|
||||||
import MemCacheProvider from '../lib/context/memCacheContextProvider'
|
import MemCacheProvider from '../lib/context/memCacheContextProvider'
|
||||||
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
||||||
import useSetupDb from '../lib/hooks/useSetupDb'
|
import useSetupDb from '../lib/hooks/useSetupDb'
|
||||||
|
import useSetupServiceWorker from '../lib/hooks/useSetupServiceWorker'
|
||||||
import '../styles/global.css'
|
import '../styles/global.css'
|
||||||
|
|
||||||
const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
|
const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
|
||||||
|
useSetupServiceWorker()
|
||||||
const ready = useSetupDb(10)
|
const ready = useSetupDb(10)
|
||||||
const validatingCredential = useProtectedRoutes(!ready)
|
const validatingCredential = useProtectedRoutes(!ready)
|
||||||
|
|
||||||
|
|
Reference in a new issue