This commit is contained in:
zramsay 2025-03-21 16:54:03 -04:00
parent ad90af2d31
commit 127a20ba86
6 changed files with 29 additions and 253 deletions

View File

@ -1,109 +0,0 @@
// src/app/api/tokens/claim/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getSessionFromCookie, getUserFromHeaders } from '../../../../lib/auth'
import { getUserPoints } from '../../../../services/userPointsService'
import { awardTokensForSighting } from '../../../../services/blockchain/tokenRewardService'
import { isWalletConnected, getWalletAddress } from '../../../../services/blockchain/seiService'
export async function POST(req: NextRequest): Promise<NextResponse> {
try {
// Log request info
console.log('Token Claim API Request:', {
path: req.url,
method: req.method,
headers: Object.fromEntries(req.headers.entries()),
hasCookies: req.headers.has('cookie')
});
// Get user from session
const session = await getSessionFromCookie(req);
const headerUser = getUserFromHeaders(req);
// Check authentication - require session OR header authentication
const isAuthenticated = !!session || !!headerUser;
if (!isAuthenticated) {
console.log('Unauthorized access attempt to token claim API');
return NextResponse.json(
{ success: false, error: 'Unauthorized. Please sign in to claim tokens.' },
{ status: 401 }
);
}
// Get user info
const userId = session?.user?.id || headerUser?.id || '';
const userEmail = session?.user?.email || headerUser?.email || '';
// Log user identification
console.log('User identification for token claim:', { userId, userEmail });
// Check if wallet is connected (client should check this too, but double-check)
if (!req.headers.get('x-wallet-address')) {
return NextResponse.json(
{ success: false, error: 'No wallet address provided. Please connect your wallet first.' },
{ status: 400 }
);
}
const walletAddress = req.headers.get('x-wallet-address');
// Get user's points
const userPoints = await getUserPoints(userId || userEmail);
console.log('User points:', userPoints);
// Check if user has points to claim
if (!userPoints || userPoints < 1) {
return NextResponse.json(
{ success: false, error: 'You need at least 1 point to claim tokens.' },
{ status: 400 }
);
}
// Parse the request body
const requestData = await req.json();
console.log('Request data:', requestData);
// Get species
const species = requestData.species || 'animal';
// Award tokens based on points (1 point = 1 token)
try {
// Award tokens through blockchain service
const rewardResult = await awardTokensForSighting(species, requestData.points);
if (rewardResult.success) {
return NextResponse.json({
success: true,
message: `Successfully claimed WILD tokens for your wildlife points!`,
tokenAmount: rewardResult.tokenAmount,
txHash: rewardResult.txHash
});
} else {
throw new Error(rewardResult.error || 'Failed to award tokens');
}
} catch (err) {
console.error('Error awarding tokens:', err);
return NextResponse.json(
{
success: false,
error: err instanceof Error ? err.message : 'Error processing token reward'
},
{ status: 500 }
);
}
} catch (error) {
console.error('Token claim failed:', {
errorName: error instanceof Error ? error.name : 'Unknown Error',
errorMessage: error instanceof Error ? error.message : String(error),
errorStack: error instanceof Error ? error.stack : 'No stack trace'
});
return NextResponse.json(
{ success: false, error: 'Failed to claim tokens' },
{ status: 500 }
);
}
}
// Ensure dynamic routing
export const dynamic = 'force-dynamic'

View File

