Skip to content

Conversation

@supersonicwisd1
Copy link
Contributor

@supersonicwisd1 supersonicwisd1 commented Sep 15, 2025

#483

Summary

• Add custom 3-step progress bar with animated transitions between steps
• Integrate CustomGdOnramperWidget with proper event handling for progress tracking
• Position G$ calculator in sidebar above FAQ using PageLayout customTabs
• Implement responsive design for mobile, tablet, and desktop viewports
• Remove duplicate progress bars (keep only custom implementation)
• Add smart contract wallet monitoring hook (placeholder for future implementation)
• Clean up unused code and fix all linting errors

Visuals

Screenshot 2025-09-15 at 11 17 28 Screenshot 2025-09-15 at 11 18 24

Test plan

  • Test progress bar animations on Buy G$ page (/buy)
  • Verify calculator functionality in sidebar
  • Test responsive design on mobile, tablet, and desktop
  • Confirm Onramper widget integration works properly
  • Verify no console errors or lint warnings

Description by Korbit AI

What change is being made?

Implement a new 3-step Buy G$ flow with a responsive Onramper widget and integrate it into the Buy GD page, including new components and helpers.

  • Add BuyProgressBar component (3-step progress with animated loading lines and step states).
    -Introduce CustomGdOnramperWidget and CustomOnramper to host an Onramper iframe with responsive sizing and event handling.
  • Update BuyGD page to use the new progress bar and Onramper widget, wiring event-driven step transitions and analytics.
  • Export and wire up the new components (CustomGdOnramperWidget) for reuse.
  • Minor locale keys alignment for translations references.

Why are these changes being made?

Provide a clear, guided 3-step user flow for purchasing G$, with a responsive, integrated Onramper widget, and centralize Onramper-related UI logic into reusable components to improve maintainability and consistency across the app.

Is this description stale? Ask me to generate a new description by commenting /korbit-generate-pr-description

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The main instruction copy under the title was hard-coded in English—please wrap it back in the i18n t macro to ensure consistent localization.
  • The useSmartContractWalletMonitor hook is currently just a placeholder that logs to console—either implement the intended balance polling or remove it until it’s functionally needed to avoid dead code.
  • Hiding the built-in Onramper progress UI via global CSS selectors is brittle; consider disabling those steps through the widget API or scoping the overrides within your CustomOnramper component instead of relying on aggressive CSS hacks.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The main instruction copy under the title was hard-coded in English—please wrap it back in the i18n `t` macro to ensure consistent localization.
- The useSmartContractWalletMonitor hook is currently just a placeholder that logs to console—either implement the intended balance polling or remove it until it’s functionally needed to avoid dead code.
- Hiding the built-in Onramper progress UI via global CSS selectors is brittle; consider disabling those steps through the widget API or scoping the overrides within your CustomOnramper component instead of relying on aggressive CSS hacks.

## Individual Comments

### Comment 1
<location> `src/components/BuyProgressBar/index.tsx:21` </location>
<code_context>
+    ]
+
+    // Handle animated progress line
+    useEffect(() => {
+        if (isLoading && currentStep > 1) {
+            // Animate progress line when loading
+            let progress = 0
+            const interval = setInterval(() => {
+                progress += 2
+                if (progress <= 100) {
+                    setAnimatedWidth(progress)
+                } else {
+                    clearInterval(interval)
+                }
+            }, 50) // 50ms intervals for smooth animation
+
+            return () => clearInterval(interval)
+        } else {
+            // Set to 100% if not loading (completed state)
+            setAnimatedWidth(100)
+        }
+    }, [isLoading, currentStep])
+
+    const getStepStatus = (stepNumber: number) => {
</code_context>

<issue_to_address>
Progress animation logic may not reset cleanly when switching steps.

Explicitly reset animatedWidth to 0 at the start of a new loading phase to prevent animation glitches when isLoading or currentStep changes rapidly.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    useEffect(() => {
        if (isLoading && currentStep > 1) {
            // Animate progress line when loading
            let progress = 0
            const interval = setInterval(() => {
                progress += 2
                if (progress <= 100) {
                    setAnimatedWidth(progress)
                } else {
                    clearInterval(interval)
                }
            }, 50) // 50ms intervals for smooth animation

            return () => clearInterval(interval)
        } else {
            // Set to 100% if not loading (completed state)
            setAnimatedWidth(100)
        }
    }, [isLoading, currentStep])
=======
    useEffect(() => {
        if (isLoading && currentStep > 1) {
            // Explicitly reset animatedWidth to 0 at the start of a new loading phase
            setAnimatedWidth(0)
            // Animate progress line when loading
            let progress = 0
            const interval = setInterval(() => {
                progress += 2
                if (progress <= 100) {
                    setAnimatedWidth(progress)
                } else {
                    clearInterval(interval)
                }
            }, 50) // 50ms intervals for smooth animation

            return () => clearInterval(interval)
        } else {
            // Set to 100% if not loading (completed state)
            setAnimatedWidth(100)
        }
    }, [isLoading, currentStep])
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `src/components/BuyProgressBar/index.tsx:56` </location>
<code_context>
+        return 'pending'
+    }
+
+    const getCircleProps = (status: string) => {
+        const baseProps = {
+            size: '12',
+            mb: 2,
+            justifyContent: 'center',
+            alignItems: 'center',
+        }
+
+        switch (status) {
+            case 'completed':
+                return { ...baseProps, bg: 'blue.500' }
+            case 'active':
+                return { ...baseProps, bg: 'blue.500' }
+            case 'loading':
+                return {
+                    ...baseProps,
</code_context>

<issue_to_address>
The 'animation' property in getCircleProps may not be supported by native-base.

Since native-base's Circle does not support the 'animation' property, consider using a custom animated component or conditional rendering to achieve the desired effect.
</issue_to_address>

### Comment 3
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:58` </location>
<code_context>
+    /**
+     * callback to get event from onramper iframe
+     */
+    const callback = useCallback(async (event: WebViewMessageEvent) => {
+        if ((event.nativeEvent.data as any).title === 'success') {
+            await AsyncStorage.setItem('gdOnrampSuccess', 'true')
+            //start the stepper
</code_context>

<issue_to_address>
Event data parsing assumes a specific structure that may not be robust.

Since event.nativeEvent.data may be a string, parse it as JSON and handle parsing errors to prevent runtime failures if the data format changes.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    /**
     * callback to get event from onramper iframe
     */
    const callback = useCallback(async (event: WebViewMessageEvent) => {
        if ((event.nativeEvent.data as any).title === 'success') {
            await AsyncStorage.setItem('gdOnrampSuccess', 'true')
            //start the stepper
            setStep(2)
        }
    }, [])
=======
    /**
     * callback to get event from onramper iframe
     */
    const callback = useCallback(async (event: WebViewMessageEvent) => {
        let eventData
        try {
            eventData = typeof event.nativeEvent.data === 'string'
                ? JSON.parse(event.nativeEvent.data)
                : event.nativeEvent.data
        } catch (error) {
            // Optionally log error or handle it
            return
        }

        if (eventData && eventData.title === 'success') {
            await AsyncStorage.setItem('gdOnrampSuccess', 'true')
            //start the stepper
            setStep(2)
        }
    }, [])
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:111` </location>
<code_context>
+    ]
+
+    // Handle animated progress line
+    useEffect(() => {
+        if (isLoading && currentStep > 1) {
+            // Animate progress line when loading
</code_context>

<issue_to_address>
Swap trigger logic may not handle repeated balance changes safely.

If an error occurs during swap, swapLock.current may remain set, preventing future swaps. Ensure swapLock.current is reset in error scenarios to allow retries.
</issue_to_address>

### Comment 5
<location> `src/components/CustomGdOnramperWidget/CustomOnramper.tsx:30` </location>
<code_context>
+    targetNetwork?: string
+    apiKey?: string
+}) => {
+    const url = new URL('https://buy.onramper.com/')
+
+    // Always include API key for proper authentication
+    if (apiKey) {
+        url.searchParams.set('apiKey', apiKey)
+    } else {
+        console.warn('Onramper: No API key provided')
+    }
+    url.searchParams.set('networkWallets', `${targetNetwork}:${targetWallet}`)
+    Object.entries(widgetParams).forEach(([k, v]: [string, any]) => {
+        url.searchParams.append(k, v)
+    })
</code_context>

<issue_to_address>
Appending widgetParams may result in duplicate query parameters.

Use url.searchParams.set for widgetParams to prevent duplicate keys when defaults and widgetParams overlap.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    Object.entries(widgetParams).forEach(([k, v]: [string, any]) => {
        url.searchParams.append(k, v)
    })
=======
    Object.entries(widgetParams).forEach(([k, v]: [string, any]) => {
        url.searchParams.set(k, v)
    })
>>>>>>> REPLACE

</suggested_fix>

### Comment 6
<location> `src/pages/gd/BuyGD/index.tsx:140` </location>
<code_context>
-                    t`
-                Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$.`
-                )}
+                Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$.
             </Text>
+
</code_context>

<issue_to_address>
Hardcoded string replaces i18n translation.

Consider reverting to i18n._(t`...`) to maintain localization support.
</issue_to_address>

### Comment 7
<location> `src/pages/gd/BuyGD/BuyGD.css:31` </location>
<code_context>
+}
+
+/* More specific targeting for Onramper built-in progress elements */
+div[style*="justify-content: space-between"]:has(div:contains("Buy cUSD")),
+div:has(div:contains("We swap cUSD to G$")),
+div:has(div:contains("Done")) {
+    display: none !important;
+}
</code_context>

<issue_to_address>
CSS selectors using :has and :contains may not be supported in all browsers.

This could prevent the progress bar from being hidden in some browsers. Please use selectors with broader compatibility or verify support for your target browsers.
</issue_to_address>

### Comment 8
<location> `src/components/CustomGdOnramperWidget/CustomOnramper.tsx:107` </location>
<code_context>
+        }
+    }, [title, step])
+
+    if (!targetWallet) {
+        return <></>
+    }
</code_context>

