Skip to content

Feat: Integrate ubeswap savings widget #619#625

Open
Oluwatomilola wants to merge 8 commits intoGoodDollar:masterfrom
Oluwatomilola:feat-savings-integration
Open

Feat: Integrate ubeswap savings widget #619#625
Oluwatomilola wants to merge 8 commits intoGoodDollar:masterfrom
Oluwatomilola:feat-savings-integration

Conversation

@Oluwatomilola
Copy link
Copy Markdown

@Oluwatomilola Oluwatomilola commented Mar 3, 2026

Description

This PR integrates the GoodDollar Savings Widget into GoodDapp as a first-class page.

Changes include:
Added a new Savings menu item to the sidebar.
Created a new /savings page using the same layout structure as Bridge/Swap/Claim.
Embedded the GooddollarSavingsWidget via a dedicated wrapper component.
Added contextual FAQs for the Savings feature.
This implements the requirements described in issue #619.

About: https://github.com/GoodDollar/GoodProtocolUI/issues/#619
fixes: #619 #619 #619

How Has This Been Tested?

Ran the app locally using yarn start.

Navigated to the new Savings route from the sidebar.

Verified that the Savings Widget loads correctly.

Confirmed layout consistency with Bridge/Swap/Claim pages.

Manually tested wallet connection and widget rendering.

Screenshot 2026-03-11 at 13 37 10

Checklist:

  • PR title matches follow: (Feature|Bug|Chore) Task Name
  • My code follows the style guidelines of this project
  • I have followed all the instructions described in the initial task (check Definitions of Done)
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have added reference to a related issue in the repository
  • I have added a detailed description of the changes proposed in the pull request. I am as descriptive as possible, assisting reviewers as much as possible.
  • I have added screenshots related to my pull request (for frontend tasks)
  • I have pasted a gif showing the feature.
  • @L03TJ3 please review

- Add Savings page component with widget integration
- Add /savings route to router
- Add Savings menu item to sidebar
- Add 6 FAQ items for Savings section
- Use split-screen layout matching other pages
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The savings widget script is reloaded on every account change but only ever appended (and only to the container div), which can lead to multiple <script> tags being added over time; consider loading it once (empty dependency array) and/or guarding with an id/existence check, and appending to document.head or body instead of the container.
  • The <div id="savings-widget-container"> only hosts the script element and does not render a specific web component instance (e.g., <gooddollar-savings-widget ... />), so if the widget does not self-mount into the container, it may never display; double-check whether explicitly rendering the custom element is required and add it if so.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The savings widget script is reloaded on every `account` change but only ever appended (and only to the container div), which can lead to multiple `<script>` tags being added over time; consider loading it once (empty dependency array) and/or guarding with an `id`/existence check, and appending to `document.head` or `body` instead of the container.
- The `<div id="savings-widget-container">` only hosts the script element and does not render a specific web component instance (e.g., `<gooddollar-savings-widget ... />`), so if the widget does not self-mount into the container, it may never display; double-check whether explicitly rendering the custom element is required and add it if so.

## Individual Comments

