diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json --- a/cashtab/package-lock.json +++ b/cashtab/package-lock.json @@ -45,7 +45,9 @@ "@types/lodash.debounce": "^4.0.9", "@types/randombytes": "^2.0.3", "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "@types/styled-components": "^5.1.34", + "@types/webpack-env": "^1.18.5", "@types/wif": "^2.0.5", "assert": "^2.0.0", "babel-jest": "^29.7.0", @@ -110,7 +112,7 @@ } }, "../modules/chronik-client": { - "version": "1.4.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "@types/ws": "^8.2.1", @@ -2858,7 +2860,7 @@ } }, "../modules/ecash-agora": { - "version": "0.1.1", + "version": "0.2.0", "license": "MIT", "dependencies": { "chronik-client": "file:../chronik-client", @@ -6890,7 +6892,7 @@ } }, "../modules/ecash-lib": { - "version": "0.2.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "ecashaddrjs": "file:../ecashaddrjs" @@ -24077,6 +24079,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "dev": true, @@ -24155,6 +24167,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/webpack-env": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.5.tgz", + "integrity": "sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wif": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/wif/-/wif-2.0.5.tgz", diff --git a/cashtab/package.json b/cashtab/package.json --- a/cashtab/package.json +++ b/cashtab/package.json @@ -62,7 +62,9 @@ "@types/lodash.debounce": "^4.0.9", "@types/randombytes": "^2.0.3", "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "@types/styled-components": "^5.1.34", + "@types/webpack-env": "^1.18.5", "@types/wif": "^2.0.5", "assert": "^2.0.0", "babel-jest": "^29.7.0", diff --git a/cashtab/src/components/Common/GoogleAnalytics.js b/cashtab/src/components/Common/GoogleAnalytics.js deleted file mode 100644 --- a/cashtab/src/components/Common/GoogleAnalytics.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2024 The Bitcoin developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -import { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; - -let ReactGA; -if (process.env.REACT_APP_BUILD_ENV !== 'extension') { - ReactGA = require('react-ga'); -} - -const RouteTracker = () => { - const location = useLocation(); - useEffect(() => { - ReactGA.pageview(location.pathname + location.search); - }, [location]); -}; - -const init = - process.env.REACT_APP_BUILD_ENV !== 'extension' - ? () => { - const isGAEnabled = process.env.NODE_ENV === 'production'; - if (isGAEnabled) { - ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS); - } - - return isGAEnabled; - } - : // We return a new function if we are building the extension, because - // in this case ReactGA is undefined and will not have an initialize method - () => { - return false; - }; - -export const Event = - process.env.REACT_APP_BUILD_ENV !== 'extension' - ? // If you are not building the extension, export GA event tracking function - (category, action, label) => { - ReactGA.event({ - category: category, - action: action, - label: label, - }); - } - : // If you are building the extension, export function that does nothing - // Note: it's not practical to conditionally remove calls to this function from all screens - // So, more practical to just define it as a do-nothing function for the extension - () => undefined; - -export default process.env.REACT_APP_BUILD_ENV !== 'extension' - ? { - RouteTracker, - init, - } - : { - RouteTracker: () => undefined, - init, - }; diff --git a/cashtab/src/components/Common/GoogleAnalytics.ts b/cashtab/src/components/Common/GoogleAnalytics.ts new file mode 100644 --- /dev/null +++ b/cashtab/src/components/Common/GoogleAnalytics.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +interface ReactGA { + pageview: (path: string) => void; + initialize: (trackingID: string) => void; + event: (event: { + category: string; + action: string; + label?: string; + }) => void; +} + +let ReactGA: ReactGA | undefined; +if (process.env.REACT_APP_BUILD_ENV !== 'extension') { + ReactGA = require('react-ga'); +} + +const RouteTracker: React.FC | (() => undefined) = + typeof ReactGA === 'undefined' + ? () => undefined + : () => { + const location = useLocation(); + useEffect(() => { + ReactGA.pageview(location.pathname + location.search); + }, [location]); + return null; + }; + +const init = + typeof ReactGA === 'undefined' + ? // We return false here to prevent rendering route tracker in non-prod and extension + // see top level index.tsx + // in this case ReactGA is undefined and will not have an initialize method + () => { + return false; + } + : () => { + const isGAEnabled = process.env.NODE_ENV === 'production'; + if (isGAEnabled) { + ReactGA.initialize( + process.env.REACT_APP_GOOGLE_ANALYTICS as string, + ); + } + + return isGAEnabled; + }; + +export const Event = + typeof ReactGA === 'undefined' + ? // If you are building the extension, export function that does nothing + // Note: it's not practical to conditionally remove calls to this function from all screens + // So, more practical to just define it as a do-nothing function for the extension + () => undefined + : // If you are not building the extension, export GA event tracking function + (category: string, action: string, label: string) => { + ReactGA.event({ + category, + action, + label, + }); + }; + +export default { + RouteTracker, + init, +}; diff --git a/cashtab/src/index.js b/cashtab/src/index.js deleted file mode 100644 --- a/cashtab/src/index.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2024 The Bitcoin developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from 'components/App/App'; -import { WalletProvider } from 'wallet/context'; -import { HashRouter as Router } from 'react-router-dom'; -import GA from 'components/Common/GoogleAnalytics'; -import { ChronikClient } from 'chronik-client'; -import { chronik as chronikConfig } from 'config/chronik'; -import { Ecc, initWasm } from 'ecash-lib'; -import { Agora } from 'ecash-agora'; - -// Initialize wasm (activate ecash-lib) at app startup -initWasm() - .then(() => { - // Initialize Ecc (used for signing txs) at app startup - const ecc = new Ecc(); - // Initialize chronik-client at app startup - const chronik = new ChronikClient(chronikConfig.urls); - // Initialize new Agora chronik wrapper at app startup - const agora = new Agora(chronik); - - const container = document.getElementById('root'); - const root = createRoot(container); - root.render( - <WalletProvider chronik={chronik} agora={agora} ecc={ecc}> - <Router> - {GA.init() && <GA.RouteTracker />} - <App /> - </Router> - </WalletProvider>, - ); - }) - .catch(console.error); - -if (module.hot) { - module.hot.accept(); -} diff --git a/cashtab/src/index.tsx b/cashtab/src/index.tsx new file mode 100644 --- /dev/null +++ b/cashtab/src/index.tsx @@ -0,0 +1,49 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import React, { useEffect } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from 'components/App/App'; +import { WalletProvider } from 'wallet/context'; +import { HashRouter as Router } from 'react-router-dom'; +import GA from 'components/Common/GoogleAnalytics'; +import { ChronikClient } from 'chronik-client'; +import { chronik as chronikConfig } from 'config/chronik'; +import { Ecc, initWasm } from 'ecash-lib'; +import { Agora } from 'ecash-agora'; + +const AppWrapper: React.FC = () => { + useEffect(() => { + const initializeApp = async () => { + await initWasm(); + }; + + initializeApp().catch(console.error); + }, []); + + return ( + <WalletProvider + chronik={new ChronikClient(chronikConfig.urls)} + agora={new Agora(new ChronikClient(chronikConfig.urls))} + ecc={new Ecc()} + > + <Router> + {GA.init() && <GA.RouteTracker />} + <App /> + </Router> + </WalletProvider> + ); +}; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render(<AppWrapper />); +} else { + console.error('Root container not found'); +} + +if (module.hot) { + module.hot.accept(); +} diff --git a/cashtab/src/wallet/context.js b/cashtab/src/wallet/context.js deleted file mode 100644 --- a/cashtab/src/wallet/context.js +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2024 The Bitcoin developers -// Distributed under the MIT software license, see the accompanying -// file COPYING or http://www.opensource.org/licenses/mit-license.php. - -import React from 'react'; -import PropTypes from 'prop-types'; -import useWallet from 'wallet/useWallet'; - -export const WalletContext = React.createContext(); - -export const WalletProvider = ({ chronik, agora, ecc, children }) => { - const wallet = useWallet(chronik, agora, ecc); - return ( - <WalletContext.Provider value={wallet}> - {children} - </WalletContext.Provider> - ); -}; - -WalletProvider.propTypes = { - chronik: PropTypes.object, - agora: PropTypes.object, - ecc: PropTypes.object, - children: PropTypes.node, -}; diff --git a/cashtab/src/wallet/context.tsx b/cashtab/src/wallet/context.tsx new file mode 100644 --- /dev/null +++ b/cashtab/src/wallet/context.tsx @@ -0,0 +1,74 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import React from 'react'; +import useWallet, { UseWalletReturnType } from 'wallet/useWallet'; +import { ChronikClient } from 'chronik-client'; +import { Agora } from 'ecash-agora'; +import { Ecc } from 'ecash-lib'; + +interface NullDefaultUseWalletReturnType { + chronik: undefined; + agora: undefined; + ecc: undefined; + chaintipBlockheight: undefined; + fiatPrice: undefined; + cashtabLoaded: undefined; + loading: undefined; + apiError: undefined; + refreshAliases: undefined; + aliases: undefined; + setAliases: undefined; + aliasServerError: undefined; + setAliasServerError: undefined; + aliasPrices: undefined; + setAliasPrices: undefined; + updateCashtabState: undefined; + processChronikWsMsg: undefined; + cashtabState: undefined; +} + +const nullDefaultUseWalletReturnType: NullDefaultUseWalletReturnType = { + chronik: undefined, + agora: undefined, + ecc: undefined, + chaintipBlockheight: undefined, + fiatPrice: undefined, + cashtabLoaded: undefined, + loading: undefined, + apiError: undefined, + refreshAliases: undefined, + aliases: undefined, + setAliases: undefined, + aliasServerError: undefined, + setAliasServerError: undefined, + aliasPrices: undefined, + setAliasPrices: undefined, + updateCashtabState: undefined, + processChronikWsMsg: undefined, + cashtabState: undefined, +}; + +export const WalletContext = React.createContext< + UseWalletReturnType | NullDefaultUseWalletReturnType +>(nullDefaultUseWalletReturnType); + +interface WalletProviderProps { + chronik: ChronikClient; + agora: Agora; + ecc: Ecc; + children: React.ReactNode; +} +export const WalletProvider: React.FC<WalletProviderProps> = ({ + chronik, + agora, + ecc, + children, +}) => { + return ( + <WalletContext.Provider value={useWallet(chronik, agora, ecc)}> + {children} + </WalletContext.Provider> + ); +}; diff --git a/cashtab/src/wallet/useWallet.ts b/cashtab/src/wallet/useWallet.ts --- a/cashtab/src/wallet/useWallet.ts +++ b/cashtab/src/wallet/useWallet.ts @@ -61,6 +61,42 @@ import CashtabCache from 'config/CashtabCache'; import { ToastIcon } from 'react-toastify/dist/types'; +export interface UseWalletReturnType { + chronik: ChronikClient; + agora: Agora; + ecc: Ecc; + chaintipBlockheight: number; + fiatPrice: number | null; + cashtabLoaded: boolean; + loading: boolean; + apiError: boolean; + refreshAliases: (address: string) => Promise<void>; + aliases: AddressAliasStatus; + setAliases: React.Dispatch<React.SetStateAction<AddressAliasStatus>>; + aliasServerError: false | string; + setAliasServerError: React.Dispatch<React.SetStateAction<false | string>>; + aliasPrices: null | AliasPrices; + setAliasPrices: React.Dispatch<React.SetStateAction<null | AliasPrices>>; + updateCashtabState: ( + key: string, + value: + | CashtabWallet[] + | CashtabCache + | CashtabContact[] + | CashtabSettings + | CashtabCacheJson + | StoredCashtabWallet[] + | (LegacyCashtabWallet | StoredCashtabWallet)[], + ) => Promise<boolean>; + processChronikWsMsg: ( + msg: WsMsgClient, + cashtabState: CashtabState, + fiatPrice: null | number, + aliasesEnabled: boolean, + ) => Promise<boolean>; + cashtabState: CashtabState; +} + const useWallet = (chronik: ChronikClient, agora: Agora, ecc: Ecc) => { const [cashtabLoaded, setCashtabLoaded] = useState<boolean>(false); const [ws, setWs] = useState<null | WsEndpoint>(null); @@ -1145,7 +1181,7 @@ updateCashtabState, processChronikWsMsg, cashtabState, - }; + } as UseWalletReturnType; }; export default useWallet;