Skip to content
This repository has been archived by the owner on May 29, 2024. It is now read-only.

Peanut-SDK integration #45

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
172 changes: 172 additions & 0 deletions components/apps/peanut/claim/1.LinkDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {useEffect, useState} from 'react';
import {IconSpinner} from '@icons/IconSpinner';
import {CHAIN_DETAILS, claimLinkGasless} from '@squirrel-labs/peanut-sdk';
import {waitForTransaction} from '@wagmi/core';
import {Button} from '@yearn-finance/web-lib/components/Button';
import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3';

import {useClaimLinkPeanut} from './useClaimLinkPeanut';

import type {ReactElement} from 'react';

function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement {
const {linkDetails, set_claimTxHash, claimTxHash} = useClaimLinkPeanut();
const {address} = useWeb3();
const [isClient, set_isClient] = useState(false);
const [isLoading, set_isLoading] = useState(false);

/*
* Claim link
* Function that handles peanut functionality to claim a link.
* This function is the gasless function, it will not require any wallet interaction by the user. Only the link, and the address are required.
*/
async function onClaimLink(): Promise<void> {
set_isLoading(true);

if (!address) {
alert('Please connect a wallet to claim to.');
set_isLoading(false);
return;
}

try {
const claimLinkGaslessResp = await claimLinkGasless({
link: linkDetails.link,
recipientAddress: address ? address.toString() : '',
APIKey: process.env.NEXT_PUBLIC_PEANUT_API_KEY ?? ''
});
waitForTransaction({hash: claimLinkGaslessResp.txHash});
set_claimTxHash(claimLinkGaslessResp.txHash);
set_isLoading(false);
onProceed();
} catch (error) {
set_isLoading(false);
console.log('error', error);
}
}

useEffect(() => {
set_isClient(true);
}, []); // to prevent hydration error

return (
<section className={'box-0'}>
<div className={'relative w-full'}>
<div className={'flex flex-col p-4 text-neutral-900 md:p-6 md:pb-4'}>
<div className={'w-full md:w-3/4'}>
<b>{'Here are some details:'}</b>
</div>
</div>

<div
className={'grid grid-rows-3 grid-cols-2 gap-4 px-6 py-4 border-t border-neutral-200'}
style={{gridTemplateColumns: 'max-content max-content'}}>
<div>
<p className={'text-s text-neutral-500'}>{'Chain:'}</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>
{linkDetails.chainId ? (
CHAIN_DETAILS[linkDetails.chainId]?.name
) : (
<IconSpinner
className={
'h-4 w-4 text-neutral-500 transition-colors group-hover:text-neutral-900'
}
/>
)}
</p>
</div>

<div>
<p className={'text-s text-neutral-500'}>{'Amount:'}</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>
{linkDetails.tokenAmount ? (
linkDetails.tokenAmount
) : (
<IconSpinner
className={
'h-4 w-4 text-neutral-500 transition-colors group-hover:text-neutral-900'
}
/>
)}
</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>{'Token:'}</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>
{linkDetails.tokenName ? (
linkDetails.tokenName
) : (
<IconSpinner
className={
'h-4 w-4 text-neutral-500 transition-colors group-hover:text-neutral-900'
}
/>
)}
</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>{'Address:'}</p>
</div>
<div>
<p className={'text-s text-neutral-500'}>
{linkDetails.tokenAddress ? (
linkDetails.tokenAddress
) : (
<IconSpinner
className={
'h-4 w-4 text-neutral-500 transition-colors group-hover:text-neutral-900'
}
/>
)}
</p>
</div>
</div>

<div
className={
'sticky inset-x-0 bottom-0 z-20 flex w-full max-w-5xl flex-row items-center justify-between rounded-b-[5px] bg-primary-600 p-4 text-primary-0 md:relative md:px-6 md:py-4'
}>
<div
className={'flex w-3/4 flex-col'}
suppressHydrationWarning>
{isClient
? linkDetails.claimed || claimTxHash
? 'This link has already been claimed'
: address
? 'You are claiming to ' + address
: 'Please connect a wallet to claim to.'
: ''}
</div>

<div className={'flex flex-col items-end w-full'}>
<Button
className={'yearn--button !w-fit !px-6 !text-sm'}
variant={'reverted'}
onClick={onClaimLink}
isDisabled={linkDetails.claimed || claimTxHash}>
{isLoading ? (
<div className={'flex flex-row gap-2 justify-center items-center'}>
{'Claiming'}
<IconSpinner
className={
'h-4 w-4 text-neutral-500 transition-colors group-hover:text-neutral-900'
}
/>
</div>
) : (
'Claim'
)}
</Button>
</div>
</div>
</div>
</section>
);
}
export default ViewLinkDetails;
38 changes: 38 additions & 0 deletions components/apps/peanut/claim/2.ClaimSuccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {useMemo} from 'react';
import {CHAIN_DETAILS} from '@squirrel-labs/peanut-sdk';
import ViewSectionHeading from '@common/ViewSectionHeading';

import {useClaimLinkPeanut} from './useClaimLinkPeanut';

import type {ReactElement} from 'react';

function ViewClaimSuccess(): ReactElement {
const {linkDetails, claimTxHash} = useClaimLinkPeanut();

const blockExplorerUrl = useMemo(() => {
return CHAIN_DETAILS[linkDetails.chainId]?.explorers[0].url;
}, [linkDetails.chainId, CHAIN_DETAILS]);

return (
<section>
<div className={'box-0 grid w-full grid-cols-12'}>
<ViewSectionHeading
title={'You have successfully claimed your funds!'}
content={'Click the transaction hash to see the transaction confirmation.'}
/>

<div className={'col-span-12 p-4 pt-0 md:p-6 md:pt-0'}>
<div className={'flex flex-col gap-2'}>
<a
href={`${blockExplorerUrl}/tx/${claimTxHash}`}
target={'_blank'}
className={'w-full text-sm text-neutral-500 md:w-3/4 underline cursor-pointer'}>
{claimTxHash}
</a>
</div>
</div>
</div>
</section>
);
}
export default ViewClaimSuccess;
124 changes: 124 additions & 0 deletions components/apps/peanut/claim/useClaimLinkPeanut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo, useState} from 'react';
import {useUpdateEffect} from '@react-hookz/web';
import {getLinkDetails} from '@squirrel-labs/peanut-sdk';
import {scrollToTargetAdjusted} from '@utils/animations';
import {HEADER_HEIGHT} from '@utils/constants';
import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3';

export enum Step {
LINKDETAILS = 'link_details',
CLAIMSUCCESS = 'claim_success'
}

export type TClaimLink = {
linkDetails: any;
set_linkDetails: Dispatch<SetStateAction<any>>;
currentStep: Step;
set_currentStep: Dispatch<SetStateAction<Step>>;
claimTxHash: string;
set_claimTxHash: Dispatch<SetStateAction<string>>;
claimUrl: string;
set_claimUrl: Dispatch<SetStateAction<string>>;
};

const defaultProps: TClaimLink = {
linkDetails: {},
set_linkDetails: (): void => undefined,
currentStep: Step.LINKDETAILS,
set_currentStep: (): void => undefined,
claimTxHash: '',
set_claimTxHash: (): void => undefined,
claimUrl: '',
set_claimUrl: (): void => undefined
};

