"use client"; import React, { useMemo, useState, useCallback } from "react"; import { TransactionReceipt } from "viem"; import { Address, useAccount, useBalance } from "wagmi"; import { RegistryCommunity, TokenGarden, isMemberQuery, } from "#/subgraph/.graphclient"; import { BtnStyle, Button, Color } from "./Button"; import { TransactionModal } from "./TransactionModal"; import { usePubSubContext } from "@/contexts/pubsub.context"; import { useChainIdFromPath } from "@/hooks/useChainIdFromPath"; import { useContractWriteWithConfirmations } from "@/hooks/useContractWriteWithConfirmations"; import { useCovenantAgreementSignature } from "@/hooks/useCovenantAgreementSignature"; import { ConditionObject, useDisableButtons } from "@/hooks/useDisableButtons"; import { useHandleAllowance } from "@/hooks/useHandleAllowance"; import { useHandleRegistration } from "@/hooks/useHandleRegistration"; import { registryCommunityABI } from "@/src/generated"; import { useErrorDetails } from "@/utils/getErrorName"; import { gte } from "@/utils/numbers"; type RegisterMemberProps = { registrationCost: bigint; registrationStakeAmount?: bigint; token: Pick; registryCommunity: Pick< RegistryCommunity, "communityName" | "id" | "covenantIpfsHash" >; memberData: isMemberQuery | undefined; }; export function RegisterMember({ registrationCost, registrationStakeAmount, token, registryCommunity, memberData, }: RegisterMemberProps) { const { id: communityAddress, communityName, covenantIpfsHash, } = registryCommunity; const { address: accountAddress } = useAccount(); const urlChainId = useChainIdFromPath(); const [isOpenModal, setIsOpenModal] = useState(false); const { publishAfterIndexed } = usePubSubContext(); const isMember = useMemo( () => memberData?.member?.memberCommunity?.[0]?.isRegistered ?? false, [memberData], ); const registryContractCallConfig = useMemo( () => ({ address: communityAddress as Address, abi: registryCommunityABI, contractName: "Registry Community", }), [communityAddress], ); // Normalize token address so both wagmi FetchTokenResult shape (address) // and subgraph shape (id) work correctly. const tokenAddress = useMemo(() => { // token may come from the subgraph shape (has `id`) or from wagmi (has `address`). return (token as any)?.address ?? (token as any)?.id; }, [token]); const normalizedToken = useMemo(() => { if (!tokenAddress) return undefined; return { address: tokenAddress, decimals: (token as any)?.decimals, symbol: (token as any)?.symbol, } as { address: string; decimals: number; symbol: string }; }, [tokenAddress, token]); const { data: accountTokenBalance } = useBalance({ address: accountAddress, token: tokenAddress as Address, chainId: urlChainId, watch: true, // only enabled when we have both account and token address enabled: !!accountAddress && !!tokenAddress, }); const accountHasBalance = useMemo( () => gte(accountTokenBalance?.value, registrationCost, token.decimals), [accountTokenBalance?.value, registrationCost, token.decimals], ); const { write: writeUnregisterMember, error: unregisterMemberError, isLoading: isUnregistering, } = useContractWriteWithConfirmations({ ...registryContractCallConfig, functionName: "unregisterMember", fallbackErrorMessage: "Error unregistering member, please report a bug.", onConfirmations: useCallback((receipt: TransactionReceipt) => { publishAfterIndexed( receipt, { topic: "member", type: "delete", containerId: communityAddress, function: "unregisterMember", id: accountAddress, chainId: urlChainId, }, accountAddress ? { optimistic: { kind: "community-member", communityId: communityAddress, memberAddress: accountAddress, isRegistered: false, stakedTokens: "0", }, } : undefined, ); }, [publishAfterIndexed, communityAddress, urlChainId, accountAddress]), }); useErrorDetails(unregisterMemberError, "unregisterMember"); const disableRegMemberBtnCondition = useMemo( () => [ { condition: !isMember && !accountHasBalance, message: "Insufficient balance", }, ], [isMember, accountHasBalance], ); const { tooltipMessage, missmatchUrl } = useDisableButtons( disableRegMemberBtnCondition, ); const { registrationTxProps: registrationTx, handleRegistration, resetState: handleRegistrationResetState, } = useHandleRegistration( communityAddress as Address, communityName ?? "", urlChainId, registrationStakeAmount ?? registrationCost, ); const { allowanceTxProps: allowanceTx, handleAllowance, resetState: handleAllowanceResetState, } = useHandleAllowance( accountAddress, // pass the normalized token object so useHandleAllowance sees a valid address normalizedToken, communityAddress as Address, registrationCost, handleRegistration, ); const message = `You agree with the terms and conditions of ${communityName} covenant: https://ipfs.io/ipfs/${covenantIpfsHash}`; const { covenantAgreementTxProps: covenantAgreementTx, handleSignature } = useCovenantAgreementSignature(message, handleAllowance, { chainId: urlChainId, communityAddress: communityAddress as Address, covenant: covenantIpfsHash, }); const handleClick = useCallback(() => { if (isMember) { writeUnregisterMember(); } else { handleAllowanceResetState(); handleRegistrationResetState(); setIsOpenModal(true); if (covenantAgreementTx.status !== "loading") { handleSignature(); } } }, [ isMember, writeUnregisterMember, handleAllowanceResetState, handleRegistrationResetState, handleSignature, ]); const buttonProps: { onClick: () => void; btnStyle: BtnStyle; color: Color; disabled: boolean; tooltip: string | undefined; } = useMemo( () => ({ onClick: handleClick, btnStyle: isMember ? "outline" : "filled", color: isMember ? "danger" : "primary", disabled: missmatchUrl || disableRegMemberBtnCondition.some((cond) => cond.condition), tooltip: tooltipMessage, }), [ isMember, handleClick, missmatchUrl, disableRegMemberBtnCondition, tooltipMessage, ], ); // TODO: check token prop return ( <> setIsOpenModal(false)} isOpen={isOpenModal} testId="register" /> ); }