@ -17,15 +17,8 @@ const Page: React.FC = (): React.ReactElement => {
const { data: session, status } = useSession()
const isAuthenticated = status === 'authenticated' && !!session
// State for user points and token claim
// State for user points
const [userPoints, setUserPoints] = useState<number | null>(null)
const [isClaimingTokens, setIsClaimingTokens] = useState(false)
const [claimResult, setClaimResult] = useState<{
success: boolean;
message: string;
tokenAmount?: number;
txHash?: string;
} | null>(null)
// Effect to fetch user points
React.useEffect(() => {
@ -105,68 +98,7 @@ const Page: React.FC = (): React.ReactElement => {
}
}
// Handle token claim
const handleClaimTokens = async () => {
if (!isAuthenticated || !session?.user) {
console.error('User not authenticated');
return;
}
if (!isWalletConnected()) {
console.error('Wallet not connected');
return;
}
if (!userPoints || userPoints < 1) {
console.error('No points to claim');
return;
}
setIsClaimingTokens(true);
setClaimResult(null);
try {
// Call our new token claim API
const response = await fetch('/api/tokens/claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-wallet-address': getWalletAddress() || ''
},
body: JSON.stringify({
points: userPoints,
species: 'wildlife' // Generic species for point-based claims
})
});
const result = await response.json();
if (result.success) {
setClaimResult({
success: true,
message: 'Successfully claimed WILD tokens!',
tokenAmount: result.tokenAmount,
txHash: result.txHash
});
// Reset points to 0 after successful claim
setUserPoints(0);
} else {
setClaimResult({
success: false,
message: result.error || 'Failed to claim tokens'
});
}
} catch (error) {
console.error('Error claiming tokens:', error);
setClaimResult({
success: false,
message: error instanceof Error ? error.message : 'Error claiming tokens'
});
} finally {
setIsClaimingTokens(false);
}
};
// No token claim needed - tokens are automatically awarded when uploading wildlife images
// Auth-locked content
const renderAuthContent = () => {
@ -184,88 +116,34 @@ const Page: React.FC = (): React.ReactElement => {
onAnalyze={handleImageAnalysis()}
/>
{/* Points & token claim section */}
{/* Points display section */}
{userPoints !== null && (
<div className="mt-6 bg-emerald-900/20 backdrop-blur-lg rounded-2xl shadow-xl border border-emerald-800/50 p-6">
<div className="mb-4">
<div className="flex items-center gap-2">
<Coins className="w-6 h-6 text-amber-400" />
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-yellow-300">
Earn WILD Tokens
<Leaf className="w-6 h-6 text-emerald-400" />
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">
Your Wildlife Points
</h2>
</div>
<p className="text-emerald-200 mt-2">
Convert your wildlife identification points to WILD tokens on the Sei blockchain
You earn points for identifying wildlife in photos
</p>
</div>
<div className="flex flex-col md:flex-row gap-4 items-stretch justify-between">
{/* Points display */}
<div className="bg-emerald-950/40 border border-emerald-800/50 rounded-xl p-4 flex flex-col items-center justify-center flex-grow">
<span className="text-4xl font-bold text-amber-300">{userPoints}</span>
<span className="text-emerald-300 text-sm mt-1">Available Points</span>
</div>
{/* Token claim button */}
<div className="flex flex-col items-stretch justify-center">
<button
onClick={handleClaimTokens}
disabled={!isWalletConnected() || !userPoints || userPoints < 1 || isClaimingTokens}
className={`
h-full px-6 rounded-xl font-semibold
flex items-center justify-center gap-2
transition-all duration-200 shadow-lg
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none
${isClaimingTokens
? 'bg-gray-600 text-gray-300'
: 'bg-gradient-to-r from-amber-500 to-yellow-500 hover:from-amber-600 hover:to-yellow-600 text-white'
}
`}
>
{isClaimingTokens ? 'Claiming...' : 'Claim WILD Tokens'}
{!isClaimingTokens && <Coins className="w-5 h-5" />}
</button>
<span className="text-4xl font-bold text-emerald-300">{userPoints}</span>
<span className="text-emerald-200 text-sm mt-1">Total Points</span>
</div>
</div>
{/* Wallet connection notice */}
{!isWalletConnected() && (
<div className="mt-4 bg-amber-900/20 border border-amber-500/20 text-amber-400 px-4 py-3 rounded-xl text-sm text-center">
Connect your wallet using the button in the navigation bar to claim WILD tokens
</div>
)}
{/* Claim result message */}
{claimResult && (
<div className={`mt-4 ${claimResult.success
? 'bg-green-900/30 border border-emerald-800/50 text-emerald-200'
: 'bg-red-900/20 border border-red-500/20 text-red-400'
} rounded-xl p-4`}>
<p>{claimResult.message}</p>
{/* Token reward details */}
{claimResult.success && claimResult.tokenAmount && (
<div className="mt-4 bg-amber-900/30 border border-amber-500/30 rounded-lg p-3 flex items-center gap-2">
<Coins className="text-amber-400 w-5 h-5" />
<div>
<p className="font-medium text-amber-300">
{claimResult.tokenAmount} WILD tokens claimed!
</p>
{claimResult.txHash && (
<p className="text-xs text-amber-400/70 mt-1">
Transaction: <a
href={`https://www.seiscan.app/atlantic-2/txs/${claimResult.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-amber-300"
>
{claimResult.txHash.substring(0, 10)}...
</a>
</p>
)}
</div>
</div>
)}
<div className="mt-4 bg-amber-900/20 border border-amber-500/20 text-amber-400 px-4 py-3 rounded-xl text-sm text-center flex items-center justify-center gap-2">
<Coins className="w-5 h-5" />
<span>Connect your wallet using the button in the navigation bar to also earn WILD tokens</span>
</div>
)}
</div>

View File

@ -4,6 +4,7 @@
import React, { useState, useRef } from 'react'
import { Leaf, Coins } from 'lucide-react'
import { APP_CONFIG } from '../config/appConfig'
import { isWalletConnected } from '../services/blockchain/seiService'
interface TokenReward {
amount?: number;
@ -157,6 +158,13 @@ const ImageAnalysisCard: React.FC<ImageAnalysisCardProps> = ({
>
{analysisState.loading ? 'Processing...' : 'Analyze' }
</button>
{!isWalletConnected() && (
<div className="bg-amber-900/20 border border-amber-500/20 text-amber-400 px-4 py-3 rounded-xl text-center flex items-center justify-center gap-2">
<Coins className="w-5 h-5" />
<span>Connect your wallet to also earn WILD tokens</span>
</div>
)}
</div>
{analysisState.error && (

View File

@ -56,11 +56,8 @@ const WildlifeIdentifier: React.FC = () => {
return;
}
// Check if wallet is connected
if (!isWalletConnected()) {
setError('Please connect your wallet first to receive rewards');
return;
}
// No need to check for wallet connection - the process works for both user types
// Tokens will be awarded automatically if wallet is connected
setIsProcessing(true);
setError(null);
@ -214,19 +211,19 @@ const WildlifeIdentifier: React.FC = () => {
<button
onClick={handleAnalyze}
disabled={isProcessing || !selectedImage || !isWalletConnected()}
disabled={isProcessing || !selectedImage}
className="w-full bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-600
hover:to-teal-600 text-white font-semibold py-4 px-6 rounded-xl
transition-all duration-200 shadow-lg hover:shadow-emerald-500/25
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
>
{isProcessing ? 'Processing...' : 'Identify Wildlife & Earn Tokens'}
{isProcessing ? 'Processing...' : 'Identify Wildlife'}
</button>
{!isWalletConnected() && (
<div className="bg-amber-900/20 border border-amber-500/20 text-amber-400 px-4 py-3 rounded-xl text-center flex items-center justify-center gap-2">
<AlertCircle className="w-5 h-5" />
<span>Please connect your wallet to earn token rewards</span>
<span>Connect your wallet to also earn WILD tokens</span>
</div>
)}
</div>

View File

@ -102,7 +102,7 @@ const WalletButton: React.FC = () => {
: 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/30'
} transition-colors`}
>
{isConnecting ? 'Connecting...' : 'Connect Wallet'}
{isConnecting ? 'Connecting...' : 'Connect for WILD'}
</button>
)}

View File

@ -50,8 +50,10 @@ export const awardTokensForSighting = async (species: string, points?: number):
walletConnected: boolean;
}> => {
try {
// Check if wallet is connected
// Check if wallet is connected - silently skip token award if not connected
// This allows the same flow to work for both types of users
if (!isWalletConnected()) {
console.log('No wallet connected, skipping token award.');
return {
success: false,
error: 'No wallet connected',