<issue_to_address>
Returning an empty fragment may not provide feedback for missing wallet.

Consider displaying a fallback UI or error message when targetWallet is missing to improve user feedback.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    if (!targetWallet) {
        return <></>
    }
=======
    if (!targetWallet) {
        return (
            <div style={{ padding: '1rem', textAlign: 'center', color: 'red' }}>
                Wallet not found. Please select a valid wallet to continue.
            </div>
        )
    }
>>>>>>> REPLACE

</suggested_fix>

### Comment 9
<location> `src/components/BuyProgressBar/index.tsx:41` </location>
<code_context>
+        }
+    }, [isLoading, currentStep])
+
+    const getStepStatus = (stepNumber: number) => {
+        // Step 1 should ALWAYS be blue (active when current, completed when past)
+        if (stepNumber === 1) {
</code_context>

<issue_to_address>
Consider refactoring the step status and props logic into arrays and lookup tables to simplify branching and improve readability.

Here’s one way to collapse much of that branching into a simple “status” array + lookup tables. This keeps exactly the same 3-step, loading/active/completed/pending behavior:

```tsx
// 1) Build a flat statuses array instead of getStepStatus():
type Status = 'completed' | 'active' | 'loading' | 'pending'
const statuses: Status[] = steps.map((_, idx) => {
  if (idx < currentStep - 1) return 'completed'
  if (idx === currentStep - 1) return isLoading ? 'loading' : 'active'
  return 'pending'
})
```

```tsx
// 2) Replace getCircleProps with a simple lookup:
const CIRCLE_VARIANTS: Record<Status, any> = {
  completed: { size: '12', bg: 'blue.500' },
  active:    { size: '12', bg: 'blue.500' },
  loading:   { size: '12', bg: 'blue.500', borderWidth: 3, borderColor: 'blue.200', animation: 'pulse 2s infinite' },
  pending:   { size: '12', bg: 'gray.300' },
}
```

```tsx
// 3) One single line-props helper:
const getLineProps = (lineIdx: number) => {
  const isBefore = lineIdx < currentStep - 1
  const isActiveLine = lineIdx === currentStep - 1 && isLoading
  const width = isBefore ? '100%' : isActiveLine ? `${animatedWidth}%` : '0%'
  return {
    bg: width === '0%' ? 'gray.300' : 'blue.500',
    width,
    transition: isActiveLine ? 'width 0.1s ease-out' : undefined,
  }
}
```

Then in your JSX you just do:

```tsx
{steps.map((step, idx) => (
  <React.Fragment key={step.number}>
    <Box flex={1} alignItems="center">
      <Circle {...CIRCLE_VARIANTS[statuses[idx]]}>
        <Text color="white" fontWeight="bold">{step.number}</Text>
      </Circle>
      <Text color={statuses[idx] === 'pending' ? 'gray.500' : 'black'}>
        {step.label}
      </Text>
    </Box>

    {idx < steps.length - 1 && (
      <Box position="absolute" /* your positioning logic */>
        <Box height="2px" borderRadius="1px" {...getLineProps(idx)} />
      </Box>
    )}
  </React.Fragment>
))}
```

This:

- Removes special-case code for step 1 in getStepStatus.
- Collapses getCircleProps into a static map.
- Collapses two branches in getLineProps into one.
- Keeps all functionality (loading/active/completed/pending animations) intact.
</issue_to_address>

### Comment 10
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:28` </location>
<code_context>
+    apiKey?: string
+}
+
+export const CustomGdOnramperWidget = ({
+    onEvents = noop,
+    selfSwap = false,
</code_context>

<issue_to_address>
Consider extracting swap logic and small UI components into custom hooks and separate files to simplify the main component.

Here are two small, focused extractions that keep all existing behavior but dramatically slim down your component:

1) Extract all swap logic (state, lock, `triggerSwap`, `useEffect`) into a custom hook:

```ts
// hooks/useGdOnramperSwap.ts
import { useState, useRef, useCallback, useEffect } from 'react'
import { useBuyGd } from '@gooddollar/web3sdk-v2'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'

interface Params {
  selfSwap: boolean
  withSwap: boolean
  donateOrExecTo?: string
  callData: string
  apiKey?: string
  account?: string
  library?: any
  gdHelperAddress?: string
  onEvents: (action: string, data?: any, error?: string) => void
  showError: () => void
}

export function useGdOnramperSwap({
  selfSwap, withSwap, donateOrExecTo, callData,
  account, library, gdHelperAddress, onEvents, showError,
}: Params) {
  const [step, setStep] = useState(0)
  const lock = useRef(false)
  const {
    createAndSwap, swap, triggerSwapTx,
    swapState, createState,
  } = useBuyGd({ donateOrExecTo, callData, withSwap })

  const internalSwap = useCallback(async () => {
    if (lock.current) return
    lock.current = true
    try {
      setStep(3)
      let txPromise
      if (selfSwap && gdHelperAddress && library && account) {
        const code = await library.getCode(gdHelperAddress)
        txPromise = code.length <= 2
          ? createAndSwap(0)
          : swap(0)
        setStep(4)
      } else if (account) {
        setStep(4)
        txPromise = triggerSwapTx()
      }
      const res = await txPromise
      if ((res as any)?.status !== 1 && !(res as any)?.ok) throw new Error('reverted')
      setStep(5)
      onEvents('buy_success')
    } catch (e: any) {
      showError()
      onEvents('buygd_swap_failed', e.message)
      setStep(0)
    } finally {
      lock.current = false
    }
  }, [
    selfSwap, gdHelperAddress, library, account,
    createAndSwap, swap, triggerSwapTx, onEvents, showError,
  ])

  // trigger swap when any helper balance > 0
  useEffect(() => {
    if (!gdHelperAddress) return
    ;(async () => {
      const cusd = await AsyncStorage.getItem('gdOnrampSuccess')
      if (!cusd) return
      await AsyncStorage.removeItem('gdOnrampSuccess')
      internalSwap()
    })()
  }, [gdHelperAddress, internalSwap])

  return { step, swapState, createState, triggerSwap: internalSwap, setStep }
}
```

Then your component becomes:

```tsx
import { useEthers, useEtherBalance, useTokenBalance } from '@usedapp/core'
import { useGdOnramperSwap } from './hooks/useGdOnramperSwap'
import { ErrorModal } from './components/ErrorModal'
import { useModal } from '@gooddollar/good-design/dist/hooks/useModal'
import { useOnramperCallback } from './hooks/useOnramperCallback'

export function CustomGdOnramperWidget(props: ICustomGdOnramperProps) {
  const { account, library } = useEthers()
  const { showModal, Modal } = useModal()
  const { onEvents, selfSwap, withSwap } = props
  const gdHelperAddress = /* get from useBuyGd or prop */
  const { step, swapState, createState, triggerSwap } = useGdOnramperSwap({
    ...props, account, library, gdHelperAddress,
    onEvents, showError: showModal
  })

  const celo = useEtherBalance(gdHelperAddress)
  const cusd = useTokenBalance(/*...*/)

  // webview callback
  const callback = useOnramperCallback(() => setStep(2))

  return (
    <>
      <Modal body={<ErrorModal />} />
      <WalletAndChainGuard validChains={[42220]}>
        <CustomOnramper
          onEvent={callback}
          step={step}
          setStep={setStep}
          /* ...other props */
        />
      </WalletAndChainGuard>
      <SignWalletModal txStatus={swapState.status}/>
      <SignWalletModal txStatus={createState.status}/>
    </>
  )
}
```

2) Pull out your tiny UI bits into their own files:

```tsx
// components/ErrorModal.tsx
import { View, Text } from 'native-base'
export const ErrorModal = () => (
  <View><Text>Something went wrong.</Text></View>
)
```

```ts
// hooks/useOnramperCallback.ts
import { useCallback } from 'react'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'
export const useOnramperCallback = (onSuccess: () => void) =>
  useCallback(async e => {
    const data = JSON.parse(e.nativeEvent.data)
    if (data.title === 'success') {
      await AsyncStorage.setItem('gdOnrampSuccess', 'true')
      onSuccess()
    }
  }, [onSuccess])
```

These two extractions keep everything working but collapse ~200 loc of mixed concerns into small, testable hooks and components.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

apiKey?: string
}

export const CustomGdOnramperWidget = ({
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider extracting swap logic and small UI components into custom hooks and separate files to simplify the main component.

Here are two small, focused extractions that keep all existing behavior but dramatically slim down your component:

  1. Extract all swap logic (state, lock, triggerSwap, useEffect) into a custom hook:
// hooks/useGdOnramperSwap.ts
import { useState, useRef, useCallback, useEffect } from 'react'
import { useBuyGd } from '@gooddollar/web3sdk-v2'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'

interface Params {
  selfSwap: boolean
  withSwap: boolean
  donateOrExecTo?: string
  callData: string
  apiKey?: string
  account?: string
  library?: any
  gdHelperAddress?: string
  onEvents: (action: string, data?: any, error?: string) => void
  showError: () => void
}

export function useGdOnramperSwap({
  selfSwap, withSwap, donateOrExecTo, callData,
  account, library, gdHelperAddress, onEvents, showError,
}: Params) {
  const [step, setStep] = useState(0)
  const lock = useRef(false)
  const {
    createAndSwap, swap, triggerSwapTx,
    swapState, createState,
  } = useBuyGd({ donateOrExecTo, callData, withSwap })

  const internalSwap = useCallback(async () => {
    if (lock.current) return
    lock.current = true
    try {
      setStep(3)
      let txPromise
      if (selfSwap && gdHelperAddress && library && account) {
        const code = await library.getCode(gdHelperAddress)
        txPromise = code.length <= 2
          ? createAndSwap(0)
          : swap(0)
        setStep(4)
      } else if (account) {
        setStep(4)
        txPromise = triggerSwapTx()
      }
      const res = await txPromise
      if ((res as any)?.status !== 1 && !(res as any)?.ok) throw new Error('reverted')
      setStep(5)
      onEvents('buy_success')
    } catch (e: any) {
      showError()
      onEvents('buygd_swap_failed', e.message)
      setStep(0)
    } finally {
      lock.current = false
    }
  }, [
    selfSwap, gdHelperAddress, library, account,
    createAndSwap, swap, triggerSwapTx, onEvents, showError,
  ])

  // trigger swap when any helper balance > 0
  useEffect(() => {
    if (!gdHelperAddress) return
    ;(async () => {
      const cusd = await AsyncStorage.getItem('gdOnrampSuccess')
      if (!cusd) return
      await AsyncStorage.removeItem('gdOnrampSuccess')
      internalSwap()
    })()
  }, [gdHelperAddress, internalSwap])

  return { step, swapState, createState, triggerSwap: internalSwap, setStep }
}

Then your component becomes:

import { useEthers, useEtherBalance, useTokenBalance } from '@usedapp/core'
import { useGdOnramperSwap } from './hooks/useGdOnramperSwap'
import { ErrorModal } from './components/ErrorModal'
import { useModal } from '@gooddollar/good-design/dist/hooks/useModal'
import { useOnramperCallback } from './hooks/useOnramperCallback'

export function CustomGdOnramperWidget(props: ICustomGdOnramperProps) {
  const { account, library } = useEthers()
  const { showModal, Modal } = useModal()
  const { onEvents, selfSwap, withSwap } = props
  const gdHelperAddress = /* get from useBuyGd or prop */
  const { step, swapState, createState, triggerSwap } = useGdOnramperSwap({
    ...props, account, library, gdHelperAddress,
    onEvents, showError: showModal
  })

  const celo = useEtherBalance(gdHelperAddress)
  const cusd = useTokenBalance(/*...*/)

  // webview callback
  const callback = useOnramperCallback(() => setStep(2))

  return (
    <>
      <Modal body={<ErrorModal />} />
      <WalletAndChainGuard validChains={[42220]}>
        <CustomOnramper
          onEvent={callback}
          step={step}
          setStep={setStep}
          /* ...other props */
        />
      </WalletAndChainGuard>
      <SignWalletModal txStatus={swapState.status}/>
      <SignWalletModal txStatus={createState.status}/>
    </>
  )
}
  1. Pull out your tiny UI bits into their own files:
// components/ErrorModal.tsx
import { View, Text } from 'native-base'
export const ErrorModal = () => (
  <View><Text>Something went wrong.</Text></View>
)
// hooks/useOnramperCallback.ts
import { useCallback } from 'react'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'
export const useOnramperCallback = (onSuccess: () => void) =>
  useCallback(async e => {
    const data = JSON.parse(e.nativeEvent.data)
    if (data.title === 'success') {
      await AsyncStorage.setItem('gdOnrampSuccess', 'true')
      onSuccess()
    }
  }, [onSuccess])

These two extractions keep everything working but collapse ~200 loc of mixed concerns into small, testable hooks and components.

Copy link
Collaborator

Choose a reason for hiding this comment

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

valid comment @supersonicwisd1

Copy link

@korbit-ai korbit-ai bot left a comment

Choose a reason for hiding this comment

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

Review by Korbit AI

Korbit automatically attempts to detect when you fix issues in new commits.
Category Issue Status
Functionality Unimplemented wallet-monitoring logic drives no functional behavior ▹ view ✅ Fix detected
Design Monolithic event handling and state transitions ▹ view ✅ Fix detected
Performance Aggressive balance polling ▹ view ✅ Fix detected
Security Exposure of user address in logs ▹ view ✅ Fix detected
Functionality Missing dependencies in useEffect ▹ view ✅ Fix detected
Performance State updates not batched ▹ view ✅ Fix detected
Security Secret API key exposed in URL ▹ view
Files scanned
File Path Reviewed
src/components/CustomGdOnramperWidget/index.ts
src/hooks/useSmartContractWalletMonitor.ts
src/components/CustomGdOnramperWidget/CustomOnramper.tsx
src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx
src/pages/gd/BuyGD/index.tsx
src/components/BuyProgressBar/index.tsx

Explore our documentation to understand the languages and file types we support and the files we ignore.

Check out our docs on how you can make Korbit work best for you and your team.

Loving Korbit!? Share us on LinkedIn Reddit and X

Comment on lines 48 to 49
const celoBalance = useEtherBalance(gdHelperAddress, { refresh: 1 })
const cusdBalance = useTokenBalance(cusd, gdHelperAddress, { refresh: 1 })

This comment was marked as resolved.

Comment on lines 61 to 104
const handleEvents = useCallback(
(event: string, data?: any, error?: string) => {
sendData({ event: 'buy', action: event, ...(error && { error: error }) })
const eventData: any = { event: 'buy', action: event }
if (data) eventData.data = data
if (error) eventData.error = error
sendData(eventData)

switch (event) {
case 'widget_clicked':
case 'widget_opened':
setCurrentStep(1)
setIsLoading(true)
break
case 'transaction_started':
setCurrentStep(1)
setIsLoading(true)
break
case 'funds_received':
setCurrentStep(2)
setIsLoading(false)
break
case 'transaction_sent':
case 'swap_started':
setCurrentStep(2)
setIsLoading(true)
break
case 'swap_completed':
case 'transaction_completed':
setCurrentStep(3)
setIsLoading(false)
break
case 'error':
setIsLoading(false)
break
case 'reset':
setCurrentStep(1)
setIsLoading(false)
break
default:
break
}
},
[sendData]
)

This comment was marked as resolved.

Comment on lines 70 to 73
case 'widget_opened':
setCurrentStep(1)
setIsLoading(true)
break
case 'transaction_started':

This comment was marked as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems valid

Copy link

Choose a reason for hiding this comment

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

Understood. Let me know if you need any assistance implementing the suggested changes.

@supersonicwisd1
Copy link
Contributor Author

@L03TJ3 can you kindly take a look at this PR draft for issue 483.

@supersonicwisd1
Copy link
Contributor Author

@L03TJ3 can you kindly take a look at this PR draft for issue 483.

@sirpy if you can review this, it would be great

apiKey?: string
}

export const CustomGdOnramperWidget = ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

valid comment @supersonicwisd1

Comment on lines 70 to 73
case 'widget_opened':
setCurrentStep(1)
setIsLoading(true)
break
case 'transaction_started':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems valid

@supersonicwisd1 supersonicwisd1 marked this pull request as ready for review October 6, 2025 09:20
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The BuyProgressBar’s setInterval-driven state updates will trigger many re-renders—consider using CSS transitions or requestAnimationFrame for smoother animations with fewer React updates.
  • The global CSS selectors used to hide Onramper’s built-in progress elements are quite brittle and may break with widget updates—try using more scoped selectors or Onramper configuration options if available.
  • The CustomGdOnramperWidget component mixes event parsing, swap logic, and UI concerns—extracting those into custom hooks or smaller subcomponents could improve readability and maintainability.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The BuyProgressBar’s setInterval-driven state updates will trigger many re-renders—consider using CSS transitions or requestAnimationFrame for smoother animations with fewer React updates.
- The global CSS selectors used to hide Onramper’s built-in progress elements are quite brittle and may break with widget updates—try using more scoped selectors or Onramper configuration options if available.
- The CustomGdOnramperWidget component mixes event parsing, swap logic, and UI concerns—extracting those into custom hooks or smaller subcomponents could improve readability and maintainability.

## Individual Comments

### Comment 1
<location> `src/components/BuyProgressBar/index.tsx:58-67` </location>
<code_context>
+        return 'pending'
+    }
+
+    const getCircleProps = (status: string) => {
+        const baseProps = {
+            size: '12',
+            mb: 2,
+            justifyContent: 'center',
+            alignItems: 'center',
+        }
+
+        switch (status) {
+            case 'completed':
+                return { ...baseProps, bg: 'blue.500' }
+            case 'active':
+                return { ...baseProps, bg: 'blue.500' }
+            case 'loading':
+                return {
+                    ...baseProps,
</code_context>

<issue_to_address>
**issue:** The 'animation' property in getCircleProps may not be supported by native-base.

Since native-base's Circle may not support the 'animation' property, consider replacing it with a component designed for loading states to ensure compatibility across platforms.
</issue_to_address>

### Comment 2
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:75-76` </location>
<code_context>
+        }
+    }, [])
+
+    const triggerSwap = async () => {
+        if (swapLock.current) return //prevent from useEffect retriggering this
+        swapLock.current = true
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** swapLock usage may not be robust against concurrent triggers.

A ref-based lock may not fully prevent race conditions during rapid calls or re-renders. Consider a more reliable locking strategy or temporarily disabling the UI to ensure only one swap occurs at a time.

Suggested implementation:

```typescript
+    const [isSwapping, setIsSwapping] = useState(false)
+
+    const triggerSwap = async () => {
+        if (isSwapping) return // Prevent concurrent swaps
+        setIsSwapping(true)

```

```typescript

```

```typescript
+        // ... swap logic here ...
+        // After swap completes (success or error), release the lock
+        setIsSwapping(false)

```

```typescript
const ErrorModal = () => (
    <View>
        <Text>Something went wrong.</Text>
    </View>
)

// Example usage in UI (replace your swap button with this logic):
// <Button onPress={triggerSwap} disabled={isSwapping}>Swap</Button>

```

- You will need to import `useState` from React at the top of your file: `import React, { useState } from 'react'`
- Update your swap button or UI element to use the `disabled={isSwapping}` prop so users cannot trigger multiple swaps.
- Ensure that any error handling or early returns in `triggerSwap` also call `setIsSwapping(false)` if the swap fails.
</issue_to_address>

### Comment 3
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:117-118` </location>
<code_context>
+    ]
+
+    // Handle animated progress line
+    useEffect(() => {
+        if (isLoading && currentStep > 1) {
+            // Explicitly reset animatedWidth to 0 at the start of a new loading phase
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Potential for repeated swap triggers if balances change rapidly.

If balances change frequently, this effect may trigger multiple swaps. Consider implementing debouncing or extra checks to prevent unnecessary repeated actions.
</issue_to_address>

### Comment 4
<location> `src/components/CustomGdOnramperWidget/CustomOnramper.tsx:114-118` </location>
<code_context>
+        }
+    }, [title, step, onGdEvent, setStep])
+
+    if (!targetWallet) {
+        return (
+            <div style={{ padding: '1rem', textAlign: 'center', color: 'red' }}>
+                Wallet not found. Please select a valid wallet to continue.
</code_context>

<issue_to_address>
**suggestion:** Rendering a raw <div> may break styling consistency in a native-base context.

Consider replacing <div> with a native-base component like <Box> or <View> to maintain consistent styling.

```suggestion
        return (
            <Box p="4" textAlign="center" color="red.500">
                Wallet not found. Please select a valid wallet to continue.
            </Box>
        )
```
</issue_to_address>

### Comment 5
<location> `src/pages/gd/BuyGD/BuyGD.css:21-30` </location>
<code_context>
+}
+
+/* Alternative approach - hide progress elements by common selectors */
+.progress-bar,
+.stepper,
+.steps-container,
+[class*="progress"],
+[class*="stepper"],
+[class*="step"] {
+    display: none !important;
+}
+
+/* More specific targeting for Onramper built-in progress elements - using broader compatibility selectors */
+div[style*="justify-content: space-between"] div,
+div[style*="flex-direction: row"] div,
+div[style*="display: flex"] div {
+    /* Target potential progress elements - may need refinement based on actual Onramper structure */
+}
+
+/* Hide elements containing specific text using data attributes or classes if available */
+[data-step="1"],
+[data-step="2"], 
+[data-step="3"],
+.onramper-step,
+.onramper-progress {
+    display: none !important;
+}
</code_context>

<issue_to_address>
**suggestion:** CSS selectors may be too broad and could hide unrelated elements.

Please scope the selectors to target only Onramper widget elements to prevent unintended side effects on other components.

Suggested implementation:

```
.onramper-widget-container .progress-bar,
.onramper-widget-container .stepper,
.onramper-widget-container .steps-container,
.onramper-widget-container [class*="progress"],
.onramper-widget-container [class*="stepper"],
.onramper-widget-container [class*="step"] {
    display: none !important;
}

```

```
.onramper-widget-container div[style*="justify-content: space-between"] div,
.onramper-widget-container div[style*="flex-direction: row"] div,
.onramper-widget-container div[style*="display: flex"] div {
    /* Target potential progress elements - may need refinement based on actual Onramper structure */
}

```

```
.onramper-widget-container [data-step="1"],
.onramper-widget-container [data-step="2"], 
.onramper-widget-container [data-step="3"],
.onramper-widget-container .onramper-step,
.onramper-widget-container .onramper-progress {
    display: none !important;
}

```
</issue_to_address>

### Comment 6
<location> `src/components/BuyProgressBar/index.tsx:12` </location>
<code_context>
+}
+
+const BuyProgressBar: React.FC<BuyProgressBarProps> = ({ currentStep, isLoading = false }) => {
+    const [animatedWidth, setAnimatedWidth] = useState(0)
+
+    const steps = [
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the component to use style maps and CSS transitions instead of manual state and interval logic for progress animation.

You can collapse most of the branching into simple style-maps and drop the manual `setInterval` entirely by using CSS transitions on `width`. For example:

```tsx
// 1) Remove animatedWidth state + useEffect entirely.

// 2) Define a single status‐to‐styles map for circles & text:
const baseCircle = {
  size: '12',
  mb: 2,
  justifyContent: 'center',
  alignItems: 'center',
};
const statusStyles = {
  completed: { bg: 'blue.500', textColor: 'white' },
  active:    { bg: 'blue.500', textColor: 'white' },
  loading:   {
    bg: 'blue.500',
    borderWidth: 3,
    borderColor: 'blue.200',
    animation: 'pulse 2s infinite',
    textColor: 'white',
  },
  pending:   { bg: 'gray.300', textColor: 'gray.500' },
} as const;

// 3) A single helper to render Circle+Label:
function Step({
  step,
  status,
}: { step: { number: number; label: string }; status: keyof typeof statusStyles }) {
  const styles = statusStyles[status];
  return (
    <Box alignItems="center" flex={1}>
      <Circle {...baseCircle} bg={styles.bg} borderWidth={styles.borderWidth} borderColor={styles.borderColor} animation={styles.animation}>
        <Text color={styles.textColor} fontWeight="bold" fontSize="md">
          {step.number}
        </Text>
      </Circle>
      <Text textAlign="center" fontSize="sm" fontFamily="subheading" color={styles.textColor}>
        {step.label}
      </Text>
    </Box>
  );
}

// 4) Simplify lines to rely on CSS transitions:
const baseLine = {
  height: '2px',
  transition: 'width 300ms ease',
};
const lineStatusMap = {
  completed: { bg: 'blue.500', width: '100%' },
  loading:   { bg: 'blue.500', width: '100%' }, // triggers the transition
  pending:   { bg: 'gray.300', width: '0%' },
} as const;

// 5) In your render loop:
<React.Fragment key={step.number}>
  <Step step={step} status={getStepStatus(step.number)} />

  {index < steps.length - 1 && (
    <Box
      position="absolute"
      top="6"
      left={`${33.33 * (index + 1) - 16.67}%`}
      right={`${66.67 - 33.33 * (index + 1) + 16.67}%`}
      zIndex={-1}
    >
      <Box {...baseLine} {...lineStatusMap[getStepStatus(step.number + 1)]} borderRadius="1px" />
    </Box>
  )}
</React.Fragment>
```

This:

- Drops the `animatedWidth` state + interval.
- Uses a single style-map for circles/text & one for lines.
- Leverages CSS transitions for smooth width animation.
- Keeps your current `getStepStatus` so logic stays intact.
</issue_to_address>

### Comment 7
<location> `src/components/CustomGdOnramperWidget/CustomOnramper.tsx:11` </location>
<code_context>
+
+export type OnramperCallback = (event: WebViewMessageEvent) => void
+
+export const CustomOnramper = ({
+    onEvent,
+    onGdEvent,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the component by extracting URL-building, step logic, and responsive styles into separate utilities and hooks.

```markdown
You can collapse much of thisgluecode into small reusable hooks/utils and lean on Native-Bases built-in responsive props to eliminate the manual breakpoint logic. For example:

1) Extract URLbuilding into a util:

