feat: implement Buy G$ page with 3-step progress bar and responsive Onramper widget - #605
Conversation
This reverts commit ad8846f.
Co-authored-by: Lewis B <lewis@ikigaistudios.eu>
There was a problem hiding this comment.
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.
| apiKey?: string | ||
| } | ||
|
|
||
| export const CustomGdOnramperWidget = ({ |
There was a problem hiding this comment.
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.
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.
Understood. Let me know if you need any assistance implementing the suggested changes.
| apiKey?: string | ||
| } | ||
|
|
||
| export const CustomGdOnramperWidget = ({ |
| case 'widget_opened': | ||
| setCurrentStep(1) | ||
| setIsLoading(true) | ||
| break | ||
| case 'transaction_started': |
…into feat/buy-gd
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
|
@supersonicwisd1 all of the fixes needs to be applied to the gdonramperwidget on the good-design package |
Hey @sirpy I have this open too GoodDollar/GoodWeb3-Mono#257 |
#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.