diff --git a/packages/app/src/components/CommunityPool/CreatePool/ReviewLaunch.tsx b/packages/app/src/components/CommunityPool/CreatePool/ReviewLaunch.tsx index 354155d4..e7b13e13 100644 --- a/packages/app/src/components/CommunityPool/CreatePool/ReviewLaunch.tsx +++ b/packages/app/src/components/CommunityPool/CreatePool/ReviewLaunch.tsx @@ -1,5 +1,5 @@ import { useScreenSize } from '@gooddollar/good-design'; -import { Box, CheckCircleIcon, Divider, HStack, Pressable, Text, TextArea, VStack } from 'native-base'; +import { Box, CheckCircleIcon, Divider, HStack, Pressable, Text, VStack } from 'native-base'; import { ReactNode, useCallback, useMemo, useState } from 'react'; import { AtIcon, DiscordIcon, EditIcon, InstagramIcon, PhoneImg, TwitterIcon, WebsiteIcon } from '../../../assets'; import { useCreatePool } from '../../../hooks/useCreatePool/useCreatePool'; diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 5fc7b573..305d4f60 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -4,7 +4,7 @@ import { isEmpty } from 'lodash'; import moment from 'moment'; import { Box, HStack, Link, Text, useBreakpointValue, VStack } from 'native-base'; import { useCallback, useMemo, useState } from 'react'; -import { Image, View } from 'react-native'; +import { View } from 'react-native'; import { useParams } from 'react-router-native'; import { TransactionReceipt } from 'viem'; import { useAccount } from 'wagmi'; diff --git a/packages/app/src/hooks/managePool/useCoreSettings.ts b/packages/app/src/hooks/managePool/useCoreSettings.ts index 9ada6b5c..54b7b1d5 100644 --- a/packages/app/src/hooks/managePool/useCoreSettings.ts +++ b/packages/app/src/hooks/managePool/useCoreSettings.ts @@ -21,7 +21,7 @@ type CoreSettingsState = { coreMembersValidator: string; }; -export const useCoreSettings = ({ poolAddress, pooltype, contractsForChain, chainId }: UseCoreSettingsParams) => { +export const useCoreSettings = ({ poolAddress, pooltype, chainId }: UseCoreSettingsParams) => { const { address } = useAccount(); const provider = useEthersProvider({ chainId }); const signer = useEthersSigner({ chainId }); diff --git a/packages/app/src/hooks/managePool/useMemberManagement.ts b/packages/app/src/hooks/managePool/useMemberManagement.ts index 3f39bdc9..ad3e19fe 100644 --- a/packages/app/src/hooks/managePool/useMemberManagement.ts +++ b/packages/app/src/hooks/managePool/useMemberManagement.ts @@ -14,10 +14,22 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem const provider = useEthersProvider({ chainId }); const signer = useEthersSigner({ chainId }); + // Fix 1: Guard the SDK memo to prevent runtime crashes during loading + const sdk = useMemo(() => { + if (!provider || !chainId) return null; + const chainIdString = chainId.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chainId as SupportedNetwork]; + return new GoodCollectiveSDK(chainIdString, provider as any, { network }); + }, [chainId, provider]); + const [memberInput, setMemberInput] = useState(''); const [memberError, setMemberError] = useState(null); + const [memberSuccess, setMemberSuccess] = useState(null); const [isAddingMembers, setIsAddingMembers] = useState(false); - const [isRemovingMember, setIsRemovingMember] = useState(false); + + // Track specific member being removed to prevent all buttons from spinning + const [removingMemberAddress, setRemovingMemberAddress] = useState(null); + const [managedMembers, setManagedMembers] = useState([]); const [totalMemberCount, setTotalMemberCount] = useState(null); @@ -26,7 +38,6 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return; } - // Use the pre-loaded member list from parent component if (initialMembers) { setManagedMembers(initialMembers); setTotalMemberCount(initialMembers.length); @@ -38,7 +49,7 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return Array.from( new Set( memberInput - .split(',') + .split(/[\n,]+/) .map((a) => a.trim()) .filter((a) => a.length > 0) .map((a) => a.toLowerCase()) @@ -46,7 +57,21 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem ); }, [memberInput]); - const validateMemberAddresses = (): string | null => { + // Fix 2: Clear success messages when the user types new input + useEffect(() => { + if (memberInput.trim() !== '') { + setMemberSuccess(null); + setMemberError(null); + } + }, [memberInput]); + + const clearStatus = () => { + setMemberError(null); + setMemberSuccess(null); + }; + + // Fix 3a: Remove arguments, rely strictly on internal parsed addresses + const validateAddresses = (): string | null => { if (!parsedMemberAddresses.length) { return 'Please enter at least one wallet address.'; } @@ -59,78 +84,90 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return null; }; + // Fix 3b: Removed "addressesToAdd" parameter to shrink the hook API const handleAddMembers = async () => { - setMemberError(null); - const error = validateMemberAddresses(); + clearStatus(); + const error = validateAddresses(); if (error) { setMemberError(error); return; } - if (!signer || !poolAddress || pooltype !== 'UBI' || !provider) { - setMemberError('Member management is currently supported for UBI pools only.'); + if (!signer || !poolAddress || !pooltype || !provider || !sdk) { + setMemberError('Pool management is not fully initialized.'); return; } - try { - setIsAddingMembers(true); + if (pooltype !== 'UBI' && pooltype !== 'DIRECT') { + setMemberError('Member management is currently supported for UBI and Direct Payments pools only.'); + return; + } - const chainIdString = chainId.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chainId as SupportedNetwork]; + // Fix 3c: Filter out addresses that are already in the pool to prevent contract reverts + const addressesToAdd = parsedMemberAddresses.filter( + (addr) => !managedMembers.some((m) => m.toLowerCase() === addr.toLowerCase()) + ); - const sdk = new GoodCollectiveSDK(chainIdString, provider, { network }); + if (addressesToAdd.length === 0) { + setMemberError('All entered addresses are already members of this pool.'); + return; + } + + try { + setIsAddingMembers(true); + const extraData = addressesToAdd.map(() => '0x'); // Empty bytes for extraData - // Use SDK method to add members - for (const addr of parsedMemberAddresses) { - const tx = await sdk.addUBIPoolMember(signer, poolAddress, addr); - await tx.wait(); - } + const tx = await sdk.addPoolMembers(signer as any, poolAddress, addressesToAdd, extraData); + await tx.wait(); - // Optimistically bump the total on-chain member count - setTotalMemberCount((prev) => (prev ?? 0) + parsedMemberAddresses.length); + setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length); setManagedMembers((prev) => { const next = new Set(prev.map((a) => a.toLowerCase())); - parsedMemberAddresses.forEach((a) => next.add(a)); + addressesToAdd.forEach((a) => next.add(a)); return Array.from(next); }); setMemberInput(''); + setMemberSuccess(`Successfully added ${addressesToAdd.length} members.`); } catch (e: any) { setMemberError(e?.reason || e?.message || 'Failed to add members.'); + setMemberSuccess(null); } finally { setIsAddingMembers(false); } }; const handleRemoveMember = async (member: string) => { - if (!signer || !poolAddress || pooltype !== 'UBI' || !provider) { - setMemberError('Member management is currently supported for UBI pools only.'); + clearStatus(); + + if (!signer || !poolAddress || !provider || !sdk) { + setMemberError('Pool management is not fully initialized.'); return; } - try { - setIsRemovingMember(true); - - const chainIdString = chainId.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chainId as SupportedNetwork]; + if (pooltype !== 'UBI') { + setMemberError('Member removal is currently supported for UBI pools only.'); + return; + } - const sdk = new GoodCollectiveSDK(chainIdString, provider, { network }); + try { + setRemovingMemberAddress(member); - // Use SDK method to remove member - const tx = await sdk.removeUBIPoolMember(signer, poolAddress, member); + const tx = await sdk.removeUBIPoolMember(signer as any, poolAddress, member); await tx.wait(); - // Optimistically decrease the total on-chain member count setTotalMemberCount((prev) => { if (prev === null) return prev; return prev > 0 ? prev - 1 : 0; }); setManagedMembers((prev) => prev.filter((m) => m.toLowerCase() !== member.toLowerCase())); + setMemberSuccess(`Successfully removed member: ${member}`); } catch (e: any) { setMemberError(e?.reason || e?.message || 'Failed to remove member.'); + setMemberSuccess(null); } finally { - setIsRemovingMember(false); + setRemovingMemberAddress(null); } }; @@ -138,11 +175,13 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem memberInput, setMemberInput, memberError, + memberSuccess, isAddingMembers, - isRemovingMember, + removingMemberAddress, managedMembers, totalMemberCount, handleAddMembers, handleRemoveMember, + parsedMemberAddresses, }; }; diff --git a/packages/app/src/pages/ManageCollectivePage.tsx b/packages/app/src/pages/ManageCollectivePage.tsx index ebd54a81..949aabe7 100644 --- a/packages/app/src/pages/ManageCollectivePage.tsx +++ b/packages/app/src/pages/ManageCollectivePage.tsx @@ -1,4 +1,4 @@ -import { HStack, Input, ScrollView, Spinner, Switch, Text, VStack } from 'native-base'; +import { HStack, ScrollView, Spinner, Switch, Text, VStack, TextArea } from 'native-base'; import { useMemo, useState } from 'react'; import { useParams } from 'react-router-native'; import { useAccount } from 'wagmi'; @@ -71,7 +71,7 @@ const ManageCollectivePage = () => { }); const memberList = useMemo(() => { - return collective?.stewardCollectives.map((steward) => steward.steward) || []; + return collective?.stewardCollectives.map((steward: any) => steward.steward) || []; }, [collective?.stewardCollectives]); const memberManagement = useMemberManagement({ @@ -417,30 +417,37 @@ const ManageCollectivePage = () => { ) : ( - {/* Add New Member Section */} - + {/* Bulk Add Members Section */} + + Paste a list of wallet addresses, separated by commas or new lines. - New Member Wallet Address - - - - + Wallet Addresses +