UNPKG

react-reward-button

Version:

Drop-in React component that sends Ethereum token rewards and shows confetti animations. Built with wagmi, ethers, and ShadCN UI.

859 lines (843 loc) 45.2 kB
import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import * as React from 'react'; import React__default, { useState, useEffect } from 'react'; import { useAccount, useConnect, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'; import { useAppKit } from '@reown/appkit/react'; import { ethers } from 'ethers'; export { ethers } from 'ethers'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // packages/react/compose-refs/src/compose-refs.tsx function setRef(ref, value) { if (typeof ref === "function") { return ref(value); } else if (ref !== null && ref !== void 0) { ref.current = value; } } function composeRefs(...refs) { return (node) => { let hasCleanup = false; const cleanups = refs.map((ref) => { const cleanup = setRef(ref, node); if (!hasCleanup && typeof cleanup == "function") { hasCleanup = true; } return cleanup; }); if (hasCleanup) { return () => { for (let i = 0; i < cleanups.length; i++) { const cleanup = cleanups[i]; if (typeof cleanup == "function") { cleanup(); } else { setRef(refs[i], null); } } }; } }; } // src/slot.tsx // @__NO_SIDE_EFFECTS__ function createSlot(ownerName) { const SlotClone = /* @__PURE__ */ createSlotClone(ownerName); const Slot2 = React.forwardRef((props, forwardedRef) => { const { children, ...slotProps } = props; const childrenArray = React.Children.toArray(children); const slottable = childrenArray.find(isSlottable); if (slottable) { const newElement = slottable.props.children; const newChildren = childrenArray.map((child) => { if (child === slottable) { if (React.Children.count(newElement) > 1) return React.Children.only(null); return React.isValidElement(newElement) ? newElement.props.children : null; } else { return child; } }); return /* @__PURE__ */ jsx(SlotClone, { ...slotProps, ref: forwardedRef, children: React.isValidElement(newElement) ? React.cloneElement(newElement, void 0, newChildren) : null }); } return /* @__PURE__ */ jsx(SlotClone, { ...slotProps, ref: forwardedRef, children }); }); Slot2.displayName = `${ownerName}.Slot`; return Slot2; } var Slot = /* @__PURE__ */ createSlot("Slot"); // @__NO_SIDE_EFFECTS__ function createSlotClone(ownerName) { const SlotClone = React.forwardRef((props, forwardedRef) => { const { children, ...slotProps } = props; if (React.isValidElement(children)) { const childrenRef = getElementRef(children); const props2 = mergeProps(slotProps, children.props); if (children.type !== React.Fragment) { props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef; } return React.cloneElement(children, props2); } return React.Children.count(children) > 1 ? React.Children.only(null) : null; }); SlotClone.displayName = `${ownerName}.SlotClone`; return SlotClone; } var SLOTTABLE_IDENTIFIER = Symbol("radix.slottable"); function isSlottable(child) { return React.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER; } function mergeProps(slotProps, childProps) { const overrideProps = { ...childProps }; for (const propName in childProps) { const slotPropValue = slotProps[propName]; const childPropValue = childProps[propName]; const isHandler = /^on[A-Z]/.test(propName); if (isHandler) { if (slotPropValue && childPropValue) { overrideProps[propName] = (...args) => { const result = childPropValue(...args); slotPropValue(...args); return result; }; } else if (slotPropValue) { overrideProps[propName] = slotPropValue; } } else if (propName === "style") { overrideProps[propName] = { ...slotPropValue, ...childPropValue }; } else if (propName === "className") { overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" "); } } return { ...slotProps, ...overrideProps }; } function getElementRef(element) { let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get; let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.ref; } getter = Object.getOwnPropertyDescriptor(element, "ref")?.get; mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.props.ref; } return element.props.ref || element.ref; } function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n} /** * Utility function for merging class names using clsx * This is inspired by shadcn/ui's cn() utility */ function cn(...inputs) { return clsx(inputs); } const buttonVariants = { variant: { default: 'reward-button--default', secondary: 'reward-button--secondary', outline: 'reward-button--outline', ghost: 'reward-button--ghost', destructive: 'reward-button--destructive', }, size: { default: 'reward-button--size-default', sm: 'reward-button--size-sm', lg: 'reward-button--size-lg', icon: 'reward-button--size-icon', }, }; const Button = React__default.forwardRef((_a, ref) => { var { className, variant = 'default', size = 'default', asChild = false, isLoading = false, loadingText = 'Loading...', isSuccess = false, disabled, children } = _a, props = __rest(_a, ["className", "variant", "size", "asChild", "isLoading", "loadingText", "isSuccess", "disabled", "children"]); const Comp = asChild ? Slot : 'button'; return (jsxs(Comp, Object.assign({ className: cn('reward-button', buttonVariants.variant[variant], buttonVariants.size[size], isLoading && 'reward-button--loading', isSuccess && 'reward-button--success', (disabled || isLoading) && 'reward-button--disabled', className), ref: ref, disabled: disabled || isLoading }, props, { children: [isLoading ? (jsxs("span", { className: "reward-button__loading", children: [jsx("span", { className: "reward-button__spinner" }), loadingText] })) : (jsx("span", { className: "reward-button__text", children: children })), isSuccess && (jsxs(Fragment, { children: [jsx("span", { className: "reward-button__confetti" }), jsx("span", { className: "reward-button__confetti-2" }), jsx("span", { className: "reward-button__confetti-3" })] }))] }))); }); Button.displayName = 'Button'; // Standard ERC20 ABI - minimal interface needed for the RewardButton const ERC20_ABI = [ { "constant": true, "inputs": [], "name": "name", "outputs": [{ "name": "", "type": "string" }], "type": "function" }, { "constant": true, "inputs": [], "name": "symbol", "outputs": [{ "name": "", "type": "string" }], "type": "function" }, { "constant": true, "inputs": [], "name": "decimals", "outputs": [{ "name": "", "type": "uint8" }], "type": "function" }, { "constant": true, "inputs": [{ "name": "_owner", "type": "address" }], "name": "balanceOf", "outputs": [{ "name": "balance", "type": "uint256" }], "type": "function" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transfer", "outputs": [{ "name": "", "type": "bool" }], "type": "function" }, { "constant": false, "inputs": [ { "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "approve", "outputs": [{ "name": "", "type": "bool" }], "type": "function" }, { "constant": false, "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "transferFrom", "outputs": [{ "name": "", "type": "bool" }], "type": "function" }, { "constant": true, "inputs": [ { "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" } ], "name": "allowance", "outputs": [{ "name": "", "type": "uint256" }], "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "owner", "type": "address" }, { "indexed": true, "name": "spender", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" } ]; // Common token addresses on Ethereum mainnet (for reference) const COMMON_TOKENS = { USDC: '0xA0b86a33E6441b6b07c2fE4c2b4B8B1d8B7a0F4c', USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7', DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F', WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }; // Default button variant configurations const BUTTON_VARIANTS = { default: 'reward-button--default', secondary: 'reward-button--secondary', outline: 'reward-button--outline', ghost: 'reward-button--ghost', destructive: 'reward-button--destructive', }; // Default button size configurations const BUTTON_SIZES = { default: 'reward-button--size-default', sm: 'reward-button--size-sm', lg: 'reward-button--size-lg', icon: 'reward-button--size-icon', }; // Default button text const DEFAULT_BUTTON_TEXT = 'Claim Reward'; // CSS class names const CSS_CLASSES = { base: 'reward-button', loading: 'reward-button--loading', disabled: 'reward-button--disabled', loadingContent: 'reward-button__loading', spinner: 'reward-button__spinner', }; const RewardButton = (_a) => { var { tokenAddress, rewardAmount, recipientAddress, senderAddress, senderPrivateKey, rpcUrl, onReward, onRewardClaimed, onRewardFailed, onRewardStarted, showRewardAmount = true, tokenSymbol = 'TOKEN', requireConnection = true, loadingText = 'Claiming Reward...', userPaysGas = false, // false means sender pays gas (default) isLoading: externalIsLoading = false, children = 'Claim Reward', variant = 'default', size = 'default', disabled = false, onClick } = _a, buttonProps = __rest(_a, ["tokenAddress", "rewardAmount", "recipientAddress", "senderAddress", "senderPrivateKey", "rpcUrl", "onReward", "onRewardClaimed", "onRewardFailed", "onRewardStarted", "showRewardAmount", "tokenSymbol", "requireConnection", "loadingText", "userPaysGas", "isLoading", "children", "variant", "size", "disabled", "onClick"]); const [state, setState] = useState({ isLoading: false, error: null, tokenInfo: null, isSuccess: false, }); const [pendingReward, setPendingReward] = useState(false); const [hasClickedOnce, setHasClickedOnce] = useState(false); // Determine if this is a reward button or regular button const isRewardMode = Boolean(tokenAddress && rewardAmount); // Only use wagmi hooks if in reward mode const { address, isConnected } = useAccount(); useConnect(); // Reown AppKit for wallet selection modal const { open: openAppKit } = useAppKit(); // Check if connected - more robust validation const isWalletConnected = isConnected && address; // 🔒 SECURITY FIX: ALWAYS require wallet connection for ANY reward transfers const effectiveRequireConnection = isRewardMode ? true : requireConnection; // 🔒 SECURITY FIX: Never fall back to recipientAddress - always use connected wallet const targetAddress = isRewardMode ? (isWalletConnected ? address : null // Only use connected wallet, no fallback ) : undefined; // Reset hasClickedOnce when wallet gets connected useEffect(() => { if (isWalletConnected && hasClickedOnce) { console.log('💡 Wallet connected - resetting click state'); setHasClickedOnce(false); } }, [isWalletConnected, hasClickedOnce]); // Debug logging for recipient address selection useEffect(() => { if (isRewardMode) { console.log('🎯 Recipient Address Selection (Security Enhanced):'); console.log(' userPaysGas:', userPaysGas); console.log(' isConnected:', isConnected); console.log(' Connected wallet address:', address); console.log(' Wallet truly connected:', isWalletConnected); console.log(' Provided recipientAddress prop (IGNORED for security):', recipientAddress); console.log(' Final target address:', targetAddress); console.log(' Using connected wallet:', isWalletConnected ? '✅ YES' : '❌ NO - WILL PROMPT TO CONNECT'); console.log(' Effective require connection:', effectiveRequireConnection); if (!isWalletConnected) { console.log('🔒 SECURITY: Wallet not connected - transfers will be blocked'); } } }, [isConnected, address, recipientAddress, targetAddress, isRewardMode, isWalletConnected, userPaysGas, effectiveRequireConnection]); // Effect to handle wallet connection and auto-proceed with reward claim useEffect(() => { // More robust check for wallet connection const isCurrentlyConnected = isConnected && address; if (pendingReward && isCurrentlyConnected && isRewardMode) { console.log('Wallet connected! Auto-proceeding with reward claim...'); setPendingReward(false); // Small delay to let the UI update setTimeout(() => { handleClaimReward(); }, 500); } }, [isConnected, address, pendingReward, isRewardMode]); // Effect to clear pending reward if wallet gets disconnected useEffect(() => { const isCurrentlyConnected = isConnected && address; if (pendingReward && !isCurrentlyConnected && isRewardMode) { console.log('Wallet disconnected while pending reward. Clearing pending state...'); setPendingReward(false); } }, [isConnected, address, pendingReward, isRewardMode]); // Track transaction hash for confirmation const [txHash, setTxHash] = useState(); // Contract write hook for executing the transaction (only if in reward mode) const { writeContract: executeTransfer, isPending: isTransactionLoading } = useWriteContract({ mutation: { onSuccess: (hash) => { console.log('📤 Transaction submitted to mempool:', hash); console.log('⏳ Waiting for blockchain confirmation...'); setTxHash(hash); // Don't call success callback yet - wait for confirmation }, onError: (error) => { console.error('❌ Transaction submission failed:', error); // Enhanced error handling for transferFrom failures let errorMessage = error.message || 'Transaction failed'; if (userPaysGas && errorMessage.includes('insufficient allowance')) { errorMessage = 'Insufficient allowance: Sender must approve your wallet address to spend tokens. Ask the sender to call approve() first.'; } else if (userPaysGas && errorMessage.includes('transfer amount exceeds allowance')) { errorMessage = 'Transfer amount exceeds allowance: The approved amount is less than the reward amount.'; } else if (userPaysGas && (errorMessage.includes('ERC20:') || errorMessage.includes('allowance'))) { errorMessage = 'Approval required: For receiver-pays-gas mode, the sender must first approve your wallet address to spend tokens.'; } setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(error); setTxHash(undefined); }, }, }); // Wait for transaction confirmation const { data: receipt, isLoading: isConfirming, isSuccess: isConfirmed, isError: isConfirmError, error: confirmError } = useWaitForTransactionReceipt({ hash: txHash, query: { enabled: !!txHash, }, }); // Handle transaction confirmation results useEffect(() => { if (isConfirmed && receipt) { console.log('✅ Transaction confirmed on blockchain:', receipt.transactionHash); console.log('📊 Transaction status:', receipt.status === 'success' ? 'SUCCESS' : 'FAILED'); if (receipt.status === 'success') { setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: null, isSuccess: true }))); onRewardClaimed === null || onRewardClaimed === void 0 ? void 0 : onRewardClaimed(receipt.transactionHash, rewardAmount || '0'); // Reset success state after 3 seconds setTimeout(() => { setState(prev => (Object.assign(Object.assign({}, prev), { isSuccess: false }))); }, 3000); } else { // Transaction was mined but failed const errorMessage = 'Transaction failed on blockchain. This usually means insufficient allowance or balance.'; console.error('❌ Transaction failed on blockchain:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error(errorMessage)); } setTxHash(undefined); } }, [isConfirmed, receipt, rewardAmount, onRewardClaimed, onRewardFailed]); // Handle confirmation errors useEffect(() => { if (isConfirmError && confirmError) { console.error('❌ Transaction confirmation error:', confirmError); const errorMessage = 'Transaction confirmation failed. Please check the blockchain explorer.'; setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(confirmError); setTxHash(undefined); } }, [isConfirmError, confirmError, onRewardFailed]); // Fetch token information (only if in reward mode) useEffect(() => { if (!isRewardMode) return; const fetchTokenInfo = () => __awaiter(void 0, void 0, void 0, function* () { if (!tokenAddress) return; try { setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: true }))); // Mock token info for demo purposes const mockTokenInfo = { symbol: tokenSymbol, decimals: 18, name: `${tokenSymbol} Token`, }; console.log('📊 Token Info:', mockTokenInfo); setState(prev => (Object.assign(Object.assign({}, prev), { tokenInfo: mockTokenInfo, isLoading: false }))); } catch (error) { console.error('❌ Failed to fetch token info:', error); setState(prev => (Object.assign(Object.assign({}, prev), { error: 'Failed to fetch token information', isLoading: false }))); } }); fetchTokenInfo(); }, [tokenAddress, tokenSymbol, isRewardMode]); const handleClaimReward = () => __awaiter(void 0, void 0, void 0, function* () { try { setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: true, error: null }))); onRewardStarted === null || onRewardStarted === void 0 ? void 0 : onRewardStarted(); console.log('🚀 Starting reward claim process...'); // Step 1: ROBUST REAL-TIME wallet connection validation console.log('🔍 Performing real-time wallet connection validation...'); console.log(' userPaysGas:', userPaysGas); console.log(' isConnected (cached):', isConnected); console.log(' address (cached):', address); console.log(' effectiveRequireConnection:', effectiveRequireConnection); // 🔒 SECURITY ENHANCEMENT: Perform real-time wallet connection check let realTimeConnectionState = false; let realTimeAddress = null; try { // Check if MetaMask/wallet is available and connected if (typeof window !== 'undefined' && window.ethereum) { // Request accounts to get real-time connection state const accounts = yield window.ethereum.request({ method: 'eth_accounts' }); realTimeConnectionState = accounts && accounts.length > 0; realTimeAddress = accounts && accounts.length > 0 ? accounts[0] : null; console.log('🔍 Real-time wallet check results:'); console.log(' Real-time connected:', realTimeConnectionState); console.log(' Real-time address:', realTimeAddress); console.log(' Cached vs Real-time match:', isConnected === realTimeConnectionState); } } catch (error) { console.error('❌ Real-time wallet check failed:', error); realTimeConnectionState = false; realTimeAddress = null; } // Use real-time connection state for security decisions const isCurrentlyConnected = realTimeConnectionState && realTimeAddress; const finalAddress = realTimeAddress || address; // 🔒 SECURITY FIX: Block ALL transfers if wallet is not connected in real-time if (!isCurrentlyConnected) { console.log('❌ SECURITY BLOCK: Wallet not connected or locked in real-time check'); console.log('🔒 SECURITY: Blocking transfer - real-time wallet connection required'); console.log('🔄 UX: Falling back to "Claim Reward" button state - user must click again to connect'); setHasClickedOnce(true); // Show "Claim Reward" button setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false }))); // Don't open modal immediately - let user choose when to connect return; } // Double-check that we have a valid address if (!finalAddress) { console.log('❌ SECURITY BLOCK: No valid wallet address available'); console.log('🔒 SECURITY: Blocking transfer - valid address required'); console.log('🔄 UX: Falling back to "Claim Reward" button state - user must click again to connect'); setHasClickedOnce(true); // Show "Claim Reward" button setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false }))); // Don't open modal immediately - let user choose when to connect return; } // Step 2: 🔒 SECURITY FIX: Always use real-time connected wallet address const finalRecipientAddress = finalAddress; // Always use real-time connected wallet console.log('🚀 Final Recipient Address Determination (Security Enhanced):'); console.log(' Payment mode:', userPaysGas ? 'User Pays Gas' : 'Sender Pays Gas'); console.log(' Wallet currently connected (real-time):', isCurrentlyConnected); console.log(' Real-time wallet address:', realTimeAddress); console.log(' Cached wallet address:', address); console.log(' Final recipient address:', finalRecipientAddress); console.log(' Address source: Real-time Connected Wallet (recipientAddress prop ignored for security)'); // Step 3: Validate recipient address is available if (isRewardMode && !finalRecipientAddress) { const errorMessage = 'Wallet connection required for reward transfers. Please connect your wallet.'; console.error('❌ No recipient address:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error(errorMessage)); return; } // Step 4: Additional validation for connected wallet mode if (effectiveRequireConnection && !isCurrentlyConnected) { const errorMessage = 'Wallet connection lost. Please reconnect your wallet.'; console.error('❌ Wallet connection lost:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error('Wallet connection lost')); return; } // Step 5: Validate required parameters for token transfer if (!tokenAddress || !rewardAmount) { const errorMessage = 'Missing token address or reward amount'; console.error('❌ Missing parameters:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error(errorMessage)); return; } // Step 6: FINAL security check - Re-validate wallet connection right before transfer try { if (typeof window !== 'undefined' && window.ethereum) { const finalCheck = yield window.ethereum.request({ method: 'eth_accounts' }); const finalConnectionState = finalCheck && finalCheck.length > 0; if (!finalConnectionState) { const errorMessage = 'Wallet disconnected immediately before transaction. Please reconnect.'; console.error('❌ FINAL SECURITY CHECK FAILED:', errorMessage); console.log('🔄 UX: Falling back to "Claim Reward" button state - user must click again to connect'); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error(errorMessage)); setHasClickedOnce(true); // Show "Claim Reward" button // Don't open modal immediately - let user choose when to connect return; } console.log('✅ FINAL SECURITY CHECK PASSED: Wallet still connected'); } } catch (error) { const errorMessage = 'Failed to verify wallet connection before transaction'; console.error('❌ FINAL SECURITY CHECK ERROR:', error); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error(errorMessage)); return; } console.log('💫 Initiating token transfer...', { tokenAddress, recipientAddress: finalRecipientAddress, amount: rewardAmount, senderAddress: senderAddress || address, tokenSymbol, userPaysGas, usingSenderWallet: Boolean(senderAddress && senderPrivateKey), walletConnected: isCurrentlyConnected, }); // Step 6: Execute token transfer based on payment mode if (userPaysGas) { // User pays gas - use transferFrom pattern console.log('🔄 User pays gas: Using transferFrom pattern...'); if (!isCurrentlyConnected) { const errorMessage = 'Wallet connection required for user-pays-gas mode.'; console.error('❌ Wallet required:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error('Wallet connection required')); return; } // Check if senderAddress and senderPrivateKey are provided for transferFrom if (!senderAddress || !senderPrivateKey) { const errorMessage = 'Sender address and private key required for user-pays-gas mode.'; console.error('❌ Sender credentials required:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error('Sender credentials required')); return; } // Step 6a: Auto-approve receiver using sender's private key console.log('🔐 Auto-approving receiver wallet using sender credentials...'); console.log(' Sender approving:', senderAddress); console.log(' Receiver being approved:', finalRecipientAddress); console.log(' Amount to approve:', rewardAmount); try { // Create provider and sender wallet const provider = new ethers.JsonRpcProvider(rpcUrl || 'https://polygon-mainnet.infura.io/v3/your-key'); const senderWallet = new ethers.Wallet(senderPrivateKey, provider); const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, senderWallet); // Approve the receiver to spend tokens console.log('📝 Submitting approval transaction...'); const approvalTx = yield tokenContract.approve(finalRecipientAddress, BigInt(rewardAmount)); console.log('📤 Approval transaction submitted:', approvalTx.hash); // Wait for approval confirmation console.log('⏳ Waiting for approval confirmation...'); yield approvalTx.wait(); console.log('✅ Receiver approved successfully!'); } catch (approvalError) { console.error('❌ Approval failed:', approvalError); const errorMessage = `Approval failed: ${approvalError instanceof Error ? approvalError.message : 'Unknown error'}`; setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(approvalError instanceof Error ? approvalError : new Error(errorMessage)); return; } // Step 6b: Now execute transferFrom with connected wallet console.log('🔄 Executing transferFrom with connected wallet paying gas...'); console.log(' From (sender):', senderAddress); console.log(' To (recipient):', finalRecipientAddress); console.log(' Amount:', rewardAmount); console.log(' Gas paid by:', address); executeTransfer({ address: tokenAddress, abi: ERC20_ABI, functionName: 'transferFrom', args: [ senderAddress, // From: sender wallet finalRecipientAddress, // To: recipient (connected wallet) BigInt(rewardAmount) // Amount ], }); } else { // Sender pays gas - use original logic if (senderAddress && senderPrivateKey && finalRecipientAddress) { // Use sender wallet for token transfer console.log('💰 Sender pays gas: Using sender wallet for token transfer...'); yield executeTokenTransferWithSenderWallet(tokenAddress, finalRecipientAddress, rewardAmount, senderPrivateKey, rpcUrl || 'https://mainnet.infura.io/v3/your-infura-key'); } else if (executeTransfer && finalRecipientAddress && tokenAddress && rewardAmount && isCurrentlyConnected) { // Use connected wallet for token transfer (only if still connected) console.log('💰 Sender pays gas: Using connected wallet for token transfer...'); // Double-check wallet is still connected before executing if (!isCurrentlyConnected) { const errorMessage = 'Wallet disconnected during transaction. Please reconnect.'; console.error('❌ Wallet disconnected:', errorMessage); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(new Error('Wallet disconnected during transaction')); return; } executeTransfer({ address: tokenAddress, abi: ERC20_ABI, functionName: 'transfer', args: [finalRecipientAddress, BigInt(rewardAmount)], }); } else { // Simulate a transaction for demo purposes console.log('🎭 Simulating token transfer for demo...'); setTimeout(() => { const mockTxHash = '0x' + Math.random().toString(16).substr(2, 64); console.log('✅ Simulated transaction completed:', mockTxHash); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: null, isSuccess: true }))); onRewardClaimed === null || onRewardClaimed === void 0 ? void 0 : onRewardClaimed(mockTxHash, rewardAmount || '0'); // Reset success state after 3 seconds setTimeout(() => { setState(prev => (Object.assign(Object.assign({}, prev), { isSuccess: false }))); }, 3000); }, 2000); } } } catch (error) { console.error('❌ Error in handleClaimReward:', error); const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: errorMessage }))); onRewardFailed === null || onRewardFailed === void 0 ? void 0 : onRewardFailed(error instanceof Error ? error : new Error(errorMessage)); } }); // Function to execute token transfer using sender wallet const executeTokenTransferWithSenderWallet = (tokenAddress, recipientAddress, amount, privateKey, rpcUrl) => __awaiter(void 0, void 0, void 0, function* () { try { console.log('🔧 Creating provider and wallet...'); // Create provider and wallet const provider = new ethers.JsonRpcProvider(rpcUrl); const wallet = new ethers.Wallet(privateKey, provider); // Create contract instance const contract = new ethers.Contract(tokenAddress, ERC20_ABI, wallet); console.log('💸 Executing token transfer from sender wallet:', { from: wallet.address, to: recipientAddress, amount: amount, tokenAddress: tokenAddress, }); // Execute the transfer const tx = yield contract.transfer(recipientAddress, BigInt(amount)); console.log('📤 Transaction submitted:', tx.hash); // Wait for transaction confirmation const receipt = yield tx.wait(); console.log('✅ Transaction confirmed:', receipt.transactionHash); setState(prev => (Object.assign(Object.assign({}, prev), { isLoading: false, error: null, isSuccess: true }))); onRewardClaimed === null || onRewardClaimed === void 0 ? void 0 : onRewardClaimed(receipt.transactionHash, amount); // Reset success state after 3 seconds setTimeout(() => { setState(prev => (Object.assign(Object.assign({}, prev), { isSuccess: false }))); }, 3000); } catch (error) { console.error('❌ Error in sender wallet transfer:', error); throw error; } }); const handleButtonClick = (event) => __awaiter(void 0, void 0, void 0, function* () { // Call the regular onClick handler first if provided try { yield (onClick === null || onClick === void 0 ? void 0 : onClick(event)); } catch (error) { console.error('❌ Error in onClick callback:', error); } if (isRewardMode) { // First click: Check wallet connection if (!hasClickedOnce && !isWalletConnected) { console.log('🎯 First click - checking wallet connection...'); setHasClickedOnce(true); console.log('💡 Wallet not connected. Button text changed to "Claim Reward". Click again to connect wallet.'); return; } // Second click but wallet still not connected: Open wallet connection modal if (hasClickedOnce && !isWalletConnected) { console.log('🎯 Second click - opening wallet connection modal...'); console.log('🔗 User clicked "Claim Reward" button - opening Reown AppKit to connect wallet'); setPendingReward(true); openAppKit(); return; } // Wallet is connected: Start reward flow yield handleClaimReward(); } else { // Regular button mode - call the onReward handler try { yield (onReward === null || onReward === void 0 ? void 0 : onReward()); } catch (error) { console.error('❌ Error in onReward callback:', error); } } }); const internalIsLoading = isRewardMode ? (state.isLoading || isTransactionLoading || isConfirming || pendingReward) : false; const finalIsLoading = externalIsLoading || internalIsLoading; const isButtonDisabled = disabled || (finalIsLoading && !pendingReward && !state.isSuccess); // Dynamic loading text based on current state const getLoadingText = () => { if (pendingReward) { return 'Connect Wallet...'; } if (isConfirming) { return 'Confirming on Blockchain...'; } if (isTransactionLoading) { return 'Submitting Reward...'; } if (state.isLoading) { return userPaysGas ? 'Approving & Claiming Reward...' : 'Claiming Reward...'; } return loadingText; }; // Dynamic button text based on state const getButtonText = () => { if (state.isSuccess) { return '🎉 Success!'; } if (pendingReward) { return 'Connect Wallet...'; } if (isConfirming) { return 'Confirming on Blockchain...'; } if (isTransactionLoading) { return 'Submitting Reward...'; } if (state.isLoading) { return userPaysGas ? 'Approving & Claiming Reward...' : 'Claiming Reward...'; } // Show "Claim Reward" if user clicked once but wallet is not connected if (isRewardMode && hasClickedOnce && !isWalletConnected) { return 'Claim Reward'; } return children; }; return (jsxs(Fragment, { children: [jsx(Button, Object.assign({}, buttonProps, { variant: variant, size: size, onClick: handleButtonClick, disabled: isButtonDisabled, isLoading: finalIsLoading, isSuccess: state.isSuccess, loadingText: getLoadingText(), children: getButtonText() })), isRewardMode && state.error && (jsx("div", { style: { fontSize: '12px', color: '#ef4444', marginTop: '8px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', borderRadius: '4px', padding: '6px 8px', }, children: state.error })), isRewardMode && userPaysGas && (jsx("div", { style: { fontSize: '12px', color: '#2563eb', marginTop: '8px', background: 'rgba(37, 99, 235, 0.1)', border: '1px solid #2563eb', borderRadius: '4px', padding: '6px 8px', }, children: jsx("div", { children: "\uD83D\uDCB0 You will pay gas fees for this transaction" }) }))] })); }; export { BUTTON_SIZES, BUTTON_VARIANTS, Button, COMMON_TOKENS, CSS_CLASSES, DEFAULT_BUTTON_TEXT, ERC20_ABI, RewardButton, cn, RewardButton as default }; //# sourceMappingURL=index.esm.js.map