Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, FormControl, Input, Text, TextArea, VStack, WarningOutlineIcon, Pressable } from 'native-base';
import { Box, FormControl, Input, Link, Pressable, Text, TextArea, VStack, WarningOutlineIcon } from 'native-base';
import { useEffect, useState } from 'react';

import { useCreatePool } from '../../../hooks/useCreatePool/useCreatePool';
Expand Down Expand Up @@ -216,13 +216,13 @@ const GetStarted = ({}: {}) => {
<FormControl.HelperText>
<Text style={styles.helperText} color="goodGrey.400">
Provide image URL for your cover photo (1200x400px recommended). Upload to{' '}
<Text style={styles.linkText} onPress={() => window.open('https://ipfs.io/', '_blank')}>
<Link href="https://ipfs.io/" isExternal _text={styles.linkText}>
IPFS
</Text>{' '}
</Link>{' '}
(free tier) or{' '}
<Text style={styles.linkText} onPress={() => window.open('https://cloudinary.com/', '_blank')}>
<Link href="https://cloudinary.com/" isExternal _text={styles.linkText}>
Cloudinary
</Text>{' '}
</Link>{' '}
for hosting.
</Text>
</FormControl.HelperText>
Expand Down Expand Up @@ -428,5 +428,6 @@ const styles = {
color: 'goodPurple.400',
textDecorationLine: 'underline',
fontWeight: '600',
cursor: 'pointer',
},
} as const;
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { Box, Text, VStack } from 'native-base';
import { useEffect, useState } from 'react';
import { useAccount, useEnsName } from 'wagmi';
import { ethers } from 'ethers';
import { useCreatePool } from '../../../hooks/useCreatePool/useCreatePool';
import {
usePoolConfigurationValidation,
PoolConfigurationFormData,
validatePoolRecipients,
} from '../../../hooks/useCreatePool/usePoolConfigurationValidation';
import { useScreenSize } from '../../../theme/hooks';
import MembersSection from './pool-configs/MembersSection';
import PayoutSettingsSection from './pool-configs/PayoutSettingsSection';
import PoolManagerFeeSection from './pool-configs/PoolManagerFeeSection';
import NavigationButtons from '../NavigationButtons';
import ClaimFrequencySection from './pool-configs/ClaimFrequencySection';
import { useEthersProvider } from '../../../hooks/useEthers';
import { assessPoolMemberEligibility, formatSkippedMembersMessage } from '../../../lib/poolMemberEligibility';

