add: ui functionality for bulk add + deployed dev env contracts#315
add: ui functionality for bulk add + deployed dev env contracts#315
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
validatePoolSettings,UBIPoolSettingsis referenced but never imported/defined andsettings.validEventsshould likely bedirectPaymentSettings.validEvents, which will currently cause type or runtime errors. - The
poolTypediscriminator invalidatePoolSettingsuses the string'DirectPayments', which is inconsistent with the'DIRECT'/'UBI'values used elsewhere (e.g., inuseMemberManagement), so this branch may never execute as intended. - In
ManageCollectivePage, theTextAreaplaceholder uses the HTML entity for a newline; in this JSX/NativeBase context you likely want an actual newline ("\n"or{"\n"}) to avoid rendering the literal entity text.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `validatePoolSettings`, `UBIPoolSettings` is referenced but never imported/defined and `settings.validEvents` should likely be `directPaymentSettings.validEvents`, which will currently cause type or runtime errors.
- The `poolType` discriminator in `validatePoolSettings` uses the string `'DirectPayments'`, which is inconsistent with the `'DIRECT'`/`'UBI'` values used elsewhere (e.g., in `useMemberManagement`), so this branch may never execute as intended.
- In `ManageCollectivePage`, the `TextArea` placeholder uses the HTML entity ` ` for a newline; in this JSX/NativeBase context you likely want an actual newline (`"\n"` or `{"\n"}`) to avoid rendering the literal entity text.
## Individual Comments
### Comment 1
<location> `packages/app/src/utils/validation.ts:37` </location>
<code_context>
+ if (directPaymentSettings.validEvents.length === 0) {
+ errors.validEvents = 'At least one valid event type is required';
+ }
+ if (directPaymentSettings.rewardPerEvent.length !== settings.validEvents.length) {
+ errors.rewardPerEvent = 'Must specify reward amount for each event type';
+ }
</code_context>
<issue_to_address>
**issue (bug_risk):** The reference to `settings.validEvents` is likely a bug and should use `directPaymentSettings.validEvents` instead.
In `validatePoolSettings`, `settings` is not in scope, so this line will fail at type-check or runtime. This should compare `rewardPerEvent.length` to `directPaymentSettings.validEvents.length` instead.
</issue_to_address>
### Comment 2
<location> `packages/app/src/pages/ManageCollectivePage.tsx:426` </location>
<code_context>
- </HStack>
+ <Text fontWeight="600">Wallet Addresses</Text>
+ <TextArea
+ placeholder="0xabc...123, 0xdef...456 0xghi...789"
+ value={memberManagement.memberInput}
+ onChangeText={memberManagement.setMemberInput}
</code_context>
<issue_to_address>
**issue (bug_risk):** The use of ` ` in the placeholder likely won’t render as a newline in React Native and may show the raw entity.
Because React Native doesn’t parse HTML entities in JSX strings, this will likely render the literal ` ` instead of a newline. To get a line break in the placeholder, use an actual newline in a template string or `{"\n"}` between the examples (e.g., `"0xabc...123, 0xdef...456\n0xghi...789"`).
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| </HStack> | ||
| <Text fontWeight="600">Wallet Addresses</Text> | ||
| <TextArea | ||
| placeholder="0xabc...123, 0xdef...456 0xghi...789" |
There was a problem hiding this comment.
issue (bug_risk): The use of in the placeholder likely won’t render as a newline in React Native and may show the raw entity.
Because React Native doesn’t parse HTML entities in JSX strings, this will likely render the literal instead of a newline. To get a line break in the placeholder, use an actual newline in a template string or {"\n"} between the examples (e.g., "0xabc...123, 0xdef...456\n0xghi...789").
6c2b17c to
cceb0e2
Compare
|
@edehvictor Hey what is the status of this? |
|
Gm @L03TJ3, I've fixed it. |
|
@edehvictor did you test the PR? any comments or confirmation about the flow working? its a code-review + qa type request. |
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- The memoized
GoodCollectiveSDKis created even whenproviderorchainIdmight be undefined/invalid; consider guarding theuseMemo(or returningnulland checking before use) so that SDK construction doesn’t rely onas anyand fail at runtime during intermediate loading states. memberSuccessmessages are only cleared on add/remove actions and not when the input changes, which can leave stale success messages visible while the user is preparing a new operation; consider resettingmemberSuccesswhenmemberInputorparsedMemberAddresseschanges.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The memoized `GoodCollectiveSDK` is created even when `provider` or `chainId` might be undefined/invalid; consider guarding the `useMemo` (or returning `null` and checking before use) so that SDK construction doesn’t rely on `as any` and fail at runtime during intermediate loading states.
- `memberSuccess` messages are only cleared on add/remove actions and not when the input changes, which can leave stale success messages visible while the user is preparing a new operation; consider resetting `memberSuccess` when `memberInput` or `parsedMemberAddresses` changes.
## Individual Comments
### Comment 1
<location path="packages/app/src/hooks/managePool/useMemberManagement.ts" line_range="91-94" />
<code_context>
- const chainIdString = chainId.toString() as `${SupportedNetwork}`;
- const network = SupportedNetworkNames[chainId as SupportedNetwork];
+ const extraData = addressesToAdd.map(() => '0x'); // Empty bytes for extraData
- const sdk = new GoodCollectiveSDK(chainIdString, provider, { network });
-
- // Use SDK method to add members
- for (const addr of parsedMemberAddresses) {
- const tx = await sdk.addUBIPoolMember(signer, poolAddress, addr);
- await tx.wait();
- }
+ // Use the SDK's addPoolMembers function for bulk addition
+ const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData);
+ await tx.wait();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider filtering out already-managed members before calling addPoolMembers.
Currently all `addressesToAdd` are sent to `sdk.addPoolMembers`, and only after success are they merged into `managedMembers`. If the contract rejects duplicates or treats them as no-ops, this could cause a revert or unnecessary gas use. Consider pre-filtering `addressesToAdd` against `managedMembers` (using the same lowercasing) so only truly new addresses are passed to the contract.
</issue_to_address>
### Comment 2
<location path="packages/app/src/pages/ManageCollectivePage.tsx" line_range="424-434" />
<code_context>
- />
- </HStack>
+ <Text fontWeight="600">Wallet Addresses</Text>
+ <TextArea
+ autoCompleteType={''}
+ placeholder="0xabc...123, 0xdef...456 0xghi...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">
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid using an empty string for autoCompleteType and adjust multiline placeholder formatting.
1) Instead of `autoCompleteType={''}`, omit the prop when you don’t want autocomplete; an empty string can conflict with the prop’s union type and cause platform warnings.
2) ` ` in the placeholder will render as literal text in React/React Native. If you want a visible line break, use a template literal or `{'
'}` in the string instead.
```suggestion
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
placeholder={`0xabc...123, 0xdef...456
0xghi...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">
```
</issue_to_address>
### Comment 3
<location path="packages/app/src/hooks/managePool/useMemberManagement.ts" line_range="62" />
<code_context>
return null;
};
- const handleAddMembers = async () => {
+ const handleAddMembers = async (addressesToAdd: string[]) => {
setMemberError(null);
</code_context>
<issue_to_address>
**issue (complexity):** Consider keeping parsing and validation internal to the hook, centralizing status and SDK readiness handling to shrink the hook’s API and simplify its control flow.
You can reduce the hook’s surface area and control flow without changing behavior by tightening the boundary around parsing/validation and simplifying status + SDK handling.
### 1. Keep parsing/validation internal to the hook
`handleAddMembers` is always used with `parsedMemberAddresses`. You don’t gain real flexibility from the `addressesToAdd` argument + `validateAddresses(addresses: string[])`, but you do couple the caller to internal parsing.
You can:
- Make `validateAddresses` parameterless and work on `parsedMemberAddresses`.
- Make `handleAddMembers` parameterless and call `validateAddresses` + use `parsedMemberAddresses` internally.
- Optionally keep `parsedMemberAddresses` in the return only for display.
```ts
const validateAddresses = (): string | null => {
if (!parsedMemberAddresses.length) {
return 'Please enter at least one wallet address.';
}
const invalid = parsedMemberAddresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr));
if (invalid) {
return `Invalid wallet address: ${invalid}`;
}
return null;
};
const handleAddMembers = async () => {
clearStatus();
const error = validateAddresses();
if (error) {
setMemberError(error);
return;
}
if (!signer || !poolAddress || !pooltype || !provider) {
setMemberError('Pool management is not fully initialized.');
return;
}
if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
return;
}
const addressesToAdd = parsedMemberAddresses;
try {
setIsAddingMembers(true);
const extraData = addressesToAdd.map(() => '0x');
const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData);
await tx.wait();
setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length);
setManagedMembers((prev) => {
const next = new Set(prev.map((a) => a.toLowerCase()));
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);
}
};
```
And in the return value, if you don’t actually need to pass addresses into `handleAddMembers` from outside, you can avoid exporting `parsedMemberAddresses` or keep it clearly read-only:
```ts
return {
memberInput,
setMemberInput,
memberError,
memberSuccess,
isAddingMembers,
isRemovingMember,
managedMembers,
totalMemberCount,
handleAddMembers, // no args
handleRemoveMember,
parsedMemberAddresses, // optional, for display only
};
```
### 2. Factor out status clearing
You clear `memberError` and `memberSuccess` in multiple places. A tiny helper removes that noise and makes the intent explicit:
```ts
const clearStatus = () => {
setMemberError(null);
setMemberSuccess(null);
};
const handleAddMembers = async () => {
clearStatus();
// ...
};
const handleRemoveMember = async (member: string) => {
clearStatus();
// ...
};
```
### 3. Optional: centralize SDK readiness check
Right now you have:
- `sdk` memoized on `chainId` and `provider`
- Repeated guards on `!signer || !poolAddress || !pooltype || !provider`
You can keep memoization but centralize the readiness check to avoid duplicating the “not initialized” logic:
```ts
const getSdkOrFail = () => {
if (!signer || !poolAddress || !pooltype || !provider) {
throw new Error('Pool management is not fully initialized.');
}
return sdk;
};
const handleAddMembers = async () => {
clearStatus();
const error = validateAddresses();
if (error) {
setMemberError(error);
return;
}
if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
return;
}
try {
setIsAddingMembers(true);
const sdkInstance = getSdkOrFail();
// use sdkInstance here
} catch (e: any) {
if (e.message === 'Pool management is not fully initialized.') {
setMemberError(e.message);
return;
}
// existing error handling
} finally {
setIsAddingMembers(false);
}
};
```
This keeps the new functionality (bulk add, success messages, DIRECT pools, memoized SDK) intact while reducing the hook’s API surface and internal branching.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const extraData = addressesToAdd.map(() => '0x'); // Empty bytes for extraData | ||
|
|
||
| const sdk = new GoodCollectiveSDK(chainIdString, provider, { network }); | ||
|
|
||
| // Use SDK method to add members | ||
| for (const addr of parsedMemberAddresses) { | ||
| const tx = await sdk.addUBIPoolMember(signer, poolAddress, addr); | ||
| await tx.wait(); | ||
| } | ||
| // Use the SDK's addPoolMembers function for bulk addition | ||
| const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData); |
There was a problem hiding this comment.
suggestion (bug_risk): Consider filtering out already-managed members before calling addPoolMembers.
Currently all addressesToAdd are sent to sdk.addPoolMembers, and only after success are they merged into managedMembers. If the contract rejects duplicates or treats them as no-ops, this could cause a revert or unnecessary gas use. Consider pre-filtering addressesToAdd against managedMembers (using the same lowercasing) so only truly new addresses are passed to the contract.
| <Text fontWeight="600">Wallet Addresses</Text> | ||
| <TextArea | ||
| autoCompleteType={''} | ||
| placeholder="0xabc...123, 0xdef...456 0xghi...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"> |
There was a problem hiding this comment.
suggestion (bug_risk): Avoid using an empty string for autoCompleteType and adjust multiline placeholder formatting.
- Instead of
autoCompleteType={''}, omit the prop when you don’t want autocomplete; an empty string can conflict with the prop’s union type and cause platform warnings. in the placeholder will render as literal text in React/React Native. If you want a visible line break, use a template literal or{' '}in the string instead.
| <Text fontWeight="600">Wallet Addresses</Text> | |
| <TextArea | |
| autoCompleteType={''} | |
| placeholder="0xabc...123, 0xdef...456 0xghi...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"> | |
| <Text fontWeight="600">Wallet Addresses</Text> | |
| <TextArea | |
| placeholder={`0xabc...123, 0xdef...456 | |
| 0xghi...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"> |
| return null; | ||
| }; | ||
|
|
||
| const handleAddMembers = async () => { |
There was a problem hiding this comment.
issue (complexity): Consider keeping parsing and validation internal to the hook, centralizing status and SDK readiness handling to shrink the hook’s API and simplify its control flow.
You can reduce the hook’s surface area and control flow without changing behavior by tightening the boundary around parsing/validation and simplifying status + SDK handling.
1. Keep parsing/validation internal to the hook
handleAddMembers is always used with parsedMemberAddresses. You don’t gain real flexibility from the addressesToAdd argument + validateAddresses(addresses: string[]), but you do couple the caller to internal parsing.
You can:
- Make
validateAddressesparameterless and work onparsedMemberAddresses. - Make
handleAddMembersparameterless and callvalidateAddresses+ useparsedMemberAddressesinternally. - Optionally keep
parsedMemberAddressesin the return only for display.
const validateAddresses = (): string | null => {
if (!parsedMemberAddresses.length) {
return 'Please enter at least one wallet address.';
}
const invalid = parsedMemberAddresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr));
if (invalid) {
return `Invalid wallet address: ${invalid}`;
}
return null;
};
const handleAddMembers = async () => {
clearStatus();
const error = validateAddresses();
if (error) {
setMemberError(error);
return;
}
if (!signer || !poolAddress || !pooltype || !provider) {
setMemberError('Pool management is not fully initialized.');
return;
}
if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
return;
}
const addressesToAdd = parsedMemberAddresses;
try {
setIsAddingMembers(true);
const extraData = addressesToAdd.map(() => '0x');
const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData);
await tx.wait();
setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length);
setManagedMembers((prev) => {
const next = new Set(prev.map((a) => a.toLowerCase()));
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);
}
};And in the return value, if you don’t actually need to pass addresses into handleAddMembers from outside, you can avoid exporting parsedMemberAddresses or keep it clearly read-only:
return {
memberInput,
setMemberInput,
memberError,
memberSuccess,
isAddingMembers,
isRemovingMember,
managedMembers,
totalMemberCount,
handleAddMembers, // no args
handleRemoveMember,
parsedMemberAddresses, // optional, for display only
};2. Factor out status clearing
You clear memberError and memberSuccess in multiple places. A tiny helper removes that noise and makes the intent explicit:
const clearStatus = () => {
setMemberError(null);
setMemberSuccess(null);
};
const handleAddMembers = async () => {
clearStatus();
// ...
};
const handleRemoveMember = async (member: string) => {
clearStatus();
// ...
};3. Optional: centralize SDK readiness check
Right now you have:
sdkmemoized onchainIdandprovider- Repeated guards on
!signer || !poolAddress || !pooltype || !provider
You can keep memoization but centralize the readiness check to avoid duplicating the “not initialized” logic:
const getSdkOrFail = () => {
if (!signer || !poolAddress || !pooltype || !provider) {
throw new Error('Pool management is not fully initialized.');
}
return sdk;
};
const handleAddMembers = async () => {
clearStatus();
const error = validateAddresses();
if (error) {
setMemberError(error);
return;
}
if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
return;
}
try {
setIsAddingMembers(true);
const sdkInstance = getSdkOrFail();
// use sdkInstance here
} catch (e: any) {
if (e.message === 'Pool management is not fully initialized.') {
setMemberError(e.message);
return;
}
// existing error handling
} finally {
setIsAddingMembers(false);
}
};This keeps the new functionality (bulk add, success messages, DIRECT pools, memoized SDK) intact while reducing the hook’s API surface and internal branching.
Description
#314
Summary by Sourcery
Add bulk member management support in the app and deploy updated development Celo contracts.
New Features:
Enhancements:
Build:
Deployment: