chore: add debugFeatures, routes and dev pages

This commit is contained in:
theborakompanioni 2025-09-14 16:48:35 +02:00
parent 4ac04d4f0a
commit 8ca65a112b
No known key found for this signature in database
GPG key ID: E8070AF0053AAC0D
12 changed files with 313 additions and 74 deletions

View file

@ -4,7 +4,7 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "VITE_JAM_DEV_MODE=true vite",
"dev:secondary": "JAM_BACKEND=jam-standalone JAM_API_PORT=29080 npm run dev",
"build": "tsc -b && vite build",
"lint": "eslint .",

View file

@ -1,35 +1,42 @@
import { useEffect, useMemo } from 'react'
import { lazy, Suspense, useEffect, useMemo } from 'react'
import type { PropsWithChildren } from 'react'
import { QueryClientProvider, useQuery } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { ThemeProvider } from 'next-themes'
import { useTranslation } from 'react-i18next'
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom'
import { toast } from 'sonner'
import { useStore } from 'zustand'
import CreateWallet from '@/components/CreateWallet'
import JamLanding from '@/components/JamLanding'
import LoginPage from '@/components/Login'
import { Logs } from '@/components/Logs'
import { Orderbook } from '@/components/Orderbook'
import SwitchWallet from '@/components/SwitchWallet'
import { EarnPage } from '@/components/earn/EarnPage'
import { Layout } from '@/components/layout/Layout'
import { Receive } from '@/components/receive/Receive'
import { SendPage } from '@/components/send/SendPage'
import { RescanChain } from '@/components/settings/RescanChain'
import { Settings } from '@/components/settings/Settings'
import { SweepPage } from '@/components/sweep/SweepPage'
import { Toaster } from '@/components/ui/sonner'
import { JAM_API_AUTH_TOKEN_RENEW_INTERVAL, JAM_JM_SESSION_REFRESH_INTERVAL } from '@/constants/jam'
import { routes } from '@/constants/routes'
import { useApiClient } from '@/hooks/useApiClient'
import { token } from '@/lib/jm-api/generated/client'
import { sessionOptions } from '@/lib/jm-api/generated/client/@tanstack/react-query.gen'
import { queryClient } from '@/lib/queryClient'
import { setIntervalDebounced } from '@/lib/utils'
import { authStore } from '@/store/authStore'
import { Logs } from './components/Logs'
import { Orderbook } from './components/Orderbook'
import { EarnPage } from './components/earn/EarnPage'
import { Receive } from './components/receive/Receive'
import { SendPage } from './components/send/SendPage'
import { RescanChain } from './components/settings/RescanChain'
import { Settings } from './components/settings/Settings'
import { SweepPage } from './components/sweep/SweepPage'
import { JAM_API_AUTH_TOKEN_RENEW_INTERVAL, JAM_JM_SESSION_REFRESH_INTERVAL } from './constants/jam'
import { sessionOptions } from './lib/jm-api/generated/client/@tanstack/react-query.gen'
import { jmSessionStore } from './store/jmSessionStore'
import { jmSessionStore } from '@/store/jmSessionStore'
import { isDebugFeatureEnabled, isDevMode } from './constants/debugFeatures'
const DevSetupPage = lazy(() => import('@/components/dev/DevSetupPage'))
const DevPage = lazy(() => import('@/components/dev/DevPage'))
const ProtectedRoute = ({ authenticated, children }: PropsWithChildren<{ authenticated: boolean }>) => {
return authenticated ? <>{children}</> : <Navigate to="/login" replace />
return authenticated ? <>{children}</> : <Navigate to={routes.login} replace />
}
function App() {
@ -47,10 +54,16 @@ function App() {
<RefreshJmSession />
<Router>
<Routes>
<Route path="/login" element={authenticated ? <Navigate to="/" replace /> : <LoginPage />} />
<Route path="/create-wallet" element={authenticated ? <Navigate to="/" replace /> : <CreateWallet />} />
<Route
path="/switch-wallet"
path={routes.login}
element={authenticated ? <Navigate to={routes.home} replace /> : <LoginPage />}
/>
<Route
path={routes.createWallet}
element={authenticated ? <Navigate to={routes.home} replace /> : <CreateWallet />}
/>
<Route
path={routes.switchWallet}
element={
<ProtectedRoute authenticated={authenticated}>
<SwitchWallet walletFileName={walletFileName!} />
@ -58,7 +71,7 @@ function App() {
}
/>
<Route
path="/"
path={routes.home}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -68,7 +81,7 @@ function App() {
}
/>
<Route
path="/receive"
path={routes.receive}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -78,7 +91,7 @@ function App() {
}
/>
<Route
path="/send"
path={routes.send}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -88,17 +101,7 @@ function App() {
}
/>
<Route
path="/settings/rescan"
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<RescanChain walletFileName={walletFileName!} />
</Layout>
</ProtectedRoute>
}
/>
<Route
path="/earn"
path={routes.earn}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -108,7 +111,7 @@ function App() {
}
/>
<Route
path="/sweep"
path={routes.sweep}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -118,7 +121,7 @@ function App() {
}
/>
<Route
path="/settings"
path={routes.settings}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -128,7 +131,7 @@ function App() {
}
/>
<Route
path="/orderbook"
path={routes.orderbook}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -138,7 +141,7 @@ function App() {
}
/>
<Route
path="/logs"
path={routes.logs}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
@ -147,7 +150,40 @@ function App() {
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/login" replace />} />
<Route
path={routes.rescan}
element={
<ProtectedRoute authenticated={authenticated}>
<Layout>
<RescanChain walletFileName={walletFileName!} />
</Layout>
</ProtectedRoute>
}
/>
{isDebugFeatureEnabled('devPage') && (
<Route
path={routes.__dev}
element={
<Layout>
<DevPage />
</Layout>
}
/>
)}
{isDebugFeatureEnabled('devSetupPage') && (
<Route
id="dev-setup"
path={routes.__devSetup}
element={
<Layout>
<Suspense fallback={<Loading />}>
<DevSetupPage />
</Suspense>
</Layout>
}
/>
)}
<Route path="*" element={<Navigate to={routes.login} replace />} />
</Routes>
<Toaster closeButton />
</Router>
@ -156,12 +192,22 @@ function App() {
)
}
const Loading = () => {
const { t } = useTranslation()
return (
<div className="m-2 flex items-center justify-center">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('global.loading')}
</div>
)
}
function RefreshApiToken() {
const client = useApiClient()
// TODO: stop this interval if no wallet is active
useEffect(() => {
if (import.meta.env.DEV) {
if (isDevMode()) {
toast.info(`[DEV] setup refresh interval`)
}
@ -182,7 +228,7 @@ function RefreshApiToken() {
if (!response.data) {
authStore.getState().clear()
if (import.meta.env.DEV) {
if (isDevMode()) {
const message = response.error?.message || response.error?.error_description || 'Unknown error.'
toast.error(`[DEV] Error while renewing auth token: ${message}`)
}
@ -194,7 +240,7 @@ function RefreshApiToken() {
},
})
if (import.meta.env.DEV) {
if (isDevMode()) {
toast.info(`[DEV] Successfully renewed auth token.`, {
id: 'token-renew-success',
})
@ -227,7 +273,7 @@ function RefreshJmSession() {
if (sessionQuery.data) {
jmSessionStore.getState().update(sessionQuery.data)
if (import.meta.env.DEV) {
if (isDevMode()) {
toast.info(`[DEV] Successfully refreshed session data.`, {
id: 'jm-session-refresh-success',
})
@ -238,7 +284,7 @@ function RefreshJmSession() {
useEffect(() => {
if (authState === undefined) {
sessionQuery.refetch().catch(() => {
if (import.meta.env.DEV) {
if (isDevMode()) {
toast.error(`[DEV] Error while refreshing session data.`)
}
})

View file

@ -3,11 +3,13 @@ import { BlocksIcon, BookOpen, Check, File, X } from 'lucide-react'
import { useTranslation, Trans } from 'react-i18next'
import { Link } from 'react-router-dom'
import { useStore } from 'zustand'
import { OrderbookDialog } from '@/components/OrderbookDialog'
import { Button } from '@/components/ui/button'
import { isDebugFeatureEnabled } from '@/constants/debugFeatures'
import { routes } from '@/constants/routes'
import { toSemVer } from '@/lib/utils'
import { jmSessionStore } from '@/store/jmSessionStore'
import packageInfo from '../../package.json'
import { OrderbookDialog } from './OrderbookDialog'
const APP_DISPLAY_VERSION = (() => {
const version = toSemVer(packageInfo.version)
@ -39,7 +41,16 @@ export function Footer() {
</Button>
</div>
<div className="flex flex-1 justify-end">
<div className="flex flex-1 justify-end gap-2">
<div className="flex items-center">
{isDebugFeatureEnabled('devSetupPage') && (
<div className="">
<Link className="light:text-yellow-800 text-sm text-yellow-200 underline" to={routes.__devSetup}>
Dev Setup
</Link>
</div>
)}
</div>
<div className="flex flex-col gap-1">
<a
href="https://github.com/joinmarket-webui/jam/tags"

View file

@ -1,24 +1,14 @@
import { useState } from 'react'
import { Info, Loader2, RefreshCw } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useStore } from 'zustand'
import { Jar } from '@/components/layout/Jar'
import { useJamDisplayContext } from '@/components/layout/display-mode-context'
import { FeeLimitDialog } from '@/components/settings/FeeLimitDialog'
import { FeeConfigErrorAlert } from '@/components/ui/FeeConfigErrorAlert'
import { FeeConfigTestComponent } from '@/components/ui/FeeConfigTestComponent'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation'
import { authStore } from '@/store/authStore'
export default function JamLanding() {
const { t } = useTranslation()
const [showFeeConfigDialog, setShowFeeConfigDialog] = useState(false)
const authState = useStore(authStore, (state) => state.state)
const { maxFeesConfigMissing } = useFeeConfigValidation()
const {
currency,
@ -62,11 +52,6 @@ export default function JamLanding() {
</div>
</div>
{/* Fee Config Error Alert */}
{maxFeesConfigMissing && (
<FeeConfigErrorAlert onOpenFeeConfig={() => setShowFeeConfigDialog(true)} className="mb-4 max-w-2xl" />
)}
{error && (
<Alert variant="destructive" className="mb-4 max-w-2xl">
<AlertDescription>
@ -131,18 +116,6 @@ export default function JamLanding() {
{t('global.refresh')}
</Button>
</div>
{import.meta.env.DEV && (
<div className="mt-8">
<FeeConfigTestComponent />
</div>
)}
<FeeLimitDialog
open={showFeeConfigDialog}
onOpenChange={setShowFeeConfigDialog}
walletFileName={authState?.walletFileName || ''}
/>
</div>
)
}

View file

@ -0,0 +1,27 @@
import { useState } from 'react'
import { useStore } from 'zustand'
import { FeeLimitDialog } from '@/components/settings/FeeLimitDialog'
import { FeeConfigTestComponent } from '@/components/ui/FeeConfigTestComponent'
import { authStore } from '@/store/authStore'
export default function DevPage() {
const authState = useStore(authStore, (state) => state.state)
const [showFeeConfigDialog, setShowFeeConfigDialog] = useState(false)
return (
<div className="mx-auto max-w-2xl space-y-3 p-4">
<h1 className="my-2 text-2xl font-semibold tracking-tight">Development</h1>
<p className="text-muted-foreground mb-4 text-sm">Development specific information</p>
<div className="mt-8">
<FeeConfigTestComponent />
<FeeLimitDialog
open={showFeeConfigDialog}
onOpenChange={setShowFeeConfigDialog}
walletFileName={authState?.walletFileName || ''}
/>
</div>
</div>
)
}

View file

@ -0,0 +1,114 @@
import { BlocksIcon, TerminalIcon } from 'lucide-react'
import { Badge } from '../ui/badge'
const DEFAULT_BASIC_AUTH = {
user: 'joinmarket',
password: 'joinmarket',
}
const LINK_JM_REGTEST_JOINMARKET2 = 'http://localhost:29080'
const LINK_JM_REGTEST_JOINMARKET2_AUTH = DEFAULT_BASIC_AUTH
const LINK_JM_REGTEST_JOINMARKET3 = 'http://localhost:30080'
const LINK_JM_REGTEST_EXPLORER = 'http://localhost:3002'
const LINK_JM_REGTEST_EXPLORER_AUTH = DEFAULT_BASIC_AUTH
const LINK_JM_REGTEST_RPC_TERMINAL = `${LINK_JM_REGTEST_EXPLORER}/rpc-terminal`
export default function DevSetupPage() {
return (
<div className="mx-auto max-w-2xl space-y-3 p-4">
<h1 className="my-2 text-2xl font-semibold tracking-tight">Development setup</h1>
<div className="flex flex-col gap-3">
<div className="mb-4">
<h5 className="text-xl font-bold">Test Wallet</h5>
<div className="my-2 ms-3">
Name: <span className="font-mono">Satoshi</span>
<br />
Password: <span className="font-mono">test</span>
</div>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="mb-4">
<h5 className="text-xl font-bold">Links</h5>
<div className="my-2">
{/*<Link className="link-dark" to={routes.__errorExample}>
Error Example Page
</Link>*/}
</div>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="mb-4">
<h5 className="text-xl font-bold">Jam Instances</h5>
<div>
<div className="flex items-center gap-2">
{/*<Sprite symbol="logo" width="24" height="24" className="me-2" />*/}
<a href={LINK_JM_REGTEST_JOINMARKET2} target="_blank" rel="noopener noreferrer" className="underline">
jm_regtest_joinmarket2 ({LINK_JM_REGTEST_JOINMARKET2})
</a>
<Badge>secondary</Badge>
</div>
<div className="my-2 ms-5">
Basic Authentication
<br />
<small>
User: <span className="font-mono">{LINK_JM_REGTEST_JOINMARKET2_AUTH.user}</span>
<br />
Password: <span className="font-mono">{LINK_JM_REGTEST_JOINMARKET2_AUTH.password}</span>
</small>
</div>
<div className="flex items-center gap-2">
{/*<Sprite symbol="logo" width="24" height="24" className="me-2" />*/}
<a href={LINK_JM_REGTEST_JOINMARKET3} target="_blank" rel="noopener noreferrer" className="underline">
jm_regtest_joinmarket3 ({LINK_JM_REGTEST_JOINMARKET3})
</a>
<Badge>tertiary</Badge>
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="mb-4">
<h5 className="text-xl font-bold">Block Explorer</h5>
<div>
{' '}
<div className="flex items-center gap-2">
<BlocksIcon className="h-4 w-4" />
<a href={LINK_JM_REGTEST_EXPLORER} target="_blank" rel="noopener noreferrer" className="underline">
jm_regtest_explorer ({LINK_JM_REGTEST_EXPLORER})
</a>
</div>
<div className="my-2 ms-5">
Basic Authentication
<br />
<small>
User: <span className="font-mono">{LINK_JM_REGTEST_EXPLORER_AUTH.user}</span>
<br />
Password: <span className="font-mono">{LINK_JM_REGTEST_EXPLORER_AUTH.password}</span>
</small>
</div>
</div>
<div>
<div className="flex items-center gap-2">
<TerminalIcon className="h-4 w-4" />
<a href={LINK_JM_REGTEST_RPC_TERMINAL} target="_blank" rel="noopener noreferrer" className="underline">
Regtest RPC Terminal ({LINK_JM_REGTEST_RPC_TERMINAL})
</a>
</div>
<div className="my-2 ms-5">
Mine a block, e.g.:
<pre>generatetoaddress 1 bcrt1qrnz0thqslhxu86th069r9j6y7ldkgs2tzgf5wx</pre>
</div>
</div>
</div>
</div>
</div>
)
}

View file

@ -22,6 +22,8 @@ import { DevBadge } from '../ui/DevBadge'
import { CollaboratorFeesForm, type CollaboratorFeesFormRef } from './CollaboratorFeesForm'
import { MiningFeesForm, type MiningFeesFormRef } from './MiningFeesForm'
//TODO: needs testing!
interface FeeLimitDialogProps {
open: boolean
onOpenChange: (open: boolean) => void

View file

@ -0,0 +1,40 @@
interface DebugFeatures {
insecureScheduleTesting: boolean
allowCreatingExpiredFidelityBond: boolean
skipWalletBackupConfirmation: boolean
errorExamplePage: boolean
devPage: boolean
devSetupPage: boolean
importDummyMnemonicPhrase: boolean
allowFeeValuesReset: boolean
enableDemoEarnReport: boolean
enableDemoOrderbook: boolean
}
const devMode = import.meta.env.DEV && import.meta.env.VITE_JAM_DEV_MODE === 'true'
const debugFeatures: DebugFeatures = {
allowCreatingExpiredFidelityBond: devMode,
insecureScheduleTesting: devMode,
skipWalletBackupConfirmation: devMode,
errorExamplePage: devMode,
devPage: devMode,
devSetupPage: devMode,
importDummyMnemonicPhrase: devMode,
allowFeeValuesReset: devMode,
enableDemoEarnReport: devMode,
enableDemoOrderbook: devMode,
}
type DebugFeature = keyof DebugFeatures
export const isDevMode = (): boolean => devMode
export const isDebugFeatureEnabled = (name: DebugFeature): boolean => {
return debugFeatures[name] || false
}
// only to be used in tests
export const __testSetDebugFeatureEnabled = (name: DebugFeature, enabled: boolean): boolean => {
return (debugFeatures[name] = enabled)
}

23
src/constants/routes.ts Normal file
View file

@ -0,0 +1,23 @@
export const routes = {
home: '/',
login: '/login',
createWallet: '/create-wallet',
switchWallet: '/switch-wallet',
receive: '/receive',
send: '/send',
rescan: '/rescan',
earn: '/earn',
sweep: '/sweep',
settings: '/settings',
orderbook: '/orderbook',
logs: '/logs',
/* walletList: '/',
wallet: '/wallet',
importWallet: '/import-wallet',
*/
__dev: '/dev',
//__devErrorExample: '/dev/error-example',
__devSetup: '/dev/setup',
}
export type Route = keyof typeof routes

View file

@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { useStore } from 'zustand'
import { isDevMode } from '@/constants/debugFeatures'
import { fetchFeatures } from '@/lib/api/logs'
import { authStore } from '@/store/authStore'
@ -45,7 +46,8 @@ export const useFeatures = () => {
const isLogsEnabled = () => {
if (error) {
return import.meta.env.DEV
// show regardless of error in development mode
return isDevMode()
}
if (features) {

View file

@ -6,6 +6,8 @@ import { useApiClient } from '@/hooks/useApiClient'
import { configgetMutation } from '@/lib/jm-api/generated/client/@tanstack/react-query.gen'
import { authStore } from '@/store/authStore'
//TODO: needs testing!
export interface FeeConfigValues {
max_cj_fee_abs?: string
max_cj_fee_rel?: string

View file

@ -1,3 +1,4 @@
import { isDevMode } from '@/constants/debugFeatures'
import { createClient } from '@/lib/jm-api'
import type { ClientOptions, UnlockWalletResponse } from '@/lib/jm-api/generated/client'
import { authStore } from '@/store/authStore'
@ -39,9 +40,7 @@ export const createApiClient = (): Client => {
const jamAuthMiddleware = createJamAuthenticationMiddleware()
client.interceptors.request.use(jamAuthMiddleware)
const isDevelopment = import.meta.env.DEV
if (isDevelopment) {
if (isDevMode()) {
client.interceptors.request.use(loggingRequestInterceptor)
client.interceptors.response.use(loggingResponseInterceptor)
}