```ts
// src/utils/onramper.ts
export function buildOnramperUrl({
  apiKey,
  targetNetwork,
  targetWallet,
  widgetParams,
}: {
  apiKey?: string
  targetNetwork: string
  targetWallet: string
  widgetParams: Record<string,string>
}): string {
  const url = new URL('https://buy.onramper.com/')
  if (apiKey) url.searchParams.set('apiKey', apiKey)
  url.searchParams.set('networkWallets', `${targetNetwork}:${targetWallet}`)
  Object.entries(widgetParams).forEach(([k,v]) => url.searchParams.set(k,v))
  return url.toString()
}
```

2) Pull your AsyncStorage + focus logic into a `useOnramperStep` hook:

```ts
// src/hooks/useOnramperStep.ts
import { useEffect } from 'react'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'
import { useWindowFocus } from '@gooddollar/good-design'

export function useOnramperStep(
  step: number,
  setStep: (n:number) => void,
  onGdEvent: (action:string) => void
) {
  const { title } = useWindowFocus()

  // returning‐user check
  useEffect(() => {
    AsyncStorage.getItem('gdOnrampSuccess').then(val => {
      if (val==='true') setStep(2)
    })
  }, [])

  // widget open event
  useEffect(() => {
    if (title==='Onramper widget' && step===0) {
      onGdEvent('buy_start')
      setStep(1)
    }
  }, [title, step])
}
```

