Skip to content
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
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/components/DonateComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/hooks/managePool/useCoreSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
107 changes: 73 additions & 34 deletions packages/app/src/hooks/managePool/useMemberManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [memberSuccess, setMemberSuccess] = useState<string | null>(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<string | null>(null);

const [managedMembers, setManagedMembers] = useState<string[]>([]);
const [totalMemberCount, setTotalMemberCount] = useState<number | null>(null);

Expand All @@ -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);
Expand All @@ -38,15 +49,29 @@ 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())
)
);
}, [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.';
}
Expand All @@ -59,90 +84,104 @@ 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);
}
};

return {
memberInput,
setMemberInput,
memberError,
memberSuccess,
isAddingMembers,
isRemovingMember,
removingMemberAddress,
managedMembers,
totalMemberCount,
handleAddMembers,
handleRemoveMember,
parsedMemberAddresses,
};
};
65 changes: 38 additions & 27 deletions packages/app/src/pages/ManageCollectivePage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -417,30 +417,37 @@ const ManageCollectivePage = () => {
</VStack>
) : (
<VStack space={6}>
{/* Add New Member Section */}
<SectionCard title="Add New Member">
{/* Bulk Add Members Section */}
<SectionCard title="Add Members">
<WarningBox type="info">Paste a list of wallet addresses, separated by commas or new lines.</WarningBox>
<VStack space={2} marginTop={4}>
<Text fontWeight="600">New Member Wallet Address</Text>
<HStack space={4} alignItems="center">
<Input
flex={1}
placeholder="0x..."
value={memberManagement.memberInput}
onChangeText={memberManagement.setMemberInput}
autoCapitalize="none"
borderRadius={8}
/>
<ActionButton
onPress={memberManagement.handleAddMembers}
isLoading={memberManagement.isAddingMembers}
isDisabled={memberManagement.isAddingMembers}
text={memberManagement.isAddingMembers ? 'Adding Member...' : 'Add Member'}
bg="goodPurple.500"
textColor="white"
borderRadius={12}
/>
</HStack>
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
autoCompleteType={undefined}
placeholder={`0xabc...123, 0xdef...456\n0xghi...789`}
value={memberManagement.memberInput}
onChangeText={memberManagement.setMemberInput}
autoCapitalize="none"
borderRadius={8}
h={120} // Set a fixed height for the textarea
/>
<Text fontSize="xs" color="gray.500">
{memberManagement.parsedMemberAddresses.length > 0
? `Parsed ${memberManagement.parsedMemberAddresses.length} unique addresses.`
: 'Enter addresses separated by commas or new lines.'}
</Text>
<ActionButton
onPress={memberManagement.handleAddMembers}
isLoading={memberManagement.isAddingMembers}
isDisabled={memberManagement.isAddingMembers || memberManagement.parsedMemberAddresses.length === 0}
text={memberManagement.isAddingMembers ? 'Adding Members...' : 'Add Members'}
bg="goodPurple.500"
textColor="white"
borderRadius={12}
width="100%"
/>
<StatusMessage type="error" message={memberManagement.memberError} />
<StatusMessage type="success" message={memberManagement.memberSuccess} /> {/* Add success message */}
</VStack>
</SectionCard>

Expand Down Expand Up @@ -484,9 +491,13 @@ const ManageCollectivePage = () => {
</Text>
<ActionButton
onPress={() => memberManagement.handleRemoveMember(member)}
isLoading={memberManagement.isRemovingMember}
isDisabled={memberManagement.isRemovingMember || memberManagement.isAddingMembers}
text={memberManagement.isRemovingMember ? 'Removing Member...' : 'Remove Member'}
isLoading={memberManagement.removingMemberAddress === member}
isDisabled={
memberManagement.removingMemberAddress !== null || memberManagement.isAddingMembers
}
text={
memberManagement.removingMemberAddress === member ? 'Removing Member...' : 'Remove Member'
}
bg="red.500"
textColor="white"
borderRadius={12}
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

194 changes: 115 additions & 79 deletions packages/contracts/deployments/development-celo/DirectPaymentsPool.json

Large diffs are not rendered by default.

186 changes: 108 additions & 78 deletions packages/contracts/deployments/development-celo/UBIPool.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const config: HardhatUserConfig = {
},
'development-celo': {
chainId: 42220,
url: `https://forno.celo.org`,
url: `https://rpc.ankr.com/celo`,
gasPrice: 25.1e9,
accounts: {
mnemonic,
Expand Down
Loading
Loading