const PoolConfiguration = () => {
const { form, nextStep, submitPartial, previousStep } = useCreatePool();
const { isDesktopView } = useScreenSize();
const { address } = useAccount();
const { address, chain } = useAccount();
const { validate, errors } = usePoolConfigurationValidation();
const chainId = chain?.id ?? 42220;
const provider = useEthersProvider({ chainId });

const [poolManagerFeeType, setPoolManagerFeeType] = useState<'default' | 'custom'>(
form.poolManagerFeeType ?? 'default'
Expand All @@ -31,6 +37,8 @@ const PoolConfiguration = () => {
const [claimAmountPerWeek, setClaimAmountPerWeek] = useState(form.claimAmountPerWeek ?? 10);
const [expectedMembers, setExpectedMembers] = useState(form.expectedMembers ?? 1);
const [customClaimFrequency, setCustomClaimFrequency] = useState(form.customClaimFrequency ?? 1);
const [recipientEligibilityError, setRecipientEligibilityError] = useState<string | undefined>();
const [isCheckingRecipients, setIsCheckingRecipients] = useState(false);
const { data: ensName } = useEnsName({ address: managerAddress as `0x${string}`, chainId: 1 });

useEffect(() => {
Expand All @@ -39,6 +47,10 @@ const PoolConfiguration = () => {
}
}, [maximumMembers, expectedMembers]);

useEffect(() => {
setRecipientEligibilityError(undefined);
}, [poolRecipients, maximumMembers]);

const handleValidate = () => {
const formData: PoolConfigurationFormData = {
poolRecipients,
Expand All @@ -54,8 +66,49 @@ const PoolConfiguration = () => {
return validate(formData);
};

const submitForm = () => {
if (handleValidate()) {
const validateRecipientEligibility = async (): Promise<boolean> => {
const recipientsValidation = validatePoolRecipients(poolRecipients, maximumMembers);
if (!recipientsValidation.isValid || recipientsValidation.memberAddresses.length === 0) {
setRecipientEligibilityError(undefined);
return recipientsValidation.isValid;
}

if (!provider || !managerAddress) {
return true;
}

try {
setIsCheckingRecipients(true);
const { skippedAddresses, validAddresses } = await assessPoolMemberEligibility({
provider,
addresses: recipientsValidation.memberAddresses,
uniquenessValidator: '0xC361A6E67822a0EDc17D899227dd9FC50BD62F42',
membersValidator: ethers.constants.AddressZero,
operatorAddress: managerAddress.toLowerCase(),
});

if (validAddresses.length !== recipientsValidation.memberAddresses.length) {
const skippedSummary = formatSkippedMembersMessage(skippedAddresses);
setRecipientEligibilityError(
skippedSummary
? `Some initial members are not eligible: ${skippedSummary}.`
: 'Some initial members cannot be added to this pool.'
);
return false;
}

setRecipientEligibilityError(undefined);
return true;
} finally {
setIsCheckingRecipients(false);
}
};

const submitForm = async () => {
const isValid = handleValidate();
const hasEligibleRecipients = await validateRecipientEligibility();

if (isValid && hasEligibleRecipients) {
submitPartial({
poolManagerFeeType,
claimFrequency: claimFrequency === 2 ? customClaimFrequency : claimFrequency,
Expand Down Expand Up @@ -113,9 +166,14 @@ const PoolConfiguration = () => {
setMaximumMembers={setMaximumMembers}
joinStatus={joinStatus}
setJoinStatus={setJoinStatus}
poolRecipients={poolRecipients}
setPoolRecipients={setPoolRecipients}
onValidate={handleValidate}
onValidateRecipients={validateRecipientEligibility}
isCheckingRecipients={isCheckingRecipients}
errors={{
maximumMembers: errors.maximumMembers,
poolRecipients: errors.poolRecipients ?? recipientEligibilityError,
}}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useCreatePool } from '../../../hooks/useCreatePool/useCreatePool';
import { printAndParseSupportError } from '../../../hooks/useContractCalls/util';
import BaseModal from '../../modals/BaseModal';
import NavigationButtons from '../NavigationButtons';
import { Linking } from 'react-native';
import { formatSocialUrls } from '../../../lib/formatSocialUrls';

const SectionHeader = ({ title, onEdit }: { title: string; onEdit: () => void }) => (
<HStack alignItems="center">
Expand Down Expand Up @@ -198,16 +200,27 @@ const ReviewLaunch = () => {
<Label>Socials</Label>
<HStack space={2}>
{socials.map((social, index) => (
<Box
<Pressable
key={index}
backgroundColor="gray.100"
width={10}
height={10}
justifyContent="center"
alignItems="center"
borderRadius={4}>
<img width={24} src={social.icon} />
</Box>
onPress={() => {
const url = form[social.name as keyof typeof form];
if (typeof url === 'string') {
const formattedUrl = formatSocialUrls[social.name as keyof typeof formatSocialUrls](url);
if (formattedUrl) {
Linking.openURL(formattedUrl);
}
}
}}>
<Box
backgroundColor="gray.100"
width={10}
height={10}
justifyContent="center"
alignItems="center"
borderRadius={4}>
<img width={24} src={social.icon} />
</Box>
</Pressable>
))}
</HStack>
</VStack>
Expand Down Expand Up @@ -246,6 +259,12 @@ const ReviewLaunch = () => {
<StatRow label="Min Claim Amount" value={`${form.claimAmountPerWeek}G$`} />
<StatRow label="Expected Members" value={form.expectedMembers} />
<StatRow label="Amount To Fund" value={`${amountToFund}G$`} />
{form.poolRecipients && form.poolRecipients.trim() !== '' && (
<StatRow
label="Initial Members"
value={form.poolRecipients.split(/[\n,]/).filter((s) => s.trim() !== '').length}
/>
)}
</VStack>
</VStack>
</VStack>
Expand All @@ -255,6 +274,7 @@ const ReviewLaunch = () => {
onBack={() => previousStep()}
onNext={handleCreatePool}
nextText={isCreating ? 'Creating...' : 'Launch Pool'}
nextDisabled={isCreating}
marginTop={6}
containerStyle={undefined}
buttonWidth="140px"
Expand All @@ -279,10 +299,17 @@ const ReviewLaunch = () => {
openModal={approvePoolModalVisible}
onClose={() => setApprovePoolModalVisible(false)}
title="APPROVE POOL CREATION"
paragraphs={[
'To create your GoodCollective pool, sign with your wallet.',
'This will deploy your pool contract and make it available for members to join.',
]}
paragraphs={
form.poolRecipients && form.poolRecipients.trim() !== ''
? [
'To create your GoodCollective pool, sign with your wallet.',
'Note: You will be asked to sign TWO transactions. The first creates the pool, and the second adds your initial members.',
]
: [
'To create your GoodCollective pool, sign with your wallet.',
'This will deploy your pool contract and make it available for members to join.',
]
}
image={PhoneImg}
/>
</VStack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Box, FormControl, Input, Radio, Text, VStack, WarningOutlineIcon } from 'native-base';
import { Box, FormControl, Input, Radio, Text, TextArea, VStack, WarningOutlineIcon } from 'native-base';

interface MembersSectionProps {
maximumMembers: number;
setMaximumMembers: (value: number) => void;
joinStatus: 'closed' | 'open';
setJoinStatus: (value: 'closed' | 'open') => void;
poolRecipients: string;
setPoolRecipients: (value: string) => void;
onValidate: () => void;
onValidateRecipients: () => Promise<void>;
isCheckingRecipients: boolean;
errors: {
maximumMembers?: string;
poolRecipients?: string;
};
}

Expand All @@ -16,7 +21,11 @@ const MembersSection = ({
setMaximumMembers,
joinStatus,
setJoinStatus,
poolRecipients,
setPoolRecipients,
onValidate,
onValidateRecipients,
isCheckingRecipients,
errors,
}: MembersSectionProps) => {
return (
Expand Down Expand Up @@ -85,6 +94,42 @@ const MembersSection = ({
</Radio.Group>
</FormControl>
</VStack>

{/* Initial Members */}
<Box backgroundColor="white" padding={4} borderWidth={1} borderColor="gray.200" borderRadius={8}>
<FormControl isInvalid={!!errors.poolRecipients}>
<FormControl.Label>
<Text variant="form-label">Initial Members (optional)</Text>
</FormControl.Label>
<FormControl.HelperText>
<Text fontSize="xs" color="gray.500">
Add wallet addresses separated by commas or new lines.
</Text>
</FormControl.HelperText>
<TextArea
value={poolRecipients}
onChangeText={(value) => setPoolRecipients(value)}
onBlur={async () => {
onValidate();
await onValidateRecipients();
}}
autoCompleteType={undefined}
placeholder="0x1234..., 0x5678..."
minH={24}
backgroundColor="white"
/>
{isCheckingRecipients && (
<FormControl.HelperText>
<Text fontSize="xs" color="gray.500">
Checking member eligibility...
</Text>
</FormControl.HelperText>
)}
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{errors.poolRecipients}
</FormControl.ErrorMessage>
</FormControl>
</Box>
</VStack>
);
};
Expand Down
24 changes: 16 additions & 8 deletions packages/app/src/components/CommunityPool/SelectCollectiveType.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Checkbox, HStack, Pressable, Text, VStack } from 'native-base';
import { Box, Button, Checkbox, HStack, Link, Pressable, Text, VStack } from 'native-base';
import { CommunityFundsIcon, ResultsBasedIcon, SegmentedAidIcon } from '../../assets';
import { PoolType, useCreatePool } from '../../hooks/useCreatePool/useCreatePool';
import { useScreenSize } from '../../theme/hooks';
Expand All @@ -13,24 +13,23 @@ const poolTypes = [
id: 'community-funds' as PoolType,
name: 'Community Funds',
icon: CommunityFundsIcon,
description: 'Facilitate money distribution to members of existing community organisations',
description: 'Distribute funds to members of an existing group or organization.',
interested: false,
disabled: false,
},
{
id: 'segmented-aid' as PoolType,
name: 'Segmented Aid',
icon: SegmentedAidIcon,
description:
'Self-sovereign, user-managed and encrypted digital demographic information allows access to specific funds via GoodOffers',
description: 'Provide funds to people who qualify by verified attributes such as age or location.',
interested: true,
disabled: true,
},
{
id: 'results-based' as PoolType,
name: 'Results-based direct payments',
icon: ResultsBasedIcon,
description: 'Provides direct payments to stewards based on verified climate action',
description: 'Reward verified actions or measurable impact through data partners.',
interested: true,
disabled: true,
},
Expand Down Expand Up @@ -78,11 +77,14 @@ const SelectType = () => {
color: '#6933FF',
},
]}>
About Various Pools
Create a Pool
</Text>
<Text style={selectCollectiveTypeStyles.subtitle}>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dignissimos ipsa ab nemo fugiat expedita, facilis
voluptatibus magni velit odio quis cumque quidem veniam fuga. Ea perferendis voluptas voluptatum in iste!
Choose how your pool distributes the funds. Only Community Funds are currently available. If you have
interest in the other pools types, reach out here:{' '}
<Link href="https://ubi.gd/GoodBuildersTG" isExternal _text={selectCollectiveTypeStyles.linkText}>
https://ubi.gd/GoodBuildersTG
</Link>
</Text>
</Box>

Expand Down Expand Up @@ -178,6 +180,12 @@ const selectCollectiveTypeStyles = {
maxWidth: '80%',
fontWeight: '400',
},
linkText: {
color: 'goodPurple.400',
textDecorationLine: 'underline',
fontWeight: '600',
cursor: 'pointer',
},
card: {
borderRadius: 12,
borderWidth: 1,
Expand Down
Loading