Repository: interledger/web-monetization-extension
Pull request #756 (2024-12-03 10:22:08)

refactor: move shared frontend files to `pages/shared/`
## Context

In https://github.com/interledger/web-monetization-extension/issues/558, we'll add a new extension page. To keep things better organized for reuse, this PR is here.

## Changes proposed in this pull request

- Move `src/popup` to `src/pages/popup`
  - Kept output dir same as before. Then we can mark entire `output/pages/**` as `web_accessible_resources` (instead of adding individual pages).
- Move reusable parts from `src/popup/{components,lib}` to `src/pages/shared/{components,lib}`
- Add alias to tsconfig so we can continue using `@/popup` instead of needing `@/shared/pages/popup`
- Move some helpers to `@/shared/helpers` (those that are used in background as well)
- 

github-actions[bot]: <!-- __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">5a748ea8466316ff0e98ee31473427573a9399bf</td>
  </tr>
  <tr>
    <td align="center">Latest job logs</td>
    <td align="center"><a href="https://github.com/interledger/web-monetization-extension/actions/runs/12137622912">Run #12137622912</a></td>
  </tr>
  <tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Chrome (568.09KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/31595702038/artifacts/2266536921">Download</a></td></tr><tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Firefox (568.12KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/31595702038/artifacts/2266536388">Download</a></td></tr>
</tbody>
</table>

Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index fc855b1..91573f7 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -16,7 +16,7 @@
 'area: popup':
   - any:
       - changed-files:
-          - any-glob-to-any-file: 'src/popup/**/*'
+          - any-glob-to-any-file: 'src/pages/popup/**/*'
 
 'area: documentation':
   - any:
diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md
index e9b0073..18b406b 100644
--- a/docs/DEVELOP.md
+++ b/docs/DEVELOP.md
@@ -99,8 +99,10 @@ The high-level project directory structure is as follows:
 │   │   ├── polyfill.ts # WM JS API polyfill, and to integrate events with website
 │   │   ├── services/ # Services to extract & monitor link tags from websites user visits
 │   │   └── keyAutoAdd/ # Content scripts for automatic key addition to wallets
-│   ├── popup/ # Source code for the popup UI
-│   ├── pages/ # Source code for additional extension pages
+│   ├── pages/ # Source code for extension pages, including popup
+│   │   ├── popup/ # Source code for the popup UI
+│   │   ├── progress-connect/ # Source code for the progress-connect (notification/layover) UI
+│   │   └── shared/ # Shared components and libraries for the frontend
 │   ├── shared/ # Shared utilities
 │   └── manifest.json # Extension's manifest - processed by Webpack depending on the target build
 ├── jest.config.ts
@@ -203,7 +205,9 @@ This content script serves as a polyfill for the WM JS API. It lets a website:
 
 This script runs in `"MAIN"` context: so its globals are available to the website to access.
 
-#### Popup
+#### Extension pages
+
+##### Popup
 
 Popup (technically, the [browser action -> default popup](https://developer.chrome.com/docs/extensions/reference/api/action#popup)) is what users will interact with. It's a React SPA written in TypeScript and uses Tailwind for styling.
 
@@ -214,7 +218,7 @@ Popup (technically, the [browser action -> default popup](https://developer.chro
   - Most of the integration is to be done in pages only, so the components can include a lot of props.
   - The `useTranslation` hook (see `TranslationContext`) can be considered pure, as its default values don't rely on any external APIs.
 
-#### Extension pages
+##### Other pages
 
 - `progress-connect`: Related to KeyAutoAdd services, this extension page (also a React SPA) gets injected into wallet provider websites as an iframe. It can serve as a notification UI and as a full-page overlay during automatic public-key addition. The content scripts and the popup communicate to it via the background and runtime ports.
 
diff --git a/esbuild/config.ts b/esbuild/config.ts
index 93ae7d7..bafe058 100644
--- a/esbuild/config.ts
+++ b/esbuild/config.ts
@@ -45,7 +45,7 @@ export const options: BuildOptions = {
       out: path.join('polyfill', 'polyfill'),
     },
     {
-      in: path.join(SRC_DIR, 'popup', 'index.tsx'),
+      in: path.join(SRC_DIR, 'pages', 'popup', 'index.tsx'),
       out: path.join('popup', 'popup'),
     },
     {
diff --git a/esbuild/dev.ts b/esbuild/dev.ts
index 02544d1..db816ac 100644
--- a/esbuild/dev.ts
+++ b/esbuild/dev.ts
@@ -102,13 +102,16 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin {
         };
       });
 
-      build.onLoad({ filter: /src\/popup\/index\.tsx$/ }, async (args) => {
-        const contents = await readFile(args.path, 'utf8');
-        return {
-          contents: contents + '\n\n\n' + reloadScriptPopup,
-          loader: 'tsx' as const,
-        };
-      });
+      build.onLoad(
+        { filter: /src\/pages\/popup\/index\.tsx$/ },
+        async (args) => {
+          const contents = await readFile(args.path, 'utf8');
+          return {
+            contents: contents + '\n\n\n' + reloadScriptPopup,
+            loader: 'tsx' as const,
+          };
+        },
+      );
       build.onLoad({ filter: /src\/pages\/.+\/index.tsx$/ }, async (args) => {
         const contents = await readFile(args.path, 'utf8');
         return {
diff --git a/esbuild/plugins.ts b/esbuild/plugins.ts
index a4a97bf..cc34670 100644
--- a/esbuild/plugins.ts
+++ b/esbuild/plugins.ts
@@ -48,7 +48,7 @@ export const getPlugins = ({
       resolveFrom: ROOT_DIR,
       assets: [
         {
-          from: toPosix(path.join(SRC_DIR, 'popup', 'index.html')),
+          from: toPosix(path.join(SRC_DIR, 'pages', 'popup', 'index.html')),
           to: toPosix(path.join(outDir, 'popup', 'index.html')),
         },
         {
diff --git a/src/background/services/monetization.ts b/src/background/services/monetization.ts
index a5597b0..19cc562 100644
--- a/src/background/services/monetization.ts
+++ b/src/background/services/monetization.ts
@@ -19,8 +19,8 @@ import {
   isErrorWithKey,
   isOkState,
   removeQueryParams,
+  transformBalance,
 } from '@/shared/helpers';
-import { transformBalance } from '@/popup/lib/utils';
 import type { AmountValue, PopupStore, Storage } from '@/shared/types';
 import type { OutgoingPayment } from '@interledger/open-payments';
 import type { Cradle } from '../container';
diff --git a/src/background/services/paymentSession.ts b/src/background/services/paymentSession.ts
index 63aee97..11990aa 100644
--- a/src/background/services/paymentSession.ts
+++ b/src/background/services/paymentSession.ts
@@ -4,8 +4,7 @@ import {
   type OutgoingPayment,
   type WalletAddress,
 } from '@interledger/open-payments/dist/types';
-import { bigIntMax, convert } from '@/shared/helpers';
-import { transformBalance } from '@/popup/lib/utils';
+import { bigIntMax, convert, transformBalance } from '@/shared/helpers';
 import {
   isInvalidReceiverError,
   isKeyRevokedError,
diff --git a/src/popup/Popup.tsx b/src/pages/popup/Popup.tsx
similarity index 95%
rename from src/popup/Popup.tsx
rename to src/pages/popup/Popup.tsx
index ee8f22a..21b4c82 100644
--- a/src/popup/Popup.tsx
+++ b/src/pages/popup/Popup.tsx
@@ -1,10 +1,9 @@
 import { MainLayout } from '@/popup/components/layout/MainLayout';
 import {
   BrowserContextProvider,
-  MessageContextProvider,
-  PopupContextProvider,
   TranslationContextProvider,
-} from './lib/context';
+} from '@/pages/shared/lib/context';
+import { MessageContextProvider, PopupContextProvider } from './lib/context';
 import { LazyMotion, domAnimation } from 'framer-motion';
 import React from 'react';
 import browser from 'webextension-polyfill';
diff --git a/src/popup/components/ConnectWalletForm.tsx b/src/pages/popup/components/ConnectWalletForm.tsx
similarity index 96%
rename from src/popup/components/ConnectWalletForm.tsx
rename to src/pages/popup/components/ConnectWalletForm.tsx
index 8655060..a16ff8f 100644
--- a/src/popup/components/ConnectWalletForm.tsx
+++ b/src/pages/popup/components/ConnectWalletForm.tsx
@@ -1,12 +1,15 @@
 import React from 'react';
-import { Button } from '@/popup/components/ui/Button';
-import { Input } from '@/popup/components/ui/Input';
-import { Switch } from '@/popup/components/ui/Switch';
-import { Code } from '@/popup/components/ui/Code';
-import { ErrorMessage } from '@/popup/components/ErrorMessage';
-import { LoadingSpinner } from '@/popup/components/LoadingSpinner';
-import { InputAmount, validateAmount } from '@/popup/components/InputAmount';
-import { toWalletAddressUrl } from '@/popup/lib/utils';
+import { Button } from '@/pages/shared/components/ui/Button';
+import { Input } from '@/pages/shared/components/ui/Input';
+import { Switch } from '@/pages/shared/components/ui/Switch';
+import { Code } from '@/pages/shared/components/ui/Code';
+import { ErrorMessage } from '@/pages/shared/components/ErrorMessage';
+import { LoadingSpinner } from '@/pages/shared/components/LoadingSpinner';
+import {
+  InputAmount,
+  validateAmount,
+} from '@/pages/shared/components/InputAmount';
+import { toWalletAddressUrl } from '@/pages/shared/lib/utils';
 import { useTranslation } from '@/popup/lib/context';
 import {
   cn,
diff --git a/src/popup/components/ErrorKeyRevoked.tsx b/src/pages/popup/components/ErrorKeyRevoked.tsx
similarity index 94%
rename from src/popup/components/ErrorKeyRevoked.tsx
rename to src/pages/popup/components/ErrorKeyRevoked.tsx
index cd7a5e3..72b52a2 100644
--- a/src/popup/components/ErrorKeyRevoked.tsx
+++ b/src/pages/popup/components/ErrorKeyRevoked.tsx
@@ -1,10 +1,10 @@
 import React from 'react';
 import { AnimatePresence, m } from 'framer-motion';
-import { WarningSign } from '@/popup/components/Icons';
-import { Button } from '@/popup/components/ui/Button';
-import { Code } from '@/popup/components/ui/Code';
-import { useTranslation } from '@/popup/lib/context';
-import { useLocalStorage } from '@/popup/lib/hooks';
+import { WarningSign } from '@/pages/shared/components/Icons';
+import { Button } from '@/pages/shared/components/ui/Button';
+import { Code } from '@/pages/shared/components/ui/Code';
+import { useTranslation } from '@/pages/shared/lib/context';
+import { useLocalStorage } from '@/pages/shared/lib/hooks';
 import type { PopupStore } from '@/shared/types';
 import type { Response } from '@/shared/messages';
 
diff --git a/src/popup/components/NotMonetized.tsx b/src/pages/popup/components/NotMonetized.tsx
similarity index 86%
rename from src/popup/components/NotMonetized.tsx
rename to src/pages/popup/components/NotMonetized.tsx
index f01f39f..a51b0ca 100644
--- a/src/popup/components/NotMonetized.tsx
+++ b/src/pages/popup/components/NotMonetized.tsx
@@ -1,5 +1,5 @@
 import React from 'react';
-import { WarningSign } from '@/popup/components/Icons';
+import { WarningSign } from '@/pages/shared/components/Icons';
 
 export const NotMonetized = ({ text }: { text: string }) => {
   return (
diff --git a/src/popup/components/OutOfFunds.tsx b/src/pages/popup/components/OutOfFunds.tsx
similarity index 91%
rename from src/popup/components/OutOfFunds.tsx
rename to src/pages/popup/components/OutOfFunds.tsx
index d403e17..d29ba97 100644
--- a/src/popup/components/OutOfFunds.tsx
+++ b/src/pages/popup/components/OutOfFunds.tsx
@@ -2,13 +2,13 @@ import React from 'react';
 import type { RecurringGrant, OneTimeGrant, AmountValue } from '@/shared/types';
 import type { AddFundsPayload, Response } from '@/shared/messages';
 import type { WalletAddress } from '@interledger/open-payments';
-import { getCurrencySymbol, transformBalance } from '@/popup/lib/utils';
-import { useTranslation } from '@/popup/lib/context';
-import { getNextOccurrence } from '@/shared/helpers';
-import { ErrorMessage } from '@/popup/components/ErrorMessage';
-import { InputAmount } from '@/popup/components/InputAmount';
-import { Button } from '@/popup/components/ui/Button';
-import { Input } from '@/popup/components/ui/Input';
+import { getCurrencySymbol } from '@/pages/shared/lib/utils';
+import { useTranslation } from '@/pages/shared/lib/context';
+import { getNextOccurrence, transformBalance } from '@/shared/helpers';
+import { ErrorMessage } from '@/pages/shared/components/ErrorMessage';
+import { InputAmount } from '@/pages/shared/components/InputAmount';
+import { Button } from '@/pages/shared/components/ui/Button';
+import { Input } from '@/pages/shared/components/ui/Input';
 
 interface OutOfFundsProps {
   info: Pick<WalletAddress, 'id' | 'assetCode' | 'assetScale'>;
diff --git a/src/popup/components/PayWebsiteForm.tsx b/src/pages/popup/components/PayWebsiteForm.tsx
similarity index 95%
rename from src/popup/components/PayWebsiteForm.tsx
rename to src/pages/popup/components/PayWebsiteForm.tsx
index 0e63e06..1b163e6 100644
--- a/src/popup/components/PayWebsiteForm.tsx
+++ b/src/pages/popup/components/PayWebsiteForm.tsx
@@ -1,10 +1,10 @@
 import React from 'react';
 import { AnimatePresence, m } from 'framer-motion';
-import { Button } from '@/popup/components/ui/Button';
-import { InputAmount } from '@/popup/components/InputAmount';
+import { Button } from '@/pages/shared/components/ui/Button';
+import { InputAmount } from '@/pages/shared/components/InputAmount';
 import { cn, type ErrorWithKeyLike } from '@/shared/helpers';
 import { useMessage, usePopupState, useTranslation } from '@/popup/lib/context';
-import { roundWithPrecision } from '@/popup/lib/utils';
+import { roundWithPrecision } from '@/pages/shared/lib/utils';
 
 type ErrorInfo = { message: string; info?: ErrorWithKeyLike };
 type ErrorsParams = 'amount' | 'pay';
diff --git a/src/popup/components/ProtectedRoute.tsx b/src/pages/popup/components/ProtectedRoute.tsx
similarity index 100%
rename from src/popup/components/ProtectedRoute.tsx
rename to src/pages/popup/components/ProtectedRoute.tsx
diff --git a/src/popup/components/Settings/Budget.tsx b/src/pages/popup/components/Settings/Budget.tsx
similarity index 92%
rename from src/popup/components/Settings/Budget.tsx
rename to src/pages/popup/components/Settings/Budget.tsx
index 2d12640..cc059b3 100644
--- a/src/popup/components/Settings/Budget.tsx
+++ b/src/pages/popup/components/Settings/Budget.tsx
@@ -1,11 +1,15 @@
 import React from 'react';
-import { Input } from '@/popup/components/ui/Input';
-import { Switch } from '@/popup/components/ui/Switch';
-import { Button } from '@/popup/components/ui/Button';
-import { InputAmount } from '@/popup/components/InputAmount';
-import { ErrorMessage } from '@/popup/components/ErrorMessage';
-import { ErrorWithKeyLike, getNextOccurrence } from '@/shared/helpers';
-import { getCurrencySymbol, transformBalance } from '@/popup/lib/utils';
+import { Input } from '@/pages/shared/components/ui/Input';
+import { Switch } from '@/pages/shared/components/ui/Switch';
+import { Button } from '@/pages/shared/components/ui/Button';
+import { InputAmount } from '@/pages/shared/components/InputAmount';
+import { ErrorMessage } from '@/pages/shared/components/ErrorMessage';
+import {
+  ErrorWithKeyLike,
+  getNextOccurrence,
+  transformBalance,
+} from '@/shared/helpers';
+import { getCurrencySymbol } from '@/pages/shared/lib/utils';
 import {
   useMessage,
   useTranslation,
diff --git a/src/popup/components/Settings/RateOfPay.tsx b/src/pages/popup/components/Settings/RateOfPay.tsx
similarity index 94%
rename from src/popup/components/Settings/RateOfPay.tsx
rename to src/pages/popup/components/Settings/RateOfPay.tsx
index c5cbfad..10548f2 100644
--- a/src/popup/components/Settings/RateOfPay.tsx
+++ b/src/pages/popup/components/Settings/RateOfPay.tsx
@@ -1,8 +1,8 @@
 import React from 'react';
-import { Switch } from '@/popup/components/ui/Switch';
-import { InputAmountMemoized as InputAmount } from '@/popup/components/InputAmount';
+import { Switch } from '@/pages/shared/components/ui/Switch';
+import { InputAmountMemoized as InputAmount } from '@/pages/shared/components/InputAmount';
 import { debounceAsync } from '@/shared/helpers';
-import { formatNumber, roundWithPrecision } from '@/popup/lib/utils';
+import { formatNumber, roundWithPrecision } from '@/pages/shared/lib/utils';
 import {
   useMessage,
   usePopupState,
diff --git a/src/popup/components/Settings/WalletInformation.tsx b/src/pages/popup/components/Settings/WalletInformation.tsx
similarity index 85%
rename from src/popup/components/Settings/WalletInformation.tsx
rename to src/pages/popup/components/Settings/WalletInformation.tsx
index ffaae74..40955e6 100644
--- a/src/popup/components/Settings/WalletInformation.tsx
+++ b/src/pages/popup/components/Settings/WalletInformation.tsx
@@ -1,9 +1,9 @@
 import React from 'react';
-import { ArrowBack } from '@/popup/components/Icons';
-import { Input } from '@/popup/components/ui/Input';
-import { Label } from '@/popup/components/ui/Label';
-import { Code } from '@/popup/components/ui/Code';
-import { Button } from '@/popup/components/ui/Button';
+import { ArrowBack } from '@/pages/shared/components/Icons';
+import { Input } from '@/pages/shared/components/ui/Input';
+import { Label } from '@/pages/shared/components/ui/Label';
+import { Code } from '@/pages/shared/components/ui/Code';
+import { Button } from '@/pages/shared/components/ui/Button';
 import { useMessage } from '@/popup/lib/context';
 import type { PopupStore } from '@/shared/types';
 
diff --git a/src/popup/components/layout/Header.tsx b/src/pages/popup/components/layout/Header.tsx
similarity index 94%
rename from src/popup/components/layout/Header.tsx
rename to src/pages/popup/components/layout/Header.tsx
index cf7ede9..932edd3 100644
--- a/src/popup/components/layout/Header.tsx
+++ b/src/pages/popup/components/layout/Header.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { Link, useLocation } from 'react-router-dom';
-import { ArrowBack, Settings } from '../Icons';
+import { ArrowBack, Settings } from '@/pages/shared/components/Icons';
 import { HeaderEmpty } from './HeaderEmpty';
 import { ROUTES_PATH } from '@/popup/Popup';
 import { useBrowser, usePopupState } from '@/popup/lib/context';
diff --git a/src/popup/components/layout/HeaderEmpty.tsx b/src/pages/popup/components/layout/HeaderEmpty.tsx
similarity index 100%
rename from src/popup/components/layout/HeaderEmpty.tsx
rename to src/pages/popup/components/layout/HeaderEmpty.tsx
diff --git a/src/popup/components/layout/MainLayout.tsx b/src/pages/popup/components/layout/MainLayout.tsx
similarity index 100%
rename from src/popup/components/layout/MainLayout.tsx
rename to src/pages/popup/components/layout/MainLayout.tsx
diff --git a/src/popup/index.css b/src/pages/popup/index.css
similarity index 100%
rename from src/popup/index.css
rename to src/pages/popup/index.css
diff --git a/src/popup/index.html b/src/pages/popup/index.html
similarity index 100%
rename from src/popup/index.html
rename to src/pages/popup/index.html
diff --git a/src/popup/index.tsx b/src/pages/popup/index.tsx
similarity index 100%
rename from src/popup/index.tsx
rename to src/pages/popup/index.tsx
diff --git a/src/popup/lib/context.tsx b/src/pages/popup/lib/context.tsx
similarity index 78%
rename from src/popup/lib/context.tsx
rename to src/pages/popup/lib/context.tsx
index 20d3308..6cb235a 100644
--- a/src/popup/lib/context.tsx
+++ b/src/pages/popup/lib/context.tsx
@@ -1,10 +1,5 @@
 import React, { type PropsWithChildren } from 'react';
-import type { Browser } from 'webextension-polyfill';
-import {
-  tFactory,
-  type ErrorWithKeyLike,
-  type Translation,
-} from '@/shared/helpers';
+
 import type { DeepNonNullable, PopupStore } from '@/shared/types';
 import {
   BACKGROUND_TO_POPUP_CONNECTION_NAME as CONNECTION_NAME,
@@ -12,6 +7,9 @@ import {
   type PopupToBackgroundMessage,
   type BackgroundToPopupMessage,
 } from '@/shared/messages';
+import { useBrowser } from '@/pages/shared/lib/context';
+
+export { useBrowser, useTranslation } from '@/pages/shared/lib/context';
 
 // #region PopupState
 
@@ -141,45 +139,7 @@ export function PopupContextProvider({ children }: PopupContextProviderProps) {
 }
 // #endregion
 
-// #region Browser
-const BrowserContext = React.createContext<Browser>({} as Browser);
-
-export const useBrowser = () => React.useContext(BrowserContext);
-
-export const BrowserContextProvider = ({
-  browser,
-  children,
-}: PropsWithChildren<{ browser: Browser }>) => {
-  return (
-    <BrowserContext.Provider value={browser}>
-      {children}
-    </BrowserContext.Provider>
-  );
-};
-
-// #endregion
-
-// #region Translation
-const TranslationContext = React.createContext<Translation>(
-  (v: string | ErrorWithKeyLike) => (typeof v === 'string' ? v : v.key),
-);
-
-export const useTranslation = () => React.useContext(TranslationContext);
-
-export const TranslationContextProvider = ({ children }: PropsWithChildren) => {
-  const browser = useBrowser();
-  const t = tFactory(browser);
-
-  return (
-    <TranslationContext.Provider value={t}>
-      {children}
-    </TranslationContext.Provider>
-  );
-};
-
-// #endregion
-
-// #region Translation
+// #region Message
 const MessageContext = React.createContext<
   MessageManager<PopupToBackgroundMessage>
 >({} as MessageManager<PopupToBackgroundMessage>);
@@ -196,5 +156,4 @@ export const MessageContextProvider = ({ children }: PropsWithChildren) => {
     </MessageContext.Provider>
   );
 };
-
 // #endregion
diff --git a/src/popup/pages/ConnectWallet.tsx b/src/pages/popup/pages/ConnectWallet.tsx
similarity index 100%
rename from src/popup/pages/ConnectWallet.tsx
rename to src/pages/popup/pages/ConnectWallet.tsx
diff --git a/src/popup/pages/ErrorKeyRevoked.tsx b/src/pages/popup/pages/ErrorKeyRevoked.tsx
similarity index 100%
rename from src/popup/pages/ErrorKeyRevoked.tsx
rename to src/pages/popup/pages/ErrorKeyRevoked.tsx
diff --git a/src/popup/pages/Home.tsx b/src/pages/popup/pages/Home.tsx
similarity index 95%
rename from src/popup/pages/Home.tsx
rename to src/pages/popup/pages/Home.tsx
index df7edf8..29ccc29 100644
--- a/src/popup/pages/Home.tsx
+++ b/src/pages/popup/pages/Home.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { usePopupState, useTranslation } from '@/popup/lib/context';
-import { Settings } from '@/popup/components/Icons';
-import { formatNumber, roundWithPrecision } from '../lib/utils';
+import { Settings } from '@/pages/shared/components/Icons';
+import { formatNumber, roundWithPrecision } from '@/pages/shared/lib/utils';
 import { PayWebsiteForm } from '@/popup/components/PayWebsiteForm';
 import { NotMonetized } from '@/popup/components/NotMonetized';
 import { formatCurrency } from '@/shared/helpers';
diff --git a/src/popup/pages/MissingHostPermission.tsx b/src/pages/popup/pages/MissingHostPermission.tsx
similarity index 95%
rename from src/popup/pages/MissingHostPermission.tsx
rename to src/pages/popup/pages/MissingHostPermission.tsx
index 41954fa..03f47c9 100644
--- a/src/popup/pages/MissingHostPermission.tsx
+++ b/src/pages/popup/pages/MissingHostPermission.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { PERMISSION_HOSTS } from '@/shared/defines';
-import { WarningSign } from '@/popup/components/Icons';
+import { WarningSign } from '@/pages/shared/components/Icons';
 import { useBrowser, useTranslation } from '@/popup/lib/context';
 
 export const Component = () => {
diff --git a/src/popup/pages/OutOfFunds.tsx b/src/pages/popup/pages/OutOfFunds.tsx
similarity index 100%
rename from src/popup/pages/OutOfFunds.tsx
rename to src/pages/popup/pages/OutOfFunds.tsx
diff --git a/src/popup/pages/OutOfFunds_AddFunds.tsx b/src/pages/popup/pages/OutOfFunds_AddFunds.tsx
similarity index 100%
rename from src/popup/pages/OutOfFunds_AddFunds.tsx
rename to src/pages/popup/pages/OutOfFunds_AddFunds.tsx
diff --git a/src/popup/pages/Settings.tsx b/src/pages/popup/pages/Settings.tsx
similarity index 97%
rename from src/popup/pages/Settings.tsx
rename to src/pages/popup/pages/Settings.tsx
index d620d8c..db59b2e 100644
--- a/src/popup/pages/Settings.tsx
+++ b/src/pages/popup/pages/Settings.tsx
@@ -6,7 +6,7 @@ import { BudgetScreen } from '@/popup/components/Settings/Budget';
 import { RateOfPayScreen } from '@/popup/components/Settings/RateOfPay';
 import { cn } from '@/shared/helpers';
 import { usePopupState } from '@/popup/lib/context';
-import { useLocalStorage } from '@/popup/lib/hooks';
+import { useLocalStorage } from '@/pages/shared/lib/hooks';
 
 const TABS = [
   { id: 'wallet', title: 'Wallet' },
diff --git a/src/pages/progress-connect/components/AppNotification.tsx b/src/pages/progress-connect/components/AppNotification.tsx
index 6ea622c..d54762e 100644
--- a/src/pages/progress-connect/components/AppNotification.tsx
+++ b/src/pages/progress-connect/components/AppNotification.tsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import { HeaderEmpty } from '@/popup/components/layout/HeaderEmpty';
-import { LoadingSpinner } from '@/popup/components/LoadingSpinner';
+import { LoadingSpinner } from '@/pages/shared/components/LoadingSpinner';
 import { Countdown } from '@/pages/progress-connect/components/Countdown';
 import { useState } from '@/pages/progress-connect/context';
 import { useBrowser } from '@/popup/lib/context';
diff --git a/src/pages/progress-connect/index.tsx b/src/pages/progress-connect/index.tsx
index dbed1ca..a7d6afb 100755
--- a/src/pages/progress-connect/index.tsx
+++ b/src/pages/progress-connect/index.tsx
@@ -7,7 +7,7 @@ import browser from 'webextension-polyfill';
 import {
   BrowserContextProvider,
   TranslationContextProvider,
-} from '@/popup/lib/context';
+} from '@/pages/shared/lib/context';
 import { StateContextProvider, UIModeProvider } from './context';
 import App from './App';
 
diff --git a/src/popup/components/ErrorMessage.tsx b/src/pages/shared/components/ErrorMessage.tsx
similarity index 100%
rename from src/popup/components/ErrorMessage.tsx
rename to src/pages/shared/components/ErrorMessage.tsx
diff --git a/src/popup/components/Icons.tsx b/src/pages/shared/components/Icons.tsx
similarity index 100%
rename from src/popup/components/Icons.tsx
rename to src/pages/shared/components/Icons.tsx
diff --git a/src/popup/components/InputAmount.tsx b/src/pages/shared/components/InputAmount.tsx
similarity index 99%
rename from src/popup/components/InputAmount.tsx
rename to src/pages/shared/components/InputAmount.tsx
index f8dddf6..6280365 100644
--- a/src/popup/components/InputAmount.tsx
+++ b/src/pages/shared/components/InputAmount.tsx
@@ -7,7 +7,7 @@ import {
   ErrorWithKeyLike,
   formatCurrency,
 } from '@/shared/helpers';
-import { useLongPress, useThrottle } from '@/popup/lib/hooks';
+import { useLongPress, useThrottle } from '@/pages/shared/lib/hooks';
 
 interface Props {
   id: string;
diff --git a/src/popup/components/LoadingSpinner.tsx b/src/pages/shared/components/LoadingSpinner.tsx
similarity index 91%
rename from src/popup/components/LoadingSpinner.tsx
rename to src/pages/shared/components/LoadingSpinner.tsx
index 6b90c5a..7a33449 100644
--- a/src/popup/components/LoadingSpinner.tsx
+++ b/src/pages/shared/components/LoadingSpinner.tsx
@@ -1,7 +1,7 @@
-import { type VariantProps, cva } from 'class-variance-authority';
 import React from 'react';
+import { type VariantProps, cva } from 'class-variance-authority';
 
-import { Spinner } from '@/popup/components/Icons';
+import { Spinner } from '@/pages/shared/components/Icons';
 
 const loadingSpinnerStyles = cva('animate-spin', {
   variants: {
diff --git a/src/popup/components/WarningMessage.tsx b/src/pages/shared/components/WarningMessage.tsx
similarity index 100%
rename from src/popup/components/WarningMessage.tsx
rename to src/pages/shared/components/WarningMessage.tsx
diff --git a/src/popup/components/ui/Button.tsx b/src/pages/shared/components/ui/Button.tsx
similarity index 96%
rename from src/popup/components/ui/Button.tsx
rename to src/pages/shared/components/ui/Button.tsx
index bc21e6f..2965a5e 100644
--- a/src/popup/components/ui/Button.tsx
+++ b/src/pages/shared/components/ui/Button.tsx
@@ -1,7 +1,7 @@
-import { type VariantProps, cva } from 'class-variance-authority';
 import React, { forwardRef } from 'react';
+import { type VariantProps, cva } from 'class-variance-authority';
 
-import { LoadingSpinner } from '@/popup/components/LoadingSpinner';
+import { LoadingSpinner } from '@/pages/shared/components/LoadingSpinner';
 import { cn } from '@/shared/helpers';
 
 const buttonVariants = cva(
diff --git a/src/popup/components/ui/Code.tsx b/src/pages/shared/components/ui/Code.tsx
similarity index 100%
rename from src/popup/components/ui/Code.tsx
rename to src/pages/shared/components/ui/Code.tsx
diff --git a/src/popup/components/ui/Input.tsx b/src/pages/shared/components/ui/Input.tsx
similarity index 97%
rename from src/popup/components/ui/Input.tsx
rename to src/pages/shared/components/ui/Input.tsx
index 30427f5..bd14fff 100644
--- a/src/popup/components/ui/Input.tsx
+++ b/src/pages/shared/components/ui/Input.tsx
@@ -1,7 +1,7 @@
 import { type VariantProps, cva } from 'class-variance-authority';
 import React, { forwardRef } from 'react';
 import { cn } from '@/shared/helpers';
-import { Label } from '@/popup/components/ui/Label';
+import { Label } from '@/pages/shared/components/ui/Label';
 
 const inputVariants = cva(
   [
diff --git a/src/popup/components/ui/Label.tsx b/src/pages/shared/components/ui/Label.tsx
similarity index 54%
rename from src/popup/components/ui/Label.tsx
rename to src/pages/shared/components/ui/Label.tsx
index b17df2f..1a710f2 100644
--- a/src/popup/components/ui/Label.tsx
+++ b/src/pages/shared/components/ui/Label.tsx
@@ -1,5 +1,5 @@
+import React from 'react';
 import { type VariantProps, cva } from 'class-variance-authority';
-import React, { forwardRef } from 'react';
 
 import { cn } from '@/shared/helpers';
 
@@ -13,13 +13,12 @@ export interface LabelProps
   children: React.ReactNode;
 }
 
-export const Label = forwardRef<HTMLLabelElement, LabelProps>(function Label(
-  { className, children, ...props },
-  ref,
-) {
-  return (
-    <label ref={ref} className={cn(labelVariants(), className)} {...props}>
-      {children}
-    </label>
-  );
-});
+export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
+  function Label({ className, children, ...props }, ref) {
+    return (
+      <label ref={ref} className={cn(labelVariants(), className)} {...props}>
+        {children}
+      </label>
+    );
+  },
+);
diff --git a/src/pages/shared/lib/context.tsx b/src/pages/shared/lib/context.tsx
new file mode 100644
index 0000000..346bd94
--- /dev/null
+++ b/src/pages/shared/lib/context.tsx
@@ -0,0 +1,43 @@
+import React, { type PropsWithChildren } from 'react';
+import type { Browser } from 'webextension-polyfill';
+import {
+  tFactory,
+  type ErrorWithKeyLike,
+  type Translation,
+} from '@/shared/helpers';
+
+// #region Browser
+const BrowserContext = React.createContext<Browser>({} as Browser);
+
+export const useBrowser = () => React.useContext(BrowserContext);
+
+export const BrowserContextProvider = ({
+  browser,
+  children,
+}: PropsWithChildren<{ browser: Browser }>) => {
+  return (
+    <BrowserContext.Provider value={browser}>
+      {children}
+    </BrowserContext.Provider>
+  );
+};
+// #endregion
+
+// #region Translation
+const TranslationContext = React.createContext<Translation>(
+  (v: string | ErrorWithKeyLike) => (typeof v === 'string' ? v : v.key),
+);
+
+export const useTranslation = () => React.useContext(TranslationContext);
+
+export const TranslationContextProvider = ({ children }: PropsWithChildren) => {
+  const browser = useBrowser();
+  const t = tFactory(browser);
+
+  return (
+    <TranslationContext.Provider value={t}>
+      {children}
+    </TranslationContext.Provider>
+  );
+};
+// #endregion
diff --git a/src/popup/lib/utils.ts b/src/pages/shared/lib/utils.ts
similarity index 81%
rename from src/popup/lib/utils.ts
rename to src/pages/shared/lib/utils.ts
index b5e2e4a..e599d65 100644
--- a/src/popup/lib/utils.ts
+++ b/src/pages/shared/lib/utils.ts
@@ -11,19 +11,6 @@ export const getCurrencySymbol = (assetCode: string): string => {
     .trim();
 };
 
-export const transformBalance = (
-  amount: string | bigint,
-  scale: number,
-): string => {
-  const value = BigInt(amount);
-  const divisor = BigInt(10 ** scale);
-
-  const integerPart = (value / divisor).toString();
-  const fractionalPart = (value % divisor).toString().padStart(scale, '0');
-
-  return `${integerPart}.${fractionalPart}`;
-};
-
 export function charIsNumber(char?: string) {
   return !!(char || '').match(/\d|\./);
 }
diff --git a/src/shared/helpers.ts b/src/shared/helpers.ts
index 1364189..a4b1bed 100644
--- a/src/shared/helpers.ts
+++ b/src/shared/helpers.ts
@@ -273,6 +273,19 @@ export function convert(value: bigint, source: number, target: number) {
   return value / BigInt(Math.pow(10, -scaleDiff));
 }
 
+export const transformBalance = (
+  amount: string | bigint,
+  scale: number,
+): string => {
+  const value = BigInt(amount);
+  const divisor = BigInt(10 ** scale);
+
+  const integerPart = (value / divisor).toString();
+  const fractionalPart = (value % divisor).toString().padStart(scale, '0');
+
+  return `${integerPart}.${fractionalPart}`;
+};
+
 export function bigIntMax<T extends bigint | AmountValue>(a: T, b: T): T {
   return BigInt(a) > BigInt(b) ? a : b;
 }
diff --git a/tsconfig.json b/tsconfig.json
index 8afd23d..53b31aa 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,7 +18,7 @@
     "noEmit": true,
     "paths": {
       "@/shared/*": ["./shared/*"],
-      "@/popup/*": ["./popup/*"],
+      "@/popup/*": ["./pages/popup/*"],
       "@/pages/*": ["./pages/*"],
       "@/background/*": ["./background/*"],
       "@/content/*": ["./content/*"],


Test patch:
diff --git a/src/content/keyAutoAdd/testWallet.ts b/src/content/keyAutoAdd/testWallet.ts
index 3a5f265..d726d1e 100644
--- a/src/content/keyAutoAdd/testWallet.ts
+++ b/src/content/keyAutoAdd/testWallet.ts
@@ -6,7 +6,7 @@ import {
   type StepRun as Run,
 } from './lib/keyAutoAdd';
 import { isTimedOut, waitForURL } from './lib/helpers';
-import { toWalletAddressUrl } from '@/popup/lib/utils';
+import { toWalletAddressUrl } from '@/pages/shared/lib/utils';
 
 // region: Steps
 type Account = {
diff --git a/src/popup/components/ui/RadioGroup.tsx b/src/pages/shared/components/ui/RadioGroup.tsx
similarity index 100%
rename from src/popup/components/ui/RadioGroup.tsx
rename to src/pages/shared/components/ui/RadioGroup.tsx
diff --git a/src/popup/components/ui/Slider.tsx b/src/pages/shared/components/ui/Slider.tsx
similarity index 100%
rename from src/popup/components/ui/Slider.tsx
rename to src/pages/shared/components/ui/Slider.tsx
diff --git a/src/popup/components/ui/Switch.tsx b/src/pages/shared/components/ui/Switch.tsx
similarity index 100%
rename from src/popup/components/ui/Switch.tsx
rename to src/pages/shared/components/ui/Switch.tsx
diff --git a/src/popup/components/ui/__tests__/Button.test.tsx b/src/pages/shared/components/ui/__tests__/Button.test.tsx
similarity index 97%
rename from src/popup/components/ui/__tests__/Button.test.tsx
rename to src/pages/shared/components/ui/__tests__/Button.test.tsx
index dab04db..356bd3f 100644
--- a/src/popup/components/ui/__tests__/Button.test.tsx
+++ b/src/pages/shared/components/ui/__tests__/Button.test.tsx
@@ -1,7 +1,7 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { Button } from '@/popup/components/ui/Button';
+import { Button } from '@/pages/shared/components/ui/Button';
 
 describe('Button', () => {
   it('should render a button with the `aria-label` attribute', () => {
diff --git a/src/popup/components/ui/__tests__/Code.test.tsx b/src/pages/shared/components/ui/__tests__/Code.test.tsx
similarity index 100%
rename from src/popup/components/ui/__tests__/Code.test.tsx
rename to src/pages/shared/components/ui/__tests__/Code.test.tsx
diff --git a/src/popup/components/ui/__tests__/Input.test.tsx b/src/pages/shared/components/ui/__tests__/Input.test.tsx
similarity index 97%
rename from src/popup/components/ui/__tests__/Input.test.tsx
rename to src/pages/shared/components/ui/__tests__/Input.test.tsx
index 5344c14..41b1f13 100644
--- a/src/popup/components/ui/__tests__/Input.test.tsx
+++ b/src/pages/shared/components/ui/__tests__/Input.test.tsx
@@ -1,7 +1,7 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { Input } from '@/popup/components/ui/Input';
+import { Input } from '@/pages/shared/components/ui/Input';
 
 describe('Input', () => {
   it('should default to `type="text"`', () => {
diff --git a/src/popup/components/ui/__tests__/RadioGroup.test.tsx b/src/pages/shared/components/ui/__tests__/RadioGroup.test.tsx
similarity index 100%
rename from src/popup/components/ui/__tests__/RadioGroup.test.tsx
rename to src/pages/shared/components/ui/__tests__/RadioGroup.test.tsx
diff --git a/src/popup/components/ui/__tests__/Slider.test.tsx b/src/pages/shared/components/ui/__tests__/Slider.test.tsx
similarity index 100%
rename from src/popup/components/ui/__tests__/Slider.test.tsx
rename to src/pages/shared/components/ui/__tests__/Slider.test.tsx
diff --git a/src/popup/components/ui/__tests__/Switch.test.tsx b/src/pages/shared/components/ui/__tests__/Switch.test.tsx
similarity index 97%
rename from src/popup/components/ui/__tests__/Switch.test.tsx
rename to src/pages/shared/components/ui/__tests__/Switch.test.tsx
index b796d7e..1bdeab2 100644
--- a/src/popup/components/ui/__tests__/Switch.test.tsx
+++ b/src/pages/shared/components/ui/__tests__/Switch.test.tsx
@@ -3,7 +3,7 @@ import '@testing-library/jest-dom';
 import { fireEvent, render, screen } from '@testing-library/react';
 import React from 'react';
 
-import { Switch } from '@/popup/components/ui/Switch';
+import { Switch } from '@/pages/shared/components/ui/Switch';
 
 describe('Switch', () => {
   it('renders without crashing', () => {
diff --git a/src/popup/lib/hooks.test.tsx b/src/pages/shared/lib/hooks.test.tsx
similarity index 100%
rename from src/popup/lib/hooks.test.tsx
rename to src/pages/shared/lib/hooks.test.tsx
diff --git a/src/popup/lib/hooks.ts b/src/pages/shared/lib/hooks.ts
similarity index 100%
rename from src/popup/lib/hooks.ts
rename to src/pages/shared/lib/hooks.ts
diff --git a/src/popup/lib/react-scan.ts b/src/pages/shared/lib/react-scan.ts
similarity index 100%
rename from src/popup/lib/react-scan.ts
rename to src/pages/shared/lib/react-scan.ts
diff --git a/src/popup/lib/utils.test.ts b/src/pages/shared/lib/utils.test.ts
similarity index 95%
rename from src/popup/lib/utils.test.ts
rename to src/pages/shared/lib/utils.test.ts
index 3d91c57..69b53ca 100644
--- a/src/popup/lib/utils.test.ts
+++ b/src/pages/shared/lib/utils.test.ts
@@ -1,4 +1,4 @@
-import { formatNumber, toWalletAddressUrl } from './utils';
+import { formatNumber, toWalletAddressUrl } from '@/pages/shared/lib/utils';
 
 describe('formatNumber', () => {
   it('should display right format for integers', () => {