const ClaimLinkPeanutContext = createContext<TClaimLink>(defaultProps);
export const ClaimLinkPeanutContextApp = ({children}: {children: React.ReactElement}): React.ReactElement => {
const {address, isActive, isWalletSafe, isWalletLedger, onConnect} = useWeb3();
const [currentStep, set_currentStep] = useState<Step>(Step.LINKDETAILS);
const [linkDetails, set_linkDetails] = useState<any>({});
const [claimTxHash, set_claimTxHash] = useState<string>('');
const [claimUrl, set_claimUrl] = useState<string>('');

/**********************************************************************************************
** This effect is used to directly ask the user to connect its wallet if it's not connected
**********************************************************************************************/
useEffect((): void => {
if (!isActive && !address) {
onConnect();
return;
}
}, [address, isActive, onConnect]);

/**********************************************************************************************
** This effect is used to handle some UI transitions and sections jumps. Once the current step
** changes, we need to scroll to the correct section.
** This effect is ignored on mount but will be triggered on every update to set the correct
** scroll position.
**********************************************************************************************/
useUpdateEffect((): void => {
setTimeout((): void => {
let currentStepContainer;
const scalooor = document?.getElementById('scalooor');

if (currentStep === Step.LINKDETAILS) {
currentStepContainer = document?.getElementById('linkDetails');
} else if (currentStep === Step.CLAIMSUCCESS) {
currentStepContainer = document?.getElementById('claimSuccess');
}
const currentElementHeight = currentStepContainer?.offsetHeight;
if (scalooor?.style) {
scalooor.style.height = `calc(100vh - ${currentElementHeight}px - ${HEADER_HEIGHT}px + 36px)`;
}
if (currentStepContainer) {
scrollToTargetAdjusted(currentStepContainer);
}
}, 0);
}, [currentStep, isWalletLedger, isWalletSafe]);

useEffect(() => {
if (claimUrl) {
peanutGetLinkDetails({claimUrl});
}
}, [claimUrl]);

/**********************************************************************************************
** This function is used to get the details of the link (amount, token, chain).
**********************************************************************************************/
async function peanutGetLinkDetails({claimUrl}: {claimUrl: string}): Promise<any> {
try {
const linkDetails = await getLinkDetails({
link: claimUrl
});
console.log('linkDetails', linkDetails);
set_linkDetails(linkDetails);
} catch (error) {
console.error(error);
}
}

const contextValue = useMemo(
(): TClaimLink => ({
currentStep,
set_currentStep,
linkDetails,
set_linkDetails,
claimTxHash,
set_claimTxHash,
claimUrl,
set_claimUrl
}),
[currentStep, set_currentStep, linkDetails, set_linkDetails, claimTxHash, set_claimTxHash]
);

return (
<ClaimLinkPeanutContext.Provider value={contextValue}>
<div className={'mx-auto w-full'}>
{children}
<div id={'scalooor'} />
</div>
</ClaimLinkPeanutContext.Provider>
);
};

export const useClaimLinkPeanut = (): TClaimLink => React.useContext(ClaimLinkPeanutContext);
48 changes: 48 additions & 0 deletions components/apps/peanut/create/1.ViewChainToSend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {NetworkSelector} from 'components/common/HeaderElements';
import {Button} from '@yearn-finance/web-lib/components/Button';
import ViewSectionHeading from '@common/ViewSectionHeading';

import type {ReactElement} from 'react';

function ViewChainToSend({onProceed}: {onProceed: VoidFunction}): ReactElement {
return (
<section>
<div className={'box-0 grid w-full grid-cols-12'}>
<ViewSectionHeading
title={'What chain are the tokens on?'}
content={'Pick the chain that you have funds on.'}
/>

<div className={'col-span-12 p-4 pt-0 md:p-6 md:pt-0'}>
<form
suppressHydrationWarning
onSubmit={async (e): Promise<void> => e.preventDefault()}
className={
'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6'
}>
<div className={'grow-1 col-span-12 flex h-10 w-full items-center md:col-span-9'}>
<div
className={
'relative flex w-full flex-row items-center space-x-4 border-[#B0D5CD] rounded-md border-solid p-2 px-4 border'
}>
<NetworkSelector
fullWidth
networks={[]}
/>
</div>{' '}
</div>
<div className={'col-span-12 md:col-span-3'}>
<Button
variant={'filled'}
className={'yearn--button !w-[160px] rounded-md !text-sm'}
onClick={onProceed}>
{'Next'}
</Button>
</div>
</form>
</div>
</div>
</section>
);
}
export default ViewChainToSend;
Loading