3) Use Native-Bases responsive style props instead of `useBreakpointValue`:

```tsx
<CentreBox
  maxW={{ base: '100%', sm: 400, md: 450, lg: 480 }}
  w  ={{ base: '95vw', sm: '90%', md: '80%', lg: '100%', xl: '200%' }}
  h  ={{ base: 500, sm: 550, md: 600, lg: 630 }}
  mb={6}
>
  <WebView
    source={{uri}}
    style={{
      borderRadius:4, borderWidth:1, borderColor:'#58585f',
      width:'100%', height:'100%'
    }}
    width="100%"
    height="100%"

  />
</CentreBox>
```

Then your component becomes:

```tsx
import { buildOnramperUrl } from 'src/utils/onramper'
import { useOnramperStep } from 'src/hooks/useOnramperStep'

export const CustomOnramper = props => {
  const { apiKey, targetNetwork='CELO', targetWallet, widgetParams, step, setStep, onGdEvent, onEvent } = props
  if (!targetWallet) return <div style={{…}}>Wallet not found…</div>

  const uri = useMemo(()=>
    buildOnramperUrl({ apiKey, targetNetwork, targetWallet, widgetParams }),
    [apiKey, targetNetwork, targetWallet, widgetParams]
  )

  useOnramperStep(step, setStep, onGdEvent)
  const isMobile = deviceDetect()

  return (
    <Box alignItems="center" mb={6} w="100%">
      {/* CentreBox as above */}
      {isMobile && <Divider… />}
    </Box>
  )
}
```

