diff --git a/assets/icons/cross.svg b/assets/icons/cross.svg
new file mode 100644
index 0000000000..90221c8183
--- /dev/null
+++ b/assets/icons/cross.svg
@@ -0,0 +1,3 @@
+
diff --git a/package.json b/package.json
index a59540cd71..fc106605a4 100644
--- a/package.json
+++ b/package.json
@@ -115,6 +115,7 @@
"graphql-request": "^5",
"html-to-draftjs": "^1.5.0",
"immutable": "^4.0.0",
+ "keccak256": "^1.0.6",
"kubernetes-models": "^4.3.1",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
diff --git a/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx
new file mode 100644
index 0000000000..28f7d08ff7
--- /dev/null
+++ b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx
@@ -0,0 +1,78 @@
+import { FC } from "react";
+import { TouchableOpacity, Image, StyleProp, ViewStyle } from "react-native";
+
+import { BrandText } from "../../BrandText";
+import { PrimaryBox } from "../../boxes/PrimaryBox";
+
+import { neutral77, secondaryColor } from "@/utils/style/colors";
+import { fontSemibold11, fontSemibold13 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const itemSize = 120;
+export const ItemView: FC<{
+ label: string;
+ subLabel?: string;
+ uri: string;
+ onPress: () => void;
+ style?: StyleProp;
+}> = ({ label, subLabel, uri, onPress, style }) => {
+ return (
+
+
+
+
+
+
+
+ {label}
+
+ {subLabel && (
+
+ {subLabel}
+
+ )}
+
+
+ );
+};
diff --git a/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx b/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx
new file mode 100644
index 0000000000..d1e5cf7956
--- /dev/null
+++ b/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx
@@ -0,0 +1,74 @@
+import { FC } from "react";
+import { View } from "react-native";
+
+import { itemSize, ItemView } from "./ItemView";
+import { BrandText } from "../../BrandText";
+import { PrimaryBox } from "../../boxes/PrimaryBox";
+
+import { DeleteButton } from "@/components/FilePreview/DeleteButton";
+import { GridList } from "@/components/layout/GridList";
+import { neutral33, neutral55 } from "@/utils/style/colors";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { LocalFileData } from "@/utils/types/files";
+
+export const SelectedFilesPreview: FC<{
+ files: LocalFileData[];
+ onPressItem: (file: LocalFileData, itemIndex: number) => void;
+ onPressDeleteItem: (itemIndex: number) => void;
+}> = ({ files, onPressItem, onPressDeleteItem }) => {
+ return (
+
+ {files.length ? (
+
+ data={files}
+ keyExtractor={(item) => item.url}
+ renderItem={({ item, index }, elemWidth) => (
+
+ onPressDeleteItem(index)}
+ style={{ top: 0, right: 0 }}
+ />
+ {
+ onPressItem(item, index);
+ }}
+ style={{ width: elemWidth }}
+ />
+
+ )}
+ minElemWidth={itemSize}
+ gap={layout.spacing_x2_5}
+ noFixedHeight
+ />
+ ) : (
+
+
+ Selected files preview
+
+
+ )}
+
+ );
+};
diff --git a/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx b/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx
new file mode 100644
index 0000000000..804cbfb1ab
--- /dev/null
+++ b/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx
@@ -0,0 +1,127 @@
+import { FC, useState } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { NetworkSelectorMenu } from "./NetworkSelectorMenu";
+import { BrandText } from "../BrandText";
+import { SVG } from "../SVG";
+import { TertiaryBox } from "../boxes/TertiaryBox";
+import { Label } from "../inputs/TextInputCustom";
+
+import chevronDownSVG from "@/assets/icons/chevron-down.svg";
+import chevronUpSVG from "@/assets/icons/chevron-up.svg";
+import { NetworkIcon } from "@/components/NetworkIcon";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { SpacerRow } from "@/components/spacer";
+import { useDropdowns } from "@/hooks/useDropdowns";
+import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork";
+import { NetworkFeature, NetworkKind } from "@/networks";
+import { neutral17, neutral77, secondaryColor } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const NetworkSelectorWithLabel: FC<{
+ label: string;
+ style?: StyleProp;
+ forceNetworkId?: string;
+ forceNetworkKind?: NetworkKind;
+ forceNetworkFeatures?: NetworkFeature[];
+}> = ({
+ style,
+ forceNetworkId,
+ forceNetworkKind,
+ forceNetworkFeatures,
+ label,
+}) => {
+ const [isDropdownOpen, setDropdownState, ref] = useDropdowns();
+ const [hovered, setHovered] = useState(false);
+ const selectedNetworkInfo = useSelectedNetworkInfo();
+
+ return (
+
+ setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onPress={() => setDropdownState(!isDropdownOpen)}
+ >
+
+
+
+
+
+
+
+
+
+ {selectedNetworkInfo?.displayName}
+
+
+
+
+
+
+
+ {isDropdownOpen && (
+ {}}
+ optionsMenuwidth={416}
+ style={{ width: "100%", marginTop: layout.spacing_x0_75 }}
+ forceNetworkId={forceNetworkId}
+ forceNetworkKind={forceNetworkKind}
+ forceNetworkFeatures={forceNetworkFeatures}
+ />
+ )}
+
+
+ );
+};
diff --git a/packages/components/navigation/getNormalModeScreens.tsx b/packages/components/navigation/getNormalModeScreens.tsx
index c7145bb147..fe998e9796 100644
--- a/packages/components/navigation/getNormalModeScreens.tsx
+++ b/packages/components/navigation/getNormalModeScreens.tsx
@@ -17,8 +17,9 @@ import { GuardiansScreen } from "@/screens/Guardians/GuardiansScreen";
import { HashtagFeedScreen } from "@/screens/HashtagFeed/HashtagFeedScreen";
import { HomeScreen } from "@/screens/Home/HomeScreen";
import { LaunchpadApplyScreen } from "@/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen";
+import { LaunchpadCreateScreen } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen";
import { LaunchpadScreen } from "@/screens/Launchpad/LaunchpadHome/LaunchpadScreen";
-import { MintCollectionScreen } from "@/screens/Launchpad/MintCollectionScreen";
+import { MintCollectionScreen } from "@/screens/Launchpad/LaunchpadHome/MintCollectionScreen";
import { LaunchpadERC20CreateSaleScreen } from "@/screens/LaunchpadERC20/LaunchpadERC20Sales/LaunchpadERC20CreateSaleScreen";
import { LaunchpadERC20SalesScreen } from "@/screens/LaunchpadERC20/LaunchpadERC20Sales/LaunchpadERC20SalesScreen";
import { LaunchpadERC20Screen } from "@/screens/LaunchpadERC20/LaunchpadERC20Screen";
@@ -229,6 +230,14 @@ export const getNormalModeScreens = ({
title: screenTitle("Launchpad (Apply)"),
}}
/>
+ null,
+ title: screenTitle("Create Collection"),
+ }}
+ />
{
+ // Since the Collection network is the selected network, we use useSelectedNetworkId (See LaunchpadBasic.tsx)
+ const selectedNetworkId = useSelectedNetworkId();
+ const selectedWallet = useSelectedWallet();
+ const { setToast } = useFeedbacks();
+ const userIPFSKey = useSelector(selectNFTStorageAPI);
+ const { pinataPinFileToIPFS, uploadFilesToPinata } = useIpfs();
+
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend and useCompleteCollection are merged
+ // const { completeCollection } = useCompleteCollection();
+
+ const createCollection = useCallback(
+ async (collectionFormValues: CollectionFormValues) => {
+ if (!selectedWallet) return false;
+ const userId = selectedWallet.userId;
+ const walletAddress = selectedWallet.address;
+
+ const signingComswasmClient =
+ await getKeplrSigningCosmWasmClient(selectedNetworkId);
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ selectedNetworkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+ if (!cosmwasmNftLaunchpadFeature) return false;
+
+ const nftLaunchpadContractClient = new NftLaunchpadClient(
+ signingComswasmClient,
+ walletAddress,
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress,
+ );
+ const pinataJWTKey =
+ collectionFormValues.assetsMetadatas?.nftApiKey ||
+ userIPFSKey ||
+ (await generateIpfsKey(selectedNetworkId, userId));
+ if (!pinataJWTKey) {
+ console.error("Project creation error: No Pinata JWT");
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Project creation error: No Pinata JWT",
+ });
+ return false;
+ }
+
+ try {
+ // ========== Cover image
+ const fileIpfsHash = await pinataPinFileToIPFS({
+ pinataJWTKey,
+ file: collectionFormValues.coverImage,
+ } as PinataFileProps);
+ if (!fileIpfsHash) {
+ console.error("Project creation error: Pin to Pinata failed");
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Project creation error: Pin to Pinata failed",
+ });
+ return false;
+ }
+
+ // ========== Whitelists
+ const whitelistAddressesFilesToUpload: LocalFileData[] = [];
+ collectionFormValues.mintPeriods.forEach((mintPeriod) => {
+ if (mintPeriod.whitelistAddressesFile)
+ whitelistAddressesFilesToUpload.push(
+ mintPeriod.whitelistAddressesFile,
+ );
+ });
+ const remoteWhitelistAddressesFiles = await uploadFilesToPinata({
+ pinataJWTKey,
+ files: whitelistAddressesFilesToUpload,
+ });
+ const mint_periods: MintPeriod[] = collectionFormValues.mintPeriods.map(
+ (mintPeriod: CollectionMintPeriodFormValues, index) => {
+ let whitelist_info: WhitelistInfo | null = null;
+ if (
+ mintPeriod.whitelistAddresses?.length &&
+ remoteWhitelistAddressesFiles[index].url
+ ) {
+ const addresses: string[] = mintPeriod.whitelistAddresses;
+ const leaves = addresses.map(keccak256);
+ const tree = new MerkleTree(leaves, keccak256);
+ const merkleRoot = tree.getRoot().toString("hex");
+ whitelist_info = {
+ addresses_count: addresses.length,
+ addresses_ipfs: remoteWhitelistAddressesFiles[index].url,
+ addresses_merkle_root: merkleRoot,
+ };
+ }
+ const price: Coin | null = mintPeriod.price
+ ? {
+ amount: mintPeriod.price.amount || "0",
+ denom: mintPeriod.price.denom,
+ }
+ : null;
+ return {
+ price,
+ end_time: mintPeriod.endTime,
+ max_tokens: mintPeriod.maxTokens
+ ? parseInt(mintPeriod.maxTokens, 10)
+ : 0,
+ limit_per_address: mintPeriod.perAddressLimit
+ ? parseInt(mintPeriod.perAddressLimit, 10)
+ : 0,
+ start_time: mintPeriod.startTime,
+ whitelist_info,
+ };
+ },
+ );
+
+ const assetsMetadataFormsValues:
+ | CollectionAssetsMetadatasFormValues
+ | undefined
+ | null = collectionFormValues.assetsMetadatas;
+
+ // ========== Final collection
+ const collection: CollectionToSubmit = {
+ name: collectionFormValues.name,
+ desc: collectionFormValues.description,
+ symbol: collectionFormValues.symbol,
+ website_link: collectionFormValues.websiteLink,
+ contact_email: collectionFormValues.email,
+ project_type: collectionFormValues.projectTypes.join(),
+ tokens_count: assetsMetadataFormsValues?.assetsMetadatas?.length || 0,
+ reveal_time: collectionFormValues.revealTime,
+ team_desc: collectionFormValues.teamDescription,
+ partners: collectionFormValues.partnersDescription,
+ investment_desc: collectionFormValues.investDescription,
+ investment_link: collectionFormValues.investLink,
+ artwork_desc: collectionFormValues.artworkDescription,
+ cover_img_uri: "ipfs://" + fileIpfsHash,
+ is_applied_previously: collectionFormValues.isPreviouslyApplied,
+ is_project_derivative: collectionFormValues.isDerivativeProject,
+ is_ready_for_mint: collectionFormValues.isReadyForMint,
+ is_dox: collectionFormValues.isDox,
+ escrow_mint_proceeds_period: parseInt(
+ collectionFormValues.escrowMintProceedsPeriod,
+ 10,
+ ),
+ dao_whitelist_count: parseInt(
+ collectionFormValues.daoWhitelistCount,
+ 10,
+ ),
+ mint_periods,
+ royalty_address: collectionFormValues.royaltyAddress,
+ royalty_percentage: collectionFormValues.royaltyPercentage
+ ? parseInt(collectionFormValues.royaltyPercentage, 10)
+ : null,
+ target_network: selectedNetworkId,
+ };
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend and useCompleteCollection are merged
+ // const collectionId = collectionFormValues.symbol;
+
+ // ========== Submit the collection through the contract
+ await nftLaunchpadContractClient.submitCollection({
+ collection,
+ });
+
+ // ========== Handle assets metadata
+ if (!assetsMetadataFormsValues?.assetsMetadatas?.length) {
+ setToast({
+ mode: "normal",
+ type: "success",
+ title: "Project submitted (Incomplete)",
+ message: "You will need to Complete the Project",
+ });
+ } else {
+ const isCompleteSuccess = false;
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend and useCompleteCollection are merged
+ // await completeCollection(
+ // collectionId,
+ // assetsMetadataFormsValues,
+ // );
+
+ if (!isCompleteSuccess) {
+ setToast({
+ mode: "normal",
+ type: "warning",
+ title: "Project submitted (Incomplete)",
+ message:
+ "Error during uploading the Assets.\nYou will need to Complete the Project",
+ });
+ } else {
+ setToast({
+ mode: "normal",
+ type: "success",
+ title: "Project submitted",
+ });
+ }
+ }
+
+ return true;
+ } catch (e: any) {
+ console.error("Error creating a NFT Collection in the Launchpad: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error creating a NFT Collection in the Launchpad",
+ message: e.message,
+ });
+ }
+ },
+ [
+ pinataPinFileToIPFS,
+ selectedWallet,
+ userIPFSKey,
+ uploadFilesToPinata,
+ selectedNetworkId,
+ setToast,
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend and useCompleteCollection are merged
+ // completeCollection,
+ ],
+ );
+
+ return {
+ createCollection,
+ };
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
index 6fd2715f5e..8850c64571 100644
--- a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
@@ -1,18 +1,23 @@
-import React from "react";
-import { StyleSheet, View } from "react-native";
+import { Linking, TextStyle, View } from "react-native";
import LaunchpadBannerImage from "@/assets/banners/launchpad.jpg";
import { BrandText } from "@/components/BrandText";
import { ImageBackgroundLogoText } from "@/components/ImageBackgroundLogoText";
+import { OmniLink } from "@/components/OmniLink";
import { ScreenContainer } from "@/components/ScreenContainer";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
import {
LargeBoxButton,
LargeBoxButtonProps,
} from "@/components/buttons/LargeBoxButton";
-import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { SpacerColumn } from "@/components/spacer";
+import { useMaxResolution } from "@/hooks/useMaxResolution";
import { ScreenFC } from "@/utils/navigation";
import { neutral77 } from "@/utils/style/colors";
import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+const MD_BREAKPOINT = 720;
const BUTTONS: LargeBoxButtonProps[] = [
{
@@ -24,7 +29,7 @@ const BUTTONS: LargeBoxButtonProps[] = [
title: "Create",
description:
"Upload your assets, enter collection metadata and deploy your collection.",
- buttonTitle: "Coming soon",
+ buttonTitle: "Open",
},
{
title: "My Collections",
@@ -34,6 +39,7 @@ const BUTTONS: LargeBoxButtonProps[] = [
];
export const LaunchpadApplyScreen: ScreenFC<"LaunchpadApply"> = () => {
+ const { width } = useMaxResolution();
return (
= () => {
Welcome
-
+
Looking for a fast and efficient way to build an NFT collection?
-
+
Teritori is the solution. Teritori is built to provide useful smart
contract interfaces that helps you build and deploy your own NFT
collections in no time.
-
-
-
-
-
+
+
+
+ Linking.openURL("https://airtable.com/shr1kU7kXW0267gNV")
+ }
+ style={{ flex: 1 }}
+ >
+
+
+
+ = MD_BREAKPOINT ? layout.spacing_x1_5 : 0,
+ marginVertical: width >= MD_BREAKPOINT ? 0 : layout.spacing_x1_5,
+ }}
+ >
+
+
+
);
};
-// FIXME: remove StyleSheet.create
-// eslint-disable-next-line no-restricted-syntax
-const styles = StyleSheet.create({
- descriptionText: StyleSheet.flatten([
- fontSemibold14,
- {
- color: neutral77,
- },
- ]),
- buttonsContainer: {
- flexDirection: "row",
- flex: 1,
- },
-});
+const descriptionTextCStyle: TextStyle = {
+ ...fontSemibold14,
+ color: neutral77,
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx
new file mode 100644
index 0000000000..abc4788118
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx
@@ -0,0 +1,188 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useMemo, useState } from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { View } from "react-native";
+import { useSelector } from "react-redux";
+
+import { BrandText } from "@/components/BrandText";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { PrimaryButton } from "@/components/buttons/PrimaryButton";
+import { SecondaryButton } from "@/components/buttons/SecondaryButton";
+import { SpacerColumn } from "@/components/spacer";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { useCreateCollection } from "@/hooks/launchpad/useCreateCollection";
+import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork";
+import { NetworkFeature } from "@/networks";
+import {
+ LaunchpadCreateStepKey,
+ LaunchpadStepper,
+} from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper";
+import { LaunchpadAdditional } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional";
+import { LaunchpadAssetsAndMetadata } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata";
+import { LaunchpadBasic } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic";
+import { LaunchpadDetails } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails";
+import { LaunchpadMinting } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting";
+import { LaunchpadTeamAndInvestment } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment";
+import { selectNFTStorageAPI } from "@/store/slices/settings";
+import { ScreenFC, useAppNavigation } from "@/utils/navigation";
+import { neutral33 } from "@/utils/style/colors";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionFormValues,
+ ZodCollectionFormValues,
+} from "@/utils/types/launchpad";
+
+export const LaunchpadCreateScreen: ScreenFC<"LaunchpadCreate"> = () => {
+ const navigation = useAppNavigation();
+ const selectedNetwork = useSelectedNetworkInfo();
+ const { setToast } = useFeedbacks();
+ const userIPFSKey = useSelector(selectNFTStorageAPI);
+ const collectionForm = useForm({
+ mode: "all",
+ defaultValues: {
+ mintPeriods: [
+ {
+ price: {
+ denom: selectedNetwork?.currencies[0].denom,
+ },
+ isOpen: true,
+ },
+ ],
+ assetsMetadatas: {
+ nftApiKey: userIPFSKey,
+ },
+ },
+ resolver: zodResolver(ZodCollectionFormValues),
+ });
+ const { createCollection } = useCreateCollection();
+ const [selectedStepKey, setSelectedStepKey] =
+ useState(1);
+ const [isLoading, setLoading] = useState(false);
+ const { setLoadingFullScreen } = useFeedbacks();
+
+ const stepContent = useMemo(() => {
+ switch (selectedStepKey) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ case 6:
+ return ;
+ default:
+ return ;
+ }
+ }, [selectedStepKey]);
+
+ const onValid = async () => {
+ setLoading(true);
+ setLoadingFullScreen(true);
+ try {
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend is merged
+ // const success =
+ await createCollection(collectionForm.getValues());
+ // TODO: Uncomment when the NFT Launchpad MyCollections frontend is merged
+ // if (success) navigation.navigate("LaunchpadMyCollections");
+ } catch (e) {
+ console.error("Error creating a NFT collection", e);
+ } finally {
+ setLoading(false);
+ setLoadingFullScreen(false);
+ }
+ };
+
+ const onInvalid = () => {
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Unable to create the collection",
+ message:
+ "Some fields are not correctly filled." +
+ "\nMaybe from the mapping file, please complete it properly.\nCheck the description for more information.",
+ });
+ };
+
+ const onPressSubmit = () => collectionForm.handleSubmit(onValid, onInvalid)();
+
+ return (
+ >}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ headerChildren={Apply to Launchpad}
+ onBackPress={() => navigation.navigate("LaunchpadApply")}
+ // TODO: Remove after tests
+ forceNetworkId="teritori-testnet"
+ >
+
+
+
+
+ {stepContent}
+
+
+
+
+ {selectedStepKey !== 1 && (
+ setSelectedStepKey(selectedStepKey - 1)}
+ />
+ )}
+
+ {selectedStepKey === 6 ? (
+
+ ) : (
+ setSelectedStepKey(selectedStepKey + 1)}
+ />
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx
new file mode 100644
index 0000000000..4a0cc1ddf3
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx
@@ -0,0 +1,245 @@
+import { Dispatch, FC, SetStateAction, useRef } from "react";
+import { useFormContext } from "react-hook-form";
+import {
+ LayoutChangeEvent,
+ ScrollView,
+ TouchableOpacity,
+ useWindowDimensions,
+ View,
+} from "react-native";
+
+import ChevronRightSvg from "@/assets/icons/chevron-right.svg";
+import RejectSVG from "@/assets/icons/reject.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import { useIsMobile } from "@/hooks/useIsMobile";
+import {
+ neutral17,
+ neutral22,
+ neutral77,
+ primaryColor,
+ primaryTextColor,
+} from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+export type LaunchpadCreateStepKey = number;
+
+interface LaunchpadStepperProps {
+ selectedStepKey: LaunchpadCreateStepKey;
+ setSelectedStepKey: Dispatch>;
+}
+
+interface LaunchpadCreateStep {
+ key: LaunchpadCreateStepKey;
+ title: string;
+}
+
+const steps: LaunchpadCreateStep[] = [
+ {
+ key: 1,
+ title: "Basic",
+ },
+ {
+ key: 2,
+ title: "Details",
+ },
+ {
+ key: 3,
+ title: "Team & Investments",
+ },
+ {
+ key: 4,
+ title: "Additional",
+ },
+ {
+ key: 5,
+ title: "Minting",
+ },
+ {
+ key: 6,
+ title: "Assets & Metadata",
+ },
+];
+
+export const LaunchpadStepper: FC = ({
+ selectedStepKey,
+ setSelectedStepKey,
+}) => {
+ const { width: windowWidth } = useWindowDimensions();
+ const scrollViewRef = useRef(null);
+ const isMobile = useIsMobile();
+ const collectionForm = useFormContext();
+
+ const hasErrors = (stepKey: number) => {
+ if (
+ (stepKey === 1 &&
+ (!!collectionForm.getFieldState("name").error ||
+ !!collectionForm.getFieldState("description").error ||
+ !!collectionForm.getFieldState("symbol").error)) ||
+ !!collectionForm.getFieldState("coverImage").error ||
+ !!collectionForm.getFieldState("assetsMetadatas.nftApiKey").error
+ ) {
+ return true;
+ }
+ if (
+ stepKey === 2 &&
+ (!!collectionForm.getFieldState("websiteLink").error ||
+ !!collectionForm.getFieldState("isDerivativeProject").error ||
+ !!collectionForm.getFieldState("projectTypes").error ||
+ !!collectionForm.getFieldState("isPreviouslyApplied").error ||
+ !!collectionForm.getFieldState("email").error)
+ ) {
+ return true;
+ }
+ if (
+ stepKey === 3 &&
+ (!!collectionForm.getFieldState("teamDescription").error ||
+ !!collectionForm.getFieldState("partnersDescription").error ||
+ !!collectionForm.getFieldState("investDescription").error ||
+ !!collectionForm.getFieldState("investLink").error)
+ ) {
+ return true;
+ }
+ if (
+ stepKey === 4 &&
+ (!!collectionForm.getFieldState("artworkDescription").error ||
+ !!collectionForm.getFieldState("isReadyForMint").error ||
+ !!collectionForm.getFieldState("isDox").error ||
+ !!collectionForm.getFieldState("daoWhitelistCount").error ||
+ !!collectionForm.getFieldState("escrowMintProceedsPeriod").error)
+ ) {
+ return true;
+ }
+ if (
+ stepKey === 5 &&
+ (!!collectionForm.getFieldState("mintPeriods").error ||
+ !!collectionForm.getFieldState("royaltyAddress").error ||
+ !!collectionForm.getFieldState("royaltyPercentage").error)
+ ) {
+ return true;
+ }
+ if (
+ stepKey === 6 &&
+ !!collectionForm.getFieldState("assetsMetadatas").error
+ ) {
+ return true;
+ }
+ };
+
+ const onSelectedItemLayout = (e: LayoutChangeEvent) => {
+ scrollViewRef.current?.scrollTo({
+ x: e.nativeEvent.layout.x,
+ animated: false,
+ });
+ };
+
+ return (
+
+
+ = 1240 && { justifyContent: "center" },
+ {
+ flexDirection: "row",
+ width: "100%",
+ },
+ ]}
+ >
+ {steps.map((step, index) => {
+ const isSelected = selectedStepKey === step.key;
+ return (
+ {
+ if (isSelected) onSelectedItemLayout(e);
+ }}
+ onPress={() => setSelectedStepKey(step.key)}
+ style={{
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ paddingHorizontal: layout.spacing_x2,
+ paddingVertical: layout.spacing_x1,
+ }}
+ >
+
+ {hasErrors(step.key) && (
+
+ )}
+
+ {step.key}
+
+
+
+ {step.title}
+
+ {steps.length !== index + 1 && (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx
new file mode 100644
index 0000000000..dac047e026
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx
@@ -0,0 +1,142 @@
+import { FC } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { ErrorText } from "@/components/ErrorText";
+import { SpacerColumn } from "@/components/spacer";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { SelectInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad";
+import { neutral55, neutral77 } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadAdditional: FC = () => {
+ const collectionForm = useFormContext();
+ const escrowMintProceedsPeriod = collectionForm.watch(
+ "escrowMintProceedsPeriod",
+ );
+ const isReadyForMint = collectionForm.watch("isReadyForMint");
+ const isDox = collectionForm.watch("isDox");
+ return (
+
+
+ Additional Information
+
+
+ Fill the additional information
+
+
+
+
+ label="Please describe your artwork: "
+ sublabel={
+
+
+ 1. Is it completely original?
+
+
+ 2. Who is the artist?
+
+
+ 3. How did your team meet the artist?
+
+
+ }
+ placeHolder="Describe here..."
+ name="artworkDescription"
+ form={collectionForm}
+ />
+
+
+ name="isReadyForMint"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(item === "Yes");
+ }}
+ label="Is your collection ready for the mint?"
+ style={{ zIndex: 3 }}
+ />
+
+ {collectionForm.getFieldState("isReadyForMint").error?.message}
+
+ >
+ )}
+ />
+
+
+
+ name="escrowMintProceedsPeriod"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(item);
+ }}
+ label="If selected for the launchpad, You will escrow mint proceeds for this time period:"
+ style={{ zIndex: 2 }}
+ />
+
+ {
+ collectionForm.getFieldState("escrowMintProceedsPeriod").error
+ ?.message
+ }
+
+ >
+ )}
+ />
+
+
+
+ name="isDox"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(item === "Yes");
+ }}
+ label="Are you dox or have you planned to dox?"
+ style={{ zIndex: 1 }}
+ />
+
+ {collectionForm.getFieldState("isDox").error?.message}
+
+ >
+ )}
+ />
+
+
+
+ label="We'd love to offer TeritoriDAO members 10% of your whitelist supply if your project is willing. Please let us know how many whitelist spots you'd be willing to allocate our DAO: "
+ placeHolder="0"
+ name="daoWhitelistCount"
+ form={collectionForm}
+ />
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx
new file mode 100644
index 0000000000..cd483bb7bd
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx
@@ -0,0 +1,193 @@
+import { FC } from "react";
+import { useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { OptimizedImage } from "@/components/OptimizedImage";
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { PrimaryButton } from "@/components/buttons/PrimaryButton";
+import { Label } from "@/components/inputs/TextInputCustom";
+import ModalBase from "@/components/modals/ModalBase";
+import { Separator } from "@/components/separators/Separator";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import {
+ neutral22,
+ neutral33,
+ neutral77,
+ neutralFF,
+ secondaryColor,
+} from "@/utils/style/colors";
+import {
+ fontSemibold14,
+ fontSemibold16,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionAssetsMetadataFormValues,
+ CollectionAssetsMetadatasFormValues,
+} from "@/utils/types/launchpad";
+
+export const AssetModal: FC<{
+ onClose: () => void;
+ isVisible: boolean;
+ elem: CollectionAssetsMetadataFormValues;
+ elemIndex: number;
+}> = ({ onClose, isVisible, elem, elemIndex }) => {
+ const assetsMetadatasForm =
+ useFormContext();
+ const namePath = `assetsMetadatas.${elemIndex}.name` as const;
+ const descriptionPath = `assetsMetadatas.${elemIndex}.description` as const;
+ const externalUrlPath = `assetsMetadatas.${elemIndex}.externalUrl` as const;
+ const youtubeUrlPath = `assetsMetadatas.${elemIndex}.youtubeUrl` as const;
+ const attributes = assetsMetadatasForm.watch(
+ `assetsMetadatas.${elemIndex}.attributes`,
+ );
+
+ return (
+
+
+ {elem.image && (
+
+ )}
+
+
+
+ Asset #{elemIndex + 1}
+
+
+ File name: {elem.image?.fileName}
+
+
+
+ }
+ hideMainSeparator
+ childrenBottom={
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+ name={namePath}
+ label="Name"
+ form={assetsMetadatasForm}
+ placeHolder="Token name"
+ disabled
+ />
+
+
+ name={descriptionPath}
+ label="Description"
+ form={assetsMetadatasForm}
+ placeHolder="Token description"
+ required={false}
+ disabled
+ />
+
+
+ name={externalUrlPath}
+ label="External URL"
+ form={assetsMetadatasForm}
+ placeHolder="https://"
+ required={false}
+ disabled
+ />
+
+
+ name={youtubeUrlPath}
+ label="Youtube URL"
+ form={assetsMetadatasForm}
+ placeHolder="https://"
+ required={false}
+ disabled
+ />
+
+
+
+
+
+ {attributes.map((attribute, index) => (
+
+
+ {`${attribute.type}: ${attribute.value}`}
+
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx
new file mode 100644
index 0000000000..f0b4613c94
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsAndMetadataIssue.tsx
@@ -0,0 +1,71 @@
+import { FC } from "react";
+import { View } from "react-native";
+
+import crossSVG from "@/assets/icons/cross.svg";
+import warningTriangleSVG from "@/assets/icons/warning-triangle.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import {
+ errorColor,
+ neutral17,
+ neutral77,
+ warningColor,
+} from "@/utils/style/colors";
+import { fontSemibold13 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export interface AssetsAndMetadataIssueObject {
+ title: string;
+ message: string;
+ type: "error" | "warning";
+}
+
+export const AssetsAndMetadataIssue: FC<{
+ issue: AssetsAndMetadataIssueObject;
+ removeIssue: () => void;
+}> = ({ issue, removeIssue }) => {
+ return (
+
+
+
+
+
+
+ {issue.title}
+
+
+
+
+ {issue.message}
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx
new file mode 100644
index 0000000000..b21d850c55
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx
@@ -0,0 +1,741 @@
+import { parse } from "papaparse";
+import pluralize from "pluralize";
+import { FC, useEffect, useRef, useState } from "react";
+import { useFieldArray, useFormContext } from "react-hook-form";
+import { SafeAreaView, TouchableOpacity, View } from "react-native";
+
+import { AssetModal } from "./AssetModal";
+import {
+ AssetsAndMetadataIssue,
+ AssetsAndMetadataIssueObject,
+} from "./AssetsAndMetadataIssue";
+
+import trashSVG from "@/assets/icons/trash.svg";
+import { BrandText } from "@/components/BrandText";
+import { SelectedFilesPreview } from "@/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview";
+import { SVG } from "@/components/SVG";
+import { FileUploaderSmall } from "@/components/inputs/FileUploaderSmall";
+import { FileUploaderSmallHandle } from "@/components/inputs/FileUploaderSmall/FileUploaderSmall.type";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { useIsMobile } from "@/hooks/useIsMobile";
+import { IMAGE_MIME_TYPES, TXT_CSV_MIME_TYPES } from "@/utils/mime";
+import {
+ NUMBERS_COMMA_SEPARATOR_REGEXP,
+ NUMBERS_REGEXP,
+ URL_REGEX,
+} from "@/utils/regex";
+import { errorColor, neutral33 } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { LocalFileData } from "@/utils/types/files";
+import {
+ CollectionAssetsAttributeFormValues,
+ CollectionAssetsMetadataFormValues,
+ CollectionAssetsMetadatasFormValues,
+} from "@/utils/types/launchpad";
+
+export const AssetsTab: React.FC = () => {
+ const isMobile = useIsMobile();
+ const { setToast } = useFeedbacks();
+ const [selectedElemIndex, setSelectedElemIndex] = useState();
+ const assetsMetadatasForm =
+ useFormContext();
+ const { fields, remove } = useFieldArray({
+ control: assetsMetadatasForm.control,
+ name: "assetsMetadatas",
+ });
+ const [assetsMappingDataRows, setAssetsMappingDataRows] = useState<
+ string[][]
+ >([]);
+ const [attributesMappingDataRows, setAttributesMappingDataRows] = useState<
+ string[][]
+ >([]);
+
+ const attributesUploaderRef = useRef(null);
+ const assetsUploaderRef = useRef(null);
+ const imagesUploaderRef = useRef(null);
+
+ const [assetModalVisible, setAssetModalVisible] = useState(false);
+ const selectedElem = fields.find((_, index) => index === selectedElemIndex);
+ const [attributesIssues, setAttributesIssues] = useState<
+ AssetsAndMetadataIssueObject[]
+ >([]);
+ const [assetsIssues, setAssetsIssues] = useState<
+ AssetsAndMetadataIssueObject[]
+ >([]);
+ const [imagesIssues, setImagesIssues] = useState<
+ AssetsAndMetadataIssueObject[]
+ >([]);
+
+ const attributesIdsSeparator = ",";
+ // Assets columns
+ const fileNameColIndex = 0;
+ const nameColIndex = 1;
+ const descriptionColIndex = 2;
+ const externalURLColIndex = 3;
+ const youtubeURLColIndex = 4;
+ const attributesColIndex = 5;
+ // Attributes (traits) columns
+ const idColIndex = 0;
+ const typeColIndex = 1;
+ const valueColIndex = 2;
+
+ const resetAllIssues = () => {
+ setAssetsIssues([]);
+ setAttributesIssues([]);
+ setImagesIssues([]);
+ };
+ // We keep showing only the warnings if a image or mapping file is selected without error
+ const resetIssuesErrors = () => {
+ setAttributesIssues((issues) =>
+ issues.filter((issue) => issue.type !== "error"),
+ );
+ setAssetsIssues((issues) =>
+ issues.filter((issue) => issue.type !== "error"),
+ );
+ setImagesIssues((issues) =>
+ issues.filter((issue) => issue.type !== "error"),
+ );
+ };
+
+ const resetAll = () => {
+ setAssetsMappingDataRows([]);
+ setAttributesMappingDataRows([]);
+ assetsMetadatasForm.setValue("assetsMetadatas", []);
+ resetAllIssues();
+ attributesUploaderRef.current?.resetFiles();
+ assetsUploaderRef.current?.resetFiles();
+ imagesUploaderRef.current?.resetFiles();
+ };
+
+ // We ignore the first row since it's the table headings
+ // We ignore unwanted empty lines from the CSV
+ const cleanDataRows = (array: string[][]) =>
+ array.filter(
+ (dataRow, dataRowIndex) => dataRow[0] !== "" && dataRowIndex > 0,
+ );
+ // Converts attributes ids as string to array of ids
+ const cleanAssetAttributesIds = (ids?: string) =>
+ ids
+ ?.split(attributesIdsSeparator)
+ .map((id) => id.trim())
+ .filter((id) => NUMBERS_COMMA_SEPARATOR_REGEXP.test(id)) || [];
+
+ // On remove image manually
+ const onRemoveImage = (index: number) => {
+ remove(index);
+ };
+ // If all images are removed, we clear the images issues and the input file images
+ useEffect(() => {
+ if (!fields.length) {
+ setImagesIssues([]);
+ imagesUploaderRef.current?.resetFiles();
+ }
+ }, [fields.length]);
+
+ // On upload attributes CSV mapping file
+ const onUploadAttributesMapingFile = (files: LocalFileData[]) => {
+ resetAllIssues();
+ setAssetsMappingDataRows([]);
+ assetsMetadatasForm.setValue("assetsMetadatas", []);
+
+ try {
+ parse(files[0].file, {
+ complete: (parseResults) => {
+ const attributesDataRows = parseResults.data;
+
+ // Controls CSV headings present on the first row.
+ if (
+ attributesDataRows[0][idColIndex] !== "id" ||
+ attributesDataRows[0][valueColIndex] !== "value" ||
+ attributesDataRows[0][typeColIndex] !== "type"
+ ) {
+ setAttributesMappingDataRows([]);
+
+ const title = "Invalid attributes mapping file";
+ const message =
+ "Please verify the headings on the first row in your attributes mapping file.\nThis file is ignored.\nCheck the description for more information.";
+ console.error(title + ".\n" + message);
+ setAttributesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "error",
+ },
+ ]);
+ return;
+ }
+
+ // Verifying that all attributes rows have an id, name and value
+ const missingIdRows: string[][] = [];
+ const missingTypeRows: string[][] = [];
+ const missingValueRows: string[][] = [];
+ const wrongIdRows: string[][] = [];
+ const rowsIndexesToRemove: number[] = [];
+
+ // Controlling attributes
+ cleanDataRows(attributesDataRows).forEach((dataRow, dataRowIndex) => {
+ const hasNoId = !dataRow[idColIndex]?.trim();
+ const hasNoValue = !dataRow[valueColIndex]?.trim();
+ const hasNoType = !dataRow[typeColIndex]?.trim();
+ const hasWrongId =
+ dataRow[idColIndex]?.trim() &&
+ !NUMBERS_REGEXP.test(dataRow[idColIndex].trim());
+
+ // Warning if no id in attribute (Ignore attribute)
+ if (hasNoId) {
+ missingIdRows.push(dataRow);
+ }
+ // Warning if no value in attribute (Ignore attribute)
+ if (hasNoValue) {
+ missingValueRows.push(dataRow);
+ }
+ // Warning if no type in attribute (Ignore attribute)
+ if (hasNoType) {
+ missingTypeRows.push(dataRow);
+ }
+ // Warning if id is not a digit (Ignore attribute)
+ if (hasWrongId) {
+ wrongIdRows.push(dataRow);
+ }
+ // We get the invalidated rows to remove
+ if (hasNoId || hasNoValue || hasNoType || hasWrongId) {
+ rowsIndexesToRemove.push(dataRowIndex);
+ }
+ });
+
+ // We remove the wrong rows from parseResults.data (The assets rows)
+ const result = cleanDataRows(parseResults.data).filter(
+ (_, index) => !rowsIndexesToRemove.includes(index),
+ );
+ // Soring the final result
+ setAttributesMappingDataRows(result);
+
+ // Handling warnings
+ if (missingIdRows.length) {
+ const title = `Incomplete ${pluralize("attribute", missingIdRows.length)}`;
+ const message = `Missing "id" in ${pluralize("attribute", missingIdRows.length, true)} that ${pluralize("has", missingIdRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAttributesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (missingTypeRows.length) {
+ const title = `Incomplete ${pluralize("attribute", missingTypeRows.length)}`;
+ const message = `Missing "type" in ${pluralize("attribute", missingTypeRows.length, true)} that ${pluralize("has", missingTypeRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAttributesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (missingValueRows.length) {
+ const title = `Incomplete ${pluralize("attribute", missingValueRows.length)}`;
+ const message = `Missing "value" in ${pluralize("attribute", missingValueRows.length, true)} that ${pluralize("has", missingValueRows.length)} been ignored.\nPlease complete properly your attributes mapping file.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAttributesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (wrongIdRows.length) {
+ const title = `Wrong id`;
+ const message = `${pluralize("attribute", wrongIdRows.length, true)} ${pluralize("has", wrongIdRows.length)} a wrong "id" value and ${pluralize("has", wrongIdRows.length)} beed ignored. Only a number is allowed.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAttributesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ },
+ });
+ } catch (e) {
+ setAttributesMappingDataRows([]);
+
+ console.error(`${e}`);
+ setToast({
+ title: "Error parsing " + files[0].file.name,
+ message: `${e}`,
+ mode: "normal",
+ type: "error",
+ });
+ }
+ };
+
+ // On upload assets CSV mapping file
+ const onUploadAssetsMappingFile = (files: LocalFileData[]) => {
+ resetIssuesErrors();
+ setAssetsIssues([]);
+ setImagesIssues([]);
+ assetsMetadatasForm.setValue("assetsMetadatas", []);
+ imagesUploaderRef.current?.resetFiles();
+
+ try {
+ parse(files[0].file, {
+ complete: (parseResults) => {
+ const assetsDataRows = parseResults.data;
+ const attributesDataRows = attributesMappingDataRows; // attributesMappingDataRows is clean here
+
+ // Controls CSV headings present on the first row.
+ if (
+ assetsDataRows[0][fileNameColIndex] !== "file_name" ||
+ assetsDataRows[0][nameColIndex] !== "name" ||
+ assetsDataRows[0][descriptionColIndex] !== "description" ||
+ assetsDataRows[0][externalURLColIndex] !== "external_url" ||
+ assetsDataRows[0][youtubeURLColIndex] !== "youtube_url" ||
+ assetsDataRows[0][attributesColIndex] !== "attributes"
+ ) {
+ setAssetsMappingDataRows([]);
+
+ const title = "Invalid assets mapping file";
+ const message =
+ "Please verify the headings on the first row in your assets mapping file.This file is ignored.\nCheck the description for more information.";
+ console.error(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "error",
+ },
+ ]);
+ return;
+ }
+
+ const missingNameRows: string[][] = [];
+ const missingAttributesRows: string[][] = [];
+ const unknownAttributesRowsInAssets: string[][] = [];
+ const wrongAttributesRowsInAssets: string[][] = [];
+ const wrongUrlsRowsInAssets: string[][] = [];
+ const rowsIndexesToRemove: number[] = [];
+
+ // Controlling assets and attributes
+ cleanDataRows(assetsDataRows).forEach(
+ (assetDataRow, assetDataRowIndex) => {
+ const hasNoName = !assetDataRow[nameColIndex]?.trim();
+ const hasNoAttribute = !assetDataRow[attributesColIndex]?.trim();
+ const hasWrongAttribute = !NUMBERS_COMMA_SEPARATOR_REGEXP.test(
+ assetDataRow[attributesColIndex],
+ );
+ const hasWrongExternalUrl =
+ assetDataRow[externalURLColIndex]?.trim() &&
+ !URL_REGEX.test(assetDataRow[externalURLColIndex].trim());
+ const hasWrongYoutubeUrl =
+ assetDataRow[youtubeURLColIndex]?.trim() &&
+ !URL_REGEX.test(assetDataRow[youtubeURLColIndex].trim());
+
+ // Warning if no name in asset (Ignore asset)
+ if (hasNoName) {
+ missingNameRows.push(assetDataRow);
+ }
+ // Warning if no attributes in asset (Ignore asset)
+ if (hasNoAttribute) {
+ missingAttributesRows.push(assetDataRow);
+ }
+ // Else, warning if wrong attributes ids in asset. We want numbers with comma separators (Ignore asset)
+ else if (hasWrongAttribute) {
+ wrongAttributesRowsInAssets.push(assetDataRow);
+ }
+ // We get unvalidated rows to remove
+ if (hasNoName || hasNoAttribute || hasWrongAttribute) {
+ rowsIndexesToRemove.push(assetDataRowIndex);
+ }
+
+ // Warning if wrong urls in asset (No incidence)
+ if (hasWrongExternalUrl || hasWrongYoutubeUrl) {
+ wrongUrlsRowsInAssets.push(assetDataRow);
+ }
+ // Warning if unknow attributes ids in asset (No incidence)
+ const assetAttributesIds = cleanAssetAttributesIds(
+ assetDataRow[attributesColIndex],
+ );
+ let nbIdsFound = 0;
+ assetAttributesIds.forEach((id) => {
+ attributesDataRows.forEach((attributeDataRow) => {
+ if (id === attributeDataRow[idColIndex]?.trim()) {
+ nbIdsFound++;
+ }
+ });
+ });
+ if (nbIdsFound < assetAttributesIds.length) {
+ unknownAttributesRowsInAssets.push(assetDataRow);
+ }
+ },
+ );
+
+ // We remove the wrong rows from parseResults.data (The assets rows)
+ const result = cleanDataRows(assetsDataRows).filter(
+ (_, index) => !rowsIndexesToRemove.includes(index),
+ );
+ // Storing the final results
+ setAssetsMappingDataRows(result);
+
+ // Handling warnings
+ if (missingNameRows.length) {
+ const title = `Incomplete ${pluralize("asset", missingNameRows.length)}`;
+ const message = `Missing "name" in ${pluralize("asset", missingNameRows.length, true)} that ${pluralize("has", missingNameRows.length)} been ignored.\nPlease complete properly your assets mapping file.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (missingAttributesRows.length) {
+ const title = `Incomplete ${pluralize("asset", missingAttributesRows.length)}`;
+ const message = `Missing "attributes" in ${pluralize("asset", missingAttributesRows.length, true)} that ${pluralize("has", missingAttributesRows.length)} been ignored.\nPlease complete properly your assets mapping file.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (wrongAttributesRowsInAssets.length) {
+ const title = `Wrong attributes`;
+ const message = `${pluralize("asset", wrongAttributesRowsInAssets.length, true)} ${pluralize("has", wrongAttributesRowsInAssets.length)} a wrong "attributes" value and ${pluralize("has", wrongAttributesRowsInAssets.length)} been ignored. Only numbers with comma separator are allwowed.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (wrongUrlsRowsInAssets.length) {
+ const title = `Wrong URLs`;
+ const message = `${pluralize("asset", wrongUrlsRowsInAssets.length, true)} ${pluralize("has", wrongUrlsRowsInAssets.length)} a wrong "youtube_url" or "external_url" value (No incidence).\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ if (unknownAttributesRowsInAssets.length) {
+ const title = `Unknown attributes`;
+ const message = `${pluralize("asset", unknownAttributesRowsInAssets.length, true)} ${pluralize("has", unknownAttributesRowsInAssets.length)} at least one "attributes" id that doesn't exist in your attributes mapping file. (No incidence)\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setAssetsIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ },
+ });
+ } catch (e) {
+ setAssetsMappingDataRows([]);
+
+ console.error(`${e}`);
+ setToast({
+ title: "Error parsing " + files[0].file.name,
+ message: `${e}`,
+ mode: "normal",
+ type: "error",
+ });
+ }
+ };
+
+ // On upload images files
+ const onUploadImages = (images: LocalFileData[]) => {
+ if (!assetsMappingDataRows.length || !attributesMappingDataRows.length)
+ return;
+ resetIssuesErrors();
+ setImagesIssues([]);
+
+ const collectionAssetsMetadatas: CollectionAssetsMetadataFormValues[] = [];
+
+ //The rows order in the CSV determines the assets order.
+ assetsMappingDataRows.forEach((assetDataRow, assetDataRowIndex) => {
+ images.forEach((image) => {
+ if (assetDataRow[fileNameColIndex] !== image.file.name) return;
+ // --- Mapping attributes
+ const mappedAttributes: CollectionAssetsAttributeFormValues[] = [];
+ const assetAttributesIds = [
+ ...new Set(cleanAssetAttributesIds(assetDataRow[attributesColIndex])),
+ ]; // We ignore duplicate attributes ids from assets
+ assetAttributesIds.forEach((assetAttributeId) => {
+ attributesMappingDataRows.forEach(
+ (attributeDataRow, attributeDataRowIndex) => {
+ if (attributeDataRow[idColIndex] === assetAttributeId) {
+ mappedAttributes.push({
+ value: attributeDataRow[valueColIndex],
+ type: attributeDataRow[typeColIndex],
+ });
+ }
+ },
+ );
+ });
+
+ // --- Mapping assets
+ const mappedAssets: CollectionAssetsMetadataFormValues = {
+ image,
+ name: assetDataRow[nameColIndex],
+ description: assetDataRow[descriptionColIndex],
+ externalUrl: assetDataRow[externalURLColIndex],
+ youtubeUrl: assetDataRow[youtubeURLColIndex],
+ attributes: mappedAttributes,
+ };
+ collectionAssetsMetadatas.push(mappedAssets);
+ });
+ });
+ assetsMetadatasForm.setValue("assetsMetadatas", collectionAssetsMetadatas);
+
+ // Handling warnings
+ if (collectionAssetsMetadatas.length < images.length) {
+ const nbUnexpectedImages =
+ images.length - collectionAssetsMetadatas.length;
+ const title = `Unexpected ${pluralize("image", nbUnexpectedImages)}`;
+ const message = `${pluralize("image", nbUnexpectedImages, true)} ${pluralize("is", nbUnexpectedImages)} not expected in your assets mapping file and ${pluralize("has", nbUnexpectedImages)} been ignored.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setImagesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+
+ if (assetsMappingDataRows.length > collectionAssetsMetadatas.length) {
+ const nbMissingImages =
+ assetsMappingDataRows.length - collectionAssetsMetadatas.length;
+ const title = `Missing ${pluralize("image", nbMissingImages)}`;
+ const message = `${pluralize("image", nbMissingImages, true)} expected in your assets mapping file ${pluralize("is", nbMissingImages)} missing.\nCheck the description for more information.`;
+ console.warn(title + ".\n" + message);
+ setImagesIssues((issues) => [
+ ...issues,
+ {
+ title,
+ message,
+ type: "warning",
+ },
+ ]);
+ }
+ };
+
+ return (
+
+ {/* ===== Issues */}
+ {attributesIssues.map((issue, index) => (
+
+ setAttributesIssues((issues) =>
+ issues.filter((_, i) => i !== index),
+ )
+ }
+ />
+ ))}
+ {assetsIssues.map((issue, index) => (
+
+ setAssetsIssues((issues) => issues.filter((_, i) => i !== index))
+ }
+ />
+ ))}
+ {imagesIssues.map((issue, index) => (
+
+ setImagesIssues((issues) => issues.filter((_, i) => i !== index))
+ }
+ />
+ ))}
+
+
+ {/* ===== Left container */}
+
+
+
+ {/* Firstly: Attributes */}
+
+
+
+
+ {/* Secondly: Assets */}
+
+
+
+
+
+
+ {/* Thirdly: Images */}
+
+
+ {(!!fields.length ||
+ !!assetsMappingDataRows.length ||
+ !!attributesMappingDataRows.length) && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+ {/* ---- Separator*/}
+
+
+ {/* ===== Right container */}
+
+ metadata.image!,
+ )}
+ onPressItem={(file, itemIndex) => {
+ setAssetModalVisible(true);
+ setSelectedElemIndex(itemIndex);
+ }}
+ onPressDeleteItem={onRemoveImage}
+ />
+
+
+ {selectedElem && selectedElemIndex !== undefined && (
+ setAssetModalVisible(false)}
+ isVisible={assetModalVisible}
+ elem={selectedElem}
+ elemIndex={selectedElemIndex}
+ />
+ )}
+
+
+ );
+};
+
+const ResetAllButton: FC<{
+ onPress: () => void;
+}> = ({ onPress }) => {
+ return (
+
+
+
+
+ Remove all files
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx
new file mode 100644
index 0000000000..bb176fbb8d
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx
@@ -0,0 +1,116 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { FC, useEffect, useState } from "react";
+import { FormProvider, useForm, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { AssetsTab } from "./AssetsTab";
+import { UriTab } from "./UriTab";
+
+import { BrandText } from "@/components/BrandText";
+import { SpacerColumn } from "@/components/spacer";
+import { Tabs } from "@/components/tabs/Tabs";
+import { useIsMobile } from "@/hooks/useIsMobile";
+import { neutral33, neutral77, primaryColor } from "@/utils/style/colors";
+import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionAssetsMetadatasFormValues,
+ CollectionFormValues,
+ ZodCollectionAssetsMetadatasFormValues,
+} from "@/utils/types/launchpad";
+
+const AssetsAndMetadataTabItems = {
+ assets: {
+ name: "Upload assets & metadata",
+ },
+ uri: {
+ name: "Use an existing base URI",
+ },
+};
+
+export const LaunchpadAssetsAndMetadata: FC = () => {
+ const isMobile = useIsMobile();
+ const [selectedTab, setSelectedTab] =
+ useState("assets");
+ const { watch, setValue } = useFormContext();
+ const collectionAssetsMetadatas = watch("assetsMetadatas");
+ const assetsMetadatasForm = useForm({
+ mode: "all",
+ defaultValues: collectionAssetsMetadatas, // Retrieve assetsMetadatas from collectionForm
+ resolver: zodResolver(ZodCollectionAssetsMetadatasFormValues),
+ });
+ const assetsMetadatas = assetsMetadatasForm.watch("assetsMetadatas");
+
+ // Plug assetsMetadatas from assetsMetadatasForm to collectionForm
+ useEffect(() => {
+ setValue("assetsMetadatas.assetsMetadatas", assetsMetadatas);
+ }, [assetsMetadatas, setValue]);
+
+ return (
+
+
+ Assets & Metadata
+
+
+
+
+ Make sure you check out{" "}
+
+ documentation
+ {" "}
+ on how to create your collection
+
+
+
+
+
+
+ {selectedTab === "assets" && (
+
+
+
+ )}
+
+ {/*TODO: Handle this ?*/}
+ {selectedTab === "uri" && }
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx
new file mode 100644
index 0000000000..c7af4a0d00
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx
@@ -0,0 +1,60 @@
+import { FC } from "react";
+import { useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { SpacerColumn } from "@/components/spacer";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { neutral77 } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const UriTab: FC = () => {
+ const collectionForm = useFormContext();
+
+ return (
+
+
+
+
+ Though Teritori's tr721 contract allows for off-chain metadata
+ storage, it is recommended to use a decentralized storage solution,
+ such as IPFS. You may head over to NFT.Storage and upload your
+ assets & metadata manually to get a base URI for your collection.
+
+
+
+
+ label="Base Token URI"
+ placeHolder="ipfs://"
+ name="baseTokenUri"
+ form={collectionForm}
+ required={false}
+ />
+
+
+ name="coverImageUri"
+ label="Cover Image URI"
+ placeHolder="ipfs://"
+ form={collectionForm}
+ required={false}
+ />
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx
new file mode 100644
index 0000000000..2461810e18
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx
@@ -0,0 +1,137 @@
+import { FC } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { ErrorText } from "@/components/ErrorText";
+import { NetworkSelectorWithLabel } from "@/components/NetworkSelector/NetworkSelectorWithLabel";
+import { FileUploaderSmall } from "@/components/inputs/FileUploaderSmall";
+import { SpacerColumn } from "@/components/spacer";
+import { NetworkFeature } from "@/networks";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { IMAGE_MIME_TYPES } from "@/utils/mime";
+import { neutral55, neutral77, primaryColor } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadBasic: FC = () => {
+ const collectionForm = useFormContext();
+ const coverImage = collectionForm.watch("coverImage");
+
+ return (
+
+
+ Create Collection
+
+
+
+
+ Make sure you check out{" "}
+
+ documentation
+ {" "}
+ on how to create your collection
+
+
+
+
+
+
+ label="Name"
+ placeHolder="My Awesome Collection"
+ name="name"
+ form={collectionForm}
+ />
+
+
+ label="Describe your project: "
+ sublabel={
+
+
+ 1. What's your concept?
+
+
+ 2. How is it different?
+
+
+ 3. What's your goal?
+
+
+ }
+ placeHolder="Describe here..."
+ name="description"
+ form={collectionForm}
+ />
+
+
+ label="Symbol"
+ placeHolder="Symbol"
+ name="symbol"
+ form={collectionForm}
+ valueModifier={(value) => value.toUpperCase()}
+ />
+
+
+ control={collectionForm.control}
+ name="coverImage"
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(files[0]);
+ }}
+ filesCount={coverImage ? 1 : 0}
+ mimeTypes={IMAGE_MIME_TYPES}
+ required
+ imageToShow={coverImage}
+ onPressDelete={() => onChange(undefined)}
+ />
+
+ {collectionForm.getFieldState("coverImage").error?.message}
+
+ >
+ )}
+ />
+
+
+
+ label="NFT.Storage JWT"
+ sublabel={
+
+ Used to upload the cover image and the assets to your NFT Storage
+
+ }
+ placeHolder="My Awesome Collection"
+ name="assetsMetadatas.nftApiKey"
+ form={collectionForm}
+ />
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx
new file mode 100644
index 0000000000..46fadde159
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx
@@ -0,0 +1,163 @@
+import { FC } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { MultipleSelectInput } from "../../../components/inputs/selectInputs/MultipleSelectInput";
+import { SelectInputLaunchpad } from "../../../components/inputs/selectInputs/SelectInputLaunchpad";
+
+import { BrandText } from "@/components/BrandText";
+import { ErrorText } from "@/components/ErrorText";
+import { SpacerColumn } from "@/components/spacer";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { neutral55, neutral77 } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadDetails: FC = () => {
+ const collectionForm = useFormContext();
+ const projectTypes = collectionForm.watch("projectTypes") || [];
+ const isDerivativeProject = collectionForm.watch("isDerivativeProject");
+ const isPreviouslyApplied = collectionForm.watch("isPreviouslyApplied");
+
+ return (
+
+
+ Collection details
+
+
+ Information about your collection
+
+
+
+
+ label="Website Link"
+ sublabel={
+
+
+ Your project's website. It must display the project's discord
+ and twitter, the roadmap/whitepaper and team's information.
+ Please, be fully transparent to facilitate your project's review
+ !
+
+
+ }
+ placeHolder="https://website..."
+ name="websiteLink"
+ form={collectionForm}
+ />
+
+
+ label="Main contact email address: "
+ placeHolder="contact@email.com"
+ name="email"
+ form={collectionForm}
+ />
+
+
+ name="isDerivativeProject"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(item === "Yes");
+ }}
+ label="Is your project a derivative project?"
+ style={{ zIndex: 3 }}
+ />
+
+ {
+ collectionForm.getFieldState("isDerivativeProject").error
+ ?.message
+ }
+
+ >
+ )}
+ />
+
+
+
+ name="projectTypes"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ const selectedProjectTypes = projectTypes.includes(item)
+ ? projectTypes.filter((data) => data !== item)
+ : [...projectTypes, item];
+ onChange(selectedProjectTypes);
+ }}
+ label="Project type:"
+ sublabel={
+
+ Multiple answers allowed
+
+ }
+ style={{ zIndex: 2 }}
+ />
+
+ {collectionForm.getFieldState("projectTypes").error?.message}
+
+ >
+ )}
+ />
+
+
+
+ name="isPreviouslyApplied"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ onChange(item === "Yes");
+ }}
+ label="Have you previously applied for the same project before?"
+ style={{ zIndex: 1 }}
+ />
+
+ {
+ collectionForm.getFieldState("isPreviouslyApplied").error
+ ?.message
+ }
+
+ >
+ )}
+ />
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx
new file mode 100644
index 0000000000..2ce7561d20
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx
@@ -0,0 +1,60 @@
+import { FC } from "react";
+import { useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { SpacerColumn } from "@/components/spacer";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { neutral55, neutral77 } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const ConfigureRoyaltyDetails: FC = () => {
+ const collectionForm = useFormContext();
+
+ return (
+
+
+ Royalty Details
+
+
+ Configure royalties
+
+
+
+
+ label="Payment Address "
+ placeHolder="teritori123456789qwertyuiopasdfghjklzxcvbnm"
+ name="royaltyAddress"
+ sublabel={
+
+
+ Address to receive royalties
+
+
+ }
+ form={collectionForm}
+ required={false}
+ />
+
+
+ label="Share Percentage "
+ placeHolder="8%"
+ name="royaltyPercentage"
+ sublabel={
+
+
+ Percentage of royalties to be paid
+
+
+ }
+ form={collectionForm}
+ required={false}
+ />
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx
new file mode 100644
index 0000000000..43c6f9736b
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx
@@ -0,0 +1,46 @@
+import { FC } from "react";
+import { UseFieldArrayRemove, UseFieldArrayUpdate } from "react-hook-form";
+
+import { LaunchpadMintPeriodAccordionBottom } from "./LaunchpadMintPeriodAccordionBottom";
+import { LaunchpadMintPeriodAccordionTop } from "./LaunchpadMintPeriodAccordionTop";
+
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import { neutral00, neutral22, neutral33 } from "@/utils/style/colors";
+import {
+ CollectionFormValues,
+ CollectionMintPeriodFormValues,
+} from "@/utils/types/launchpad";
+
+export const LaunchpadMintPeriodAccordion: FC<{
+ elem: CollectionMintPeriodFormValues;
+ elemIndex: number;
+ remove: UseFieldArrayRemove;
+ update: UseFieldArrayUpdate;
+ closeAll: () => void;
+}> = ({ elem, elemIndex, remove, update, closeAll }) => {
+ return (
+
+
+
+ {elem.isOpen && (
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx
new file mode 100644
index 0000000000..352429ce67
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx
@@ -0,0 +1,207 @@
+import { FC } from "react";
+import {
+ Controller,
+ UseFieldArrayRemove,
+ UseFieldArrayUpdate,
+ useFormContext,
+} from "react-hook-form";
+import { View, TouchableOpacity } from "react-native";
+
+import trashSVG from "@/assets/icons/trash.svg";
+import { BrandText } from "@/components/BrandText";
+import { ErrorText } from "@/components/ErrorText";
+import { SVG } from "@/components/SVG";
+import { CsvTextRowsInput } from "@/components/inputs/CsvTextRowsInput";
+import { DateTimeInput } from "@/components/inputs/DateTimeInput";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import { getCurrency } from "@/networks";
+import { CurrencyInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { errorColor, neutral55, neutral77 } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionFormValues,
+ CollectionMintPeriodFormValues,
+} from "@/utils/types/launchpad";
+
+export const LaunchpadMintPeriodAccordionBottom: FC<{
+ elem: CollectionMintPeriodFormValues;
+ update: UseFieldArrayUpdate;
+ remove: UseFieldArrayRemove;
+ elemIndex: number;
+}> = ({ elem, elemIndex, remove, update }) => {
+ // Since the Collection network is the selected network, we use useSelectedNetworkId (See LaunchpadBasic.tsx)
+ const networkId = useSelectedNetworkId();
+ const collectionForm = useFormContext();
+ const amountPath = `mintPeriods.${elemIndex}.price.amount` as const;
+ const startTimePath = `mintPeriods.${elemIndex}.startTime` as const;
+ const endTimePath = `mintPeriods.${elemIndex}.endTime` as const;
+ const maxTokensPath = `mintPeriods.${elemIndex}.maxTokens` as const;
+ const perAddressLimitPath =
+ `mintPeriods.${elemIndex}.perAddressLimit` as const;
+ const mintPeriods = collectionForm.watch("mintPeriods");
+ const amount = collectionForm.watch(amountPath);
+ const startTime = collectionForm.watch(startTimePath);
+ const endTime = collectionForm.watch(endTimePath);
+ const selectedCurrency = getCurrency(networkId, elem.price.denom);
+
+ return (
+
+
+ name={amountPath}
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+ <>
+ {
+ update(elemIndex, {
+ ...elem,
+ price: { ...elem.price, denom: currency.denom },
+ });
+ }}
+ onChangeAmountAtomics={(amountAtomics) => {
+ onChange(amountAtomics);
+ }}
+ required={false}
+ />
+
+ {collectionForm.getFieldState(amountPath).error?.message}
+
+ >
+ )}
+ />
+
+
+
+ label="Max Tokens"
+ placeHolder="0"
+ name={maxTokensPath}
+ sublabel={
+
+
+ Maximum number of mintable tokens
+
+
+ }
+ form={collectionForm}
+ required={false}
+ />
+
+
+ label="Per Address Limit"
+ placeHolder="0"
+ name={perAddressLimitPath}
+ sublabel={
+
+
+ Maximum number of mintable tokens per address
+
+
+ }
+ form={collectionForm}
+ required={false}
+ />
+
+
+ name={startTimePath}
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+
+ )}
+ />
+
+
+
+ name={endTimePath}
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+
+ )}
+ />
+
+
+
+
+ Whitelist Addresses
+
+
+ Select a TXT or CSV file that contains the whitelisted addresses (One
+ address per line)
+
+
+
+
+ update(elemIndex, {
+ ...elem,
+ whitelistAddressesFile: file,
+ whitelistAddresses: rows,
+ })
+ }
+ />
+
+
+ {
+ // Can remove periods only if more than one (There will be least one period left)
+ (elemIndex > 0 || mintPeriods.length > 1) && (
+ <>
+
+
+
+ remove(elemIndex)}
+ >
+
+
+
+ Remove Mint Period
+
+
+ >
+ )
+ }
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx
new file mode 100644
index 0000000000..e0ec55e36e
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx
@@ -0,0 +1,95 @@
+import { FC } from "react";
+import { UseFieldArrayUpdate } from "react-hook-form";
+import { TouchableOpacity, View } from "react-native";
+
+import chevronDownSVG from "@/assets/icons/chevron-down.svg";
+import chevronUpSVG from "@/assets/icons/chevron-up.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn } from "@/components/spacer";
+import { secondaryColor } from "@/utils/style/colors";
+import { fontSemibold16 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionFormValues,
+ CollectionMintPeriodFormValues,
+} from "@/utils/types/launchpad";
+
+export const LaunchpadMintPeriodAccordionTop: FC<{
+ elem: CollectionMintPeriodFormValues;
+ elemIndex: number;
+ update: UseFieldArrayUpdate;
+ closeAll: () => void;
+}> = ({ elem, elemIndex, update, closeAll }) => {
+ if (elem.isOpen) {
+ return (
+ update(elemIndex, { ...elem, isOpen: false })}
+ style={{
+ paddingTop: layout.spacing_x1,
+ paddingHorizontal: layout.spacing_x1,
+ }}
+ >
+
+
+ {`Period #${elemIndex + 1}`}
+
+
+
+
+
+
+
+
+ );
+ } else {
+ return (
+ {
+ closeAll();
+ update(elemIndex, { ...elem, isOpen: true });
+ }}
+ >
+
+
+ {`Period #${elemIndex + 1}`}
+
+
+
+
+
+
+ );
+ }
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx
new file mode 100644
index 0000000000..7879fa7d15
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx
@@ -0,0 +1,119 @@
+import { FC, Fragment, useCallback } from "react";
+import { useFieldArray, useFormContext } from "react-hook-form";
+import { TouchableOpacity, View } from "react-native";
+
+import addSVG from "@/assets/icons/add-secondary.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useSelectedNetworkInfo } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { getNetworkFeature, NetworkFeature } from "@/networks";
+import { LaunchpadMintPeriodAccordion } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion";
+import { secondaryColor } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadMintPeriods: FC = () => {
+ const selectedWallet = useSelectedWallet();
+ const networkId = selectedWallet?.networkId || "";
+ const collectionForm = useFormContext();
+ const selectedNetwork = useSelectedNetworkInfo();
+
+ const { update, append, remove } = useFieldArray({
+ control: collectionForm.control,
+ name: "mintPeriods",
+ });
+ const mintPeriods = collectionForm.watch("mintPeriods");
+
+ const closeAll = useCallback(() => {
+ mintPeriods.map((elem, index) => {
+ update(index, { ...elem, isOpen: false });
+ });
+ }, [mintPeriods, update]);
+
+ const createMintPeriod = useCallback(() => {
+ if (!selectedNetwork) return;
+ closeAll();
+ const feature = getNetworkFeature(
+ networkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+ if (!feature) {
+ throw new Error("This network does not support nft launchpad");
+ }
+ append({
+ price: { denom: selectedNetwork.currencies[0].denom, amount: "" },
+ maxTokens: "",
+ perAddressLimit: "",
+ startTime: 0,
+ endTime: 0,
+ isOpen: true,
+ });
+ }, [networkId, closeAll, append, selectedNetwork]);
+
+ return (
+
+
+ {mintPeriods.map((elem, index) => {
+ return (
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ Add Minting Period
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx
new file mode 100644
index 0000000000..048c004a3b
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx
@@ -0,0 +1,67 @@
+import { FC } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { ConfigureRoyaltyDetails } from "./ConfigureRoyaltyDetails";
+
+import { BrandText } from "@/components/BrandText";
+import { DateTimeInput } from "@/components/inputs/DateTimeInput";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn } from "@/components/spacer";
+import { LaunchpadMintPeriods } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods";
+import { neutral77 } from "@/utils/style/colors";
+import { fontSemibold14, fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadMinting: FC = () => {
+ const collectionForm = useFormContext();
+ const revealTime = collectionForm.watch("revealTime");
+ return (
+
+
+ Minting details
+
+
+ Configure the global minting settings
+
+
+
+
+ name="revealTime"
+ control={collectionForm.control}
+ render={({ field: { onChange } }) => (
+
+ )}
+ />
+
+
+
+
+ Minting Periods
+
+
+ Configure the minting periods, a whitelist can be applied
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx
new file mode 100644
index 0000000000..61458ad3ea
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx
@@ -0,0 +1,110 @@
+import { FC } from "react";
+import { useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { SpacerColumn } from "@/components/spacer";
+import { TextInputLaunchpad } from "@/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad";
+import { neutral55, neutral77 } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold20,
+} from "@/utils/style/fonts";
+import { CollectionFormValues } from "@/utils/types/launchpad";
+
+export const LaunchpadTeamAndInvestment: FC = () => {
+ const collectionForm = useFormContext();
+
+ return (
+
+
+ Team & Investments
+
+
+ Fill the information about the team and investors
+
+
+
+
+ label="Describe your team: "
+ sublabel={
+
+
+ 1. How many core members are you? ( Working on the project daily
+ )
+
+
+ 2. Who does what in your team?
+
+
+ 3. Past accomplishments or projects?
+
+
+ 4. How did you guys meet?
+
+
+ 5. Please add Linkedin links for all your members.
+
+
+ }
+ placeHolder="Describe here..."
+ name="teamDescription"
+ form={collectionForm}
+ />
+
+
+ label="Do you have any partners on the project? "
+ sublabel={
+
+
+ If yes, who are they? What do they do for you?
+
+
+ }
+ placeHolder="Type here..."
+ name="partnersDescription"
+ form={collectionForm}
+ />
+
+
+ label="What have you invested in this project so far? "
+ sublabel={
+
+
+ 1. How much upfront capital has been invested?
+
+
+ 2. Have you raised outside funding for the project?
+
+
+ 3. How long has the project been worked on?
+
+
+ 4. Is there a proof of concept or demo to show?
+
+
+ }
+ placeHolder="Type here..."
+ name="investDescription"
+ form={collectionForm}
+ />
+
+
+ label="Investment links and attachments "
+ sublabel={
+
+
+ Please provide any relevant links regarding your investment. You
+ can also post a google drive link.
+
+
+ }
+ placeHolder="Type here..."
+ name="investLink"
+ form={collectionForm}
+ />
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx
new file mode 100644
index 0000000000..c5e9a406ed
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx
@@ -0,0 +1,233 @@
+import { Decimal } from "@cosmjs/math";
+import { FC, Fragment, ReactElement, useRef, useState } from "react";
+import { StyleProp, TextInput, View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { ErrorText } from "@/components/ErrorText";
+import { Box, BoxStyle } from "@/components/boxes/Box";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { Label } from "@/components/inputs/TextInputCustom";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useDropdowns } from "@/hooks/useDropdowns";
+import {
+ allNetworks,
+ CurrencyInfo,
+ getNativeCurrency,
+ getNetwork,
+} from "@/networks";
+import { SelectableCurrencySmall } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall";
+import { SelectedCurrencySmall } from "@/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall";
+import { validateFloatWithDecimals } from "@/utils/formRules";
+import {
+ errorColor,
+ neutral17,
+ neutral22,
+ neutral33,
+ neutral77,
+ secondaryColor,
+} from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const CurrencyInputLaunchpad: FC<{
+ label?: string;
+ placeHolder?: string;
+ subtitle?: ReactElement;
+ sublabel?: ReactElement;
+ required?: boolean;
+ error?: string;
+ networkId: string;
+ onSelectCurrency: (currency: CurrencyInfo) => void;
+ onChangeAmountAtomics: (amountAtomics: string) => void;
+ amountAtomics?: string;
+ currency?: CurrencyInfo;
+ boxStyle?: StyleProp;
+}> = ({
+ label,
+ placeHolder = "0",
+ sublabel,
+ subtitle,
+ required = true,
+ error,
+ networkId,
+ onSelectCurrency,
+ onChangeAmountAtomics,
+ boxStyle,
+ amountAtomics,
+ currency,
+}) => {
+ const network = getNetwork(networkId);
+ const currencies: CurrencyInfo[] = network?.currencies || [];
+ const [selectedCurrency, setSelectedCurrency] = useState(
+ currency || currencies[0],
+ );
+ const selectedCurrencyNetwork = allNetworks.find(
+ (network) =>
+ !!network.currencies.find(
+ (currency) => currency.denom === selectedCurrency?.denom,
+ ),
+ );
+ const selectedNativeCurrency = getNativeCurrency(
+ selectedCurrencyNetwork?.id,
+ selectedCurrency?.denom,
+ );
+ const [value, setValue] = useState(
+ selectedNativeCurrency && amountAtomics
+ ? Decimal.fromAtomics(
+ amountAtomics,
+ selectedNativeCurrency.decimals,
+ ).toString()
+ : "",
+ );
+
+ const inputRef = useRef(null);
+ const [isDropdownOpen, setDropdownState, dropdownRef] = useDropdowns();
+ const [hovered, setHovered] = useState(false);
+ const boxHeight = 40;
+
+ const onChangeText = (text: string) => {
+ if (!text) {
+ setValue("");
+ onChangeAmountAtomics("");
+ return;
+ }
+
+ if (
+ selectedNativeCurrency &&
+ validateFloatWithDecimals(text, selectedNativeCurrency.decimals)
+ ) {
+ setValue(text);
+ onChangeAmountAtomics(
+ Decimal.fromUserInput(
+ text.endsWith(".") ? text + "0" : text,
+ selectedNativeCurrency.decimals,
+ ).atomics,
+ );
+ }
+ };
+
+ const onPressSelectableCurrency = (currency: CurrencyInfo) => {
+ setValue("");
+ setSelectedCurrency(currency);
+ onChangeAmountAtomics("");
+ onSelectCurrency(currency);
+ setDropdownState(false);
+ };
+
+ if (!selectedCurrencyNetwork)
+ return Invalid network;
+ if (!selectedNativeCurrency)
+ return (
+
+ Invalid native currency
+
+ );
+ return (
+ setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onPress={() => inputRef?.current?.focus()}
+ style={{ zIndex: 1 }}
+ >
+ {/*---- Label*/}
+ {label && (
+ <>
+
+
+ {subtitle}
+
+ {sublabel && sublabel}
+
+ >
+ )}
+
+
+ {/*---- Input*/}
+
+
+
+
+
+ {/*---- Selected currency*/}
+
+
+ {/*---- Dropdown selectable currencies */}
+ {currencies?.length && isDropdownOpen && (
+
+ {currencies?.map((currencyInfo, index) => (
+
+
+ onPressSelectableCurrency(currencyInfo)}
+ />
+
+ ))}
+
+ )}
+
+
+ {error}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx
new file mode 100644
index 0000000000..fecac28491
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx
@@ -0,0 +1,42 @@
+import { FC } from "react";
+import { TouchableOpacity, View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { CurrencyIcon } from "@/components/CurrencyIcon";
+import { CurrencyInfo, getNativeCurrency } from "@/networks";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const SelectableCurrencySmall: FC<{
+ onPressItem: () => void;
+ currency: CurrencyInfo;
+ networkId: string;
+}> = ({ onPressItem, currency, networkId }) => {
+ return (
+ <>
+
+
+
+
+
+ {getNativeCurrency(networkId, currency?.denom)?.displayName}
+
+
+
+
+ >
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx
new file mode 100644
index 0000000000..0d655e1dbb
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx
@@ -0,0 +1,73 @@
+import { forwardRef } from "react";
+import { TouchableOpacity, View } from "react-native";
+
+import chevronDownSVG from "@/assets/icons/chevron-down.svg";
+import chevronUpSVG from "@/assets/icons/chevron-up.svg";
+import { BrandText } from "@/components/BrandText";
+import { CurrencyIcon } from "@/components/CurrencyIcon";
+import { SVG } from "@/components/SVG";
+import { NativeCurrencyInfo } from "@/networks";
+import { secondaryColor } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const SelectedCurrencySmall = forwardRef<
+ View,
+ {
+ currency?: NativeCurrencyInfo;
+ selectedNetworkId: string;
+ isDropdownOpen: boolean;
+ setDropdownState: (val: boolean) => void;
+ disabled?: boolean;
+ }
+>(
+ (
+ { currency, selectedNetworkId, isDropdownOpen, setDropdownState, disabled },
+ ref,
+ ) => {
+ return (
+
+ setDropdownState(!isDropdownOpen)}
+ style={{
+ flexDirection: "row",
+ alignItems: "center",
+ }}
+ disabled={disabled}
+ >
+
+
+
+
+ {currency?.displayName || "ERROR"}
+
+ {!disabled && (
+
+ )}
+
+
+
+
+ );
+ },
+);
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx
new file mode 100644
index 0000000000..2a15418c34
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx
@@ -0,0 +1,98 @@
+import { ReactElement, useRef, useState } from "react";
+import {
+ FieldValues,
+ Path,
+ useController,
+ UseFormReturn,
+} from "react-hook-form";
+import { TextInput, TextInputProps, TextStyle } from "react-native";
+
+import { ErrorText } from "@/components/ErrorText";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { Label } from "@/components/inputs/TextInputCustom";
+import { SpacerColumn } from "@/components/spacer";
+import { neutral22, neutral77, secondaryColor } from "@/utils/style/colors";
+import { fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+interface TextInputLaunchpadProps
+ extends Omit {
+ label: string;
+ placeHolder: string;
+ form: UseFormReturn;
+ name: Path;
+ sublabel?: ReactElement;
+ valueModifier?: (value: string) => string;
+ required?: boolean;
+ disabled?: boolean;
+}
+
+export const TextInputLaunchpad = ({
+ form,
+ name,
+ label,
+ placeHolder,
+ sublabel,
+ valueModifier,
+ disabled,
+ required = true,
+ ...restProps
+}: TextInputLaunchpadProps) => {
+ const inputRef = useRef(null);
+ const [hovered, setHovered] = useState(false);
+ const { fieldState, field } = useController({
+ name,
+ control: form.control,
+ });
+ return (
+ setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onPress={() => inputRef?.current?.focus()}
+ style={{ width: "100%", marginBottom: layout.spacing_x2 }}
+ disabled={disabled}
+ >
+
+ {sublabel && sublabel}
+
+
+
+ valueModifier
+ ? field.onChange(valueModifier(text))
+ : field.onChange(text)
+ }
+ value={field.value || ""}
+ ref={inputRef}
+ editable={!disabled}
+ {...restProps}
+ />
+
+
+ {fieldState.error?.message}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx
new file mode 100644
index 0000000000..7059171bb3
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx
@@ -0,0 +1,167 @@
+import { FC, ReactElement, useState } from "react";
+import { TouchableOpacity, View, ViewStyle } from "react-native";
+
+import chevronDownSVG from "@/assets/icons/chevron-down.svg";
+import chevronUpSVG from "@/assets/icons/chevron-up.svg";
+import { BrandText } from "@/components/BrandText";
+import { Checkbox } from "@/components/Checkbox";
+import { SVG } from "@/components/SVG";
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { Label } from "@/components/inputs/TextInputCustom";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn } from "@/components/spacer";
+import { useDropdowns } from "@/hooks/useDropdowns";
+import {
+ neutral22,
+ neutral44,
+ neutral55,
+ neutral77,
+ secondaryColor,
+} from "@/utils/style/colors";
+import { fontMedium14, fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+interface Props {
+ style?: ViewStyle;
+ onDropdownClosed?: () => void;
+ dropdownOptions: string[];
+ placeHolder?: string;
+ onPressItem: (item: string) => void;
+ items: string[];
+ label: string;
+ sublabel?: ReactElement;
+ required?: boolean;
+}
+
+export const MultipleSelectInput: FC = ({
+ style,
+ dropdownOptions,
+ placeHolder,
+ items,
+ label,
+ onPressItem,
+ sublabel,
+ required = true,
+}) => {
+ const [isDropdownOpen, setDropdownState, ref] = useDropdowns();
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
+ setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onPress={() => setDropdownState(!isDropdownOpen)}
+ >
+
+ {sublabel && sublabel}
+
+
+
+
+
+ 0 ? secondaryColor : neutral77,
+ },
+ ]}
+ >
+ {items?.length > 0 ? items.join(", ") : placeHolder}
+
+
+
+
+
+ {isDropdownOpen && (
+
+ {dropdownOptions.map((item, index) => (
+ {
+ onPressItem(item);
+ }}
+ key={index}
+ style={{
+ paddingTop: layout.spacing_x1_5,
+ width: "100%",
+ }}
+ >
+
+
+
+
+ {item}
+
+
+ {dropdownOptions.length - 1 !== index && (
+ <>
+
+
+ >
+ )}
+
+ ))}
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx
new file mode 100644
index 0000000000..addeb120f2
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx
@@ -0,0 +1,160 @@
+import { FC, useState } from "react";
+import { TouchableOpacity, View, ViewStyle } from "react-native";
+
+import chevronDownSVG from "@/assets/icons/chevron-down.svg";
+import chevronUpSVG from "@/assets/icons/chevron-up.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { Label } from "@/components/inputs/TextInputCustom";
+import { Separator } from "@/components/separators/Separator";
+import { SpacerColumn } from "@/components/spacer";
+import { useDropdowns } from "@/hooks/useDropdowns";
+import {
+ neutral22,
+ neutral44,
+ neutral55,
+ neutral77,
+ secondaryColor,
+} from "@/utils/style/colors";
+import { fontMedium14, fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+interface Props {
+ style?: ViewStyle;
+ onDropdownClosed?: () => void;
+ dropdownOptions: string[];
+ placeHolder?: string;
+ onPressItem: (item: string) => void;
+ item?: string;
+ label: string;
+ required?: boolean;
+}
+
+export const SelectInputLaunchpad: FC = ({
+ style,
+ dropdownOptions,
+ placeHolder,
+ item,
+ label,
+ onPressItem,
+ required = true,
+}) => {
+ const [isDropdownOpen, setDropdownState, ref] = useDropdowns();
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
+ setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ onPress={() => setDropdownState(!isDropdownOpen)}
+ >
+
+
+
+
+
+
+ {item ? item : placeHolder}
+
+
+
+
+
+ {isDropdownOpen && (
+
+ {dropdownOptions.map((item, index) => (
+ {
+ setDropdownState(false);
+ onPressItem(item);
+ }}
+ key={index}
+ style={{
+ paddingTop: layout.spacing_x1_5,
+ width: "100%",
+ }}
+ >
+
+ {item}
+
+
+ {dropdownOptions.length - 1 !== index && (
+ <>
+
+
+ >
+ )}
+
+ ))}
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx b/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx
index 6337588388..09c1048c09 100644
--- a/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx
+++ b/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { View } from "react-native";
import {
@@ -7,12 +6,14 @@ import {
Sort,
SortDirection,
} from "@/api/marketplace/v1/marketplace";
+import { BrandText } from "@/components/BrandText";
import { ScreenContainer } from "@/components/ScreenContainer";
import { CollectionsCarouselHeader } from "@/components/carousels/CollectionsCarouselHeader";
import { CollectionGallery } from "@/components/collections/CollectionGallery";
import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
import { getNetwork, NetworkFeature } from "@/networks";
import { ScreenFC } from "@/utils/navigation";
+import { fontSemibold20 } from "@/utils/style/fonts";
import { layout } from "@/utils/style/layout";
export const LaunchpadScreen: ScreenFC<"Launchpad"> = () => {
@@ -21,6 +22,7 @@ export const LaunchpadScreen: ScreenFC<"Launchpad"> = () => {
return (
Launchpad}
>
= ({
notFound,
refetch: refetchCollectionInfo,
} = useCollectionInfo(id);
- const { setToastError } = useFeedbacks();
+ const { setToast } = useFeedbacks();
const { showConnectWalletModal, showNotEnoughFundsModal } =
useWalletControl();
const [viewWidth, setViewWidth] = useState(0);
@@ -283,11 +282,13 @@ export const MintCollectionScreen: ScreenFC<"MintCollection"> = ({
});
return;
}
- setToastError(initialToastError);
+ setToast(initialToast);
if (!wallet) {
- setToastError({
+ setToast({
title: "Error",
- message: `no wallet`,
+ message: "no wallet",
+ type: "error",
+ mode: "normal",
});
return;
}
@@ -299,9 +300,11 @@ export const MintCollectionScreen: ScreenFC<"MintCollection"> = ({
await ethereumMint(network, wallet);
break;
default:
- setToastError({
+ setToast({
title: "Error",
message: `unsupported network ${network?.id}`,
+ type: "error",
+ mode: "normal",
});
return;
}
@@ -311,9 +314,11 @@ export const MintCollectionScreen: ScreenFC<"MintCollection"> = ({
setMinted(false);
} catch (e) {
if (e instanceof Error) {
- return setToastError({
+ return setToast({
title: "Mint failed",
message: prettyError(e),
+ type: "error",
+ mode: "normal",
});
}
console.error(e);
@@ -326,7 +331,7 @@ export const MintCollectionScreen: ScreenFC<"MintCollection"> = ({
cosmosMint,
ethereumMint,
network,
- setToastError,
+ setToast,
wallet,
showNotEnoughFundsModal,
]);
@@ -441,7 +446,7 @@ export const MintCollectionScreen: ScreenFC<"MintCollection"> = ({
}}
/>
-
+
{/* Upper section */}
= ({
)}
-
+
{mintTermsConditionsURL && (
= ({
margin: layout.spacing_x2,
}}
>
-
+
{info.image ? (
= ({
/>
)}
-
+
= ({
{info.mintStarted ? (
<>
{info.mintPhases.map((phase, index) => {
- return ;
+ return ;
})}
= ({
);
};
-const AttributesCard: React.FC<{
- style?: StyleProp;
+const AttributesCard: FC<{
+ style?: StyleProp;
label: string;
value: string;
}> = ({ style, label, value }) => {
return (
-
{value}
-
+
);
};
-const PresaleActivy: React.FC<{
+const PresaleActivity: FC<{
info: MintPhase;
}> = ({ info }) => {
const now = Long.fromNumber(Date.now() / 1000);
@@ -902,7 +908,7 @@ const PresaleActivy: React.FC<{
);
};
-const PhaseCountdown: React.FC<{
+const PhaseCountdown: FC<{
onCountdownEnd?: () => void;
startsAt?: number;
}> = ({ onCountdownEnd, startsAt }) => {
@@ -939,7 +945,7 @@ const PhaseCountdown: React.FC<{
);
};
-const PublicSaleActivity: React.FC<{
+const PublicSaleActivity: FC<{
started?: boolean;
startsAt?: number;
ended: boolean;
@@ -970,7 +976,7 @@ const PublicSaleActivity: React.FC<{
);
};
-const MintNotStartedActivity: React.FC = () => {
+const MintNotStartedActivity: FC = () => {
return (
{
}
return true;
};
+
+export const validateFloatWithDecimals = (value: string, decimals: number) => {
+ const regexp = new RegExp(
+ `^([0-9]+[.]?[0-9]{0,${decimals}}|[.][0-9]{1,${decimals}})$`,
+ );
+ return regexp.test(value);
+};
diff --git a/packages/utils/ipfs.ts b/packages/utils/ipfs.ts
index d61e633e33..92218156cc 100644
--- a/packages/utils/ipfs.ts
+++ b/packages/utils/ipfs.ts
@@ -34,6 +34,19 @@ const ipfsPathToWeb2URL = (path: string) => {
return gatewayURL;
};
+export const isIpfsPathValid = (path: string) => {
+ try {
+ path = path.substring("ipfs://".length);
+ const separatorIndex = path.indexOf("/");
+ const cidString =
+ separatorIndex === -1 ? path : path.substring(0, separatorIndex);
+ const cid = CID.parse(cidString);
+ return !!cid.toV1().toString();
+ } catch {
+ return false;
+ }
+};
+
/** Get the web2 url for a web3 uri or passthrough if not a web3 uri
* Only supports ipfs for now
*/
diff --git a/packages/utils/navigation.ts b/packages/utils/navigation.ts
index b13e114a2d..511938851e 100644
--- a/packages/utils/navigation.ts
+++ b/packages/utils/navigation.ts
@@ -33,6 +33,7 @@ export type RootStackParamList = {
Launchpad: undefined;
LaunchpadApply: undefined;
+ LaunchpadCreate: undefined;
LaunchpadERC20: undefined;
LaunchpadERC20Tokens?: { network?: string };
@@ -216,6 +217,7 @@ const getNavConfig: (homeScreen: keyof RootStackParamList) => NavConfig = (
// ==== Launchpad
Launchpad: "launchpad",
LaunchpadApply: "launchpad/apply",
+ LaunchpadCreate: "launchpad/create",
// ==== Launchpad ERC20
LaunchpadERC20: "launchpad-erc20",
diff --git a/packages/utils/regex.ts b/packages/utils/regex.ts
index a21ab7a143..9291042809 100644
--- a/packages/utils/regex.ts
+++ b/packages/utils/regex.ts
@@ -6,3 +6,5 @@ export const HTML_TAG_REGEXP = /(<([^>]+)>)/gi;
export const GIF_URL_REGEX = /https?:\/\/.*\.(gif)(\?.*)?$/;
export const NUMBERS_REGEXP = /^\d+$/;
export const LETTERS_REGEXP = /^[A-Za-z]+$/;
+export const EMAIL_REGEXP = /^[\w-]+@([\w-]+\.)+[\w-]{2,4}$/;
+export const NUMBERS_COMMA_SEPARATOR_REGEXP = /^\s*\d+(\s*,\s*\d+)*\s*$/;
diff --git a/packages/utils/types/files.ts b/packages/utils/types/files.ts
index d058a8da34..a2bd47f6cb 100644
--- a/packages/utils/types/files.ts
+++ b/packages/utils/types/files.ts
@@ -32,12 +32,18 @@ const ZodBaseFileData = z.object({
isThumbnailImage: z.boolean().optional(),
base64Image: z.string().optional(),
});
-type BaseFileData = z.infer;
-export interface LocalFileData extends BaseFileData {
- file: File;
- thumbnailFileData?: BaseFileData & { file: File };
-}
+export const ZodLocalFileData = z
+ .object({
+ ...ZodBaseFileData.shape,
+ thumbnailFileData: ZodBaseFileData.extend({
+ file: z.instanceof(File),
+ }).optional(),
+ })
+ .extend({
+ file: z.instanceof(File),
+ });
+export type LocalFileData = z.infer;
export const ZodRemoteFileData = z.object({
...ZodBaseFileData.shape,
diff --git a/packages/utils/types/launchpad.ts b/packages/utils/types/launchpad.ts
new file mode 100644
index 0000000000..57a88bd84f
--- /dev/null
+++ b/packages/utils/types/launchpad.ts
@@ -0,0 +1,181 @@
+import { z } from "zod";
+
+import { CollectionProject } from "@/contracts-clients/nft-launchpad";
+import { DEFAULT_FORM_ERRORS } from "@/utils/errors";
+import { isIpfsPathValid } from "@/utils/ipfs";
+import {
+ EMAIL_REGEXP,
+ LETTERS_REGEXP,
+ NUMBERS_REGEXP,
+ URL_REGEX,
+} from "@/utils/regex";
+import { ZodLocalFileData } from "@/utils/types/files";
+const ZodCoin = z.object({
+ amount: z
+ .string()
+ .trim()
+ .min(1, DEFAULT_FORM_ERRORS.required)
+ .refine(
+ (value) => !value || NUMBERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyNumbers,
+ )
+ .optional(),
+ denom: z.string().trim(),
+});
+
+export type Coin = z.infer;
+
+// ===== Shapes to build front objects
+const ZodCollectionMintPeriodFormValues = z.object({
+ price: ZodCoin,
+ maxTokens: z
+ .string()
+ .trim()
+ .refine(
+ (value) => !value || NUMBERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyNumbers,
+ )
+ .optional(),
+ perAddressLimit: z
+ .string()
+ .trim()
+ .refine(
+ (value) => !value || NUMBERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyNumbers,
+ )
+ .optional(),
+ startTime: z.number().min(1, DEFAULT_FORM_ERRORS.required),
+ endTime: z.number().optional(),
+ whitelistAddressesFile: ZodLocalFileData.optional(),
+ whitelistAddresses: z.array(z.string()).optional(),
+ isOpen: z.boolean(),
+});
+
+export const ZodCollectionAssetsAttributeFormValues = z.object({
+ value: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ type: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+});
+
+export const ZodCollectionAssetsMetadataFormValues = z.object({
+ image: ZodLocalFileData,
+ externalUrl: z
+ .string()
+ .trim()
+ // We ignore the URL format control since externalUrl is optional
+ // .refine(
+ // (value) => !value || URL_REGEX.test(value),
+ // DEFAULT_FORM_ERRORS.onlyUrl,
+ // )
+ .optional(),
+ description: z.string().trim().optional(),
+ name: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ youtubeUrl: z.string().trim().optional(),
+ attributes: z.array(ZodCollectionAssetsAttributeFormValues),
+});
+
+export const ZodCollectionAssetsMetadatasFormValues = z.object({
+ assetsMetadatas: z.array(ZodCollectionAssetsMetadataFormValues).optional(),
+ nftApiKey: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+});
+
+export const ZodCollectionFormValues = z.object({
+ name: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ description: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ symbol: z
+ .string()
+ .trim()
+ .toUpperCase()
+ .min(1, DEFAULT_FORM_ERRORS.required)
+ .refine(
+ (value) => LETTERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyLetters,
+ ),
+ websiteLink: z
+ .string()
+ .trim()
+ .min(1, DEFAULT_FORM_ERRORS.required)
+ .refine((value) => URL_REGEX.test(value), DEFAULT_FORM_ERRORS.onlyUrl),
+ email: z
+ .string()
+ .trim()
+ .min(1, DEFAULT_FORM_ERRORS.required)
+ .refine((value) => EMAIL_REGEXP.test(value), DEFAULT_FORM_ERRORS.onlyEmail),
+ projectTypes: z.array(z.string().trim()).min(1, DEFAULT_FORM_ERRORS.required),
+ revealTime: z.number().optional(),
+ teamDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ partnersDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ investDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ investLink: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ artworkDescription: z.string().trim().min(1, DEFAULT_FORM_ERRORS.required),
+ coverImage: ZodLocalFileData,
+ isPreviouslyApplied: z.boolean(),
+ isDerivativeProject: z.boolean(),
+ isReadyForMint: z.boolean(),
+ isDox: z.boolean(),
+ escrowMintProceedsPeriod: z
+ .string()
+ .trim()
+ .min(1, DEFAULT_FORM_ERRORS.required),
+ daoWhitelistCount: z
+ .string()
+ .trim()
+ .min(1, DEFAULT_FORM_ERRORS.required)
+ .refine(
+ (value) => NUMBERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyNumbers,
+ ),
+ mintPeriods: z.array(ZodCollectionMintPeriodFormValues).nonempty(),
+ royaltyAddress: z.string().trim().optional(),
+ royaltyPercentage: z
+ .string()
+ .trim()
+ .refine(
+ (value) => !value || NUMBERS_REGEXP.test(value),
+ DEFAULT_FORM_ERRORS.onlyNumbers,
+ )
+ .optional(),
+ assetsMetadatas: ZodCollectionAssetsMetadatasFormValues.optional(),
+ baseTokenUri: z
+ .string()
+ .trim()
+ .refine(
+ (value) => !value || isIpfsPathValid(value),
+ DEFAULT_FORM_ERRORS.onlyIpfsUri,
+ )
+ .optional(),
+ coverImageUri: z
+ .string()
+ .trim()
+ .refine(
+ (value) => !value || isIpfsPathValid(value),
+ DEFAULT_FORM_ERRORS.onlyIpfsUri,
+ )
+ .optional(),
+});
+
+export type CollectionFormValues = z.infer;
+
+export type CollectionAssetsAttributeFormValues = z.infer<
+ typeof ZodCollectionAssetsAttributeFormValues
+>;
+
+export type CollectionMintPeriodFormValues = z.infer<
+ typeof ZodCollectionMintPeriodFormValues
+>;
+
+export type CollectionAssetsMetadataFormValues = z.infer<
+ typeof ZodCollectionAssetsMetadataFormValues
+>;
+
+export type CollectionAssetsMetadatasFormValues = z.infer<
+ typeof ZodCollectionAssetsMetadatasFormValues
+>;
+
+// ===== Shapes to build objects to sent to API
+export type CollectionToSubmit = Omit<
+ CollectionProject,
+ "deployed_address" | "base_token_uri" | "owner"
+>;
+
+// ===== Shapes to build objects received from API
+// ...Coming soon
diff --git a/yarn.lock b/yarn.lock
index 2f0530e1f2..71c0810d8e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14765,6 +14765,29 @@ __metadata:
languageName: node
linkType: hard
+"keccak256@npm:^1.0.6":
+ version: 1.0.6
+ resolution: "keccak256@npm:1.0.6"
+ dependencies:
+ bn.js: ^5.2.0
+ buffer: ^6.0.3
+ keccak: ^3.0.2
+ checksum: decafb4b37adcfa6d06b6a5d28546d0d7a9f01ccf4b8cc8963cf8188fcc79a230d7e22988e860813623c602d764259734423e38fd7b9aadfeb409d6928a1d4cf
+ languageName: node
+ linkType: hard
+
+"keccak@npm:^3.0.2":
+ version: 3.0.4
+ resolution: "keccak@npm:3.0.4"
+ dependencies:
+ node-addon-api: ^2.0.0
+ node-gyp: latest
+ node-gyp-build: ^4.2.0
+ readable-stream: ^3.6.0
+ checksum: 2bf27b97b2f24225b1b44027de62be547f5c7326d87d249605665abd0c8c599d774671c35504c62c9b922cae02758504c6f76a73a84234d23af8a2211afaaa11
+ languageName: node
+ linkType: hard
+
"keyv@npm:^4.0.0, keyv@npm:^4.5.3":
version: 4.5.4
resolution: "keyv@npm:4.5.4"
@@ -16329,6 +16352,15 @@ __metadata:
languageName: node
linkType: hard
+"node-addon-api@npm:^2.0.0":
+ version: 2.0.2
+ resolution: "node-addon-api@npm:2.0.2"
+ dependencies:
+ node-gyp: latest
+ checksum: 31fb22d674648204f8dd94167eb5aac896c841b84a9210d614bf5d97c74ef059cc6326389cf0c54d2086e35312938401d4cc82e5fcd679202503eb8ac84814f8
+ languageName: node
+ linkType: hard
+
"node-dir@npm:^0.1.17":
version: 0.1.17
resolution: "node-dir@npm:0.1.17"
@@ -16359,6 +16391,17 @@ __metadata:
languageName: node
linkType: hard
+"node-gyp-build@npm:^4.2.0":
+ version: 4.8.4
+ resolution: "node-gyp-build@npm:4.8.4"
+ bin:
+ node-gyp-build: bin.js
+ node-gyp-build-optional: optional.js
+ node-gyp-build-test: build-test.js
+ checksum: 8b81ca8ffd5fa257ad8d067896d07908a36918bc84fb04647af09d92f58310def2d2b8614d8606d129d9cd9b48890a5d2bec18abe7fcff54818f72bedd3a7d74
+ languageName: node
+ linkType: hard
+
"node-gyp-build@npm:^4.3.0":
version: 4.8.0
resolution: "node-gyp-build@npm:4.8.0"
@@ -20466,6 +20509,7 @@ __metadata:
graphql-request: ^5
html-to-draftjs: ^1.5.0
immutable: ^4.0.0
+ keccak256: ^1.0.6
kubernetes-models: ^4.3.1
leaflet: ^1.9.4
leaflet.markercluster: ^1.5.3