Skip to content

feat: implement Buy G$ page with 3-step progress bar and responsive Onramper widget - #605

Merged
L03TJ3 merged 29 commits into
GoodDollar:draft-buygd-testingfrom
supersonicwisd1:feat/buy-gd
Aug 4, 2026
Merged

feat: implement Buy G$ page with 3-step progress bar and responsive Onramper widget#605
L03TJ3 merged 29 commits into
GoodDollar:draft-buygd-testingfrom
supersonicwisd1:feat/buy-gd

Conversation

@supersonicwisd1

@supersonicwisd1 supersonicwisd1 commented Sep 15, 2025

Copy link
Copy Markdown
Contributor

#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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

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.

Comment thread src/components/BuyProgressBar/index.tsx
Comment thread src/components/BuyProgressBar/index.tsx Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomOnramper.tsx Outdated
Comment thread src/pages/gd/BuyGD/BuyGD.css Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomOnramper.tsx Outdated
Comment thread src/components/BuyProgressBar/index.tsx
apiKey?: string
}

export const CustomGdOnramperWidget = ({

Copy link
Copy Markdown

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
Copy Markdown
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 thread src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx Outdated

@korbit-ai korbit-ai Bot left a comment

Copy link
Copy Markdown

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 thread src/hooks/useSmartContractWalletMonitor.ts Outdated
Comment thread src/hooks/useSmartContractWalletMonitor.ts Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomOnramper.tsx Outdated
Comment thread src/components/CustomGdOnramperWidget/CustomOnramper.tsx Outdated
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 thread src/pages/gd/BuyGD/index.tsx Outdated
Comment on lines +70 to +74
case 'widget_opened':
setCurrentStep(1)
setIsLoading(true)
break
case 'transaction_started':

This comment was marked as resolved.

Copy link
Copy Markdown
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
Copy Markdown

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
Copy Markdown
Contributor Author

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

@supersonicwisd1

Copy link
Copy Markdown
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

Comment thread src/components/BuyProgressBar/index.tsx
apiKey?: string
}

export const CustomGdOnramperWidget = ({

Copy link
Copy Markdown
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 thread src/pages/gd/BuyGD/BuyGD.css
Comment thread src/pages/gd/BuyGD/index.tsx Outdated
Comment on lines +70 to +74
case 'widget_opened':
setCurrentStep(1)
setIsLoading(true)
break
case 'transaction_started':

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems valid

@L03TJ3 L03TJ3 linked an issue Nov 21, 2025 that may be closed by this pull request
9 tasks
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@sirpy

sirpy commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

@supersonicwisd1 all of the fixes needs to be applied to the gdonramperwidget on the good-design package
once you are done provide a video showing everything works.
@L03TJ3 maybe it is better to restart the work here as a new widget?
this has been open for too long

@supersonicwisd1

supersonicwisd1 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor Author

@supersonicwisd1 all of the fixes needs to be applied to the gdonramperwidget on the good-design package once you are done provide a video showing everything works. @L03TJ3 maybe it is better to restart the work here as a new widget? this has been open for too long

Hey @sirpy I have this open too GoodDollar/GoodWeb3-Mono#257

@L03TJ3 L03TJ3 moved this to Ready-For-Assignment in GoodBounties Apr 24, 2026
@github-project-automation github-project-automation Bot moved this from Ready-For-Assignment to In Progress in GoodBounties Apr 24, 2026
@L03TJ3 L03TJ3 moved this from In Progress to In Review in GoodBounties Apr 24, 2026
@L03TJ3 L03TJ3 moved this from In Review to Blocked in GoodBounties Apr 29, 2026
@L03TJ3 L03TJ3 removed this from GoodBounties May 22, 2026
@L03TJ3
L03TJ3 changed the base branch from master to draft-buygd-testing August 4, 2026 06:22
@L03TJ3
L03TJ3 merged commit 7473cb5 into GoodDollar:draft-buygd-testing Aug 4, 2026
2 checks passed
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$"

3 participants