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
JavaScript
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