diff --git a/package-lock.json b/package-lock.json index d6c0ab6..32746f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@cosmjs/stargate": "^0.32.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@moonpay/moonpay-react": "^1.10.6", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^2.0.0", "bip32": "^4.0.0", @@ -51,6 +52,10 @@ "vite": "^7.3.1", "vite-plugin-node-polyfills": "^0.25.0", "vite-plugin-web-extension": "^4.5.0" + }, + "engines": { + "_comment": "Node version defined in .nvmrc (single source of truth)", + "node": ">=22.12.0" } }, "node_modules/@adobe/css-tools": { @@ -3125,6 +3130,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@moonpay/moonpay-react": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@moonpay/moonpay-react/-/moonpay-react-1.10.6.tgz", + "integrity": "sha512-o+fa3FDKITbOTlkKToT49865ZGKkoGT1/eI0XJnYh9KSNMz3oo2PuwDKJHqLLZ5qE642ZAebksWdEElRySD43Q==", + "license": "MIT", + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", diff --git a/package.json b/package.json index 8a73fac..dbbddf2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@cosmjs/stargate": "^0.32.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@moonpay/moonpay-react": "^1.10.6", "@noble/hashes": "^1.3.3", "@noble/secp256k1": "^2.0.0", "bip32": "^4.0.0", diff --git a/src/popup/components/MoonPaySDKWidget.tsx b/src/popup/components/MoonPaySDKWidget.tsx new file mode 100644 index 0000000..fefc492 --- /dev/null +++ b/src/popup/components/MoonPaySDKWidget.tsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { Box, Text, Button, VStack } from '@chakra-ui/react'; +import { MoonPayProvider, MoonPayBuyWidget, MoonPaySellWidget } from '@moonpay/moonpay-react'; + +interface MoonPaySDKWidgetProps { + flow: 'buy' | 'sell'; + cryptoCode?: string; + walletAddress?: string; + amount?: string; + colorCode?: string; + onClose?: () => void; +} + +// MoonPay API Key - only from environment variable +const MOONPAY_API_KEY = import.meta.env.VITE_MOONPAY_API_KEY || ''; + +/** + * MoonPay SDK Widget for Web App + * + * Uses the official @moonpay/moonpay-react SDK with overlay variant. + * This opens as a popup/modal over the app. + * This component is ONLY used in the web app build, NOT in the extension. + */ +const MoonPaySDKWidget: React.FC = ({ + flow, + cryptoCode = 'usdc_base', + walletAddress, + colorCode = '#3182CE', + onClose, +}) => { + const [showWidget, setShowWidget] = useState(false); + const [showMaintenance, setShowMaintenance] = useState(false); + + const handleWidgetClose = () => { + setShowWidget(false); + onClose?.(); + }; + + const handleOpenWidget = () => { + if (!MOONPAY_API_KEY) { + setShowMaintenance(true); + return; + } + setShowWidget(true); + }; + + const isBuy = flow === 'buy'; + const colorScheme = isBuy ? 'teal' : 'orange'; + + // Show maintenance message if no API key + if (showMaintenance) { + return ( + + + + 🔧 + + + Under Maintenance + + + {isBuy ? 'Buy' : 'Sell'} functionality is temporarily unavailable. Please check back + later. + + + We apologize for any inconvenience. + + + + + ); + } + + // Common configuration - using overlay variant for popup experience + const commonConfig = { + apiKey: MOONPAY_API_KEY, + variant: 'overlay' as const, + colorCode: colorCode.replace('#', ''), + language: 'en', + }; + + // Buy widget configuration + const buyConfig = { + ...commonConfig, + defaultCurrencyCode: cryptoCode, + walletAddress: walletAddress || undefined, + baseCurrencyCode: 'usd', + }; + + // Sell widget configuration + const sellConfig = { + ...commonConfig, + defaultBaseCurrencyCode: cryptoCode, + refundWalletAddress: walletAddress || undefined, + quoteCurrencyCode: 'usd', + }; + + return ( + + + + + {isBuy + ? 'Buy crypto with credit card, debit card, or bank transfer.' + : 'Sell crypto and receive funds to your bank account.'} + + + Powered by MoonPay • Secure & Fast + + + + + + {/* Render the widget (it shows as overlay when visible) */} + {showWidget && MOONPAY_API_KEY && ( + <> + {isBuy ? ( + + ) : ( + + )} + + )} + + + ); +}; + +export default MoonPaySDKWidget; diff --git a/src/popup/components/MoonPayWidget.tsx b/src/popup/components/MoonPayWidget.tsx index 418165d..2e6a9d1 100644 --- a/src/popup/components/MoonPayWidget.tsx +++ b/src/popup/components/MoonPayWidget.tsx @@ -1,5 +1,8 @@ -import React, { useState } from 'react'; -import { Box, Spinner, Text } from '@chakra-ui/react'; +import React, { useState, lazy, Suspense } from 'react'; +import { Box, Spinner, Text, VStack, Button } from '@chakra-ui/react'; + +// Lazy load SDK widget only for web builds +const MoonPaySDKWidgetLazy = __IS_WEB_BUILD__ ? lazy(() => import('./MoonPaySDKWidget')) : null; interface MoonPayWidgetProps { flow: 'buy' | 'sell'; @@ -7,33 +10,107 @@ interface MoonPayWidgetProps { walletAddress: string; amount?: string; colorCode?: string; + onClose?: () => void; } -// MoonPay API Key -const MOONPAY_API_KEY = - import.meta.env.VITE_MOONPAY_API_KEY || 'pk_test_pKULLlqQbOAEd7usXz7yUiVCc8yNBNGY'; +// MoonPay API Key - only from environment variable +const MOONPAY_API_KEY = import.meta.env.VITE_MOONPAY_API_KEY || ''; /** - * MoonPay Widget using iframe embedding - * - * Note: Chrome extension Manifest V3 prohibits loading remote scripts, - * so we use iframe-based embedding instead of the MoonPay Web SDK. + * MoonPay Widget - Universal Component + * + * - Web App: Uses @moonpay/moonpay-react SDK for embedded widget experience + * - Extension: Not used (Deposit/Withdraw pages open MoonPay in new tab instead) + * + * Note: This component includes iframe fallback for extension builds, but is + * currently only utilized by the web app. Extension builds use direct tab navigation + * for MoonPay transactions to avoid iframe CSP restrictions. */ -const MoonPayWidget: React.FC = ({ +const MoonPayWidget: React.FC = (props) => { + const { flow, cryptoCode, walletAddress, amount, colorCode = '#3182CE', onClose } = props; + + // For web builds, use the SDK widget with lazy loading + if (__IS_WEB_BUILD__ && MoonPaySDKWidgetLazy) { + return ( + + + + Loading MoonPay SDK... + + + } + > + + + ); + } + + // Fallback: use iframe-based embedding (not currently used in extension builds) + return ; +}; + +/** + * Iframe-based MoonPay Widget for Extension + */ +const MoonPayIframeWidget: React.FC = ({ flow, cryptoCode, walletAddress, amount, colorCode = '#3182CE', + onClose, }) => { const [loading, setLoading] = useState(true); + // Show maintenance message if no API key + if (!MOONPAY_API_KEY) { + const isBuy = flow === 'buy'; + return ( + + + + 🔧 + + + Under Maintenance + + + {isBuy ? 'Buy' : 'Sell'} functionality is temporarily unavailable. Please check back + later. + + + We apologize for any inconvenience. + + + {onClose && ( + + )} + + ); + } + // Build the MoonPay iframe URL const buildIframeUrl = (): string => { - const baseUrl = flow === 'buy' - ? 'https://buy.moonpay.com' - : 'https://sell.moonpay.com'; - + const baseUrl = flow === 'buy' ? 'https://buy.moonpay.com' : 'https://sell.moonpay.com'; + const params = new URLSearchParams({ apiKey: MOONPAY_API_KEY, theme: 'dark', diff --git a/src/popup/pages/Deposit.tsx b/src/popup/pages/Deposit.tsx index aa90741..905117d 100644 --- a/src/popup/pages/Deposit.tsx +++ b/src/popup/pages/Deposit.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, VStack, @@ -17,10 +17,10 @@ import browser from 'webextension-polyfill'; import { useWalletStore } from '@/store/walletStore'; import { useNetworkStore } from '@/store/networkStore'; import { networkRegistry } from '@/lib/networks'; +import MoonPayWidget from '@/popup/components/MoonPayWidget'; // MoonPay supported cryptocurrencies mapping const MOONPAY_CRYPTO_CODES: Record = { - // EVM chains - Base USDC is the primary option 'base-mainnet': 'usdc_base', 'ethereum-mainnet': 'eth', 'bnb-mainnet': 'bnb_bsc', @@ -28,22 +28,19 @@ const MOONPAY_CRYPTO_CODES: Record = { 'arbitrum-mainnet': 'eth_arbitrum', 'optimism-mainnet': 'eth_optimism', 'avalanche-mainnet': 'avax_cchain', - // Cosmos chains 'cosmoshub-4': 'atom', 'osmosis-1': 'osmo', - 'beezee-1': '', // Not supported - 'atomone-1': '', // Not supported - // UTXO chains + 'beezee-1': '', + 'atomone-1': '', 'bitcoin-mainnet': 'btc', 'litecoin-mainnet': 'ltc', 'dogecoin-mainnet': 'doge', 'zcash-mainnet': 'zec', - 'flux-mainnet': '', // Not supported - 'ravencoin-mainnet': '', // Not supported - 'bitcoinz-mainnet': '', // Not supported + 'flux-mainnet': '', + 'ravencoin-mainnet': '', + 'bitcoinz-mainnet': '', }; -// Display names for MoonPay assets (what users are actually buying) const MOONPAY_DISPLAY_NAMES: Record = { 'base-mainnet': 'USDC (Base)', 'ethereum-mainnet': 'ETH (Ethereum)', @@ -60,17 +57,72 @@ const MOONPAY_DISPLAY_NAMES: Record = { 'zcash-mainnet': 'ZEC (Zcash)', }; -// Default network for deposits const DEFAULT_DEPOSIT_NETWORK = 'base-mainnet'; - -// MoonPay API Key (must be provided via VITE_MOONPAY_API_KEY; empty string disables MoonPay) const MOONPAY_API_KEY = import.meta.env.VITE_MOONPAY_API_KEY ?? ''; interface DepositProps { onBack: () => void; } +/** + * Deposit Page + * - Web App: Shows simplified MoonPay SDK widget with overlay popup + * - Extension: Shows network selection and opens MoonPay in new tab + */ const Deposit: React.FC = ({ onBack }) => { + // For web app, render simplified view with MoonPay SDK + if (__IS_WEB_BUILD__) { + return ; + } + + // Extension version with full network selection + return ; +}; + +/** + * Simplified Deposit for Web App - Just MoonPay widget + */ +const DepositWeb: React.FC = ({ onBack }) => { + return ( + + + } + variant="ghost" + color="gray.400" + _hover={{ color: 'white', bg: 'whiteAlpha.100' }} + onClick={onBack} + size="sm" + /> + + Deposit + + + Buy Crypto + + + + + + + + + Powered by{' '} + + MoonPay + + + + + + ); +}; + +/** + * Full Deposit for Extension - Network selection + external MoonPay tab + */ +const DepositExtension: React.FC = ({ onBack }) => { const { selectedAccount, getAddressForChain, getBitcoinAddress, getEvmAddress } = useWalletStore(); const { loadPreferences, isLoaded: networkPrefsLoaded, isNetworkEnabled } = useNetworkStore(); @@ -80,20 +132,17 @@ const Deposit: React.FC = ({ onBack }) => { const [walletAddress, setWalletAddress] = useState(''); const [loadingAddress, setLoadingAddress] = useState(false); - // Load network preferences on mount useEffect(() => { if (!networkPrefsLoaded) { loadPreferences(); } }, [networkPrefsLoaded, loadPreferences]); - // Get supported networks for MoonPay - filter by user preferences const supportedNetworks = networkRegistry .getAll() .filter((n) => isNetworkEnabled(n.id)) .filter((n) => MOONPAY_CRYPTO_CODES[n.id] && MOONPAY_CRYPTO_CODES[n.id] !== ''); - // Ensure selected network is valid; if disabled, select the first available useEffect(() => { if ( networkPrefsLoaded && @@ -104,7 +153,6 @@ const Deposit: React.FC = ({ onBack }) => { } }, [networkPrefsLoaded, supportedNetworks, selectedNetwork]); - // Get current network config const networkConfig = networkRegistry.get(selectedNetwork); const cryptoCode = MOONPAY_CRYPTO_CODES[selectedNetwork] || ''; const isSupported = cryptoCode !== ''; @@ -112,7 +160,6 @@ const Deposit: React.FC = ({ onBack }) => { MOONPAY_DISPLAY_NAMES[selectedNetwork] || (networkConfig ? `${networkConfig.symbol} (${networkConfig.name})` : 'crypto'); - // Fetch wallet address when network changes useEffect(() => { const fetchAddress = async () => { if (!networkConfig || !selectedAccount) { @@ -149,7 +196,6 @@ const Deposit: React.FC = ({ onBack }) => { getEvmAddress, ]); - // Build MoonPay URL for external link fallback const buildMoonPayUrl = () => { const baseUrl = 'https://buy.moonpay.com'; const params = new URLSearchParams({ @@ -180,7 +226,6 @@ const Deposit: React.FC = ({ onBack }) => { return ( - {/* Header */} = ({ onBack }) => { - {/* Loading State */} {!networkPrefsLoaded ? ( @@ -210,7 +254,6 @@ const Deposit: React.FC = ({ onBack }) => { ) : ( <> - {/* Network Selection */} Select Network @@ -231,7 +274,6 @@ const Deposit: React.FC = ({ onBack }) => { - {/* Wallet Address Display */} @@ -263,7 +305,6 @@ const Deposit: React.FC = ({ onBack }) => { - {/* MoonPay Action */} {!isSupported ? ( = ({ onBack }) => { )} - {/* Footer */} Powered by{' '} diff --git a/src/popup/pages/Withdraw.tsx b/src/popup/pages/Withdraw.tsx index 0eb5d44..3704868 100644 --- a/src/popup/pages/Withdraw.tsx +++ b/src/popup/pages/Withdraw.tsx @@ -17,6 +17,7 @@ import browser from 'webextension-polyfill'; import { useWalletStore } from '@/store/walletStore'; import { useNetworkStore } from '@/store/networkStore'; import { networkRegistry } from '@/lib/networks'; +import MoonPayWidget from '@/popup/components/MoonPayWidget'; // MoonPay supported cryptocurrencies for selling (off-ramp) const MOONPAY_SELL_CODES: Record = { @@ -44,7 +45,67 @@ interface WithdrawProps { onBack: () => void; } +/** + * Withdraw Page + * - Web App: Shows simplified MoonPay SDK widget with overlay popup + * - Extension: Shows network selection and opens MoonPay in new tab + */ const Withdraw: React.FC = ({ onBack }) => { + if (__IS_WEB_BUILD__) { + return ; + } + return ; +}; + +/** + * Simplified Withdraw for Web App - Just MoonPay widget + */ +const WithdrawWeb: React.FC = ({ onBack }) => { + return ( + + + } + variant="ghost" + color="gray.400" + _hover={{ color: 'white', bg: 'whiteAlpha.100' }} + onClick={onBack} + size="sm" + /> + + Withdraw + + + Sell Crypto + + + + + + + + + Powered by{' '} + + MoonPay + + + + + + ); +}; + +/** + * Full Withdraw for Extension - Network selection + external MoonPay tab + */ +const WithdrawExtension: React.FC = ({ onBack }) => { const { selectedAccount, getAddressForChain, getBitcoinAddress, getEvmAddress } = useWalletStore(); const { loadPreferences, getEnabledNetworks, isLoaded: networkPrefsLoaded } = useNetworkStore(); @@ -54,17 +115,14 @@ const Withdraw: React.FC = ({ onBack }) => { const [walletAddress, setWalletAddress] = useState(''); const [loadingAddress, setLoadingAddress] = useState(false); - // Load network preferences on mount useEffect(() => { loadPreferences(); }, [loadPreferences]); - // Get supported networks for MoonPay sell from user preferences const supportedNetworks = getEnabledNetworks().filter( (n) => MOONPAY_SELL_CODES[n.id] && MOONPAY_SELL_CODES[n.id] !== '' ); - // Validate and update selected network when preferences load or supported networks change useEffect(() => { if (networkPrefsLoaded && supportedNetworks.length > 0) { const isSelectedNetworkAvailable = supportedNetworks.some((n) => n.id === selectedNetwork); @@ -74,12 +132,10 @@ const Withdraw: React.FC = ({ onBack }) => { } }, [networkPrefsLoaded, supportedNetworks, selectedNetwork]); - // Get current network config const networkConfig = networkRegistry.get(selectedNetwork); const cryptoCode = MOONPAY_SELL_CODES[selectedNetwork] || ''; const isSupported = cryptoCode !== ''; - // Fetch wallet address when network changes useEffect(() => { const fetchAddress = async () => { if (!networkConfig || !selectedAccount) { @@ -116,7 +172,6 @@ const Withdraw: React.FC = ({ onBack }) => { getEvmAddress, ]); - // Build MoonPay Sell URL for external link fallback const buildMoonPaySellUrl = () => { const baseUrl = 'https://sell.moonpay.com'; const params = new URLSearchParams({ diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 87f4982..7f89e2e 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -8,3 +8,11 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +/** + * Build-time constant to distinguish web app from extension. + * Set via Vite's define config in vite.config.web.ts and vite.config.js. + * - true in web build (vite.config.web.ts) + * - false in extension build (vite.config.js) + */ +declare const __IS_WEB_BUILD__: boolean; diff --git a/vite.config.js b/vite.config.js index 73ed3f7..ad95f22 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,6 +9,10 @@ export default defineConfig({ '@': resolve(__dirname, './src'), }, }, + define: { + // Build-time constant to distinguish extension from web app + __IS_WEB_BUILD__: JSON.stringify(false), + }, build: { outDir: 'dist', emptyOutDir: true, diff --git a/vite.config.web.ts b/vite.config.web.ts index 819dd78..4a08b7a 100644 --- a/vite.config.web.ts +++ b/vite.config.web.ts @@ -27,6 +27,8 @@ export default defineConfig({ define: { global: 'globalThis', 'process.env': {}, + // Build-time constant to distinguish web app from extension + __IS_WEB_BUILD__: JSON.stringify(true), }, optimizeDeps: { esbuildOptions: {