Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions components/Loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState, useEffect } from 'react'
import { DotLoader } from 'react-spinners'
import style from './loading.module.css'

interface LoadingProps {
size?: number
color?: string
}

export default function Loading ({ size = 60, color }: LoadingProps): React.ReactElement {
const [accentColor, setAccentColor] = useState(color ?? '#0ac18e')

useEffect(() => {
if (color === undefined) {
const computedColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim()
if (computedColor !== '') {
setAccentColor(computedColor)
}
}
}, [color])

return (
<div className={style.loading_container}>
<DotLoader
color={accentColor}
size={size}
speedMultiplier={2}
/>
</div>
)
}
7 changes: 7 additions & 0 deletions components/Loading/loading.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.loading_container {
display: flex;
justify-content: center;
align-items: center;
min-height: 50vh;
width: 100%;
}
16 changes: 11 additions & 5 deletions components/TopBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from 'next/link'
import { useState, useEffect } from 'react'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reconsider the date refactoring to avoid initial render flash.

This refactor introduces dateString state initialized as an empty string, which causes a brief flash of empty content before the useEffect populates it. The date calculation could be done directly without state, or the initial state could be set to a default value.

Additionally, this change appears unrelated to the loading spinner feature and introduces unnecessary complexity.

Consider one of these solutions:

Solution 1 (preferred): Compute date directly without state