This keeps all behavior intact, but dramatically cuts down inline complexity and makes each piece individually testable.
</issue_to_address>

### Comment 8
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:28` </location>
<code_context>
+    apiKey?: string
+}
+
+export const CustomGdOnramperWidget = ({
+    onEvents = noop,
+    selfSwap = false,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the component by extracting the Onramper event handling and swap logic into custom hooks to simplify and declutter the main component.

```suggestion
// 1) Extract the Onramper iframe logic into a `useOnramper` hook.
// src/hooks/useOnramper.ts
import { useCallback, useState } from 'react'
import { WebViewMessageEvent } from 'react-native-webview'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'

export function useOnramper(onEvents: (action: string, data?: any, error?: string) => void) {
  const [step, setStep] = useState(0)

  const onEvent = useCallback(async (event: WebViewMessageEvent) => {
    try {
      const data =
        typeof event.nativeEvent.data === 'string'
          ? JSON.parse(event.nativeEvent.data)
          : event.nativeEvent.data
      if (data?.title === 'success') {
        await AsyncStorage.setItem('gdOnrampSuccess', 'true')
        setStep(2)
        onEvents('onramp_success')
      }
    } catch {
      /* ignore */
    }
  }, [onEvents])

  return { step, setStep, onEvent }
}


// 2) Extract the swap-flow & lock logic into `useSwapFlow`.
// src/hooks/useSwapFlow.ts
import { useEffect, useRef } from 'react'
import { useBuyGd } from '@gooddollar/web3sdk-v2'

interface SwapParams {
  selfSwap: boolean
  withSwap: boolean
  donateOrExecTo?: string
  callData: string
  account?: string
  library?: any
  gdHelperAddress?: string
  onEvents: (action: string, data?: any, error?: string) => void
  showError: () => void
  setStep: (step: number) => void
}
export function useSwapFlow({
  selfSwap,
  withSwap,
  donateOrExecTo,
  callData,
  account,
  library,
  gdHelperAddress,
  onEvents,
  showError,
  setStep,
}: SwapParams) {
  const swapLock = useRef(false)
  const { createAndSwap, swap, swapState, createState, triggerSwapTx } = useBuyGd({
    donateOrExecTo,
    callData,
    withSwap,
  })

  const triggerSwap = async () => {
    if (swapLock.current) return
    swapLock.current = true
    try {
      setStep(3)
      let txPromise
      if (selfSwap && gdHelperAddress && library && account) {
        const code = await library.getCode(gdHelperAddress)
        txPromise =
          code.length <= 2 ? createAndSwap(0 /* oracle min */) : swap(0)
      } else {
        setStep(4)
        txPromise = triggerSwapTx()
      }

      const res = await txPromise
      if (res?.status !== 1 && !res?.ok) throw new Error('swap failed')
      setStep(5)
      onEvents('buy_success')
    } catch (e: any) {
      showError()
      onEvents('buygd_swap_failed', e.message)
      setStep(0)
    } finally {
      swapLock.current = false
    }
  }

  return { swapState, createState, triggerSwap }
}


// 3) In your component, wire them together:
import { useOnramper } from 'src/hooks/useOnramper'
import { useSwapFlow } from 'src/hooks/useSwapFlow'

export const CustomGdOnramperWidget = (props: ICustomGdOnramperProps) => {
  const { onEvents, selfSwap, withSwap, donateOrExecTo, callData, apiKey } = props
  const { account, library } = useEthers()
  const { showModal, Modal } = useModal()
  const { SignWalletModal } = useSignWalletModal()

  // 3.a Onramper
  const { step, setStep, onEvent } = useOnramper(onEvents)

  // 3.b Swap logic
  const { swapState, createState, triggerSwap } = useSwapFlow({
    selfSwap,
    withSwap,
    donateOrExecTo,
    callData,
    account,
    library,
    gdHelperAddress,
    onEvents,
    showError: () => showModal(),
    setStep,
  })

  // 3.c Balance watch to auto-trigger
  useEffect(() => {
    if ((cusdBalance?.gt(0) || celoBalance?.gt(0))) {
      AsyncStorage.removeItem('gdOnrampSuccess')
      triggerSwap()
    }
  }, [cusdBalance, celoBalance])

  return (
    <>
      <Modal body={<ErrorModal />} />
      <WalletAndChainGuard validChains={[42220]}>
        <CustomOnramper
          onEvent={onEvent}
          targetWallet={gdHelperAddress || ''}
          step={step}
          setStep={setStep}
          targetNetwork="CELO"
          widgetParams={{ onlyCryptos: 'CUSD_CELO', isAddressEditable: false }}
          onGdEvent={onEvents}
          apiKey={apiKey}
        />
      </WalletAndChainGuard>
      {/* single SignWalletModal covering both statuses */}
      <SignWalletModal txStatuses={[swapState.status, createState.status]} />
    </>
  )
}
```

This splits the iframe parsing, AsyncStorage flag, swap logic, and modal orchestration into small focused hooks/components—maintaining all existing behavior while significantly flattening and decluttering the main component.
</issue_to_address>

### Comment 9
<location> `src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx:82-104` </location>
<code_context>
            if (selfSwap && gdHelperAddress && library && account) {
                const minAmount = 0 // we let contract use oracle for minamount, we might calculate it for more precision in the future
                const code = await library.getCode(gdHelperAddress)
                let swapTx
                if (code.length <= 2) {
                    swapTx = createAndSwap(minAmount)
                } else {
                    swapTx = swap(minAmount)
                }

                setStep(4)
                // after tx sent progress the stepper
                const res = await swapTx
                if (res?.status !== 1) throw Error('reverted')
            } else {
                if (account) {
                    //or backends sends swap tx
                    setStep(4)
                    const tx = await triggerSwapTx()

                    if (!tx?.ok) throw Error('reverted')
                }
            }

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge else clause's nested if statement into `else if` ([`merge-else-if`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/merge-else-if))

```suggestion
            if (selfSwap && gdHelperAddress && library && account) {
                            const minAmount = 0 // we let contract use oracle for minamount, we might calculate it for more precision in the future
                            const code = await library.getCode(gdHelperAddress)
                            let swapTx
                            if (code.length <= 2) {
                                swapTx = createAndSwap(minAmount)
                            } else {
                                swapTx = swap(minAmount)
                            }

                            setStep(4)
                            // after tx sent progress the stepper
                            const res = await swapTx
                            if (res?.status !== 1) throw Error('reverted')
                        }
            else if (account) {
                                //or backends sends swap tx
                                setStep(4)
                                const tx = await triggerSwapTx()

                                if (!tx?.ok) throw Error('reverted')
                            }

```

<br/><details><summary>Explanation</summary>Flattening if statements nested within else clauses generates code that is
easier to read and expand upon.
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 58 to 67
const getCircleProps = (status: string) => {
const baseProps = {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
}

switch (status) {
case 'completed':
Copy link

Choose a reason for hiding this comment

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

issue: The 'animation' property in getCircleProps may not be supported by native-base.

Since native-base's Circle may not support the 'animation' property, consider replacing it with a component designed for loading states to ensure compatibility across platforms.

Comment on lines +114 to +118
return (
<div style={{ padding: '1rem', textAlign: 'center', color: 'red' }}>
Wallet not found. Please select a valid wallet to continue.
</div>
)
Copy link

Choose a reason for hiding this comment

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

suggestion: Rendering a raw

may break styling consistency in a native-base context.

Consider replacing

with a native-base component like or to maintain consistent styling.

Suggested change
return (
<div style={{ padding: '1rem', textAlign: 'center', color: 'red' }}>
Wallet not found. Please select a valid wallet to continue.
</div>
)
return (
<Box p="4" textAlign="center" color="red.500">
Wallet not found. Please select a valid wallet to continue.
</Box>
)

}

const BuyProgressBar: React.FC<BuyProgressBarProps> = ({ currentStep, isLoading = false }) => {
const [animatedWidth, setAnimatedWidth] = useState(0)
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the component to use style maps and CSS transitions instead of manual state and interval logic for progress animation.

You can collapse most of the branching into simple style-maps and drop the manual setInterval entirely by using CSS transitions on width. For example:

// 1) Remove animatedWidth state + useEffect entirely.

// 2) Define a single status‐to‐styles map for circles & text:
const baseCircle = {
  size: '12',
  mb: 2,
  justifyContent: 'center',
  alignItems: 'center',
};
const statusStyles = {
  completed: { bg: 'blue.500', textColor: 'white' },
  active:    { bg: 'blue.500', textColor: 'white' },
  loading:   {
    bg: 'blue.500',
    borderWidth: 3,
    borderColor: 'blue.200',
    animation: 'pulse 2s infinite',
    textColor: 'white',
  },
  pending:   { bg: 'gray.300', textColor: 'gray.500' },
} as const;

// 3) A single helper to render Circle+Label:
function Step({
  step,
  status,
}: { step: { number: number; label: string }; status: keyof typeof statusStyles }) {
  const styles = statusStyles[status];
  return (
    <Box alignItems="center" flex={1}>
      <Circle {...baseCircle} bg={styles.bg} borderWidth={styles.borderWidth} borderColor={styles.borderColor} animation={styles.animation}>
        <Text color={styles.textColor} fontWeight="bold" fontSize="md">
          {step.number}
        </Text>
      </Circle>
      <Text textAlign="center" fontSize="sm" fontFamily="subheading" color={styles.textColor}>
        {step.label}
      </Text>
    </Box>
  );
}

// 4) Simplify lines to rely on CSS transitions:
const baseLine = {
  height: '2px',
  transition: 'width 300ms ease',
};
const lineStatusMap = {
  completed: { bg: 'blue.500', width: '100%' },
  loading:   { bg: 'blue.500', width: '100%' }, // triggers the transition
  pending:   { bg: 'gray.300', width: '0%' },
} as const;

// 5) In your render loop:
<React.Fragment key={step.number}>
  <Step step={step} status={getStepStatus(step.number)} />

  {index < steps.length - 1 && (
    <Box
      position="absolute"
      top="6"
      left={`${33.33 * (index + 1) - 16.67}%`}
      right={`${66.67 - 33.33 * (index + 1) + 16.67}%`}
      zIndex={-1}
    >
      <Box {...baseLine} {...lineStatusMap[getStepStatus(step.number + 1)]} borderRadius="1px" />
    </Box>
  )}
</React.Fragment>

This:

  • Drops the animatedWidth state + interval.
  • Uses a single style-map for circles/text & one for lines.
  • Leverages CSS transitions for smooth width animation.
  • Keeps your current getStepStatus so logic stays intact.


export type OnramperCallback = (event: WebViewMessageEvent) => void

export const CustomOnramper = ({
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the component by extracting URL-building, step logic, and responsive styles into separate utilities and hooks.

You can collapse much of this “glue” code into small reusable hooks/utils and lean on Native-Base’s built-in responsive props to eliminate the manual breakpoint logic. For example:

1) Extract URL‐building into a util:

```ts
// src/utils/onramper.ts
export function buildOnramperUrl({
  apiKey,
  targetNetwork,
  targetWallet,
  widgetParams,
}: {
  apiKey?: string
  targetNetwork: string
  targetWallet: string
  widgetParams: Record<string,string>
}): string {
  const url = new URL('https://buy.onramper.com/')
  if (apiKey) url.searchParams.set('apiKey', apiKey)
  url.searchParams.set('networkWallets', `${targetNetwork}:${targetWallet}`)
  Object.entries(widgetParams).forEach(([k,v]) => url.searchParams.set(k,v))
  return url.toString()
}
  1. Pull your AsyncStorage + focus logic into a useOnramperStep hook:
// src/hooks/useOnramperStep.ts
import { useEffect } from 'react'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'
import { useWindowFocus } from '@gooddollar/good-design'

export function useOnramperStep(
  step: number,
  setStep: (n:number) => void,
  onGdEvent: (action:string) => void
) {
  const { title } = useWindowFocus()

  // returning‐user check
  useEffect(() => {
    AsyncStorage.getItem('gdOnrampSuccess').then(val => {
      if (val==='true') setStep(2)
    })
  }, [])

  // widget open event
  useEffect(() => {
    if (title==='Onramper widget' && step===0) {
      onGdEvent('buy_start')
      setStep(1)
    }
  }, [title, step])
}
  1. Use Native-Base’s responsive style props instead of useBreakpointValue:
<CentreBox
  maxW={{ base: '100%', sm: 400, md: 450, lg: 480 }}
  w  ={{ base: '95vw', sm: '90%', md: '80%', lg: '100%', xl: '200%' }}
  h  ={{ base: 500, sm: 550, md: 600, lg: 630 }}
  mb={6}
>
  <WebView
    source={{uri}}
    style={{
      borderRadius:4, borderWidth:1, borderColor:'#58585f',
      width:'100%', height:'100%'
    }}
    width="100%"
    height="100%"
    
  />
</CentreBox>

Then your component becomes:

import { buildOnramperUrl } from 'src/utils/onramper'
import { useOnramperStep } from 'src/hooks/useOnramperStep'

export const CustomOnramper = props => {
  const { apiKey, targetNetwork='CELO', targetWallet, widgetParams, step, setStep, onGdEvent, onEvent } = props
  if (!targetWallet) return <div style={{}}>Wallet not found…</div>

  const uri = useMemo(()=>
    buildOnramperUrl({ apiKey, targetNetwork, targetWallet, widgetParams }),
    [apiKey, targetNetwork, targetWallet, widgetParams]
  )

  useOnramperStep(step, setStep, onGdEvent)
  const isMobile = deviceDetect()

  return (
    <Box alignItems="center" mb={6} w="100%">
      {/* CentreBox as above */}
      {isMobile && <Divider… />}
    </Box>
  )
}

This keeps all behavior intact, but dramatically cuts down inline complexity and makes each piece individually testable.

apiKey?: string
}

