updated create-pool and implemented bulk addMember#318
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
Welcome.tsx, the terms of use are rendered as two separate clickableTextelements with identicalonPresshandlers; consider consolidating this into a single clearly-labeled touch target to reduce duplication and improve accessibility/UX. - The
projectIdgeneration inCreatePoolProviderusesreplace(' ', '/'), which only replaces the first space; if multiple spaces are expected, consider using a global regex orsplit/jointo normalize the name consistently. - In
usePoolConfigurationValidation, the comparisonmembers.length > maximumMemberswill silently do nothing whenmaximumMembersis undefined; if this check should only run when a max is explicitly set, consider guarding it with an explicitmaximumMembers != nullto make the intent clearer.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `Welcome.tsx`, the terms of use are rendered as two separate clickable `Text` elements with identical `onPress` handlers; consider consolidating this into a single clearly-labeled touch target to reduce duplication and improve accessibility/UX.
- The `projectId` generation in `CreatePoolProvider` uses `replace(' ', '/')`, which only replaces the first space; if multiple spaces are expected, consider using a global regex or `split/join` to normalize the name consistently.
- In `usePoolConfigurationValidation`, the comparison `members.length > maximumMembers` will silently do nothing when `maximumMembers` is undefined; if this check should only run when a max is explicitly set, consider guarding it with an explicit `maximumMembers != null` to make the intent clearer.
## Individual Comments
### Comment 1
<location path="packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx" line_range="105-113" />
<code_context>
return false;
}
+ const rawRecipients = form.poolRecipients?.trim() ?? '';
+ const memberAddresses = rawRecipients ? parseMemberAddresses(rawRecipients) : [];
+ if (rawRecipients) {
+ const memberError = validateMemberAddresses(memberAddresses);
+ if (memberError) {
+ console.error(memberError);
+ return false;
+ }
+ if (form.maximumMembers && memberAddresses.length > form.maximumMembers) {
+ console.error(`Members count (${memberAddresses.length}) exceeds maximum (${form.maximumMembers}).`);
+ return false;
</code_context>
<issue_to_address>
**suggestion:** Recipient validation logic is duplicated here and in `usePoolConfigurationValidation`, which risks drift.
Parsing and validating recipient addresses (including the `maximumMembers` length check) now happens both in this context and in `usePoolConfigurationValidation`. This duplication makes it easy for rules (regex, formatting, limits) to diverge. Consider centralizing this in a shared utility—building on `parseMemberAddresses`/`validateMemberAddresses`—and having both the hook and context call the same higher-level validator.
Suggested implementation:
```typescript
const sdk = new GoodCollectiveSDK(chainIdString, signer.provider as ethers.providers.Provider, { network });
const validatePoolRecipients = (
rawRecipients: string | undefined,
maximumMembers?: number,
): {
isValid: boolean;
memberAddresses: string[];
error?: string;
} => {
const trimmedRecipients = rawRecipients?.trim() ?? '';
if (!trimmedRecipients) {
return { isValid: true, memberAddresses: [] };
}
const memberAddresses = parseMemberAddresses(trimmedRecipients);
const memberError = validateMemberAddresses(memberAddresses);
if (memberError) {
return { isValid: false, memberAddresses, error: memberError };
}
if (maximumMembers && memberAddresses.length > maximumMembers) {
return {
isValid: false,
memberAddresses,
error: `Members count (${memberAddresses.length}) exceeds maximum (${maximumMembers}).`,
};
}
return { isValid: true, memberAddresses };
};
// Final form validation
```
```typescript
const recipientsValidation = validatePoolRecipients(form.poolRecipients, form.maximumMembers);
if (!recipientsValidation.isValid) {
if (recipientsValidation.error) {
console.error(recipientsValidation.error);
}
return false;
}
const { memberAddresses } = recipientsValidation;
```
To fully address the duplication with `usePoolConfigurationValidation`, you should:
1. Move `validatePoolRecipients` into a shared utility module (for example, `packages/app/src/utils/poolRecipients.ts`) so it is not tied to this context.
2. Update this file to import `validatePoolRecipients` from that shared module instead of defining it inline.
3. Update `usePoolConfigurationValidation` to call the shared `validatePoolRecipients` helper (and remove its local recipient parsing/validation logic), ensuring both the hook and this context share the same rules and error messages.
</issue_to_address>
### Comment 2
<location path="packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx" line_range="150-156" />
<code_context>
// Calculate cycle length based on claim frequency
const cycleLengthDays = form.claimFrequency && form.claimFrequency <= 7 ? 7 : form.claimFrequency || 1;
+ const onlyMembers = form.joinStatus === 'closed';
const ubiSettings: UBISettings = {
claimPeriodDays: ethers.BigNumber.from(form.claimFrequency || 1),
minActiveUsers: ethers.BigNumber.from(1),
maxClaimAmount: ethers.utils.parseEther(String(form.claimAmountPerWeek || 0)),
maxMembers: form.maximumMembers || form.expectedMembers || 100,
- onlyMembers: form.poolRecipients ? form.canNewMembersJoin ?? false : false,
+ onlyMembers,
cycleLengthDays: ethers.BigNumber.from(cycleLengthDays),
claimForEnabled: false,
</code_context>
<issue_to_address>
**question:** Revisiting the `onlyMembers` logic now that it’s based solely on `joinStatus`.
Previously, `onlyMembers` also depended on `form.poolRecipients` being set, but now it’s purely `joinStatus === 'closed'`. With this change, a pool that is `closed` and has no initial members will still have `onlyMembers = true`, which may prevent anyone from ever joining. If that’s not the intended behavior, consider conditioning `onlyMembers` on both `joinStatus` and the presence of initial members, or clearly define and enforce the expected behavior for empty-member pools.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Hi @L03TJ3 , I just made a video walkthrough showing the functional fixes |
L03TJ3
left a comment
There was a problem hiding this comment.
Homepage
-
homepage styles are not aligned with figma (no emphasising with bold text). also, test on mobile screens. the spacing of the words is weird.
-
the confirm terms of use should be a clickable link, not include the link (figma was just showing where the link should lead to. So: terms of use itself should be clickable, redirecting to > url
-
the checkbox should be confirmed before able to click on get started
create a pool screen
- the link is not clickable
|
Hi @L03TJ3 ,,,, |
|
Hey, sorry that I see this so late. but you produced a demo-video but not actually launched the pool? |
|
Hi @L03TJ3. I attempted to launch with multiple initial members, but the flow fails at bulk‑add with |
|
Okay. Lets do it like this. So we just have to test the bulk-add. you can pull in the changes of this pull-request (your other assigned item): #315 and include the changes here. |
|
Alright @L03TJ3 Makes sense |
|
I tested both bulk-add flows related to
Important Note on UBI Pools Results
Demo Video |
|
|
||
| const eligibleAmount = BigInt(claimAmountStr || '0'); | ||
|
|
||
| // Check if user has actually claimed by calling hasClaimed(address) on the contract |
| joinStatus?: 'closed' | 'open'; | ||
| }; | ||
|
|
||
| export const parseMemberAddresses = (input?: string): string[] => { |
There was a problem hiding this comment.
this is a direct replication of the localized logic in useMemberManagement.
Please make it global helpers and apply here and also in useMemberManagement
| return Array.from(new Set(normalized)); | ||
| }; | ||
|
|
||
| export const validateMemberAddresses = (members: string[]): string | null => { |
There was a problem hiding this comment.
there is viem.isAddress, no need for localized check
|
Updates made:
One open point on the member list refresh logic: Do we already have a dedicated subgraph source for current pool members that you want me to use here instead? |
|
The build errors have been resolved Also wanted to flag. my Telegram account has been temporarily frozen so I have no way to reach you there at the moment. GitHub is the best way to get me for now. Apologies for any inconvenience @L03TJ3 |
|
for security purposes we recently introduced that commits have to be signed. for your long list of commits the following command should work after you have setup the gpg signing: |
Address the four blockers raised on PR GoodDollar#318: 1. onlyMembers guard: block deploying a closed pool with zero initial members. Validation now errors on the poolRecipients field when joinStatus is 'closed' and no addresses are provided, and createPool re-checks the same invariant before deploy as defense in depth. Without this, a closed pool could deploy with onlyMembers=true and no members - permanently inaccessible. 2. Chain-aware uniqueness validator: replace the hardcoded mainnet IdentityV2 address (0xC361A6E67822a0EDc17D899227dd9FC50BD62F42) with getUniquenessValidatorAddress(chainId), which resolves the address from the @gooddollar/goodprotocol deployment manifest by chainId (and by REACT_APP_NETWORK on Celo mainnet for staging/dev/QA). Returns AddressZero for unknown chains so existing isZeroAddress checks treat it as 'no uniqueness check'. Used in both PoolConfiguration eligibility checks and CreatePoolContext deploy. 3. Drop fragile status[7] tuple indexing: useMemberManagement now reads status.membersCount by name. The typechain UBIPool.status() return type already exposes membersCount as a named field, so the 'as unknown as { membersCount?: BigNumber }' cast and the status[7] positional fallback were both redundant and would have broken silently if the contract signature changed. 4. Recoverable two-step create flow: when the pool deploys but the follow-up addPoolMembers tx fails, throw a typed PoolMembersAddError carrying the deployed address. ReviewLaunch detects it and shows a dedicated 'POOL CREATED, MEMBERS NOT ADDED' modal with the failure reason and a button that navigates to /collective/<address>/manage so the user can retry adding members instead of being stranded with a deployed but empty pool.
Per PR review: the link previously rendered the full URL (https://ubi.gd/GoodBuildersTG) as the visible link text. Replace it with the natural phrase 'reach out here' so the surrounding sentence reads cleanly and the URL is only present in the href.
Per PR review: membersValidator was still written as a literal ethers.constants.AddressZero at two call sites (PoolConfiguration eligibility preview and CreatePoolContext deploy), so it could drift between the two and could not be configured per-chain without a code change. Add getMembersValidatorAddress(chainId) in models/constants.ts and use it at both call sites. The default return value remains AddressZero because that is the protocol-correct default - the pool contracts explicitly skip the validator call when the address is zero (see UBIPool.sol:286-288 and DirectPaymentsPool.sol:216-218), and there is no canonical members-validator deployment in either the goodprotocol or contracts deployment manifests. The helper supports a per-chain env override (REACT_APP_MEMBERS_VALIDATOR_<chainId>) with viem-style isAddress validation, so a future deployment can plug in a custom IMembersValidator without touching the create-pool flow. This mirrors the chain-aware pattern already used for getUniquenessValidatorAddress and removes the duplicated literal that the reviewer flagged.
Was 'development-celo', which made anything reading env.REACT_APP_NETWORK without an override resolve to the dev GoodDollar deployment. After making the uniqueness validator chain-aware that meant local builds without an .env hit the dev Identity contract, where prod-verified addresses aren't whitelisted. Most other call sites already had a '|| development-celo' fallback so this only affects the no-env case.
Upstream/master keeps this at development-celo by design - production deploys override it via env vars at build time. The previous switch to production-celo here was meant to make the local dev server work without an .env but diverged from upstream conventions. Going back; for local production testing set REACT_APP_NETWORK=production-celo in a local .env.
IdentityV2 is a chain-level singleton in practice - all GoodDollar deployment variants on Celo mainnet (production / staging / development / pre-production) gate whitelist state off the production registry. The previous helper re-ordered candidates by REACT_APP_NETWORK, which meant locally without an .env it would resolve to the dev IdentityV2 (where prod-verified addresses aren't whitelisted) and silently fail eligibility. Drop the env re-ordering. Map chainId 42220 -> production-celo and 44787 -> alfajores directly. Same runtime behavior on Celo as the old hardcoded 0xC361... literal, but expressed through the deployment manifest so the hardcoded-address concern stays addressed.
Reviewer pointed out envs are managed through defaults.ts, not ad-hoc REACT_APP_MEMBERS_VALIDATOR_<chainId> names per call site. The MVP doesn't expose a custom members validator anyway - the plan is to add a form field on the create-pool flow later, falling back to 0x0 when the user leaves it blank. Until then this stays at AddressZero. Keep the helper as a single source of truth so the eligibility preview in PoolConfiguration and the deploy-time settings in CreatePoolContext read from one place instead of duplicating the literal.
Old dev-v1.0.13 endpoint is no longer reachable; the live subgraph is served at the unpinned 'version/latest' alias (confirmed in PR review). Without this the homepage stats render as 0 and managePool's member load silently fails because every query 404s.
The hook was replaying RoleGranted/RoleRevoked logs in the last 9500 blocks (~5h on Celo) to rebuild the member set, so anything older silently dropped off. Pool data already comes through the subgraph with the steward list attached, so take that as the steady-state source and seed managedMembers from it. Post-tx refresh stays on chain for UBI via sdk.getUBIPoolMembers so the list updates before the subgraph indexes the events. For DirectPayments pools we just merge the locally-added addresses and let the next subgraph poll reconcile - there's no SDK helper for that pool type. Also fixes a typo guard that was checking pooltype === 'DIRECT' when the subgraph emits 'DirectPayments', so the DirectPayments branch was effectively dead.
Fixed h={120} squashed the textarea on small screens. Switch to minH
so it can expand as the user pastes more addresses, with totalLines
giving native-base a sensible starting size.
isManager is now boolean | undefined where undefined means 'not yet
resolved for the current inputs'. checkingRole is derived from that
plus an in-flight flag, which removes the separate hasResolvedRoleCheck
boolean, the lastResolvedRoleKeyRef, the roleKey join string, and the
preliminary useEffect that only existed to reset the ref.
External return shape stays { isManager: boolean, checkingRole: boolean }
so the callers in ViewCollective and ManageCollectivePage don't change.
The 'don't flash non-manager UI before the on-chain check completes'
behavior the old code was trying to preserve is now expressed by the
checkingRole derivation directly.
When the second tx fails the user has to retype the same addresses on the manage page, which is painful for any non-trivial member list. Carry the addresses through PoolMembersAddError and render them in a scrollable box on the recovery modal with a copy-to-clipboard button. Clipboard falls back to a hidden textarea + execCommand for older webviews where navigator.clipboard isn't available.
3a7e01c had the hook read collective.stewardCollectives from the subgraph, but that entity is only created inside the UBIClaimed and NFTClaimed handlers (packages/subgraph/src/mappings/ubipool.ts and mappings/pool.ts). It records claim history, not current membership, so: - members added via addPoolMembers but who haven't claimed yet are missing from the list - members who were removed but claimed in the past stay in the list That meant the manage UI would, after a subgraph refresh, drop the freshly-added members from the create-pool flow. Switch to sdk.getUBIPoolMembers, which is the helper Lewis pointed to in the review. It scans MEMBER_ROLE grant/revoke events on the pool contract and reflects actual membership. DirectPayments pools have no equivalent SDK helper today; the manage UI already restricts most editing to UBI, so we keep an empty/local list there and let the optimistic merge cover add flows.
MembersSectionProps.onValidateRecipients was typed as () => Promise<void> but the implementation (validateRecipientEligibility) returns Promise<boolean>. The boolean return value is actually used to gate submission, so fix the prop type to match. CreatePoolContext was passing poolSettings.uniquenessValidator and membersValidator directly to assessPoolMemberEligibility, but UBIPoolSettings types those fields as PromiseOrValue<string> (SDK contract type) while the function expects string. Since we set them from our string-returning helpers (getUniquenessValidatorAddress and getMembersValidatorAddress), a safe cast is correct here.
8b178ba to
90da5ce
Compare
|
Done @L03TJ3 , all my commits are now showing up with the Verified badge. Thank you!! |
#317
Title: Create‑pool copy refresh + bulk add members
Summary:
Testing:
Summary by Sourcery
Refresh the create-pool flow copy and add support for specifying and bulk-adding initial pool members.
New Features:
Enhancements: