Repository: interledger/web-monetization-extension
Pull request #97 (2024-02-12 21:59:23)

chore: Move load
Solves #43 and partially #78

raducristianpopa: <!-- __WM_EXTENSION_BUILD_PREVIEWS__ -->
<h2>Extension builds preview</h2>

<table role="table">
<thead>
  <tr>
    <th>Name</th>
    <th>Link</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td align="center">Latest commit</td>
    <td align="center">d6a579b27fc9594dca41ff700848b07219431727</td>
  </tr>
  <tr>
    <td align="center">Latest job logs</td>
    <td align="center"><a href="https://github.com/interledger/web-monetization-extension/actions/runs/7878449011">Run #7878449011</a></td>
  </tr>
  <tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Chrome (138.08KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/20689582022/artifacts/1239736127">Download</a></td></tr><tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Firefox (138.11KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/20689582022/artifacts/1239736134">Download</a></td></tr><tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Opera (138KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/20689582022/artifacts/1239736136">Download</a></td></tr><tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Edge (138KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/20689582022/artifacts/1239736130">Download</a></td></tr>
</tbody>
</table>

Patch:
diff --git a/src/background/Background.ts b/src/background/Background.ts
index f13911f..b7ca80a 100644
--- a/src/background/Background.ts
+++ b/src/background/Background.ts
@@ -1,4 +1,4 @@
-import { runtime, tabs } from 'webextension-polyfill'
+import { Runtime, runtime, tabs } from 'webextension-polyfill'
 
 import { PaymentFlowService } from '@/background/grantFlow'
 
@@ -23,15 +23,24 @@ class Background {
 
   subscribeToMessages() {
     this.subscriptions = this.messageHandlers.map((handler: any) => {
-      const listener: any = async (message: EXTMessage) => {
+      const listener: any = (
+        message: EXTMessage,
+        sender: Runtime.MessageSender,
+        sendResponse: (res: any) => void,
+      ) => {
         if (handler.type === message.type) {
-          try {
-            await handler.callback(message.data, this)
-          } catch (error) {
-            console.log('[===== Error in MessageListener =====]', error)
-            return error
-          }
+          handler
+            .callback(message.data, this)
+            .then((res: any) => {
+              sendResponse(res)
+            })
+            .catch((error: any) => {
+              console.log('[===== Error in MessageListener =====]', error)
+              sendResponse(error)
+            })
         }
+
+        return true
       }
 
       runtime.onMessage.addListener(listener)
diff --git a/src/background/grantFlow.ts b/src/background/grantFlow.ts
index 18fac5f..ce5cd93 100644
--- a/src/background/grantFlow.ts
+++ b/src/background/grantFlow.ts
@@ -94,6 +94,9 @@ export class PaymentFlowService {
     this.continuationRequestToken = continuationRequest.continuationRequestToken
 
     const currentTabId = await this.getCurrentActiveTabId()
+
+    await tabs.sendMessage(currentTabId ?? 0, { type: 'LOAD' })
+
     await tabs.sendMessage(currentTabId ?? 0, { type: 'START_PAYMENTS' })
   }
 
diff --git a/src/components/button.tsx b/src/components/button.tsx
index 4566fc0..6d6f75a 100644
--- a/src/components/button.tsx
+++ b/src/components/button.tsx
@@ -16,9 +16,11 @@ const buttonVariants = cva(
       variant: {
         default: 'bg-button-base text-white hover:bg-button-base-hover',
         destructive: 'bg-error text-error hover:bg-error-hover',
+        ghost: '',
       },
       size: {
         default: 'py-4 px-6 font-medium',
+        icon: 'h-6 w-6',
       },
       fullWidth: {
         true: 'w-full',
diff --git a/src/components/code.tsx b/src/components/code.tsx
new file mode 100644
index 0000000..e82b0d1
--- /dev/null
+++ b/src/components/code.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+
+import { cn } from '@/utils/cn'
+
+import { Button } from './button'
+import { CheckIcon, ClipboardIcon } from './icons'
+
+interface CodeProps extends React.HTMLAttributes<HTMLDivElement> {
+  value: string
+}
+
+export const Code = ({ value, className, ...props }: CodeProps) => {
+  return (
+    <div
+      className={cn(
+        'flex items-center justify-between gap-x-2 rounded-xl bg-nav-active p-4 text-medium break-all',
+        className,
+      )}
+      {...props}>
+      <code>{value}</code>
+      <CopyButton value={value} />
+    </div>
+  )
+}
+
+interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
+  value: string
+}
+
+const CopyButton = ({ value, ...props }: CopyButtonProps) => {
+  const [hasCopied, setHasCopied] = React.useState(false)
+
+  React.useEffect(() => {
+    if (hasCopied === true) {
+      setTimeout(() => {
+        setHasCopied(false)
+      }, 2000)
+    }
+  }, [hasCopied])
+
+  return (
+    <Button
+      {...props}
+      aria-label="copy"
+      variant="ghost"
+      size="icon"
+      className="text-primary rounded-sm"
+      onClick={() => {
+        navigator.clipboard.writeText(value)
+        setHasCopied(true)
+      }}>
+      {hasCopied ? <CheckIcon className="h-6 w-6" /> : <ClipboardIcon className="h-6 w-6" />}
+    </Button>
+  )
+}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 11950a5..7964b5d 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -93,3 +93,64 @@ export const DollarSign = (props: React.SVGProps<SVGSVGElement>) => {
     </svg>
   )
 }
+
+export const WarningSign = (props: React.SVGProps<SVGSVGElement>) => {
+  return (
+    <svg
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}>
+      <mask
+        id="mask0_140_3633"
+        style={{ maskType: 'alpha' }}
+        maskUnits="userSpaceOnUse"
+        x="0"
+        y="0"
+        width="24"
+        height="24">
+        <rect width="24" height="24" fill="#C4C4C4" />
+      </mask>
+      <g mask="url(#mask0_140_3633)">
+        <path
+          d="M11 13H13V7H11V13ZM12 17C12.2833 17 12.521 16.904 12.713 16.712C12.9043 16.5207 13 16.2833 13 16C13 15.7167 12.9043 15.479 12.713 15.287C12.521 15.0957 12.2833 15 12 15C11.7167 15 11.4793 15.0957 11.288 15.287C11.096 15.479 11 15.7167 11 16C11 16.2833 11.096 16.5207 11.288 16.712C11.4793 16.904 11.7167 17 12 17ZM12 22C10.6167 22 9.31667 21.7373 8.1 21.212C6.88333 20.6873 5.825 19.975 4.925 19.075C4.025 18.175 3.31267 17.1167 2.788 15.9C2.26267 14.6833 2 13.3833 2 12C2 10.6167 2.26267 9.31667 2.788 8.1C3.31267 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.31233 8.1 2.787C9.31667 2.26233 10.6167 2 12 2C13.3833 2 14.6833 2.26233 15.9 2.787C17.1167 3.31233 18.175 4.025 19.075 4.925C19.975 5.825 20.6873 6.88333 21.212 8.1C21.7373 9.31667 22 10.6167 22 12C22 13.3833 21.7373 14.6833 21.212 15.9C20.6873 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6873 15.9 21.212C14.6833 21.7373 13.3833 22 12 22ZM12 20C14.2167 20 16.1043 19.221 17.663 17.663C19.221 16.1043 20 14.2167 20 12C20 9.78333 19.221 7.89567 17.663 6.337C16.1043 4.779 14.2167 4 12 4C9.78333 4 7.896 4.779 6.338 6.337C4.77933 7.89567 4 9.78333 4 12C4 14.2167 4.77933 16.1043 6.338 17.663C7.896 19.221 9.78333 20 12 20Z"
+          fill="#EF4444"
+        />
+      </g>
+    </svg>
+  )
+}
+
+export const ClipboardIcon = (props: React.SVGProps<SVGSVGElement>) => {
+  return (
+    <svg
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}>
+      <path
+        d="M9 18C8.45 18 7.97933 17.8043 7.588 17.413C7.196 17.021 7 16.55 7 16V4C7 3.45 7.196 2.979 7.588 2.587C7.97933 2.19567 8.45 2 9 2H18C18.55 2 19.021 2.19567 19.413 2.587C19.8043 2.979 20 3.45 20 4V16C20 16.55 19.8043 17.021 19.413 17.413C19.021 17.8043 18.55 18 18 18H9ZM9 16H18V4H9V16ZM5 22C4.45 22 3.979 21.8043 3.587 21.413C3.19567 21.021 3 20.55 3 20V6H5V20H16V22H5Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+export const CheckIcon = (props: React.SVGProps<SVGSVGElement>) => {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={1.5}
+      stroke="currentColor"
+      className="w-6 h-6"
+      {...props}>
+      <path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
+    </svg>
+  )
+}
\ No newline at end of file
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx
index 77b84fb..cd37c4f 100644
--- a/src/components/layout/header.tsx
+++ b/src/components/layout/header.tsx
@@ -4,6 +4,8 @@ import { runtime } from 'webextension-polyfill'
 
 import { ArrowBack, Settings } from '../icons'
 import { ROUTES } from '../router-provider'
+import { usePopup } from '@/providers/popup.state'
+import { Switch } from '../switch'
 
 const Logo = runtime.getURL('assets/images/logo.svg')
 
@@ -29,6 +31,15 @@ const NavigationButton = () => {
 }
 
 export const Header = () => {
+  const {
+    data: { wmEnabled },
+    setData,
+  } = usePopup()
+
+  const switchWmEnabled = () => {
+    setData(prevState => ({ ...prevState, wmEnabled: !prevState.wmEnabled }))
+  }
+
   return (
     <div className="flex flex-row items-center justify-between py-8">
       <div className="flex flex-row items-center">
@@ -37,6 +48,7 @@ export const Header = () => {
       </div>
       <div className="flex flex-row items-center">
         <NavigationButton />
+        <Switch checked={wmEnabled} onChange={switchWmEnabled} className="ml-2" />
       </div>
     </div>
   )
diff --git a/src/components/radio-group.tsx b/src/components/radio-group.tsx
index 862cc0a..4f1cb44 100644
--- a/src/components/radio-group.tsx
+++ b/src/components/radio-group.tsx
@@ -86,6 +86,7 @@ export interface RadioGroupProps
   disabled?: boolean
   items: Omit<RadioProps, 'name'>[]
   name: string
+  handleChange?: (value: string) => void
 }
 
 export const RadioGroup = ({
@@ -95,8 +96,13 @@ export const RadioGroup = ({
   fullWidth,
   disabled,
   className,
+  handleChange,
+  value,
 }: RadioGroupProps) => {
-  const checkedItem = useMemo(() => items.findIndex(item => item.checked), [items])
+  const checkedItem = useMemo(
+    () => items.findIndex(item => item.checked || item.value === value),
+    [items, value],
+  )
   const [selected, setSelected] = useState(checkedItem)
 
   const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -140,7 +146,10 @@ export const RadioGroup = ({
           disabled={disabled}
           checked={selected === index}
           noSelected={selected === -1 && index === 0}
-          onChange={() => setSelected(index)}
+          onChange={() => {
+            setSelected(index)
+            if (handleChange) handleChange(item.value)
+          }}
         />
       ))}
     </div>
diff --git a/src/content/index.ts b/src/content/index.ts
new file mode 100755
index 0000000..5ede1b8
--- /dev/null
+++ b/src/content/index.ts
@@ -0,0 +1,14 @@
+import { runtime } from 'webextension-polyfill'
+
+import { initMonetizationTagManager } from '@/utils/monetizationTagManager'
+
+import { loadObserver } from './linksObserver'
+import MessageListener from './messageListener'
+
+runtime.onMessage.addListener(MessageListener)
+
+// DEBUG PURPOSE
+loadObserver()
+
+// TBD - check logic
+initMonetizationTagManager()
diff --git a/src/content/index.tsx b/src/content/index.tsx
deleted file mode 100755
index 56d02f7..0000000
--- a/src/content/index.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { runtime } from 'webextension-polyfill'
-
-import { initMonetizationTagManager } from '@/utils/monetizationTagManager'
-import { wm2Polyfill } from '@/utils/polyfill'
-
-import { loadObserver } from './linksObserver'
-import MessageListener from './messageListener'
-
-// import "./content.css";
-
-runtime.onMessage.addListener(MessageListener)
-
-function inject(configure: (_script: HTMLScriptElement) => void) {
-  const script = document.createElement('script')
-  configure(script)
-  document.documentElement.appendChild(script)
-  // document.documentElement.removeChild(script)
-}
-
-// eslint-disable-next-line @typescript-eslint/no-extra-semi
-;(function injectCode(code: string) {
-  inject(script => (script.innerHTML = code))
-})(wm2Polyfill)
-
-loadObserver()
-initMonetizationTagManager()
diff --git a/src/content/messageListener.ts b/src/content/messageListener.ts
index e61fed3..709a491 100644
--- a/src/content/messageListener.ts
+++ b/src/content/messageListener.ts
@@ -1,16 +1,15 @@
-// import { Runtime } from 'webextension-polyfill'
-
 import { PaymentSender } from '@/content/monetization'
 
 const paymentSender = new PaymentSender()
 
-export const onRequest = async (
-  msg: EXTMessage,
-  // sender: Runtime.SendMessageOptionsType,
-): Promise<EXTResponse | undefined> => {
-  // console.log('~~~~~~~', msg)
-
+export const onRequest = async (msg: EXTMessage): Promise<EXTResponse | undefined> => {
   switch (msg.type) {
+    case 'LOAD': {
+      const monetizationTag = document.querySelector('link[rel="monetization"]')
+      monetizationTag?.dispatchEvent(new Event('load'))
+      break
+    }
+
     case 'IS_MONETIZATION_READY': {
       const monetizationTag = document.querySelector('link[rel="monetization"]')
 
diff --git a/src/content/static/index.ts b/src/content/static/index.ts
new file mode 100644
index 0000000..7bdea7c
--- /dev/null
+++ b/src/content/static/index.ts
@@ -0,0 +1,13 @@
+import { wm2Polyfill } from '@/content/static/polyfill'
+
+function inject(configure: (_script: HTMLScriptElement) => void) {
+  const script = document.createElement('script')
+  configure(script)
+  document.documentElement.appendChild(script)
+  document.documentElement.removeChild(script)
+}
+
+// eslint-disable-next-line @typescript-eslint/no-extra-semi
+;(function injectCode(code: string) {
+  inject(script => (script.innerHTML = code))
+})(wm2Polyfill)
diff --git a/src/utils/polyfill.ts b/src/content/static/polyfill.ts
similarity index 96%
rename from src/utils/polyfill.ts
rename to src/content/static/polyfill.ts
index 6acf624..e394da2 100644
--- a/src/utils/polyfill.ts
+++ b/src/content/static/polyfill.ts
@@ -1,7 +1,5 @@
 // language=JavaScript
 export const wm2Polyfill = `
-  var monetizationTag = document.querySelector('link[rel="monetization"]');
-  monetizationTag.dispatchEvent(new Event('load'));
   const dbg = () => {
   }
   dbg('setonmonetization property start')
diff --git a/src/manifest/chrome-v3.json b/src/manifest/chrome-v3.json
index 622b6a8..f505e50 100644
--- a/src/manifest/chrome-v3.json
+++ b/src/manifest/chrome-v3.json
@@ -1,39 +1,37 @@
 {
-    "name": "__MSG_appName__",
-    "version": "1.0.1",
-    "manifest_version": 3,
-    "description": "__MSG_appDescription__",
-    "icons": {
-        "34": "assets/icons/icon-34.png",
-        "128": "assets/icons/icon-128.png"
+  "name": "__MSG_appName__",
+  "version": "1.0.1",
+  "manifest_version": 3,
+  "description": "__MSG_appDescription__",
+  "icons": {
+    "34": "assets/icons/icon-34.png",
+    "128": "assets/icons/icon-128.png"
+  },
+  "default_locale": "en",
+  "content_scripts": [
+    {
+      "matches": ["http://*/*", "https://*/*"],
+      "js": ["content/content.js"]
     },
-    "default_locale": "en",
-    "content_scripts": [
-        {
-            "matches": ["http://*/*", "https://*/*"],
-            "js": ["content/content.js"]
-        }
-    ],
-    "background": {
-        "service_worker": "background/background.js"
-    },
-    "permissions": ["tabs", "storage"],
-    "host_permissions": ["http://*/*", "https://*/*"],
-    "options_ui": {
-        "page": "options/index.html"
-    },
-    "action": {
-        "default_icon": {
-            "16": "assets/icons/icon-16.png",
-            "48": "assets/icons/icon-48.png"
-        },
-        "default_title": "Web Monetization",
-        "default_popup": "popup/index.html"
-    },
-    "web_accessible_resources": [
-        {
-            "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"],
-            "matches": ["<all_urls>"]
-        }
-    ]
+    {
+      "run_at": "document_start",
+      "matches": ["http://*/*", "https://*/*"],
+      "js": ["contentStatic/contentStatic.js"]
+    }
+  ],
+  "background": {
+    "service_worker": "background/background.js"
+  },
+  "permissions": ["tabs", "storage"],
+  "host_permissions": ["http://*/*", "https://*/*"],
+  "action": {
+    "default_title": "Web Monetization",
+    "default_popup": "popup/index.html"
+  },
+  "web_accessible_resources": [
+    {
+      "resources": ["assets/*", "content/*", "options/*", "popup/*", "background/*"],
+      "matches": ["<all_urls>"]
+    }
+  ]
 }
diff --git a/src/manifest/chrome.json b/src/manifest/chrome.json
index 8805a11..ed41c94 100644
--- a/src/manifest/chrome.json
+++ b/src/manifest/chrome.json
@@ -12,6 +12,11 @@
     {
       "matches": ["http://*/*", "https://*/*", "<all_urls>"],
       "js": ["content/content.js"]
+    },
+    {
+      "run_at": "document_start",
+      "matches": ["http://*/*", "https://*/*"],
+      "js": ["contentStatic/contentStatic.js"]
     }
   ],
   "background": {
diff --git a/src/popup/Popup.scss b/src/popup/Popup.scss
index da855dc..62e29e0 100644
--- a/src/popup/Popup.scss
+++ b/src/popup/Popup.scss
@@ -33,7 +33,6 @@ body {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  min-height: 192px;
   flex-basis: auto;
   padding: 0 20px 10px;
 
diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx
index 82d733f..f0cfab5 100644
--- a/src/popup/Popup.tsx
+++ b/src/popup/Popup.tsx
@@ -3,7 +3,7 @@ import './Popup.scss'
 import React from 'react'
 
 import { RouterProvider } from '@/components/router-provider'
-import { PopupProvider } from '@/providers/popup-context'
+import { PopupProvider } from '@/providers/popup.provider'
 
 const Popup = () => {
   return (
diff --git a/src/popup/index.css b/src/popup/index.css
index ea53da1..a2c5f51 100644
--- a/src/popup/index.css
+++ b/src/popup/index.css
@@ -14,6 +14,7 @@
 
     /* Background colors */
     --bg-primary: 59 130 246;
+    --bg-nav-active: 226 232 240;
     --bg-error: 254 226 226;
     --bg-error-hover: 254 202 202;
     --bg-button-base: 86 183 181;
@@ -29,6 +30,6 @@
 
     /* Popup */
     --popup-width: 448px;
-    --popup-height: 559px;
+    --popup-height: 600px;
   }
 }
diff --git a/src/popup/pages/Settings.tsx b/src/popup/pages/Settings.tsx
index eacdd66..86a0b54 100644
--- a/src/popup/pages/Settings.tsx
+++ b/src/popup/pages/Settings.tsx
@@ -1,5 +1,160 @@
-import React from 'react'
+import React, { useState } from 'react'
+
+import { Input } from '@/components/input'
+import { Label } from '@/components/label'
+import { RadioGroup } from '@/components/radio-group'
+import { Button } from '@/components/button'
+import { DollarSign, WarningSign } from '@/components/icons'
+
+import { usePopup } from '@/providers/popup.state'
 
 export const Settings = () => {
-  return <>Settings</>
+  const {
+    data: {
+      connected,
+      wmEnabled,
+      wallet,
+      amount,
+      amountType: { recurring },
+    },
+    setData,
+  } = usePopup()
+
+  const [_wallet, setWallet] = useState<string>(wallet)
+  const [_walletError, setWalletError] = useState<string>('')
+  const [_amount, setAmount] = useState<string>(`${amount}`)
+  const [_amountError, setAmountError] = useState<string>('')
+  const [_recurring, setRecurring] = useState<string>(`${recurring}`)
+  const [loading, setLoading] = useState<boolean>(false)
+
+  const handleChangeWallet = (event: any) => {
+    setWallet(event.target.value)
+  }
+
+  const handleChangeAmount = (event: any) => {
+    setAmount(event.target.value)
+  }
+
+  const handleChangeRecurring = (value: string) => {
+    setRecurring(value)
+  }
+
+  const handleConnect = () => {
+    if (loading) return
+
+    setWalletError('')
+    setAmountError('')
+
+    setLoading(true)
+    let errors = false
+
+    if (!_wallet) {
+      errors = true
+      setWalletError('Please fill in wallet address')
+    }
+    if (!_amount) {
+      errors = true
+      setAmountError('Please fill in the amount')
+    } else if (!parseFloat(_amount)) {
+      errors = true
+      setAmountError('Amount must be a number bigger than 0')
+    }
+
+    if (errors) {
+      setLoading(false)
+      return
+    }
+
+    setWalletError('')
+    setAmountError('')
+
+    setData(prevState => ({
+      ...prevState,
+      amount: parseFloat(_amount),
+      wallet: _wallet,
+      amountType: { recurring: _recurring === 'true' },
+      connected: true,
+    }))
+
+    setLoading(false)
+  }
+
+  const handleDisconnect = () => {
+    setData(prevState => ({
+      ...prevState,
+      connected: false,
+    }))
+  }
+
+  const switchConnect = () => {
+    if (!connected) handleConnect()
+    else handleDisconnect()
+  }
+
+  if (!wmEnabled)
+    return (
+      <div className="flex items-center">
+        <WarningSign />
+        <p className="text-medium text-medium pl-2">Web Monetization has been turned off.</p>
+      </div>
+    )
+
+  return (
+    <>
+      <Label className="text-base font-medium	text-medium mb-4">Connect your wallet</Label>
+      <div className="mb-4 pl-2">
+        <p className="text-xs font-medium	text-medium inline">
+          Get a wallet address from a provider before connecting it below. Please find a list of
+          available wallets
+        </p>
+        <a
+          href="https://webmonetization.org/docs/resources/op-wallets/"
+          className="text-xs font-medium text-primary inline pl-1"
+          target="_blank"
+          rel="noreferrer">
+          here.
+        </a>
+      </div>
+      <Input
+        value={_wallet}
+        id="wallet"
+        name="wallet"
+        onChange={handleChangeWallet}
+        errorMessage={_walletError}
+        disabled={connected}
+        placeholder="Enter your wallet address"
+      />
+      <Label className="text-base font-medium	text-medium mb-4 mt-8">Amount</Label>
+      <Input
+        value={_amount}
+        type="number"
+        id="amount"
+        name="amount"
+        onChange={handleChangeAmount}
+        disabled={connected}
+        errorMessage={_amountError}
+        icon={<DollarSign />}
+      />
+      <RadioGroup
+        handleChange={handleChangeRecurring}
+        items={[
+          { label: 'Recurring monthly amount', value: 'true' },
+          { label: 'Single-use amount', value: 'false' },
+        ]}
+        name="recurring"
+        id="recurring"
+        value={_recurring}
+        className="mb-8 pl-2 mt-5"
+        disabled={connected}
+      />
+      <Button
+        className="w-full"
+        onClick={switchConnect}
+        disabled={loading}
+        loading={loading}
+        aria-label="connect">
+        {connected ? 'Disconnect' : 'Connect'}
+      </Button>
+    </>
+  )
 }
diff --git a/src/providers/index.ts b/src/providers/index.ts
index 35b996f..f6e55cc 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -1,3 +1,3 @@
-import { PopupProvider } from './popup-context'
+import { PopupProvider } from './popup.provider'
 
 export { PopupProvider }
diff --git a/src/providers/popup-context.tsx b/src/providers/popup.provider.tsx
similarity index 95%
rename from src/providers/popup-context.tsx
rename to src/providers/popup.provider.tsx
index 392f2c7..db7634d 100644
--- a/src/providers/popup-context.tsx
+++ b/src/providers/popup.provider.tsx
@@ -3,6 +3,8 @@ import React, { createContext, useState } from 'react'
 import { PopupContextValue, TPopupContext } from './providers.interface'
 
 export const defaultData = {
+  connected: false,
+  wallet: '',
   amount: 0,
   amountType: {
     recurring: true,
diff --git a/src/providers/popup.state.tsx b/src/providers/popup.state.tsx
new file mode 100644
index 0000000..9671d9e
--- /dev/null
+++ b/src/providers/popup.state.tsx
@@ -0,0 +1,11 @@
+import { useContext } from 'react'
+
+import { PopupContext } from './popup.provider'
+
+export const usePopup = () => {
+  const context = useContext(PopupContext)
+  if (context === undefined) {
+    throw new Error('usePopup must be used with a PopupContext')
+  }
+  return context
+}
diff --git a/src/providers/providers.interface.ts b/src/providers/providers.interface.ts
index 9be002f..1460316 100644
--- a/src/providers/providers.interface.ts
+++ b/src/providers/providers.interface.ts
@@ -6,6 +6,8 @@ export interface PopupContextValue {
 }
 
 export type TPopupContext = {
+  connected: boolean
+  wallet: string
   amount: number
   amountType: {
     recurring: boolean
diff --git a/src/types/message.d.ts b/src/types/message.d.ts
index 8dc5b54..8f46fc7 100644
--- a/src/types/message.d.ts
+++ b/src/types/message.d.ts
@@ -10,6 +10,7 @@ declare type EXTMessageType =
   | 'STOP_PAYMENTS'
   | 'PAYMENT_SUCCESS'
   | 'PAUSE_PAYMENTS'
+  | 'LOAD'
 
 declare type EXTMessage<T = any> = {
   type: EXTMessageType
diff --git a/tailwind.config.ts b/tailwind.config.ts
index f44ecc3..2b9443c 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -21,6 +21,7 @@ module.exports = {
       backgroundColor: {
         primary: 'rgb(var(--bg-primary) / <alpha-value>)',
         error: 'rgb(var(--bg-error) / <alpha-value>)',
+        'nav-active': 'rgb(var(--bg-nav-active) / <alpha-value>)',
         'error-hover': 'rgb(var(--bg-error-hover) / <alpha-value>)',
         'button-base': 'rgb(var(--bg-button-base) / <alpha-value>)',
         'button-base-hover': 'rgb(var(--bg-button-base-hover) / <alpha-value>)',
diff --git a/webpack.config.utils.ts b/webpack.config.utils.ts
index 3361087..32614c3 100644
--- a/webpack.config.utils.ts
+++ b/webpack.config.utils.ts
@@ -49,7 +49,7 @@ const EnvConfig: EnvironmentConfig = {
         ? Directories.DIST_DIR
         : Directories.DEV_DIR,
   ...(process.env.NODE_ENV ? { NODE_ENV: process.env.NODE_ENV } : { NODE_ENV: 'development' }),
-  ...(process.env.TARGET ? { TARGET: process.env.TARGET } : { TARGET: 'chrome' }),
+  ...(process.env.TARGET ? { TARGET: process.env.TARGET } : { TARGET: 'chrome-v3' }),
 }
 
 /**
@@ -109,7 +109,8 @@ export const getOutput = (browserDir: string, outputDir = Directories.DEV_DIR) =
 export const getEntry = (sourceDir = Directories.SRC_DIR) => {
   return {
     popup: [path.resolve(__dirname, `${sourceDir}/popup/index.tsx`)],
-    content: [path.resolve(__dirname, `${sourceDir}/content/index.tsx`)],
+    content: [path.resolve(__dirname, `${sourceDir}/content/index.ts`)],
+    contentStatic: [path.resolve(__dirname, `${sourceDir}/content/static/index.ts`)],
     background: [path.resolve(__dirname, `${sourceDir}/background/index.ts`)],
   }
 }


Test patch:
diff --git a/src/components/__tests__/code.test.tsx b/src/components/__tests__/code.test.tsx
new file mode 100644
index 0000000..12f3bd1
--- /dev/null
+++ b/src/components/__tests__/code.test.tsx
@@ -0,0 +1,30 @@
+import { fireEvent, render } from '@testing-library/react'
+import React from 'react'
+
+import { Code } from '../code'
+
+describe('Code', () => {
+  it('should render the code component', () => {
+    const { queryByRole, container } = render(<Code value="test" />)
+    const code = container.querySelector('code')
+
+    expect(code).toBeInTheDocument()
+    expect(code).toHaveTextContent('test')
+    expect(queryByRole('button')).toHaveAttribute('aria-label', 'copy')
+  })
+
+  it('calls clipboard.writeText with the correct value', () => {
+    Object.assign(navigator, {
+      clipboard: {
+        writeText: jest.fn(),
+      },
+    })
+
+    const { getByRole } = render(<Code value="test" />)
+    const copyButton = getByRole('button')
+    expect(copyButton).toBeInTheDocument()
+
+    fireEvent.click(copyButton)
+    expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test')
+  })
+})
diff --git a/src/providers/__tests__/popup-context.test.tsx b/src/providers/__tests__/popup-context.test.tsx
index ed841a6..cb6996b 100644
--- a/src/providers/__tests__/popup-context.test.tsx
+++ b/src/providers/__tests__/popup-context.test.tsx
@@ -1,7 +1,7 @@
 import { act, render, screen } from '@testing-library/react'
 import React, { useContext } from 'react'
 
-import { defaultData, PopupContext, PopupProvider } from '../popup-context'
+import { defaultData, PopupContext, PopupProvider } from '../popup.provider'
 
 const TestComponent = () => {
   const { data, setData } = useContext(PopupContext)