-import { useState, useEffect } from 'react'
+import React from 'react'
 import style from './topbar.module.css'

 interface TopBarProps {
   title: string
   user?: string
 }

 export default function TopBar ({ title, user }: TopBarProps): JSX.Element {
-  const [dateString, setDateString] = useState<string>('')
-
-  useEffect(() => {
-    const currentDate = new Date()
-    const month = currentDate.toLocaleString('en-US', { month: 'long' })
-    const day = currentDate.getDate()
-    const year = currentDate.getFullYear()
-    setDateString(`${month} ${day}, ${year}`)
-  }, [])
+  const currentDate = new Date()
+  const month = currentDate.toLocaleString('en-US', { month: 'long' })
+  const day = currentDate.getDate()
+  const year = currentDate.getFullYear()
+  const dateString = `${month} ${day}, ${year}`

   return (

Solution 2: Initialize with a computed value to prevent flash

 export default function TopBar ({ title, user }: TopBarProps): JSX.Element {
-  const [dateString, setDateString] = useState<string>('')
+  const [dateString, setDateString] = useState<string>(() => {
+    const currentDate = new Date()
+    const month = currentDate.toLocaleString('en-US', { month: 'long' })
+    const day = currentDate.getDate()
+    const year = currentDate.getFullYear()
+    return `${month} ${day}, ${year}`
+  })

-  useEffect(() => {
-    const currentDate = new Date()
-    const month = currentDate.toLocaleString('en-US', { month: 'long' })
-    const day = currentDate.getDate()
-    const year = currentDate.getFullYear()
-    setDateString(`${month} ${day}, ${year}`)
-  }, [])

Also applies to: 11-19, 26-26

🤖 Prompt for AI Agents
In components/TopBar/index.tsx around lines 2 and also lines 11-19 and 26, the
refactor introduced dateString state initialized to an empty string causing a
flash; remove the unnecessary state and useEffect and compute the formatted date
inline (or if you must keep state, initialize it with the computed date value
rather than an empty string) so the component renders the date synchronously
without a blank flash; also drop unrelated changes to keep this file focused on
the loading spinner feature.

import style from './topbar.module.css'

interface TopBarProps {
Expand All @@ -7,17 +8,22 @@ interface TopBarProps {
}

export default function TopBar ({ title, user }: TopBarProps): JSX.Element {
const currentDate = new Date()
const month = currentDate.toLocaleString('en-US', { month: 'long' })
const day = currentDate.getDate()
const year = currentDate.getFullYear()
const [dateString, setDateString] = useState<string>('')

useEffect(() => {
const currentDate = new Date()
const month = currentDate.toLocaleString('en-US', { month: 'long' })
const day = currentDate.getDate()
const year = currentDate.getFullYear()
setDateString(`${month} ${day}, ${year}`)
}, [])

return (
<div className={style.topbar_ctn}>
<div className={style.title_ctn}>
<h2>{title}</h2>
<span>
{month} {day}, {year}
{dateString}
</span>
</div>
<div className={style.profile_ctn}>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.32.2",
"react-select": "^5.8.3",
"react-spinners": "^0.14.1",
"react-table": "^7.8.0",
"react-timezone-select": "^3.2.8",
"react-to-print": "^3.1.0",
Expand Down
11 changes: 11 additions & 0 deletions pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { multiBlockchainClient } from 'services/chronikService'
import { MainNetworkSlugsType } from 'constants/index'
import SubscribedAddresses from 'components/Admin/SubscribedAddresses'
import ChronikURLs from 'components/Admin/ChronikURLs'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
Expand Down Expand Up @@ -57,6 +58,7 @@ interface IProps {
export default function Admin ({ user, isAdmin, chronikUrls }: IProps): JSX.Element {
const router = useRouter()
const [users, setUsers] = useState<UserWithSupertokens[]>([])
const [loading, setLoading] = useState(true)

useEffect(() => {
if (user === null || !isAdmin) {
Expand All @@ -68,10 +70,19 @@ export default function Admin ({ user, isAdmin, chronikUrls }: IProps): JSX.Elem
void (async () => {
const usersJSON = await (await fetch('/api/users')).json()
setUsers(usersJSON)
setLoading(false)
})()
}, [])

if (user !== null && isAdmin) {
if (loading) {
return (
<div className={style.admin_ctn}>
<h2>Admin Dashboard</h2>
<Loading />
</div>
)
}
return <>
<div className={style.admin_ctn}>
<h2>Admin Dashboard</h2>
Expand Down
4 changes: 2 additions & 2 deletions pages/button/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react'
import Page from 'components/Page'
import { PaybuttonDetail } from 'components/Paybutton'
import { PaybuttonWithAddresses } from 'services/paybuttonService'
import { PaybuttonTransactions } from 'components/Transaction'
Expand All @@ -18,6 +17,7 @@ import { fetchUserProfileFromId } from 'services/userService'
import { removeUnserializableFields } from 'utils'
import moment from 'moment-timezone'
import Button from 'components/Button'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
supertokensNode.init(SuperTokensConfig.backendConfig())
Expand Down Expand Up @@ -214,6 +214,6 @@ export default function PayButton (props: PaybuttonProps): React.ReactElement {
}

return (
<Page />
<Loading />
)
}
13 changes: 12 additions & 1 deletion pages/buttons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GetServerSideProps } from 'next'
import TopBar from 'components/TopBar'
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
import { removeUnserializableFields } from 'utils/index'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
Expand Down Expand Up @@ -47,18 +48,20 @@ interface PaybuttonsState {
paybuttons: PaybuttonWithAddresses[]
wallets: WalletWithAddressesWithPaybuttons[]
error: String
loading: boolean
}

export default class Buttons extends React.Component<PaybuttonsProps, PaybuttonsState> {
constructor (props: PaybuttonsProps) {
super(props)
this.props = props
this.state = { paybuttons: [], wallets: [], error: '' }
this.state = { paybuttons: [], wallets: [], error: '', loading: true }
}

async componentDidMount (): Promise<void> {
await this.fetchPaybuttons()
await this.fetchWallets()
this.setState({ loading: false })
}

async fetchPaybuttons (): Promise<void> {
Expand Down Expand Up @@ -113,6 +116,14 @@ export default class Buttons extends React.Component<PaybuttonsProps, Paybuttons
}

render (): React.ReactElement {
if (this.state.loading) {
return (
<>
<TopBar title="Buttons" user={this.props.user.stUser?.email} />
<Loading />
</>
)
}
return (
<>
<TopBar title="Buttons" user={this.props.user.stUser?.email} />
Expand Down
10 changes: 9 additions & 1 deletion pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userServ
import moment from 'moment-timezone'
import SettingsIcon from '../../assets/settings-slider-icon.png'
import Image from 'next/image'
import Loading from 'components/Loading'

const Chart = dynamic(async () => await import('components/Chart'), {
ssr: false
Expand Down Expand Up @@ -148,7 +149,14 @@ export default function Dashboard ({ user }: PaybuttonsProps): React.ReactElemen
}
}, [activePeriodString, dashboardData])

if (dashboardData === undefined || activePeriod === undefined) return <></>
if (dashboardData === undefined || activePeriod === undefined) {
return (
<>
<TopBar title="Dashboard" user={user.stUser?.email} />
<Loading />
</>
)
}

return (
<>
Expand Down
11 changes: 9 additions & 2 deletions pages/networks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UserNetworksInfo } from 'services/networkService'
import TopBar from 'components/TopBar'
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
import { removeUnserializableFields } from 'utils/index'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
Expand Down Expand Up @@ -81,13 +82,19 @@ export default class Networks extends React.Component<NetworksProps, NetworksSta
}

render (): React.ReactElement {
if (this.state.networks !== []) {
if (this.state.networks.length === 0) {
return (
<>
<TopBar title="Networks" user={this.props.user.stUser?.email} />
<NetworkList networks={this.state.networks} userNetworks={this.state.userNetworks} />
<Loading />
</>
)
}
return (
<>
<TopBar title="Networks" user={this.props.user.stUser?.email} />
<NetworkList networks={this.state.networks} userNetworks={this.state.userNetworks} />
</>
)
Comment on lines +85 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the loading condition to avoid conflating empty state with loading state.

Using networks.length === 0 to determine loading state will cause the spinner to display indefinitely when a user legitimately has no networks. This conflates the initial loading state with an empty data state.

Add a dedicated loading boolean state similar to the pattern used in pages/buttons/index.tsx and pages/admin/index.tsx:

 interface NetworksState {
   networks: Network[]
   userNetworks: UserNetworksInfo[]
+  loading: boolean
 }

 export default class Networks extends React.Component<NetworksProps, NetworksState> {
   constructor (props: NetworksProps) {
     super(props)
     this.props = props
     this.state = {
       networks: [],
-      userNetworks: []
+      userNetworks: [],
+      loading: true
     }
   }

   async componentDidMount (): Promise<void> {
     await this.fetchNetworks()
+    this.setState({ loading: false })
   }

Then update the render condition:

   render (): React.ReactElement {
-    if (this.state.networks.length === 0) {
+    if (this.state.loading) {
       return (
         <>
           <TopBar title="Networks" user={this.props.user.stUser?.email} />
           <Loading />
         </>
       )
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pages/networks/index.tsx around lines 85-98, the component currently uses
this.state.networks.length === 0 to decide to show the Loading spinner, which
mixes initial loading with a legitimate empty-networks state; add a boolean
loading state (e.g. this.state.loading = true initially), set loading = false
after the async fetch completes (in both success and error paths), and update
the render logic to show <Loading /> only when loading === true, otherwise
render the TopBar and NetworkList (or an explicit empty-state message) so an
empty networks array does not display the spinner indefinitely.

}
}
12 changes: 12 additions & 0 deletions pages/payments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import InvoiceModal from 'components/Transaction/InvoiceModal'
import { TransactionWithAddressAndPricesAndInvoices } from 'services/transactionService'
import { fetchOrganizationForUser } from 'services/organizationService'
import { InvoiceWithTransaction } from 'services/invoiceService'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
Expand Down Expand Up @@ -76,6 +77,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
const [selectedTransactionYears, setSelectedTransactionYears] = useState<number[]>([])

const [loading, setLoading] = useState(false)
const [pageLoading, setPageLoading] = useState(true)
const [buttons, setButtons] = useState<any[]>([])
const [selectedButtonIds, setSelectedButtonIds] = useState<any[]>([])
const [showFilters, setShowFilters] = useState<boolean>(false)
Expand Down Expand Up @@ -178,6 +180,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp

setPaybuttonNetworks(networkIds)
setTransactionYears(years)
setPageLoading(false)
}

useEffect(() => {
Expand Down Expand Up @@ -474,6 +477,15 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp
setEndDate('')
}

if (pageLoading) {
return (
<>
<TopBar title="Payments" user={user?.stUser?.email} />
<Loading />
</>
)
}

return (
<>
<TopBar title="Payments" user={user?.stUser?.email} />
Expand Down
14 changes: 13 additions & 1 deletion pages/wallets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { UserNetworksInfo } from 'services/networkService'
import TopBar from 'components/TopBar'
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
import { removeUnserializableFields } from 'utils/index'
import Loading from 'components/Loading'

export const getServerSideProps: GetServerSideProps = async (context) => {
// this runs on the backend, so we must call init on supertokens-node SDK
Expand Down Expand Up @@ -48,6 +49,7 @@ interface WalletsState {
walletsWithPaymentInfo: WalletWithPaymentInfo[]
userAddresses: AddressWithPaybuttons[]
networksInfo: UserNetworksInfo[]
loading: boolean
}

export default class Wallets extends React.Component<WalletsProps, WalletsState> {
Expand All @@ -56,13 +58,15 @@ export default class Wallets extends React.Component<WalletsProps, WalletsState>
this.state = {
walletsWithPaymentInfo: [],
userAddresses: [],
networksInfo: []
networksInfo: [],
loading: true
}
}

async componentDidMount (): Promise<void> {
await this.fetchWallets()
await this.fetchNetworks()
this.setState({ loading: false })
}
Comment on lines 66 to 70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add error handling to prevent infinite loading state.

If fetchWallets or fetchNetworks throws an error, setState({ loading: false }) at line 69 will never execute, leaving users stuck on the loading screen indefinitely.

Apply this diff to add proper error handling:

 async componentDidMount (): Promise<void> {
-  await this.fetchWallets()
-  await this.fetchNetworks()
-  this.setState({ loading: false })
+  try {
+    await this.fetchWallets()
+    await this.fetchNetworks()
+  } catch (error) {
+    console.error('Error loading wallet data:', error)
+    // Optionally show error UI to user
+  } finally {
+    this.setState({ loading: false })
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async componentDidMount (): Promise<void> {
await this.fetchWallets()
await this.fetchNetworks()
this.setState({ loading: false })
}
async componentDidMount (): Promise<void> {
try {
await this.fetchWallets()
await this.fetchNetworks()
} catch (error) {
console.error('Error loading wallet data:', error)
// Optionally show error UI to user
} finally {
this.setState({ loading: false })
}
}
🤖 Prompt for AI Agents
In pages/wallets/index.tsx around lines 66 to 70, wrap the async
componentDidMount sequence in a try/catch/finally so that any exception from
fetchWallets or fetchNetworks is caught and loading is always set to false; in
the catch block set an error state or log the error (e.g., setState({ error:
errorMessage })) and in the finally call setState({ loading: false }) to ensure
the UI exits the loading state even on failure.


async fetchWallets (): Promise<void> {
Expand Down Expand Up @@ -102,6 +106,14 @@ export default class Wallets extends React.Component<WalletsProps, WalletsState>
}

render (): React.ReactElement {
if (this.state.loading) {
return (
<>
<TopBar title="Wallets" user={this.props.user.stUser?.email} />
<Loading />
</>
)
}
return (
<>
<TopBar title="Wallets" user={this.props.user.stUser?.email} />
Expand Down
9 changes: 7 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2294,7 +2294,7 @@ chronik-client-cashtokens@^3.1.1-rc0:
dependencies:
"@types/ws" "^8.2.1"
axios "^1.6.3"
ecashaddrjs "file:../../../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs"
ecashaddrjs "file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs"
isomorphic-ws "^4.0.1"
protobufjs "^6.8.8"
ws "^8.3.0"
Expand Down Expand Up @@ -2830,7 +2830,7 @@ ecashaddrjs@^1.0.7:
big-integer "1.6.36"
bs58check "^3.0.1"

ecashaddrjs@^2.0.0, "ecashaddrjs@file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs":
ecashaddrjs@^2.0.0, "ecashaddrjs@file:../../../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs":
version "2.0.0"
resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874"
integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw==
Expand Down Expand Up @@ -6008,6 +6008,11 @@ react-select@^5.8.3:
react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.2.0"

react-spinners@^0.14.1:
version "0.14.1"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.14.1.tgz#de7d7d6b3e6d4f29d9620c65495b502c7dd90812"
integrity sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag==

react-table@^7.8.0:
version "7.8.0"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2"
Expand Down