Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/app/src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type ActionButtonProps = {
onPress?: any;
width?: string;
borderRadius?: number;
isDisabled?: boolean;
};

export const buttonStyles = {
Expand Down Expand Up @@ -40,7 +41,16 @@ export const buttonStyles = {
},
};

const ActionButton = ({ href, text, bg, textColor, onPress, width = '100%', borderRadius = 50 }: ActionButtonProps) => {
const ActionButton = ({
href,
text,
bg,
textColor,
onPress,
width = '100%',
borderRadius = 50,
isDisabled = false,
}: ActionButtonProps) => {
const responsiveStyles = useBreakpointValue({
base: {
button: {
Expand Down Expand Up @@ -71,7 +81,12 @@ const ActionButton = ({ href, text, bg, textColor, onPress, width = '100%', bord
const { buttonContainer, button, buttonText } = responsiveStyles ?? {};

const content = (
<Pressable {...button} onPress={onPress} backgroundColor={bg} paddingBottom={0}>
<Pressable
{...button}
onPress={isDisabled ? undefined : onPress}
backgroundColor={isDisabled ? 'gray.300' : bg}
opacity={isDisabled ? 0.6 : 1}
paddingBottom={0}>
<Text {...buttonText} color={textColor}>
{text}
</Text>
Expand Down
137 changes: 137 additions & 0 deletions packages/app/src/components/ClaimRewardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useState, useEffect } from 'react';
import { View } from 'native-base';
import { useAccount } from 'wagmi';
import RoundedButton from './RoundedButton';
import { Colors } from '../utils/colors';
import { useClaimReward } from '../hooks/useClaimReward';
import BaseModal from './modals/BaseModal';
import ProcessingModal from './modals/ProcessingModal';
import { PhoneImg, ThankYouImg } from '../assets';
import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts';
import { useGetTokenPrice } from '../hooks';
import { ClaimTimer } from './ClaimTimer';

type ClaimStatus = 'idle' | 'confirm' | 'processing' | 'success' | 'error';

interface ClaimRewardButtonProps {
poolAddress: `0x${string}`;
poolType: string;
poolName?: string;
onSuccess?: () => void;
eligibleAmount?: bigint;
hasClaimed?: boolean;
nextClaimTime?: number;
claimPeriodDays?: number;
}

export const ClaimRewardButton: React.FC<ClaimRewardButtonProps> = ({
poolAddress,
poolType,
poolName,
onSuccess,
eligibleAmount = 0n,
hasClaimed = false,
nextClaimTime,
claimPeriodDays,
}) => {
const { address } = useAccount();
const { claimReward, isConfirming, isSuccess, isError, error } = useClaimReward(poolAddress, poolType);
const { price: tokenPrice } = useGetTokenPrice('G$');
const [status, setStatus] = useState<ClaimStatus>('idle');
const [errorMessage, setErrorMessage] = useState<string | undefined>();

const { wei: formattedAmount } = calculateGoodDollarAmounts(eligibleAmount.toString(), tokenPrice, 2);

const handleClaimClick = () => {
setStatus('confirm');
};

const handleConfirmClaim = async () => {
setStatus('processing');
try {
await claimReward();
// success is handled by useEffect listening to isSuccess
} catch (err) {
// Only handle truly unexpected errors here
const message = err instanceof Error ? err.message : 'Failed to claim reward';
setErrorMessage(message);
setStatus('error');
}
};

// Handle success state from hook
useEffect(() => {
if (isSuccess && !isConfirming) {
setStatus('success');
onSuccess?.();
}
}, [isSuccess, isConfirming, onSuccess]);

// Handle error state from hook
useEffect(() => {
if (isError && error) {
const message = error.message || 'Failed to claim reward';
setErrorMessage(message);
setStatus('error');
}
}, [isError, error]);

if (!address) {
return null;
}

// If already claimed, show timer
if (hasClaimed && nextClaimTime && claimPeriodDays) {
return <ClaimTimer nextClaimTime={nextClaimTime} />;
}

// Show button even if amount is 0 (pool might not have funds yet, but user is a member)

return (
<View>
<RoundedButton
title={`Claim Reward${formattedAmount ? ` (G$ ${formattedAmount})` : ' (...)'}`}
backgroundColor={Colors.orange[100]}
color={Colors.orange[300]}
onPress={handleClaimClick}
/>
<BaseModal
openModal={status === 'confirm'}
onClose={() => setStatus('idle')}
onConfirm={handleConfirmClaim}
title="CLAIM REWARD"
paragraphs={[
`You are eligible to claim ${formattedAmount ? `G$ ${formattedAmount}` : '...'} from ${
poolName || 'this pool'
}.`,
'To claim your reward, please sign with your wallet.',
]}
image={PhoneImg}
confirmButtonText="CLAIM"
/>
<ProcessingModal openModal={status === 'processing'} />
<BaseModal
openModal={status === 'success'}
onClose={() => setStatus('idle')}
onConfirm={() => setStatus('idle')}
title="SUCCESS!"
paragraphs={[`You have successfully claimed your reward from ${poolName || 'the pool'}!`]}
image={ThankYouImg}
confirmButtonText="OK"
/>
<BaseModal
type="error"
openModal={status === 'error'}
onClose={() => {
setErrorMessage(undefined);
setStatus('idle');
}}
onConfirm={() => {
setErrorMessage(undefined);
setStatus('idle');
}}
errorMessage={errorMessage ?? ''}
/>
</View>
);
};
60 changes: 60 additions & 0 deletions packages/app/src/components/ClaimTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState, useEffect } from 'react';
import { View, Text, VStack } from 'native-base';
import moment from 'moment';
import { formatTime } from '../lib/formatTime';

interface ClaimTimerProps {
nextClaimTime: number;
}

export const ClaimTimer: React.FC<ClaimTimerProps> = ({ nextClaimTime }) => {
const [timeRemaining, setTimeRemaining] = useState<number>(0);

useEffect(() => {
const updateTimer = () => {
const now = moment().unix();
const remaining = Math.max(0, nextClaimTime - now);
setTimeRemaining(remaining);
};

updateTimer();
const interval = setInterval(updateTimer, 1000);

return () => clearInterval(interval);
}, [nextClaimTime]);

if (timeRemaining === 0) {
return (
<View>
<Text fontSize="sm" color="gray.500">
You can claim again now
</Text>
</View>
);
}

const days = Math.floor(timeRemaining / 86400);
const hours = Math.floor((timeRemaining % 86400) / 3600);
const minutes = Math.floor((timeRemaining % 3600) / 60);
const seconds = timeRemaining % 60;

return (
<VStack space={2} alignItems="center" padding={4} backgroundColor="gray.50" borderRadius={8}>
<Text fontSize="md" fontWeight="bold" textAlign="center">
Already Claimed
</Text>
<Text fontSize="sm" color="gray.600" textAlign="center">
You can claim again in:
</Text>
<Text fontSize="lg" fontWeight="bold" color="orange.500" textAlign="center">
{days > 0 && `${days}d `}
{hours > 0 && `${hours}h `}
{minutes > 0 && `${minutes}m `}
{seconds}s
</Text>
<Text fontSize="xs" color="gray.500" textAlign="center">
Next claim: {formatTime(nextClaimTime)}
</Text>
</VStack>
);
};
102 changes: 102 additions & 0 deletions packages/app/src/components/JoinPoolButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useEffect, useState } from 'react';
import { View } from 'native-base';
import { useAccount } from 'wagmi';
import RoundedButton from './RoundedButton';
import { Colors } from '../utils/colors';
import { useJoinPool } from '../hooks/useJoinPool';
import BaseModal from './modals/BaseModal';
import ProcessingModal from './modals/ProcessingModal';
import { PhoneImg, ThankYouImg } from '../assets';

interface JoinPoolButtonProps {
poolAddress: `0x${string}`;
poolType: string;
poolName?: string;
onSuccess?: () => void;
}

export const JoinPoolButton: React.FC<JoinPoolButtonProps> = ({ poolAddress, poolType, poolName, onSuccess }) => {
const { address } = useAccount();
const { joinPool, isSimulating, isConfirming, isSuccess, isError, error, hash } = useJoinPool(poolAddress, poolType);
const [showJoinModal, setShowJoinModal] = useState(false);
const [showProcessingModal, setShowProcessingModal] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | undefined>();

const handleJoinClick = () => {
setShowJoinModal(true);
};

const handleConfirmJoin = async () => {
setShowJoinModal(false);
setShowProcessingModal(true);
try {
await joinPool();
} catch (err) {
setShowProcessingModal(false);
const message = err instanceof Error ? err.message : 'Failed to join pool';
setErrorMessage(message);
}
};

useEffect(() => {
if (isSuccess && !isConfirming && hash) {
setShowProcessingModal(false);
setShowSuccessModal(true);
// Wait a bit for the transaction to be indexed, then call onSuccess
setTimeout(() => {
onSuccess?.();
}, 1000);
}
}, [isSuccess, isConfirming, hash, onSuccess]);

useEffect(() => {
if (isError && error) {
setShowProcessingModal(false);
const message = error.message || 'Failed to join pool';
setErrorMessage(message);
}
}, [isError, error]);

if (!address) {
return null;
}

return (
<View>
<RoundedButton
title="Join Pool"
backgroundColor={Colors.green[100]}
color={Colors.green[200]}
onPress={handleJoinClick}
/>
<BaseModal
openModal={showJoinModal}
onClose={() => setShowJoinModal(false)}
onConfirm={handleConfirmJoin}
title="JOIN POOL"
paragraphs={[`To join ${poolName || 'this pool'}, please sign with your wallet.`]}
image={PhoneImg}
confirmButtonText="JOIN"
confirmDisabled={isSimulating}
/>
<ProcessingModal openModal={showProcessingModal} />
<BaseModal
openModal={showSuccessModal}
onClose={() => setShowSuccessModal(false)}
onConfirm={() => setShowSuccessModal(false)}
title="SUCCESS!"
paragraphs={[`You have successfully joined ${poolName || 'the pool'}!`]}
image={ThankYouImg}
confirmButtonText="OK"
/>
<BaseModal
type="error"
openModal={!!errorMessage}
onClose={() => setErrorMessage(undefined)}
onConfirm={() => setErrorMessage(undefined)}
errorMessage={errorMessage ?? ''}
/>
</View>
);
};
Loading
Loading