### Comment 1
<location path="src/pages/gd/Savings/index.tsx" line_range="37-46" />
<code_context>
+    useEffect(() => {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid reloading the widget script on every account change unless the widget actually depends on it.

Because the effect depends on `account` but the script URL/injection don’t, every account change tears down and reinjects the script, causing extra network/CPU work and potential re‑init bugs. If the widget truly doesn’t depend on `account`, make the dependency array `[]` so it runs only on mount. If it does depend on `account`, prefer passing that via the widget’s own API (e.g., attributes/props) instead of reloading the script itself.

Suggested implementation:

```typescript
    useEffect(() => {

```

```typescript
    }, [])

```

If the dependency array is not exactly `[account]` (for example `[account, chainId]` or similar), you should update that full dependency array to `[]` instead, so that the widget script is only loaded once on mount and not reloaded on account changes. If the widget actually needs `account` to render correctly, prefer passing `account` into the widget via its attributes/props or configuration object inside the effect, rather than by reloading the script tag itself.
</issue_to_address>

### Comment 2
<location path="src/pages/gd/Savings/index.tsx" line_range="22" />
<code_context>
+    </VStack>
+)
+
+const SavingsWidgetContainer: React.FC = () => {
+    const containerRef = useRef<HTMLDivElement>(null)
+    const [isLoading, setIsLoading] = useState(true)
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the widget container to use a reusable script-loading hook, a single derived status, and NativeBase layout components to simplify control flow and concerns.

You can simplify this component without changing behavior by extracting the script-loading concern, tightening dependencies, and consolidating state.

### 1. Extract script loading into a small hook

This removes DOM manipulation from the page component and makes it reusable/testable:

```ts
// hooks/useExternalScript.ts
import { useEffect, useState } from 'react'

export const useExternalScript = (src: string) => {
  const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle')

  useEffect(() => {
    if (!src) return
    setStatus('loading')

    const script = document.createElement('script')
    script.src = src
    script.async = true
    script.defer = true

    script.onload = () => setStatus('ready')
    script.onerror = () => setStatus('error')

    document.body.appendChild(script)

    return () => {
      script.onload = null
      script.onerror = null
      script.remove()
    }
  }, [src])

  return status
}
```

Then your widget container becomes focused on layout:

```tsx
const SavingsWidgetContainer: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null)
  const status = useExternalScript(
    'https://unpkg.com/@gooddollar/savings-widget@latest/dist/index.js'
  )
  const isLoading = status === 'loading'
  const hasError = status === 'error'
  const isReady = status === 'ready'

  // ...rendering below...
}
```

### 2. Remove unnecessary `account` dependency

The script doesn’t depend on `account`, so reloading it on account change adds complexity and flicker. If you keep the current inline approach, you can change:

```ts
useEffect(() => {
  // ...
}, [account])
```

to:

```ts
useEffect(() => {
  // ...
}, []) // load once per mount
```

### 3. Consolidate loading/error/success handling

Instead of two booleans and inline `display` logic, derive a single `status`:

```tsx
const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading')

useEffect(() => {
  // on success:
  setStatus('ready')
  // on error:
  setStatus('error')
}, [])
```

Then JSX becomes more linear:

```tsx
{status === 'loading' && (
  <VStack alignItems="center" justifyContent="center" minH="300px" w="100%">
    <Spinner color="gdPrimary" size="lg" />
    <Text fontSize="sm" color="goodGrey.400" mt={4}>
      Loading savings widget...
    </Text>
  </VStack>
)}

{status === 'error' && (
  <VStack alignItems="center" justifyContent="center" minH="300px" w="100%">
    <Text fontSize="sm" color="red.500" textAlign="center">
      Unable to load the savings widget. Please try again later.
    </Text>
  </VStack>
)}

<Box
  as="div"
  ref={containerRef}
  id="savings-widget-container"
  w="100%"
  minH="500px"
  display={status === 'ready' ? 'flex' : 'none'}
/>
```

### 4. Keep layout consistent with NativeBase

Instead of a raw `div`, use `Box as="div"` to stay within the NativeBase layout system (as above). This keeps styling consistent and avoids mixing paradigms while still giving the web component the `div` it needs.
</issue_to_address>

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

Comment on lines +37 to +46
useEffect(() => {
const loadWidget = async () => {
try {
setIsLoading(true)
setHasError(false)

// Try to load the savings widget script from CDN
// The widget is available as a web component from GoodSDKs/savings-widget
const script = document.createElement('script')
script.src = 'https://unpkg.com/@gooddollar/savings-widget@latest/dist/index.js'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid reloading the widget script on every account change unless the widget actually depends on it.

Because the effect depends on account but the script URL/injection don’t, every account change tears down and reinjects the script, causing extra network/CPU work and potential re‑init bugs. If the widget truly doesn’t depend on account, make the dependency array [] so it runs only on mount. If it does depend on account, prefer passing that via the widget’s own API (e.g., attributes/props) instead of reloading the script itself.

Suggested implementation:

    useEffect(() => {
    }, [])

If the dependency array is not exactly [account] (for example [account, chainId] or similar), you should update that full dependency array to [] instead, so that the widget script is only loaded once on mount and not reloaded on account changes. If the widget actually needs account to render correctly, prefer passing account into the widget via its attributes/props or configuration object inside the effect, rather than by reloading the script tag itself.

</VStack>
)

const SavingsWidgetContainer: React.FC = () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the widget container to use a reusable script-loading hook, a single derived status, and NativeBase layout components to simplify control flow and concerns.

You can simplify this component without changing behavior by extracting the script-loading concern, tightening dependencies, and consolidating state.

1. Extract script loading into a small hook

This removes DOM manipulation from the page component and makes it reusable/testable:

// hooks/useExternalScript.ts
import { useEffect, useState } from 'react'

export const useExternalScript = (src: string) => {
  const [status, setStatus] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle')

  useEffect(() => {
    if (!src) return
    setStatus('loading')

    const script = document.createElement('script')
    script.src = src
    script.async = true
    script.defer = true

    script.onload = () => setStatus('ready')
    script.onerror = () => setStatus('error')

    document.body.appendChild(script)

    return () => {
      script.onload = null
      script.onerror = null
      script.remove()
    }
  }, [src])

  return status
}

Then your widget container becomes focused on layout:

const SavingsWidgetContainer: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null)
  const status = useExternalScript(
    'https://unpkg.com/@gooddollar/savings-widget@latest/dist/index.js'
  )
  const isLoading = status === 'loading'
  const hasError = status === 'error'
  const isReady = status === 'ready'

  // ...rendering below...
}

2. Remove unnecessary account dependency

The script doesn’t depend on account, so reloading it on account change adds complexity and flicker. If you keep the current inline approach, you can change:

useEffect(() => {
  // ...
}, [account])

to:

useEffect(() => {
  // ...
}, []) // load once per mount

3. Consolidate loading/error/success handling

Instead of two booleans and inline display logic, derive a single status:

const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading')

useEffect(() => {
  // on success:
  setStatus('ready')
  // on error:
  setStatus('error')
}, [])

Then JSX becomes more linear:

{status === 'loading' && (
  <VStack alignItems="center" justifyContent="center" minH="300px" w="100%">
    <Spinner color="gdPrimary" size="lg" />
    <Text fontSize="sm" color="goodGrey.400" mt={4}>
      Loading savings widget...
    </Text>
  </VStack>
)}

{status === 'error' && (
  <VStack alignItems="center" justifyContent="center" minH="300px" w="100%">
    <Text fontSize="sm" color="red.500" textAlign="center">
      Unable to load the savings widget. Please try again later.
    </Text>
  </VStack>
)}

<Box
  as="div"
  ref={containerRef}
  id="savings-widget-container"
  w="100%"
  minH="500px"
  display={status === 'ready' ? 'flex' : 'none'}
/>

4. Keep layout consistent with NativeBase

Instead of a raw div, use Box as="div" to stay within the NativeBase layout system (as above). This keeps styling consistent and avoids mixing paradigms while still giving the web component the div it needs.

Copy link
Copy Markdown
Author

@Oluwatomilola Oluwatomilola left a comment

Choose a reason for hiding this comment

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

@L03TJ3 kindly review

@L03TJ3
Copy link
Copy Markdown
Collaborator

L03TJ3 commented Mar 30, 2026

@Oluwatomilola your build fails, can you check the build logs

@Oluwatomilola
Copy link
Copy Markdown
Author

Alright, on it.

- Fix faqSavingsCopy array left unclosed after merge with master (copies.tsx)
- Replace Box with plain div to fix TypeScript error on 'as' prop (Savings/index.tsx)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

dont need to be committed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Noted. I would remove it.

Comment on lines +84 to +104
<VStack alignItems="center" justifyContent="center" minH="300px" w="100%">
<Text fontSize="sm" color="red.500" textAlign="center" mb={4} fontWeight="bold">
Unable to Load Savings Widget
</Text>
<Text fontSize="xs" color="goodGrey.500" textAlign="center" mb={3}>
The widget script is not available. To enable this feature:
</Text>
<VStack fontSize="xs" color="goodGrey.400" textAlign="left" alignItems="flex-start" space={1}>
<Text>1. Clone GoodSDKs/packages/savings-widget</Text>
<Text>2. Run: yarn install && yarn build</Text>
<Text>3. Copy dist/index.global.js to public/</Text>
<Text>4. Restart the development server</Text>
</VStack>
<Button
size="sm"
colorScheme="blue"
variant="ghost"
mt={3}
onPress={() =>
window.open(
'https://github.com/GoodDollar/GoodSDKs/tree/main/packages/savings-widget',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is this?
why would this make any sense to show on a user-facing page?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

My bad.
Exposing build/setup instructions in a user-facing page is not appropriate. That was a misstep on my part while trying to handle the missing widget script case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, I have.
I will read through it again to see what I'm missing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

revert any changes that don't are not related to the requested change

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Alright, will do that.

)

const SavingsWidgetContainer: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I had expected you followed the readme at least and how to use it.
there are ways that the iife (the web-component compiled file, not the one from dist)
could have worked.

But you might have had some blockers, and I would have expected them to be raised.
It could have been an easy fix if you would have followed the readme.
You will now have to remove and revert a lot of this.

Please, when things are unclear, ask questions, push draft PRs.
you raised a couple of blockers before but there was never code for me to review.
and 'I have some eslint errors' is really hard to help with.

However. what was also missing was a published npm package.
That is my bad.
I am going to publish it now and you can use add @goodsdks/savings-widget as package. You can look at the demo-webcomponents to see how it can be used: https://github.com/GoodDollar/GoodSDKs/blob/main/apps/demo-webcomponents/src/index.js

The demo loads it high-level in html. once you import from the package you can use the html anywhere in react. (if anything not working, please raise blockers early)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the detailed feedback.

My apologies for deviating from the intended integration approach.
I did follow the README. I cloned the goodSDK quite alright, but I was a bit unclear on the subsequent steps to integrating the component and I indeed had blockers. I should have stated them clearly, my bad.

Moving forward I will raise blockers early, and I’ll make sure to push draft PRs with partial progress next time so issues can be caught earlier.

Please can you add a link to the npm package if it's available.

I’ll go ahead and make the required changes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you can add it with yarn: yarn add @goodsdks/savings-widget

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feat: Integrate ubeswap savings widget

2 participants