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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,10 @@
},
"dependencies": {
"@babel/runtime": "^7.18.9",
"@gooddollar/good-design": "^0.4.51",
"@gooddollar/good-design": "file:temp-packages/@gooddollar-good-design-0.4.52.tgz",
"@gooddollar/goodprotocol": "2.2.1",
"@gooddollar/web3sdk": "0.1.59",
"@gooddollar/web3sdk-v2": "^0.4.46",
"@gooddollar/web3sdk-v2": "file:temp-packages/@gooddollar-web3sdk-v2-0.4.48.tgz",
"@goodsdks/savings-widget": "^1.0.0",
"@headlessui/react": "1.5.0",
"@lingui/format-json": "^4.0.0",
Expand Down
203 changes: 203 additions & 0 deletions src/components/BuyProgressBar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import React, { useEffect, useState, useMemo } from 'react'
import { Box, HStack, Circle, Text } from 'native-base'

export type BuyStep = 1 | 2 | 3

export interface StepConfig {
number: number
label: string
}

interface BuyProgressBarProps {
currentStep: BuyStep
isLoading?: boolean
steps?: StepConfig[]
}

const BuyProgressBar: React.FC<BuyProgressBarProps> = ({
currentStep,
isLoading = false,
steps = [
{ number: 1, label: 'Buy cUSD' },
{ number: 2, label: 'We swap cUSD to G$' },
{ number: 3, label: 'Done' },
],
}) => {
const [animatedWidth, setAnimatedWidth] = useState(0)

// Handle animated progress line
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])

const getStepStatus = (stepNumber: number) => {
// Step 1 should ALWAYS be blue (active when current, completed when past)
if (stepNumber === 1) {
if (currentStep === 1) {
return isLoading ? 'loading' : 'active'
} else {
return 'completed' // Step 1 is completed when we're on step 2 or 3
}
}
// Steps 2 and 3 follow normal logic
if (stepNumber < currentStep) return 'completed'
if (stepNumber === currentStep) return isLoading ? 'loading' : 'active'
return 'pending'
}

// Memoize circle props objects to avoid recreation on every render

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (complexity): Consider simplifying the progress bar by removing unnecessary memoization and bespoke status logic, deriving connector styles from step status, using flex-based layout, and optionally replacing JS-driven animation with CSS-only animation.

You can keep the same behavior while simplifying a few areas:

  1. Drop useMemo for circlePropsMap

The object is static and cheap to recreate; memoization here adds indirection without benefit.

// Remove useMemo import and hook, and define once
const circlePropsMap = {
  completed: {
    size: '12',
    mb: 2,
    justifyContent: 'center',
    alignItems: 'center',
    bg: 'blue.500',
  },
  active: {
    size: '12',
    mb: 2,
    justifyContent: 'center',
    alignItems: 'center',
    bg: 'blue.500',
  },
  loading: {
    size: '12',
    mb: 2,
    justifyContent: 'center',
    alignItems: 'center',
    bg: 'blue.500',
    borderWidth: 3,
    borderColor: 'blue.200',
    animation: 'pulse 2s infinite',
  },
  pending: {
    size: '12',
    mb: 2,
    justifyContent: 'center',
    alignItems: 'center',
    bg: 'gray.300',
  },
}

const getCircleProps = (status: StepStatus) =>
  circlePropsMap[status] ?? circlePropsMap.pending
  1. Unify status model and remove special casing

You can represent the step status with a single type and avoid the bespoke stepNumber === 1 logic by expressing that in the generic rules:

type StepStatus = 'pending' | 'active' | 'loading' | 'completed'

const getStepStatus = (stepNumber: number): StepStatus => {
  if (stepNumber < currentStep) return 'completed'
  if (stepNumber === currentStep) return isLoading ? 'loading' : 'active'
  return 'pending'
}

// If you need “step 1 is always blue”, encode it via styling instead of status branching:
const isFirstStep = stepNumber === 1
const circleProps = {
  ...getCircleProps(status),
  bg: isFirstStep || status !== 'pending' ? 'blue.500' : 'gray.300',
}

This keeps the “step 1 always blue when not pending” behavior but simplifies the state logic.

  1. Simplify line state based on right-hand step status

Instead of separate lineIndex and currentStep branches, derive line props from the status of the step it leads to:

const getLinePropsForStep = (toStepNumber: number): { bg: string; width: string } => {
  const status = getStepStatus(toStepNumber)

  if (status === 'loading') {
    return {
      bg: 'blue.500',
      width: `${animatedWidth}%`,
    }
  }
  if (status === 'completed' || status === 'active') {
    return {
      bg: 'blue.500',
      width: '100%',
    }
  }
  return {
    bg: 'gray.300',
    width: '100%',
  }
}

// usage:
<Box height="100%" {...getLinePropsForStep(step.number + 1)} borderRadius="1px" />

This removes the need for lineIndex and makes the “state machine” easier to follow.

  1. Use flex instead of magic percentage positioning

You can avoid the 33.33/16.67 calculations by making the connectors flex between circles:

<HStack alignItems="center" justifyContent="space-between">
  {steps.map((step, index) => {
    const status = getStepStatus(step.number)

    return (
      <React.Fragment key={step.number}>
        <Box alignItems="center">
          {/* circle + label */}
        </Box>

        {index < steps.length - 1 && (
          <Box flex={1} mx={2} height="2px" bg="gray.300">
            <Box height="100%" {...getLinePropsForStep(step.number + 1)} borderRadius="1px" />
          </Box>
        )}
      </React.Fragment>
    )
  })}
</HStack>

This keeps the same visual intent but removes manual left/right positioning.

  1. Optional: replace setInterval with CSS-only animation

If acceptable, you can avoid interval management entirely and keep the loading bar animated via CSS:

const getLinePropsForStep = (toStepNumber: number) => {
  const status = getStepStatus(toStepNumber)

  if (status === 'loading') {
    return {
      bg: 'blue.500',
      width: '100%',
      // assuming a keyframe like `@keyframes loadingBar { from { width: 0 } to { width: 100% } }`
      animation: 'loadingBar 2s infinite',
    }
  }
  // active/completed/pending same as before, no extra state
}

This removes animatedWidth state and useEffect while preserving the animated look.

const circlePropsMap = useMemo(
() => ({
completed: {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
bg: 'blue.500',
},
active: {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
bg: 'blue.500',
},
loading: {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
bg: 'blue.500',
borderWidth: 3,
borderColor: 'blue.200',
animation: 'pulse 2s infinite',
},
pending: {
size: '12',
mb: 2,
justifyContent: 'center',
alignItems: 'center',
bg: 'gray.300',
},
}),
[]
)

const getCircleProps = (status: string) => {
return circlePropsMap[status as keyof typeof circlePropsMap] || circlePropsMap.pending
}

const getLineProps = (stepNumber: number, lineIndex: number) => {
// Line between step 1 and 2 (lineIndex = 0)
if (lineIndex === 0) {
if (currentStep === 1 && isLoading) {
// Animation state: "1 Blue with progress bar animation"
return {
bg: 'blue.500',
width: `${animatedWidth}%`,
transition: 'width 0.1s ease-out',
}
Comment on lines +108 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: The stepNumber parameter in getLineProps is unused and the line positioning relies on brittle magic percentages.

This makes the signature misleading and tightly couples the layout to exactly three steps, so any change to steps (length or spacing) will likely break line alignment. Please either compute line positions based on steps.length and the actual flex layout, or explicitly constrain/document a fixed steps structure, and remove the unused stepNumber parameter.

Suggested implementation:

    const getCircleProps = (status: string) => {
        return circlePropsMap[status as keyof typeof circlePropsMap] || circlePropsMap.pending
    }

    /**
     * Returns style props for the connecting line segments between steps.
     *
     * This implementation assumes a fixed three-step layout where:
     * - lineIndex = 0 is the line between step 1 and 2
     * - lineIndex = 1 is the line between step 2 and 3
     *
     * If the steps structure changes (length or spacing), this function
     * must be updated accordingly or refactored to derive positions from
     * the steps array and the flex layout.
     */
    const getLineProps = (lineIndex: number) => {
        // Line between step 1 and 2 (lineIndex = 0)
        if (lineIndex === 0) {
            if (currentStep === 1 && isLoading) {
                // Animation state: "1 Blue with progress bar animation"
                return {
                    bg: 'blue.500',
                    width: `${animatedWidth}%`,
                    transition: 'width 0.1s ease-out',
                }
            } else if (currentStep >= 2) {
  1. Remove the stepNumber argument from every call site of getLineProps in src/components/BuyProgressBar/index.tsx, so calls become getLineProps(lineIndex) instead of getLineProps(stepNumber, lineIndex).
  2. If there is a steps array or dynamic steps layout elsewhere in the file, consider refactoring getLineProps to derive any non-animated widths/positions from steps.length and the flex container (for example, by using flex="1" or width="100%" on line containers) instead of hard-coded percentage widths.
  3. If additional branches inside getLineProps rely on hard-coded percentages tied to specific step indexes, update their comments to match the documented three-step assumption or refactor them to use layout-based calculations as in point 2.

} else if (currentStep >= 2) {
// Static line when step 2 or higher
return {
bg: 'blue.500',
width: '100%',
}
}
}

// Line between step 2 and 3 (lineIndex = 1)
if (lineIndex === 1) {
if (currentStep === 2 && isLoading) {
// Animation state: "2 Blue with progress bar animation"
return {
bg: 'blue.500',
width: `${animatedWidth}%`,
transition: 'width 0.1s ease-out',
}
} else if (currentStep >= 3) {
// Static line when step 3
return {
bg: 'blue.500',
width: '100%',
}
}
}

// Default: gray line (not active)
return {
bg: 'gray.300',
width: '100%',
}
}

const getTextColor = (status: string) => {
return status === 'pending' ? 'gray.500' : 'black'
}

return (
<Box width="100%" mb={6} mt={4} data-testid="custom-progress-bar">
<HStack justifyContent="space-between" alignItems="flex-start" position="relative">
{steps.map((step, index) => {
const status = getStepStatus(step.number)

return (
<React.Fragment key={step.number}>
<Box alignItems="center" flex={1} position="relative">
<Circle {...getCircleProps(status)}>
<Text color="white" fontWeight="bold" fontSize="md">
{step.number}
</Text>
</Circle>
<Text
textAlign="center"
fontSize="sm"
color={getTextColor(status)}
fontFamily="subheading"
maxWidth="120px"
lineHeight="tight"
>
{step.label}
</Text>
</Box>

{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}%`}
height="2px"
bg="gray.300"
zIndex={-1}
>
<Box height="100%" {...getLineProps(step.number + 1, index)} borderRadius="1px" />
</Box>
)}
</React.Fragment>
)
})}
</HStack>
</Box>
)
}

export { BuyProgressBar }
8 changes: 6 additions & 2 deletions src/language/locales/af/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr "Verander"

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr "Bevestig hierdie transaksie in jou beursie"
msgid "Congratulations!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr ""
msgid "Success!"
msgstr "Sukses!"

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
8 changes: 6 additions & 2 deletions src/language/locales/ar/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr "تغيير"

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr "قم بتأكيد هذه المعاملة في محفظتك"
msgid "Congratulations!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr ""
msgid "Success!"
msgstr "النجاح!"

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
8 changes: 6 additions & 2 deletions src/language/locales/ca/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr ""
msgid "Congratulations!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr ""
msgid "Success!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
8 changes: 6 additions & 2 deletions src/language/locales/cs/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr ""
msgid "Congratulations!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr ""
msgid "Success!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
8 changes: 6 additions & 2 deletions src/language/locales/da/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr ""
msgid "Congratulations!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr ""
msgid "Success!"
msgstr ""

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
8 changes: 6 additions & 2 deletions src/language/locales/de/catalog.po
Original file line number Diff line number Diff line change
Expand Up @@ -2784,7 +2784,7 @@ msgstr ""
msgid "Change"
msgstr "Ändern"

#: src/pages/gd/BuyGD/index.tsx:84
#: src/pages/gd/BuyGD/index.tsx:83
msgid "Choose the currency you want to use and buy cUSD. Your cUSD is then automatically converted into G$."
msgstr ""

Expand Down Expand Up @@ -2895,6 +2895,10 @@ msgstr "Bestätigen Sie diese Transaktion in Ihrer Brieftasche"
msgid "Congratulations!"
msgstr "Herzliche Glückwünsche!"

#: src/pages/gd/BuyGD/index.tsx:90
msgid "Connect a wallet to buy G$"
msgstr ""

#: src/pages/gd/MicroBridge/index.tsx:36
#: src/pages/gd/Portfolio/index.tsx:459
msgid "Connect a wallet to see your portfolio"
Expand Down Expand Up @@ -3358,7 +3362,7 @@ msgstr "Ab 1.0 steigt Ihr Multiplikator nach einem Monat nach einem Monat zum Ve
msgid "Success!"
msgstr "Erfolg!"

#: src/pages/gd/BuyGD/index.tsx:71
#: src/pages/gd/BuyGD/index.tsx:70
msgid "Support global financial inclusion and contribute to social impact by purchasing GoodDollars (G$)."
msgstr ""

Expand Down
Loading
Loading