-
Notifications
You must be signed in to change notification settings - Fork 21
feat: implement Buy G$ page with 3-step progress bar and responsive Onramper widget #605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This reverts commit ad8846f.
Co-authored-by: Lewis B <[email protected]>
There was a problem hiding this 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
tmacro 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
src/components/CustomGdOnramperWidget/CustomGdOnramperWidget.tsx
Outdated
Show resolved
Hide resolved
| apiKey?: string | ||
| } | ||
|
|
||
| export const CustomGdOnramperWidget = ({ |
There was a problem hiding this comment.
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:
- 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}/>
</>
)
}- 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
valid comment @supersonicwisd1
There was a problem hiding this 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 |
|---|---|---|
| Unimplemented wallet-monitoring logic drives no functional behavior ▹ view | ✅ Fix detected | |
| Monolithic event handling and state transitions ▹ view | ✅ Fix detected | |
| Aggressive balance polling ▹ view | ✅ Fix detected | |
| Exposure of user address in logs ▹ view | ✅ Fix detected | |
| Missing dependencies in useEffect ▹ view | ✅ Fix detected | |
| State updates not batched ▹ view | ✅ Fix detected | |
| 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.
| const celoBalance = useEtherBalance(gdHelperAddress, { refresh: 1 }) | ||
| const cusdBalance = useTokenBalance(cusd, gdHelperAddress, { refresh: 1 }) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| case 'widget_opened': | ||
| setCurrentStep(1) | ||
| setIsLoading(true) | ||
| break | ||
| case 'transaction_started': |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems valid
There was a problem hiding this comment.
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.
| apiKey?: string | ||
| } | ||
|
|
||
| export const CustomGdOnramperWidget = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
valid comment @supersonicwisd1
| case 'widget_opened': | ||
| setCurrentStep(1) | ||
| setIsLoading(true) | ||
| break | ||
| case 'transaction_started': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems valid
There was a problem hiding this 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 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()
}
```
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-Base’s 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const getCircleProps = (status: string) => { | ||
| const baseProps = { | ||
| size: '12', | ||
| mb: 2, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| } | ||
|
|
||
| switch (status) { | ||
| case 'completed': |
There was a problem hiding this comment.
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.
| return ( | ||
| <div style={{ padding: '1rem', textAlign: 'center', color: 'red' }}> | ||
| Wallet not found. Please select a valid wallet to continue. | ||
| </div> | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Rendering a raw
Consider replacing
| 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) |
There was a problem hiding this comment.
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
animatedWidthstate + interval. - Uses a single style-map for circles/text & one for lines.
- Leverages CSS transitions for smooth width animation.
- Keeps your current
getStepStatusso logic stays intact.
|
|
||
| export type OnramperCallback = (event: WebViewMessageEvent) => void | ||
|
|
||
| export const CustomOnramper = ({ |
There was a problem hiding this comment.
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()
}- Pull your AsyncStorage + focus logic into a
useOnramperStephook:
// 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])
}- 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 = ({ |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this 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 |
|---|---|---|
| Loose typing with 'any' ▹ view | ✅ Fix detected | |
| Non-reusable error modal component ▹ view | ✅ Fix detected | |
| Inefficient event parsing and error handling ▹ view | ✅ Fix detected | |
| Hardcoded Steps Configuration ▹ view | ✅ Fix detected | |
| Step 1 loading animation missing ▹ view | ✅ Fix detected | |
| Inefficient object creation in helper functions ▹ view | ✅ Fix detected | |
| URL reconstruction on every render ▹ view | ✅ Fix detected | |
| 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.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| const ErrorModal = () => ( | ||
| <View> | ||
| <Text>Something went wrong.</Text> | ||
| </View> | ||
| ) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| onGdEvent: (action: string) => void | ||
| step: number | ||
| setStep: (step: number) => void | ||
| widgetParams?: any |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| 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.
This comment was marked as resolved.
Sorry, something went wrong.
| useEffect(() => { | ||
| const isOnramping = async () => { | ||
| const isOnramping = await AsyncStorage.getItem('gdOnrampSuccess') | ||
| if (isOnramping === 'true') { | ||
| setStep(2) | ||
| } | ||
| } | ||
|
|
||
| void isOnramping() | ||
| }, []) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
#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
Test plan
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.
-Introduce CustomGdOnramperWidget and CustomOnramper to host an Onramper iframe with responsive sizing and event handling.
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.