export const CustomGdOnramperWidget = ({
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the component by extracting the Onramper event handling and swap logic into custom hooks to simplify and declutter the main component.

Suggested change
export const CustomGdOnramperWidget = ({
// 1) Extract the Onramper iframe logic into a `useOnramper` hook.
// src/hooks/useOnramper.ts
import { useCallback, useState } from 'react'
import { WebViewMessageEvent } from 'react-native-webview'
import { AsyncStorage } from '@gooddollar/web3sdk-v2'
export function useOnramper(onEvents: (action: string, data?: any, error?: string) => void) {
const [step, setStep] = useState(0)
const onEvent = useCallback(async (event: WebViewMessageEvent) => {
try {
const data =
typeof event.nativeEvent.data === 'string'
? JSON.parse(event.nativeEvent.data)
: event.nativeEvent.data
if (data?.title === 'success') {
await AsyncStorage.setItem('gdOnrampSuccess', 'true')
setStep(2)
onEvents('onramp_success')
}
} catch {
/* ignore */
}
}, [onEvents])
return { step, setStep, onEvent }
}
// 2) Extract the swap-flow & lock logic into `useSwapFlow`.
// src/hooks/useSwapFlow.ts
import { useEffect, useRef } from 'react'
import { useBuyGd } from '@gooddollar/web3sdk-v2'
interface SwapParams {
selfSwap: boolean
withSwap: boolean
donateOrExecTo?: string
callData: string
account?: string
library?: any
gdHelperAddress?: string
onEvents: (action: string, data?: any, error?: string) => void
showError: () => void
setStep: (step: number) => void
}
export function useSwapFlow({
selfSwap,
withSwap,
donateOrExecTo,
callData,
account,
library,
gdHelperAddress,
onEvents,
showError,
setStep,
}: SwapParams) {
const swapLock = useRef(false)
const { createAndSwap, swap, swapState, createState, triggerSwapTx } = useBuyGd({
donateOrExecTo,
callData,
withSwap,
})
const triggerSwap = async () => {
if (swapLock.current) return
swapLock.current = true
try {
setStep(3)
let txPromise
if (selfSwap && gdHelperAddress && library && account) {
const code = await library.getCode(gdHelperAddress)
txPromise =
code.length <= 2 ? createAndSwap(0 /* oracle min */) : swap(0)
} else {
setStep(4)
txPromise = triggerSwapTx()
}
const res = await txPromise
if (res?.status !== 1 && !res?.ok) throw new Error('swap failed')
setStep(5)
onEvents('buy_success')
} catch (e: any) {
showError()
onEvents('buygd_swap_failed', e.message)
setStep(0)
} finally {
swapLock.current = false
}
}
return { swapState, createState, triggerSwap }
}
// 3) In your component, wire them together:
import { useOnramper } from 'src/hooks/useOnramper'
import { useSwapFlow } from 'src/hooks/useSwapFlow'
export const CustomGdOnramperWidget = (props: ICustomGdOnramperProps) => {
const { onEvents, selfSwap, withSwap, donateOrExecTo, callData, apiKey } = props
const { account, library } = useEthers()
const { showModal, Modal } = useModal()
const { SignWalletModal } = useSignWalletModal()
// 3.a Onramper
const { step, setStep, onEvent } = useOnramper(onEvents)
// 3.b Swap logic
const { swapState, createState, triggerSwap } = useSwapFlow({
selfSwap,
withSwap,
donateOrExecTo,
callData,
account,
library,
gdHelperAddress,
onEvents,
showError: () => showModal(),
setStep,
})
// 3.c Balance watch to auto-trigger
useEffect(() => {
if ((cusdBalance?.gt(0) || celoBalance?.gt(0))) {
AsyncStorage.removeItem('gdOnrampSuccess')
triggerSwap()
}
}, [cusdBalance, celoBalance])
return (
<>
<Modal body={<ErrorModal />} />
<WalletAndChainGuard validChains={[42220]}>
<CustomOnramper
onEvent={onEvent}
targetWallet={gdHelperAddress || ''}
step={step}
setStep={setStep}
targetNetwork="CELO"
widgetParams={{ onlyCryptos: 'CUSD_CELO', isAddressEditable: false }}
onGdEvent={onEvents}
apiKey={apiKey}
/>
</WalletAndChainGuard>
{/* single SignWalletModal covering both statuses */}
<SignWalletModal txStatuses={[swapState.status, createState.status]} />
</>
)
}

This splits the iframe parsing, AsyncStorage flag, swap logic, and modal orchestration into small focused hooks/components—maintaining all existing behavior while significantly flattening and decluttering the main component.

Copy link

@korbit-ai korbit-ai bot left a comment

Choose a reason for hiding this comment

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

Review by Korbit AI

Korbit automatically attempts to detect when you fix issues in new commits.
Category Issue Status
Design Loose typing with 'any' ▹ view ✅ Fix detected
Design Non-reusable error modal component ▹ view ✅ Fix detected
Performance Inefficient event parsing and error handling ▹ view ✅ Fix detected
Design Hardcoded Steps Configuration ▹ view ✅ Fix detected
Functionality Step 1 loading animation missing ▹ view ✅ Fix detected
Performance Inefficient object creation in helper functions ▹ view ✅ Fix detected
Performance URL reconstruction on every render ▹ view ✅ Fix detected
Performance Uncached AsyncStorage reads ▹ view ✅ Fix detected
Files scanned
File Path Reviewed
src/components/CustomGdOnramperWidget/index.ts
src/pages/gd/BuyGD/index.tsx
src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx
src/components/CustomGdOnramperWidget/CustomOnramper.tsx
src/components/BuyProgressBar/index.tsx

Explore our documentation to understand the languages and file types we support and the files we ignore.

Check out our docs on how you can make Korbit work best for you and your team.

Loving Korbit!? Share us on LinkedIn Reddit and X

Comment on lines 58 to 73
const callback = useCallback(async (event: WebViewMessageEvent) => {
let eventData
try {
eventData =
typeof event.nativeEvent.data === 'string' ? JSON.parse(event.nativeEvent.data) : event.nativeEvent.data
} catch (error) {
// Optionally log error or handle it
return
}

if (eventData && eventData.title === 'success') {
await AsyncStorage.setItem('gdOnrampSuccess', 'true')
//start the stepper
setStep(2)
}
}, [])

This comment was marked as resolved.

Comment on lines 13 to 17
const ErrorModal = () => (
<View>
<Text>Something went wrong.</Text>
</View>
)

This comment was marked as resolved.

Comment on lines 14 to 18
const steps = [
{ number: 1, label: 'Buy cUSD' },
{ number: 2, label: 'We swap cUSD to G$' },
{ number: 3, label: 'Done' },
]

This comment was marked as resolved.

Comment on lines 21 to 41
useEffect(() => {
if (isLoading && currentStep > 1) {
// Explicitly reset animatedWidth to 0 at the start of a new loading phase
setAnimatedWidth(0)
// Animate progress line when loading
let progress = 0
const interval = setInterval(() => {
progress += 2
if (progress <= 100) {
setAnimatedWidth(progress)
} else {
clearInterval(interval)
}
}, 50) // 50ms intervals for smooth animation

return () => clearInterval(interval)
} else {
// Set to 100% if not loading (completed state)
setAnimatedWidth(100)
}
}, [isLoading, currentStep])

This comment was marked as resolved.

Comment on lines 58 to 82
const getCircleProps = (status: string) => {
const baseProps = {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
}

switch (status) {
case 'completed':
return { ...baseProps, bg: 'blue.500' }
case 'active':
return { ...baseProps, bg: 'blue.500' }
case 'loading':
return {
...baseProps,
bg: 'blue.500',
borderWidth: 3,
borderColor: 'blue.200',
animation: 'pulse 2s infinite',
}
default:
return { ...baseProps, bg: 'gray.300' }
}
}

This comment was marked as resolved.

onGdEvent: (action: string) => void
step: number
setStep: (step: number) => void
widgetParams?: any

This comment was marked as resolved.

Comment on lines 30 to 51
const url = new URL('https://buy.onramper.com/')

// Always include API key for proper authentication
// SECURITY NOTE: Onramper API keys are designed for client-side use
// - These are PUBLIC API keys specifically intended for browser environments
// - They are NOT secret keys and are safe to expose in client-side code
// - Similar to Google Maps API keys, they're restricted by domain/referrer
// - This follows Onramper's official integration documentation
// - See: https://docs.onramper.com for official security guidelines
if (apiKey) {
url.searchParams.set('apiKey', apiKey)
} else {
console.warn('Onramper: No API key provided')
}
url.searchParams.set('networkWallets', `${targetNetwork}:${targetWallet}`)
Object.entries(widgetParams).forEach(([k, v]: [string, any]) => {
url.searchParams.set(k, v)
})

const { title } = useWindowFocus()

const uri = url.toString()

This comment was marked as resolved.

Comment on lines 95 to 104
useEffect(() => {
const isOnramping = async () => {
const isOnramping = await AsyncStorage.getItem('gdOnrampSuccess')
if (isOnramping === 'true') {
setStep(2)
}
}

void isOnramping()
}, [])

This comment was marked as resolved.

@L03TJ3 L03TJ3 linked an issue Nov 21, 2025 that may be closed by this pull request
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement "Buy G$"

2 participants