= T extends T ? keyof T : never;
+export type Exact = P extends Builtin ? P
+ : P & { [K in keyof P]: Exact
} & { [K in Exclude>]: never };
+
+function longToNumber(long: Long): number {
+ if (long.gt(globalThis.Number.MAX_SAFE_INTEGER)) {
+ throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
+ }
+ return long.toNumber();
+}
+
+if (_m0.util.Long !== Long) {
+ _m0.util.Long = Long as any;
+ _m0.configure();
+}
+
+function isSet(value: any): boolean {
+ return value !== null && value !== undefined;
+}
+
+export class GrpcWebError extends globalThis.Error {
+ constructor(message: string, public code: grpc.Code, public metadata: grpc.Metadata) {
+ super(message);
+ }
+}
diff --git a/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx
new file mode 100644
index 0000000000..57ba7f483b
--- /dev/null
+++ b/packages/components/FilePreview/SelectedFilesPreview/ItemView.tsx
@@ -0,0 +1,79 @@
+import React, { FC } from "react";
+import { TouchableOpacity, Image, StyleProp, ViewStyle } from "react-native";
+
+import { neutral77, secondaryColor } from "../../../utils/style/colors";
+import { fontSemibold11, fontSemibold13 } from "../../../utils/style/fonts";
+import { layout } from "../../../utils/style/layout";
+import { BrandText } from "../../BrandText";
+import { PrimaryBox } from "../../boxes/PrimaryBox";
+
+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..97b9859393
--- /dev/null
+++ b/packages/components/FilePreview/SelectedFilesPreview/SelectedFilesPreview.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+import { View } from "react-native";
+
+import { itemSize, ItemView } from "./ItemView";
+import { neutral33, neutral55 } from "../../../utils/style/colors";
+import { fontSemibold20 } from "../../../utils/style/fonts";
+import { LocalFileData } from "../../../utils/types/files";
+import { BrandText } from "../../BrandText";
+import { PrimaryBox } from "../../boxes/PrimaryBox";
+
+import { DeleteButton } from "@/components/FilePreview/DeleteButton";
+import { GridList } from "@/components/layout/GridList";
+import { layout } from "@/utils/style/layout";
+
+export const SelectedFilesPreview: React.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..0cef927569
--- /dev/null
+++ b/packages/components/NetworkSelector/NetworkSelectorWithLabel.tsx
@@ -0,0 +1,127 @@
+import React, { useState } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { NetworkSelectorMenu } from "./NetworkSelectorMenu";
+import chevronDownSVG from "../../../assets/icons/chevron-down.svg";
+import chevronUpSVG from "../../../assets/icons/chevron-up.svg";
+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";
+import { BrandText } from "../BrandText";
+import { SVG } from "../SVG";
+import { TertiaryBox } from "../boxes/TertiaryBox";
+import { Label } from "../inputs/TextInputCustom";
+
+import { NetworkIcon } from "@/components/NetworkIcon";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { SpacerRow } from "@/components/spacer";
+
+export const NetworkSelectorWithLabel: React.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/dao/DAOProposals.tsx b/packages/components/dao/DAOProposals.tsx
index 5df9bfe14d..eadb50de3c 100644
--- a/packages/components/dao/DAOProposals.tsx
+++ b/packages/components/dao/DAOProposals.tsx
@@ -46,10 +46,11 @@ export const DAOProposals: React.FC<{
// TODO: double check we properly use threshold and quorum
// TODO: use correct threshold, quorum and total power for passed/executed proposals
-const ProposalRow: React.FC<{
+export const ProposalRow: React.FC<{
daoId: string | undefined;
proposal: AppProposalResponse;
-}> = ({ daoId, proposal }) => {
+ style?: StyleProp;
+}> = ({ daoId, proposal, style }) => {
const [network] = parseUserId(daoId);
const halfGap = 24;
@@ -109,13 +110,16 @@ const ProposalRow: React.FC<{
return (
{
component={LaunchpadApplyScreen}
options={{
header: () => null,
- title: screenTitle("Launchpad (Apply)"),
+ title: screenTitle("Apply to Launchpad"),
+ }}
+ />
+ null,
+ title: screenTitle("Create Collection"),
+ }}
+ />
+ null,
+ title: screenTitle("Complete Collection"),
+ }}
+ />
+ null,
+ title: screenTitle("My Collections"),
}}
/>
-
{
title: screenTitle("Launchpad ERC20 Create Sale"),
}}
/>
+ null,
+ title: screenTitle("Launchpad Administration Dashboard"),
+ }}
+ />
+ null,
+ title: screenTitle("Launchpad Applications"),
+ }}
+ />
+ null,
+ title: screenTitle("Launchpad Ready Applications"),
+ }}
+ />
+ null,
+ title: screenTitle("Launchpad Application Review"),
+ }}
+ />
{/* ==== Multisig */}
{
const isSidebarExpanded = useSelector(selectSidebarExpanded);
const selectedApps = useSelector(selectCheckedApps);
const availableApps = useSelector(selectAvailableApps);
+ const userId = useSelectedWallet()?.userId;
+ const { isUserLaunchpadAdmin } = useIsUserLaunchpadAdmin(userId);
+
const [developerMode] = useDeveloperMode();
const dispatch = useAppDispatch();
// on mobile sidebar is not expanded on load
@@ -50,6 +56,7 @@ export const useSidebar = () => {
);
}
}, [availableApps, dispatch, selectedApps.length]);
+
const dynamicSidebar = useMemo(() => {
const dynamicAppsSelection = [] as {
[key: string]: any;
@@ -91,21 +98,42 @@ export const useSidebar = () => {
return;
}
- dynamicAppsSelection[element] = SIDEBAR_LIST[option.id]
- ? SIDEBAR_LIST[option.id]
- : {
- id: option.id,
- title: option.title,
- route: option.route,
- url: option.url,
- icon: option.icon,
- };
+ if (SIDEBAR_LIST[option.id]) {
+ const newOption = cloneDeep(SIDEBAR_LIST[option.id]);
+
+ // Sidebar restriction (Hide items or nested items):
+ // Launchpad Admin
+ if (
+ !isUserLaunchpadAdmin &&
+ newOption.id === "Launchpad" &&
+ newOption.nested &&
+ has(newOption, "nested.admin")
+ ) {
+ delete newOption.nested.admin;
+ }
+
+ dynamicAppsSelection[element] = newOption;
+ } else {
+ dynamicAppsSelection[element] = {
+ id: option.id,
+ title: option.title,
+ route: option.route,
+ url: option.url,
+ icon: option.icon,
+ };
+ }
});
dynamicAppsSelection["dappstore"] = SIDEBAR_LIST["DAppsStore"];
return dynamicAppsSelection;
- }, [forceDAppsList, availableApps, selectedApps, developerMode]);
+ }, [
+ availableApps,
+ selectedApps,
+ developerMode,
+ isUserLaunchpadAdmin,
+ forceDAppsList,
+ ]);
const toggleSidebar = () => {
dispatch(setSidebarExpanded(!isSidebarExpanded));
diff --git a/packages/hooks/launchpad/useCompleteCollection.ts b/packages/hooks/launchpad/useCompleteCollection.ts
new file mode 100644
index 0000000000..ad252a07a5
--- /dev/null
+++ b/packages/hooks/launchpad/useCompleteCollection.ts
@@ -0,0 +1,181 @@
+import { useCallback } from "react";
+import { useSelector } from "react-redux";
+
+import { Metadata, Trait } from "@/api/launchpad/v1/launchpad";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad";
+import { useIpfs } from "@/hooks/useIpfs";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { getNetworkFeature, NetworkFeature } from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+import { selectNFTStorageAPI } from "@/store/slices/settings";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+import { generateIpfsKey, isIpfsPathValid } from "@/utils/ipfs";
+import { LocalFileData, RemoteFileData } from "@/utils/types/files";
+import { CollectionAssetsMetadatasFormValues } from "@/utils/types/launchpad";
+
+export const useCompleteCollection = () => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const selectedWallet = useSelectedWallet();
+ const { setToast } = useFeedbacks();
+ const userIPFSKey = useSelector(selectNFTStorageAPI);
+ const { uploadFilesToPinata } = useIpfs();
+
+ const completeCollection = useCallback(
+ async (
+ collectionId: string,
+ assetsMetadatasFormsValues: CollectionAssetsMetadatasFormValues,
+ ) => {
+ if (!selectedWallet) return false;
+ const userId = selectedWallet.userId;
+ const walletAddress = selectedWallet.address;
+ const networkId = selectedWallet.networkId;
+
+ const signingComswasmClient =
+ await getKeplrSigningCosmWasmClient(selectedNetworkId);
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ selectedNetworkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+ if (!cosmwasmNftLaunchpadFeature) return false;
+
+ const launchpadBackendClient = mustGetLaunchpadClient(networkId);
+
+ const nftLaunchpadContractClient = new NftLaunchpadClient(
+ signingComswasmClient,
+ walletAddress,
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress,
+ );
+ const pinataJWTKey =
+ assetsMetadatasFormsValues.nftApiKey ||
+ userIPFSKey ||
+ (await generateIpfsKey(selectedNetworkId, userId));
+ if (!pinataJWTKey) {
+ console.error("upload file err : No Pinata JWT");
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Files upload failed",
+ });
+ return false;
+ }
+
+ try {
+ const metadatas: Metadata[] = [];
+ if (!assetsMetadatasFormsValues.assetsMetadatas?.length) return false;
+
+ // IMPORTANT TODO:
+ // For now, for simplicity, we upload images to ipfs from client side then this backend will
+ // only check if images have been pinnned correctly.
+ // ===> Please, see go/pkg/launchpad/service.go
+ const assetsMetadataImages: LocalFileData[] =
+ assetsMetadatasFormsValues.assetsMetadatas.map(
+ (assetMetadata) => assetMetadata.image,
+ );
+ const remoteAssetsMetadataImages: RemoteFileData[] =
+ await uploadFilesToPinata({
+ files: assetsMetadataImages,
+ pinataJWTKey,
+ });
+
+ if (!assetsMetadataImages?.length) {
+ console.error("Error: Seems to be no image uploaded to IPFS");
+ setToast({
+ title: "Seems to be no image uploaded to IPFS",
+ message: "Please try again",
+ type: "error",
+ mode: "normal",
+ });
+ return false;
+ }
+
+ assetsMetadatasFormsValues.assetsMetadatas.forEach(
+ (assetMetadata, index) => {
+ const image = remoteAssetsMetadataImages[index];
+ if (!isIpfsPathValid(image.url)) {
+ setToast({
+ title:
+ "At least one uploaded image has an invalid IPFS hash and has beed ignored",
+ message: "Please try again",
+ type: "error",
+ mode: "normal",
+ });
+ return;
+ }
+ if (!assetMetadata.attributes.length) return;
+ const attributes: Trait[] = assetMetadata.attributes.map(
+ (attribute) => {
+ return {
+ value: attribute.value,
+ traitType: attribute.type,
+ };
+ },
+ );
+
+ // Tr721Metadata
+ metadatas.push({
+ image: image.url,
+ externalUrl: assetMetadata.externalUrl,
+ description: assetMetadata.description,
+ name: assetMetadata.name,
+ youtubeUrl: assetMetadata.youtubeUrl,
+ attributes,
+ // TODO: Hanlde these ?
+ // imageData: "",
+ // backgroundColor: "",
+ // animationUrl: "",
+ // youtubeUrl: "",
+ // royaltyPercentage: 5,
+ // royaltyPaymentAddress: "",
+ });
+ },
+ );
+
+ // ========== Send Metadata of this collection to the backend
+ const { merkleRoot } = await launchpadBackendClient.UploadMetadatas({
+ sender: walletAddress,
+ projectId: collectionId,
+ pinataJwt: pinataJWTKey,
+ networkId: selectedNetworkId,
+ metadatas,
+ });
+
+ // ========== Provide the merkle root through the contract
+ await nftLaunchpadContractClient.updateMerkleRoot({
+ collectionId,
+ merkleRoot,
+ });
+
+ setToast({
+ mode: "normal",
+ type: "success",
+ title: "Collection completed",
+ });
+
+ return true;
+ } catch (e: any) {
+ console.error(
+ "Error completing a NFT Collection in the Launchpad: ",
+ e,
+ );
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error completing a NFT Collection in the Launchpad",
+ message: e.message,
+ });
+ return false;
+ }
+ },
+ [
+ selectedNetworkId,
+ selectedWallet,
+ setToast,
+ userIPFSKey,
+ uploadFilesToPinata,
+ ],
+ );
+
+ return { completeCollection };
+};
diff --git a/packages/hooks/launchpad/useCreateCollection.ts b/packages/hooks/launchpad/useCreateCollection.ts
new file mode 100644
index 0000000000..b631c04cee
--- /dev/null
+++ b/packages/hooks/launchpad/useCreateCollection.ts
@@ -0,0 +1,241 @@
+import keccak256 from "keccak256"; // Tested and this lib is compatible with merkle tree libs from Rust and Go
+import { MerkleTree } from "merkletreejs";
+import { useCallback } from "react";
+import { useSelector } from "react-redux";
+
+import { Coin } from "./../../contracts-clients/nft-launchpad/NftLaunchpad.types";
+
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import {
+ MintPeriod,
+ NftLaunchpadClient,
+ WhitelistInfo,
+} from "@/contracts-clients/nft-launchpad";
+import { useCompleteCollection } from "@/hooks/launchpad/useCompleteCollection";
+import { PinataFileProps, useIpfs } from "@/hooks/useIpfs";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { getNetworkFeature, NetworkFeature } from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+import { selectNFTStorageAPI } from "@/store/slices/settings";
+import { generateIpfsKey } from "@/utils/ipfs";
+import { LocalFileData } from "@/utils/types/files";
+import {
+ CollectionAssetsMetadatasFormValues,
+ CollectionFormValues,
+ CollectionMintPeriodFormValues,
+ CollectionToSubmit,
+} from "@/utils/types/launchpad";
+
+export const useCreateCollection = () => {
+ // 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();
+ 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,
+ };
+ 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 = 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,
+ completeCollection,
+ ],
+ );
+
+ return {
+ createCollection,
+ };
+};
diff --git a/packages/hooks/launchpad/useGetLaunchpadAdmin.ts b/packages/hooks/launchpad/useGetLaunchpadAdmin.ts
new file mode 100644
index 0000000000..ba35aac90b
--- /dev/null
+++ b/packages/hooks/launchpad/useGetLaunchpadAdmin.ts
@@ -0,0 +1,69 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { useSelectedNetworkId } from "./../useSelectedNetwork";
+import useSelectedWallet from "./../useSelectedWallet";
+
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad";
+import { getNetworkFeature, NetworkFeature, getUserId } from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+
+export const useGetLaunchpadAdmin = () => {
+ const selectedWallet = useSelectedWallet();
+ const userAddress = selectedWallet?.address;
+ const selectedNetworkId = useSelectedNetworkId();
+ const { setToast } = useFeedbacks();
+
+ const { data, ...other } = useQuery(
+ ["getLaunchpadAdmin", userAddress, selectedNetworkId],
+ async () => {
+ try {
+ if (!userAddress) {
+ throw Error("No user address");
+ }
+ const signingComswasmClient =
+ await getKeplrSigningCosmWasmClient(selectedNetworkId);
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ selectedNetworkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+
+ if (!cosmwasmNftLaunchpadFeature) {
+ throw Error("No Launchpad feature");
+ }
+
+ const nftLaunchpadContractClient = new NftLaunchpadClient(
+ signingComswasmClient,
+ userAddress,
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress,
+ );
+
+ if (!nftLaunchpadContractClient) {
+ throw Error("Launchpad contract client not found");
+ }
+
+ // The Launchapd Admin DAO is the deployer set in the config of the nft-launchpad contract
+ const config = await nftLaunchpadContractClient.getConfig();
+ if (!config.launchpad_admin) {
+ throw Error("No Launchpad admin set");
+ }
+
+ const adminDaoId = getUserId(selectedNetworkId, config.launchpad_admin);
+
+ return adminDaoId;
+ } catch (e: any) {
+ console.error("Error getting Launchpad admin: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error getting Launchpad admin",
+ message: e.message,
+ });
+ }
+ },
+ {
+ enabled: !!userAddress,
+ },
+ );
+ return { launchpadAdminId: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts b/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts
new file mode 100644
index 0000000000..0598831e33
--- /dev/null
+++ b/packages/hooks/launchpad/useIsUserLaunchpadAdmin.ts
@@ -0,0 +1,84 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { NftLaunchpadClient } from "@/contracts-clients/nft-launchpad";
+import {
+ getNetworkFeature,
+ parseUserId,
+ NetworkFeature,
+ getUserId,
+} from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+import { mustGetDAOClient } from "@/utils/backend";
+
+export const useIsUserLaunchpadAdmin = (userId?: string) => {
+ const { setToast } = useFeedbacks();
+
+ const { data, ...other } = useQuery(
+ ["isUserLaunchpadAdmin", userId],
+ async () => {
+ try {
+ const [network, userAddress] = parseUserId(userId);
+ if (!userId) {
+ throw new Error("Invalid sender");
+ }
+ if (!network) {
+ throw new Error("Invalid network");
+ }
+ const networkId = network.id;
+
+ const signingComswasmClient =
+ await getKeplrSigningCosmWasmClient(networkId);
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ networkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+
+ if (!cosmwasmNftLaunchpadFeature) {
+ throw new Error("No Launchpad feature");
+ }
+
+ const daoClient = mustGetDAOClient(networkId);
+ const nftLaunchpadContractClient = new NftLaunchpadClient(
+ signingComswasmClient,
+ userAddress,
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress,
+ );
+
+ if (!daoClient) {
+ throw new Error("DAO client not found");
+ }
+ if (!nftLaunchpadContractClient) {
+ throw new Error("Launchpad contract client not found");
+ }
+
+ // The Launchapd Admin DAO is the deployer set in the config of the nft-launchpad contract
+ const config = await nftLaunchpadContractClient.getConfig();
+ if (!config.launchpad_admin) {
+ throw new Error("No Launchpad admin set");
+ }
+
+ const adminDaoId = getUserId(networkId, config.launchpad_admin);
+ const { isMember } = await daoClient.IsUserDAOMember({
+ userId,
+ daoId: adminDaoId,
+ });
+
+ return isMember;
+ } catch (e: any) {
+ console.error("Error veryfing Launchpad admin: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error veryfing Launchpad admin",
+ message: e.message,
+ });
+ return false;
+ }
+ },
+ {
+ enabled: !!userId,
+ },
+ );
+ return { isUserLaunchpadAdmin: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useLaunchpadProjectById.ts b/packages/hooks/launchpad/useLaunchpadProjectById.ts
new file mode 100644
index 0000000000..9313df1f1f
--- /dev/null
+++ b/packages/hooks/launchpad/useLaunchpadProjectById.ts
@@ -0,0 +1,49 @@
+import { useQuery } from "@tanstack/react-query";
+
+import {
+ LaunchpadProject,
+ LaunchpadProjectByIdRequest,
+ LaunchpadProjectByIdResponse,
+} from "@/api/launchpad/v1/launchpad";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+
+export const useLaunchpadProjectById = (req: LaunchpadProjectByIdRequest) => {
+ const { setToast } = useFeedbacks();
+ const networkId = req.networkId;
+ // const userAddress = req.userAddress;
+ const projectId = req.projectId;
+
+ const { data, ...other } = useQuery(
+ [
+ "launchpadProjectById",
+ projectId,
+ networkId,
+ // , userAddress
+ ],
+ async () => {
+ try {
+ const client = mustGetLaunchpadClient(networkId);
+ if (
+ !client
+ // || !userAddress
+ ) {
+ return null;
+ }
+ const response: LaunchpadProjectByIdResponse =
+ await client.LaunchpadProjectById(req);
+ return response.project || null;
+ } catch (e: any) {
+ console.error("Error getting launchpad project: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error getting launchpad project",
+ message: e.message,
+ });
+ return null;
+ }
+ },
+ );
+ return { launchpadProject: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useLaunchpadProjects.ts b/packages/hooks/launchpad/useLaunchpadProjects.ts
new file mode 100644
index 0000000000..dea7a68e82
--- /dev/null
+++ b/packages/hooks/launchpad/useLaunchpadProjects.ts
@@ -0,0 +1,56 @@
+import { useQuery } from "@tanstack/react-query";
+
+import {
+ LaunchpadProject,
+ LaunchpadProjectsRequest,
+ LaunchpadProjectsResponse,
+} from "@/api/launchpad/v1/launchpad";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+
+export const useLaunchpadProjects = (req: LaunchpadProjectsRequest) => {
+ const { setToast } = useFeedbacks();
+ const networkId = req.networkId;
+ // const userAddress = req.userAddress;
+
+ const { data, ...other } = useQuery(
+ [
+ "collectionsByCreator",
+ networkId,
+ // , userAddress
+ ],
+ async () => {
+ const launchpadProjects: LaunchpadProject[] = [];
+
+ try {
+ const client = mustGetLaunchpadClient(networkId);
+
+ if (
+ !client
+ // || !userAddress
+ ) {
+ return [];
+ }
+ const response: LaunchpadProjectsResponse =
+ await client.LaunchpadProjects(req);
+ response.projects.forEach((data) => {
+ if (!data) {
+ return;
+ }
+ launchpadProjects.push(data);
+ });
+ } catch (e: any) {
+ console.error("Error getting launchpad projects: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error getting launchpad projects",
+ message: e.message,
+ });
+ }
+
+ return launchpadProjects;
+ },
+ );
+ return { launchpadProjects: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts b/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts
new file mode 100644
index 0000000000..3dcb508e18
--- /dev/null
+++ b/packages/hooks/launchpad/useLaunchpadProjectsByCreator.ts
@@ -0,0 +1,60 @@
+import { useQuery } from "@tanstack/react-query";
+
+import {
+ LaunchpadProjectsByCreatorRequest,
+ LaunchpadProjectsByCreatorResponse,
+ LaunchpadProject,
+} from "@/api/launchpad/v1/launchpad";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+
+export const useLaunchpadProjectsByCreator = (
+ req: LaunchpadProjectsByCreatorRequest,
+) => {
+ const { setToast } = useFeedbacks();
+ const networkId = req.networkId;
+ const creatorId = req.creatorId;
+ // const userAddress = req.userAddress;
+
+ const { data, ...other } = useQuery(
+ [
+ "launchpadProjectsByCreator",
+ networkId,
+ creatorId,
+ // , userAddress
+ ],
+ async () => {
+ const launchpadProjects: LaunchpadProject[] = [];
+
+ try {
+ const client = mustGetLaunchpadClient(networkId);
+
+ if (
+ !client ||
+ !creatorId
+ // !userAddress
+ ) {
+ return [];
+ }
+ const response: LaunchpadProjectsByCreatorResponse =
+ await client.LaunchpadProjectsByCreator(req);
+ response.projects.forEach((data) => {
+ if (!data) {
+ return;
+ }
+ launchpadProjects.push(data);
+ });
+ } catch (e: any) {
+ console.error("Error getting launchpad projects: ", e);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error getting launchpad projects",
+ message: e.message,
+ });
+ }
+ return launchpadProjects;
+ },
+ );
+ return { launchpadProjects: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts b/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts
new file mode 100644
index 0000000000..f000798b67
--- /dev/null
+++ b/packages/hooks/launchpad/useLaunchpadProjectsCounts.ts
@@ -0,0 +1,41 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { LaunchpadProjectsCountsRequest } from "@/api/launchpad/v1/launchpad";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+
+export const useLaunchpadProjectsCounts = (
+ req: LaunchpadProjectsCountsRequest,
+) => {
+ const { setToast } = useFeedbacks();
+ const networkId = req.networkId;
+
+ const { data, ...other } = useQuery(
+ ["launchpadProjectsCounts", networkId],
+ async () => {
+ try {
+ const client = mustGetLaunchpadClient(networkId);
+ if (!client) {
+ throw new Error("Missing client");
+ }
+
+ const { statusCounts } = await client.LaunchpadProjectsCounts(req);
+ return statusCounts;
+ } catch (err: any) {
+ const title = "Error getting launchpad projects counts";
+ const message = err instanceof Error ? err.message : `${err}`;
+ console.error(title, message);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title,
+ message,
+ });
+ }
+ },
+ {
+ enabled: !!networkId,
+ },
+ );
+ return { statusCounts: data, ...other };
+};
diff --git a/packages/hooks/launchpad/useProposeApproveProject.ts b/packages/hooks/launchpad/useProposeApproveProject.ts
new file mode 100644
index 0000000000..752927af51
--- /dev/null
+++ b/packages/hooks/launchpad/useProposeApproveProject.ts
@@ -0,0 +1,148 @@
+import { useCallback } from "react";
+
+import { useGetLaunchpadAdmin } from "./useGetLaunchpadAdmin";
+import { useIsUserLaunchpadAdmin } from "./useIsUserLaunchpadAdmin";
+import { DaoProposalSingleClient } from "../../contracts-clients/dao-proposal-single/DaoProposalSingle.client";
+import { useDAOMakeProposal } from "../dao/useDAOMakeProposal";
+import { useDAOFirstProposalModule } from "../dao/useDAOProposalModules";
+
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { getNetworkFeature, NetworkFeature } from "@/networks";
+import { getKeplrSigningCosmWasmClient } from "@/networks/signer";
+import { mustGetLaunchpadClient } from "@/utils/backend";
+
+export const useProposeApproveProject = () => {
+ const { launchpadAdminId } = useGetLaunchpadAdmin(); // It's a DAO
+ const makeProposal = useDAOMakeProposal(launchpadAdminId);
+ const selectedNetworkId = useSelectedNetworkId();
+ const selectedWallet = useSelectedWallet();
+ const userAddress = selectedWallet?.address;
+ const { isUserLaunchpadAdmin } = useIsUserLaunchpadAdmin(
+ selectedWallet?.userId,
+ );
+ const { setToast } = useFeedbacks();
+ const { daoFirstProposalModule } =
+ useDAOFirstProposalModule(launchpadAdminId);
+
+ // Make a proposal deploy_collection and approve it
+ const proposeApproveProject = useCallback(
+ async (projectId: string) => {
+ try {
+ if (!isUserLaunchpadAdmin) {
+ throw new Error("Unauthorized");
+ }
+ if (!userAddress) {
+ throw new Error("Invalid sender");
+ }
+ if (!daoFirstProposalModule?.address) {
+ throw new Error("Invalid DAO Proposal module");
+ }
+
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ selectedNetworkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+
+ if (!cosmwasmNftLaunchpadFeature) {
+ throw new Error("No Launchpad feature");
+ }
+
+ // ---- Make the proposal
+ const makeProposalRes = await makeProposal(userAddress, {
+ title: "Approve Project " + projectId,
+ description: "",
+ msgs: [
+ {
+ wasm: {
+ execute: {
+ contract_addr:
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress,
+ msg: Buffer.from(
+ JSON.stringify({
+ deploy_collection: {
+ collection_id: projectId,
+ },
+ }),
+ ).toString("base64"),
+ funds: [],
+ },
+ },
+ },
+ ],
+ });
+
+ // ---- Get the freshly made proposal id
+ const event = makeProposalRes.events.find((ev) =>
+ ev.attributes.some((attr) => attr.key === "proposal_id"),
+ );
+ const proposalId = event?.attributes.find(
+ (attribute) => attribute.key === "proposal_id",
+ )?.value;
+ if (!proposalId) {
+ throw new Error("Failed to retreive the proposal");
+ }
+
+ const signingCosmWasmClient =
+ await getKeplrSigningCosmWasmClient(selectedNetworkId);
+ const daoProposalClient = new DaoProposalSingleClient(
+ signingCosmWasmClient,
+ userAddress,
+ daoFirstProposalModule?.address,
+ );
+
+ // ---- Approve the proposal
+ await daoProposalClient.vote(
+ { proposalId: parseInt(proposalId, 10), vote: "yes" },
+ "auto",
+ );
+
+ // ---- Update the DB by adding proposalId to the project
+ // The proposal has always at least a vote "yes". So this project is considered as
+ const launchpadBackendClient =
+ mustGetLaunchpadClient(selectedNetworkId);
+ const { approved } = await launchpadBackendClient.ProposeApproveProject(
+ {
+ sender: userAddress,
+ projectId,
+ networkId: selectedNetworkId,
+ proposalId,
+ },
+ );
+
+ if (!approved) {
+ throw new Error(
+ "Failed to update the project status after first approbation",
+ );
+ }
+
+ setToast({
+ mode: "normal",
+ type: "success",
+ title: "Successfully approved the Launchpad Collection",
+ });
+ } catch (err: unknown) {
+ console.error("Error approving the Launchpad Collection: ", err);
+ if (err instanceof Error) {
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Error approving the Launchpad Collection",
+ message: err.message,
+ });
+ }
+ }
+ },
+ [
+ selectedNetworkId,
+ userAddress,
+ setToast,
+ isUserLaunchpadAdmin,
+ daoFirstProposalModule,
+ makeProposal,
+ ],
+ );
+
+ return { proposeApproveProject, launchpadAdminId };
+};
diff --git a/packages/networks/teritori-testnet/index.ts b/packages/networks/teritori-testnet/index.ts
index 667f98dae3..12c8bc36a0 100644
--- a/packages/networks/teritori-testnet/index.ts
+++ b/packages/networks/teritori-testnet/index.ts
@@ -41,7 +41,7 @@ const cosmwasmNftLaunchpadFeature: CosmWasmNFTLaunchpad = {
};
const riotContractAddressGen0 =
- "tori1r8raaqul4j05qtn0t05603mgquxfl8e9p7kcf7smwzcv2hc5rrlq0vket0";
+ "tori1hzz0s0ucrhdp6tue2lxk3c03nj6f60qy463we7lgx0wudd72ctmstg4wkc";
const riotContractAddressGen1 = "";
export const teritoriTestnetNetwork: CosmosNetworkInfo = {
diff --git a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx
index 2b82e3faa4..8c24348429 100644
--- a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx
+++ b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx
@@ -4,9 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import { ScrollView, View } from "react-native";
import { useSelector } from "react-redux";
-import priceSVG from "../../../assets/icons/price.svg";
-import useSelectedWallet from "../../hooks/useSelectedWallet";
-
+import priceSVG from "@/assets/icons/price.svg";
import { BrandText } from "@/components/BrandText";
import { SVG } from "@/components/SVG";
import { ScreenContainer } from "@/components/ScreenContainer";
@@ -25,6 +23,7 @@ import { useFeedPosting } from "@/hooks/feed/useFeedPosting";
import { useIpfs } from "@/hooks/useIpfs";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
import { NetworkFeature } from "@/networks";
import { selectNFTStorageAPI } from "@/store/slices/settings";
import { feedPostingStep, FeedPostingStepId } from "@/utils/feed/posting";
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx
new file mode 100644
index 0000000000..e43fd0c181
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen.tsx
@@ -0,0 +1,192 @@
+import React, { useState } from "react";
+import { useWindowDimensions, View } from "react-native";
+
+import { ConfirmedsTable } from "./../components/ConfirmedsTable";
+import { ReviewingsTable } from "./../components/ReviewingsTable";
+import { ApplicationStatusCard } from "./components/ApplicationStatusCard";
+import { CompletesTable } from "../components/CompletesTable";
+import { IncompletesTable } from "../components/IncompletesTable";
+
+import { StatusCount } from "@/api/launchpad/v1/launchpad";
+import { BrandText } from "@/components/BrandText";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { PrimaryButtonOutline } from "@/components/buttons/PrimaryButtonOutline";
+import { SpacerColumn } from "@/components/spacer";
+import { Tabs } from "@/components/tabs/Tabs";
+import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin";
+import { useLaunchpadProjectsCounts } from "@/hooks/launchpad/useLaunchpadProjectsCounts";
+import { useAppNavigation } from "@/hooks/navigation/useAppNavigation";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { statusToCount } from "@/utils/launchpad";
+import { errorColor } from "@/utils/style/colors";
+import { fontSemibold20, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+const MD_BREAKPOINT = 820;
+export type LaunchpadAdminDashboardTabsListType =
+ | "INCOMPLETE"
+ | "COMPLETE"
+ | "REVIEWING"
+ | "CONFIRMED";
+
+export const launchpadAdminTabs = (statusCounts?: StatusCount[]) => {
+ return {
+ INCOMPLETE: {
+ name: "INCOMPLETE",
+ badgeCount: statusToCount("INCOMPLETE", statusCounts),
+ },
+ COMPLETE: {
+ name: "COMPLETE",
+ badgeCount: statusToCount("COMPLETE", statusCounts),
+ },
+ REVIEWING: {
+ name: "REVIEWING",
+ badgeCount: statusToCount("REVIEWING", statusCounts),
+ },
+ CONFIRMED: {
+ name: "CONFIRMED",
+ badgeCount: statusToCount("CONFIRMED", statusCounts),
+ },
+ };
+};
+
+export const LaunchpadAdministrationOverviewScreen: React.FC = () => {
+ const navigation = useAppNavigation();
+ const selectedNetworkId = useSelectedNetworkId();
+ const userId = useSelectedWallet()?.userId;
+ const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } =
+ useIsUserLaunchpadAdmin(userId);
+
+ const { width } = useWindowDimensions();
+ const { statusCounts } = useLaunchpadProjectsCounts({
+ networkId: selectedNetworkId,
+ });
+
+ const [selectedTab, setSelectedTab] =
+ useState("INCOMPLETE");
+
+ if (!isUserLaunchpadAdmin)
+ return (
+ >}
+ headerChildren={
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+ }
+ responsive
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+
+ );
+
+ return (
+ >}
+ headerChildren={
+ Administration Dashboard
+ }
+ responsive
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+
+
+ Launchpad Administration Overview
+
+
+
+ = MD_BREAKPOINT ? "row" : "column",
+ justifyContent: "center",
+ }}
+ >
+
+ = MD_BREAKPOINT ? layout.spacing_x1_5 : 0,
+ marginVertical: width >= MD_BREAKPOINT ? 0 : layout.spacing_x1_5,
+ }}
+ />
+ navigation.navigate("LaunchpadReadyApplications")
+ // : undefined
+ // }
+ isReady
+ />
+
+
+
+
+
+ {selectedTab === "INCOMPLETE" ? (
+
+ ) : selectedTab === "COMPLETE" ? (
+
+ ) : selectedTab === "REVIEWING" ? (
+
+ ) : selectedTab === "CONFIRMED" ? (
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+ navigation.navigate("LaunchpadApplications")}
+ style={{ alignSelf: "center" }}
+ />
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx
new file mode 100644
index 0000000000..03369a6d1f
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadAdministrationOverview/components/ApplicationStatusCard.tsx
@@ -0,0 +1,72 @@
+import React from "react";
+import { View, StyleProp, TouchableOpacity } from "react-native";
+
+import chevronRightSVG from "@/assets/icons/chevron-right.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { BoxStyle } from "@/components/boxes/Box";
+import { PrimaryBox } from "@/components/boxes/PrimaryBox";
+import {
+ neutral00,
+ neutral11,
+ neutral30,
+ neutral44,
+} from "@/utils/style/colors";
+import { fontSemibold16, fontSemibold24 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const ApplicationStatusCard: React.FC<{
+ label: string;
+ count: number;
+ style?: StyleProp;
+ onPress?: () => void;
+ isReady?: boolean;
+}> = ({ label, style, count, isReady, onPress }) => {
+ return (
+
+
+ {label}
+
+ {count}
+
+ {onPress && !!count && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx
new file mode 100644
index 0000000000..5165727f75
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen.tsx
@@ -0,0 +1,216 @@
+import React, { useMemo, useState } from "react";
+import { View } from "react-native";
+
+import { Separator } from "./../../../../components/separators/Separator";
+import { ApplicationDetail } from "./components/ApplicationDetail";
+import { CreatorInformation } from "./components/CreatorInformation";
+import { InvestmentInformation } from "./components/InvestmentInformation";
+import { MintingInformation } from "./components/MintingInformation";
+import { ProjectInformation } from "./components/ProjectInformation";
+import { TeamInformation } from "./components/TeamInformation";
+import { Status } from "../../../../api/launchpad/v1/launchpad";
+import { PrimaryButton } from "../../../../components/buttons/PrimaryButton";
+import { ProposalRow } from "../../../../components/dao/DAOProposals";
+import { useDAOProposalById } from "../../../../hooks/dao/useDAOProposalById";
+import { useProposeApproveProject } from "../../../../hooks/launchpad/useProposeApproveProject";
+
+import { BrandText } from "@/components/BrandText";
+import { NotFound } from "@/components/NotFound";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { SpacerColumn } from "@/components/spacer";
+import { useInvalidateDAOProposals } from "@/hooks/dao/useDAOProposals";
+import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin";
+import { useLaunchpadProjectById } from "@/hooks/launchpad/useLaunchpadProjectById";
+import { useAppNavigation } from "@/hooks/navigation/useAppNavigation";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { parseCollectionData } from "@/utils/launchpad";
+import { ScreenFC } from "@/utils/navigation";
+import { errorColor } from "@/utils/style/colors";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+// =====> TODO: SHOW ALL DATA, MINT PERIODS, ASSETS, ETC
+
+export const launchpadReviewBreakpointM = 800;
+export const launchpadReviewBreakpointS = 600;
+export const launchpadReviewBreakpointSM = 400;
+
+export const LaunchpadApplicationReviewScreen: ScreenFC<
+ "LaunchpadApplicationReview"
+> = ({ route }) => {
+ const { id: projectId } = route.params;
+ const navigation = useAppNavigation();
+ const selectedNetworkId = useSelectedNetworkId();
+ const [isApproveLoading, setApproveLoading] = useState(false);
+ const userId = useSelectedWallet()?.userId;
+ const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } =
+ useIsUserLaunchpadAdmin(userId);
+ const { proposeApproveProject, launchpadAdminId } =
+ useProposeApproveProject();
+ const invalidateDAOProposals = useInvalidateDAOProposals(launchpadAdminId);
+ const { launchpadProject, isLoading: isProjectsLoading } =
+ useLaunchpadProjectById({
+ projectId,
+ networkId: selectedNetworkId,
+ });
+ const collectionData =
+ launchpadProject && parseCollectionData(launchpadProject);
+ const { daoProposal } = useDAOProposalById(
+ launchpadAdminId,
+ launchpadProject?.proposalId
+ ? parseInt(launchpadProject.proposalId, 10)
+ : undefined,
+ );
+ const isLoading = useMemo(
+ () => isUserAdminLoading || isProjectsLoading,
+ [isUserAdminLoading, isProjectsLoading],
+ );
+ const isUserOwner =
+ launchpadProject?.creatorId && launchpadProject.creatorId === userId;
+
+ const onPressApprove = async () => {
+ setApproveLoading(true);
+ try {
+ await proposeApproveProject(projectId);
+ } catch (e) {
+ console.error("Error approving the collection", e);
+ setApproveLoading(false);
+ } finally {
+ invalidateDAOProposals();
+ }
+ setTimeout(() => {
+ setApproveLoading(false);
+ }, 1000);
+ };
+
+ const onBackPress = () => {
+ const routes = navigation.getState().routes;
+ const previousScreen = routes[routes.length - 2];
+ if (
+ previousScreen &&
+ previousScreen.name !== "LaunchpadComplete" &&
+ previousScreen.name !== "LaunchpadCreate" &&
+ navigation.canGoBack()
+ ) {
+ navigation.goBack();
+ } else if (isUserLaunchpadAdmin) {
+ navigation.navigate("LaunchpadAdministrationOverview");
+ } else {
+ navigation.navigate("LaunchpadMyCollections");
+ }
+ };
+
+ if (!isUserLaunchpadAdmin || !isUserOwner) {
+ return (
+ >}
+ headerChildren={
+
+ {isLoading ? "Loading..." : "Unauthorized"}
+
+ }
+ responsive
+ onBackPress={onBackPress}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+ {isLoading ? "Loading..." : "Unauthorized"}
+
+
+ );
+ }
+
+ if (!launchpadProject || !collectionData) {
+ return (
+ >}
+ headerChildren={
+
+ {isLoading ? "Loading..." : "Application not found"}
+
+ }
+ responsive
+ onBackPress={onBackPress}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+ {isLoading ? (
+
+ Loading...
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+ >}
+ headerChildren={
+ Application Review
+ }
+ responsive
+ onBackPress={onBackPress}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+ {selectedNetworkId !== launchpadProject.networkId ? (
+ Wrong network
+ ) : (
+
+
+
+
+ {daoProposal &&
+ isUserLaunchpadAdmin &&
+ launchpadProject.status !== Status.STATUS_INCOMPLETE ? (
+ <>
+
+
+ >
+ ) : launchpadProject.status !== Status.STATUS_INCOMPLETE &&
+ isUserLaunchpadAdmin ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx
new file mode 100644
index 0000000000..7ac87ef8d6
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationCard.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { StyleProp, useWindowDimensions } from "react-native";
+
+import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen";
+
+import { BrandText } from "@/components/BrandText";
+import { BoxStyle } from "@/components/boxes/Box";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { neutral77 } from "@/utils/style/colors";
+import { fontMedium14, fontSemibold12 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const ApplicationCard: React.FC<{
+ title: string;
+ value: string;
+ style?: StyleProp;
+}> = ({ title, value, style }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+ = launchpadReviewBreakpointSM && { flex: 1 },
+ style,
+ ]}
+ >
+
+ {title}
+
+ {value}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx
new file mode 100644
index 0000000000..5a2cd31799
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ApplicationDetail.tsx
@@ -0,0 +1,157 @@
+import React from "react";
+import { View, Linking } from "react-native";
+
+import { PrimaryButton } from "./../../../../../components/buttons/PrimaryButton";
+import { useAppNavigation } from "./../../../../../hooks/navigation/useAppNavigation";
+import { getCollectionId } from "./../../../../../networks/index";
+import { launchpadProjectStatus } from "./../../../../../utils/launchpad";
+import { ApplicationCard } from "./ApplicationCard";
+import { Status } from "../../../../../api/launchpad/v1/launchpad";
+import { StatusBadge } from "../../../components/StatusBadge";
+
+import launchpadApplySVG from "@/assets/icons/launchpad-apply.svg";
+import websiteSVG from "@/assets/icons/website.svg";
+import { BrandText } from "@/components/BrandText";
+import { OptimizedImage } from "@/components/OptimizedImage";
+import { BoxStyle } from "@/components/boxes/Box";
+import { SocialButton } from "@/components/buttons/SocialButton";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useMaxResolution } from "@/hooks/useMaxResolution";
+import { neutralFF } from "@/utils/style/colors";
+import { fontSemibold14, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+const breakpointM = 900;
+
+export const ApplicationDetail: React.FC<{
+ collectionData: CollectionDataResult;
+ projectStatus: Status;
+}> = ({ collectionData, projectStatus }) => {
+ const { width } = useMaxResolution();
+ const navigation = useAppNavigation();
+
+ const onPressDeployedCollectionButton = () => {
+ if (collectionData.deployed_address) {
+ const id = getCollectionId(
+ collectionData.target_network,
+ collectionData.deployed_address,
+ );
+ navigation.navigate("MintCollection", { id });
+ }
+ };
+
+ return (
+ = breakpointM ? "row" : "column-reverse",
+ alignItems: width >= breakpointM ? "flex-start" : "center",
+ justifyContent: "center",
+ }}
+ >
+ {/* ===== Left container */}
+
+
+
+ {launchpadProjectStatus(projectStatus) === "INCOMPLETE" && (
+ <>
+
+
+ navigation.navigate("LaunchpadComplete", {
+ id: collectionData.symbol,
+ })
+ }
+ />
+ >
+ )}
+
+
+ {collectionData.name}
+
+
+
+
+
+
+
+ {collectionData.desc}
+
+
+ Linking.openURL(collectionData.website_link)}
+ />
+
+ {!!collectionData.deployed_address && (
+
+ )}
+
+
+
+ {width >= breakpointM ? (
+
+ ) : (
+
+ )}
+
+ {/* ===== Right container */}
+ = breakpointM ? 534 : 380}
+ width={width >= breakpointM ? 534 : 380}
+ style={[
+ {
+ height: width >= breakpointM ? 534 : 380,
+ width: width >= breakpointM ? 534 : 380,
+ },
+ width >= breakpointM && { flex: 1 },
+ ]}
+ sourceURI={collectionData.cover_img_uri}
+ />
+
+ );
+};
+
+const applicationCardCStyle: BoxStyle = { width: 100, maxWidth: 140 };
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx
new file mode 100644
index 0000000000..9821319bc2
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/CreatorInformation.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { View, useWindowDimensions } from "react-native";
+
+import { ApplicationCard } from "./ApplicationCard";
+
+import { BrandText } from "@/components/BrandText";
+import { useNSUserInfo } from "@/hooks/useNSUserInfo";
+import { parseUserId } from "@/networks";
+import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { tinyAddress } from "@/utils/text";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+export const CreatorInformation: React.FC<{
+ creatorId: string;
+ collectionData: CollectionDataResult;
+}> = ({ creatorId, collectionData }) => {
+ const { width } = useWindowDimensions();
+ const creatorNSInfo = useNSUserInfo(creatorId);
+ const [, creatorAddress] = parseUserId(creatorId);
+ const creatorDisplayName =
+ creatorNSInfo?.metadata?.tokenId || tinyAddress(creatorAddress);
+
+ return (
+
+ Creator information
+ = launchpadReviewBreakpointM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ gap: layout.spacing_x1_5,
+ flexWrap: "wrap",
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx
new file mode 100644
index 0000000000..e163d476d5
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/InvestmentInformation.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import { View, useWindowDimensions } from "react-native";
+
+import { ApplicationCard } from "./ApplicationCard";
+import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen";
+
+import { BrandText } from "@/components/BrandText";
+import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+export const InvestmentInformation: React.FC<{
+ collectionData: CollectionDataResult;
+}> = ({ collectionData }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+
+ Investment information
+ = launchpadReviewBreakpointM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ gap: layout.spacing_x1_5,
+ flexWrap: "wrap",
+ }}
+ >
+
+
+
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ marginVertical: layout.spacing_x2,
+ gap: layout.spacing_x1_5,
+ flexWrap: "wrap",
+ }}
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx
new file mode 100644
index 0000000000..3be8c8d046
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/LinkCard.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { FlatList, StyleProp, View } from "react-native";
+
+import { BrandText } from "@/components/BrandText";
+import { BoxStyle } from "@/components/boxes/Box";
+import { TertiaryBox } from "@/components/boxes/TertiaryBox";
+import { neutral77 } from "@/utils/style/colors";
+import { fontSemibold12, fontSemibold14 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const LinkCard: React.FC<{
+ title: string;
+ linksData: { title: string; link: string }[];
+ style?: StyleProp;
+}> = ({ title, linksData, style }) => {
+ return (
+
+
+ {title}
+
+ (
+
+
+ {item.title}
+
+ {/* Linking.openURL(item.link)} style={{flex: 1}}>*/}
+
+ {item.link}
+
+ {/**/}
+
+ )}
+ />
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx
new file mode 100644
index 0000000000..963e4d6000
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/MintingInformation.tsx
@@ -0,0 +1,156 @@
+import moment from "moment";
+import React, { Fragment } from "react";
+import { useWindowDimensions, View } from "react-native";
+
+import { ApplicationCard } from "./ApplicationCard";
+import { launchpadReviewBreakpointSM } from "../LaunchpadApplicationReviewScreen";
+
+import { BrandText } from "@/components/BrandText";
+import { SpacerColumn } from "@/components/spacer";
+import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionDataResult,
+ MintPeriodDataResult,
+} from "@/utils/types/launchpad";
+
+export const MintingInformation: React.FC<{
+ collectionData: CollectionDataResult;
+}> = ({ collectionData }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+
+
+ Minting Information
+
+
+
+
+ {!!(
+ collectionData.royalty_address && collectionData.royalty_percentage
+ ) && (
+ = launchpadReviewBreakpointM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+ {!!collectionData.royalty_address && (
+
+ )}
+ {!!collectionData.royalty_percentage && (
+
+ )}
+
+ )}
+
+ {collectionData.mint_periods.map((mintPeriod, index) => (
+
+
+
+
+ ))}
+
+ );
+};
+
+const MintPeriod: React.FC<{
+ mintPeriod: MintPeriodDataResult;
+ index: number;
+}> = ({ mintPeriod, index }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+ <>
+ {`Minting Period #${index + 1}`}
+
+ {!!mintPeriod.price && (
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+
+
+ )}
+
+ {!!(mintPeriod.max_tokens && mintPeriod.limit_per_address) && (
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+ {!!mintPeriod.max_tokens && (
+
+ )}
+ {!!mintPeriod.limit_per_address && (
+
+ )}
+
+ )}
+
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+ {!!mintPeriod.end_time && (
+
+ )}
+
+ >
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx
new file mode 100644
index 0000000000..c23a5aab79
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/ProjectInformation.tsx
@@ -0,0 +1,110 @@
+import React from "react";
+import { useWindowDimensions, View } from "react-native";
+
+import { ApplicationCard } from "./ApplicationCard";
+import {
+ launchpadReviewBreakpointS,
+ launchpadReviewBreakpointSM,
+} from "../LaunchpadApplicationReviewScreen";
+
+import { BrandText } from "@/components/BrandText";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+export const ProjectInformation: React.FC<{
+ collectionData: CollectionDataResult;
+}> = ({ collectionData }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+
+
+ Project information
+
+
+
+
+
+ = launchpadReviewBreakpointS ? "row" : "column",
+ marginVertical: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ flexWrap: "wrap",
+ flex: 1,
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+
+
+
+ = launchpadReviewBreakpointSM ? "row" : "column",
+ flexWrap: "wrap",
+ flex: 1,
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+
+
+
+
+ = launchpadReviewBreakpointS ? "row" : "column",
+ // marginTop: layout.spacing_x2,
+ flexWrap: "wrap",
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx
new file mode 100644
index 0000000000..4ef85ef14b
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/components/TeamInformation.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { useWindowDimensions, View } from "react-native";
+
+import { ApplicationCard } from "./ApplicationCard";
+
+import { BrandText } from "@/components/BrandText";
+import { launchpadReviewBreakpointM } from "@/screens/Launchpad/LaunchpadAdmin/LaunchpadApplicationReview/LaunchpadApplicationReviewScreen";
+import { fontSemibold20 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+export const TeamInformation: React.FC<{
+ collectionData: CollectionDataResult;
+}> = ({ collectionData }) => {
+ const { width } = useWindowDimensions();
+
+ return (
+
+ Team information
+ = launchpadReviewBreakpointM ? "row" : "column",
+ marginTop: layout.spacing_x2,
+ gap: layout.spacing_x1_5,
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx
new file mode 100644
index 0000000000..4e97dc864c
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadApplications/LaunchpadApplicationsScreen.tsx
@@ -0,0 +1,136 @@
+import React, { useState } from "react";
+import { View } from "react-native";
+
+import { CompletesTable } from "./../components/CompletesTable";
+import { IncompletesTable } from "./../components/IncompletesTable";
+import {
+ LaunchpadAdminDashboardTabsListType,
+ launchpadAdminTabs,
+} from "../LaunchpadAdministrationOverview/LaunchpadAdministrationOverviewScreen";
+import { ConfirmedsTable } from "../components/ConfirmedsTable";
+import { ReviewingsTable } from "../components/ReviewingsTable";
+
+import { BrandText } from "@/components/BrandText";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { SpacerColumn } from "@/components/spacer";
+import { Tabs } from "@/components/tabs/Tabs";
+import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin";
+import { useLaunchpadProjectsCounts } from "@/hooks/launchpad/useLaunchpadProjectsCounts";
+import { useAppNavigation } from "@/hooks/navigation/useAppNavigation";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { errorColor, neutral33 } from "@/utils/style/colors";
+import { fontSemibold20, fontSemibold28 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const LaunchpadApplicationsScreen: React.FC = () => {
+ const navigation = useAppNavigation();
+ const selectedNetworkId = useSelectedNetworkId();
+ const userId = useSelectedWallet()?.userId;
+ const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } =
+ useIsUserLaunchpadAdmin(userId);
+ const { statusCounts } = useLaunchpadProjectsCounts({
+ networkId: selectedNetworkId,
+ });
+
+ const [selectedTab, setSelectedTab] =
+ useState("INCOMPLETE");
+
+ if (!isUserLaunchpadAdmin) {
+ return (
+ >}
+ headerChildren={
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+ }
+ responsive
+ onBackPress={() =>
+ navigation.navigate("LaunchpadAdministrationOverview")
+ }
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+
+ );
+ }
+
+ return (
+ >}
+ headerChildren={
+ Administration Dashboard
+ }
+ responsive
+ onBackPress={() => navigation.navigate("LaunchpadAdministrationOverview")}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+
+ Launchpad Applications
+
+
+
+
+
+
+ {selectedTab === "INCOMPLETE" ? (
+
+ ) : selectedTab === "COMPLETE" ? (
+
+ ) : selectedTab === "REVIEWING" ? (
+
+ ) : selectedTab === "CONFIRMED" ? (
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx
new file mode 100644
index 0000000000..7503411397
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/LaunchpadReadyApplicationsScreen.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import { View } from "react-native";
+
+import { LaunchpadReadyApplicationsTable } from "./components/LaunchpadReadyApplicationsTable";
+
+import { Sort, SortDirection, Status } from "@/api/launchpad/v1/launchpad";
+import { BrandText } from "@/components/BrandText";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { SpacerColumn } from "@/components/spacer";
+import { useIsUserLaunchpadAdmin } from "@/hooks/launchpad/useIsUserLaunchpadAdmin";
+import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects";
+import { useAppNavigation } from "@/hooks/navigation/useAppNavigation";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { errorColor } from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold20,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const LaunchpadReadyApplicationsScreen: React.FC = () => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const navigation = useAppNavigation();
+ const userId = useSelectedWallet()?.userId;
+ const { isUserLaunchpadAdmin, isLoading: isUserAdminLoading } =
+ useIsUserLaunchpadAdmin(userId);
+ const { launchpadProjects = [] } = useLaunchpadProjects({
+ networkId: selectedNetworkId,
+ offset: 0,
+ limit: 100, // TODO: Pagination
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ status: Status.STATUS_COMPLETE, // TODO: Or STATUS_CONFIRMED ?
+ });
+
+ if (!isUserLaunchpadAdmin) {
+ return (
+ >}
+ headerChildren={
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+ }
+ responsive
+ onBackPress={() =>
+ navigation.navigate("LaunchpadAdministrationOverview")
+ }
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+ {isUserAdminLoading ? "Loading..." : "Unauthorized"}
+
+
+ );
+ }
+
+ return (
+ >}
+ headerChildren={
+ Administration Dashboard
+ }
+ responsive
+ onBackPress={() => navigation.navigate("LaunchpadAdministrationOverview")}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ >
+
+
+ Ready to Launch
+
+
+
+ {launchpadProjects?.length ? (
+
+ ) : (
+
+ There is no application ready to launch
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx
new file mode 100644
index 0000000000..2fd8bfabe5
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/LaunchpadReadyApplications/components/LaunchpadReadyApplicationsTable.tsx
@@ -0,0 +1,122 @@
+import React from "react";
+import { FlatList, View } from "react-native";
+
+import {
+ commonColumns,
+ LaunchpadTablesCommonColumns,
+} from "../../../LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns";
+import { StatusBadge } from "../../../components/StatusBadge";
+
+import { LaunchpadProject } from "@/api/launchpad/v1/launchpad";
+import { OmniLink } from "@/components/OmniLink";
+import { TableCell } from "@/components/table/TableCell";
+import { TableHeader } from "@/components/table/TableHeader";
+import { TableRow } from "@/components/table/TableRow";
+import { CellBrandText } from "@/components/table/TableTextCell";
+import { TableWrapper } from "@/components/table/TableWrapper";
+import { TableColumns } from "@/components/table/utils";
+import { parseCollectionData } from "@/utils/launchpad";
+import { screenContentMaxWidthLarge } from "@/utils/style/layout";
+
+const columns: TableColumns = {
+ ...commonColumns,
+ projectReadinessForMint: {
+ label: "Project Readiness for Mint",
+ minWidth: 200,
+ flex: 2,
+ },
+ whitelistQuantity: {
+ label: "Whitelist quantity",
+ minWidth: 100,
+ flex: 1,
+ },
+ premiumMarketingPackage: {
+ label: "Premium marketing package",
+ minWidth: 160,
+ flex: 1.8,
+ },
+ basicMarketingPackage: {
+ label: "Basic marketing package",
+ minWidth: 140,
+ flex: 1.2,
+ },
+};
+
+const breakpointM = 1120;
+
+export const LaunchpadReadyApplicationsTable: React.FC<{
+ rows: LaunchpadProject[];
+}> = ({ rows }) => {
+ const renderItem = ({
+ item,
+ index,
+ }: {
+ item: LaunchpadProject;
+ index: number;
+ }) => {
+ const collectionData = parseCollectionData(item);
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+ item.symbol}
+ />
+
+
+ );
+};
+
+const LaunchpadReadyApplicationsTableRow: React.FC<{
+ launchpadProject: LaunchpadProject;
+ index: number;
+}> = ({ launchpadProject, index }) => {
+ const collectionData = parseCollectionData(launchpadProject);
+ if (!collectionData) return null;
+ return (
+
+
+
+
+ TODO
+
+
+
+
+
+ TODO
+
+ TODO
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx
new file mode 100644
index 0000000000..526b26f1af
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/components/CompletesTable.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable";
+import {
+ Sort,
+ SortDirection,
+ Status,
+} from "../../../../api/launchpad/v1/launchpad";
+import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork";
+
+import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects";
+
+export const CompletesTable: FC<{
+ limit: number;
+}> = ({ limit }) => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const { launchpadProjects = [] } = useLaunchpadProjects({
+ networkId: selectedNetworkId,
+ offset: 0,
+ limit,
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ status: Status.STATUS_COMPLETE,
+ });
+ return ;
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx
new file mode 100644
index 0000000000..c89124b166
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/components/ConfirmedsTable.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable";
+import {
+ Sort,
+ SortDirection,
+ Status,
+} from "../../../../api/launchpad/v1/launchpad";
+import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork";
+
+import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects";
+
+export const ConfirmedsTable: FC<{
+ limit: number;
+}> = ({ limit }) => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const { launchpadProjects = [] } = useLaunchpadProjects({
+ networkId: selectedNetworkId,
+ offset: 0,
+ limit,
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ status: Status.STATUS_CONFIRMED,
+ });
+ return ;
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx
new file mode 100644
index 0000000000..35fc1407ec
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/components/IncompletesTable.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable";
+import {
+ Sort,
+ SortDirection,
+ Status,
+} from "../../../../api/launchpad/v1/launchpad";
+import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork";
+
+import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects";
+
+export const IncompletesTable: FC<{
+ limit: number;
+}> = ({ limit }) => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const { launchpadProjects = [] } = useLaunchpadProjects({
+ networkId: selectedNetworkId,
+ offset: 0,
+ limit,
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ status: Status.STATUS_INCOMPLETE,
+ });
+ return ;
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx
new file mode 100644
index 0000000000..f0d2b0641a
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/components/LaunchpadCollectionsTable.tsx
@@ -0,0 +1,176 @@
+import React from "react";
+import { FlatList, View } from "react-native";
+
+import {
+ commonColumns,
+ LaunchpadTablesCommonColumns,
+} from "../../LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns";
+
+import { LaunchpadProject } from "@/api/launchpad/v1/launchpad";
+import externalLinkSVG from "@/assets/icons/external-link.svg";
+import { Link } from "@/components/Link";
+import { OmniLink } from "@/components/OmniLink";
+import { SVG } from "@/components/SVG";
+import { TableCell } from "@/components/table/TableCell";
+import { TableHeader } from "@/components/table/TableHeader";
+import { TableRow } from "@/components/table/TableRow";
+import { TableTextCell } from "@/components/table/TableTextCell";
+import { TableWrapper } from "@/components/table/TableWrapper";
+import { TableColumns } from "@/components/table/utils";
+import { parseCollectionData } from "@/utils/launchpad";
+import { secondaryColor } from "@/utils/style/colors";
+import { screenContentMaxWidthLarge } from "@/utils/style/layout";
+
+const columns: TableColumns = {
+ ...commonColumns,
+ twitterURL: {
+ label: "Twitter",
+ minWidth: 60,
+ flex: 0.25,
+ },
+ discordURL: {
+ label: "Discord",
+ minWidth: 60,
+ flex: 0.25,
+ },
+ expectedTotalSupply: {
+ label: "Expected Total Supply",
+ minWidth: 140,
+ flex: 1.8,
+ },
+ expectedPublicMintPrice: {
+ label: "Expected Public Mint Price",
+ minWidth: 150,
+ flex: 1.8,
+ },
+ expectedMintDate: {
+ label: "Expected Mint Date",
+ minWidth: 100,
+ flex: 1,
+ },
+};
+const breakpointM = 1110;
+
+// Displays collection_data as CollectionDataResult[] from many launchpad_projects
+export const LaunchpadCollectionsTable: React.FC<{
+ rows: LaunchpadProject[];
+}> = ({ rows }) => {
+ const renderItem = ({
+ item,
+ index,
+ }: {
+ item: LaunchpadProject;
+ index: number;
+ }) => {
+ const collectionData = parseCollectionData(item);
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+ item.symbol}
+ />
+
+
+ );
+};
+
+const LaunchpadCollectionsTableRow: React.FC<{
+ launchpadProject: LaunchpadProject;
+ index: number;
+}> = ({ launchpadProject, index }) => {
+ const collectionData = parseCollectionData(launchpadProject);
+ if (!collectionData) return null;
+ return (
+
+ {/* */}
+
+
+
+
+ {/* */}
+
+
+
+
+
+
+ {/* */}
+
+
+
+
+
+
+ {`${collectionData.tokens_count}`}
+
+
+
+ TODO REMOVE
+
+
+
+ TODO REMOVE
+
+
+ {/*TODO: Three dots here with possible actions on the collection ?*/}
+ {/**/}
+
+ {/* */}
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx b/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx
new file mode 100644
index 0000000000..5bb0dd3cc4
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadAdmin/components/ReviewingsTable.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+import { LaunchpadCollectionsTable } from "./LaunchpadCollectionsTable";
+import {
+ Sort,
+ SortDirection,
+ Status,
+} from "../../../../api/launchpad/v1/launchpad";
+import { useSelectedNetworkId } from "../../../../hooks/useSelectedNetwork";
+
+import { useLaunchpadProjects } from "@/hooks/launchpad/useLaunchpadProjects";
+
+export const ReviewingsTable: FC<{
+ limit: number;
+}> = ({ limit }) => {
+ const selectedNetworkId = useSelectedNetworkId();
+ const { launchpadProjects = [] } = useLaunchpadProjects({
+ networkId: selectedNetworkId,
+ offset: 0,
+ limit,
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ status: Status.STATUS_REVIEWING,
+ });
+ return ;
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
index 6fd2715f5e..0644d7a9ff 100644
--- a/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadApplyScreen.tsx
@@ -1,18 +1,25 @@
import React from "react";
-import { StyleSheet, View } from "react-native";
+import { Linking, TextStyle, View } from "react-native";
+
+import {
+ LargeBoxButton,
+ LargeBoxButtonProps,
+} from "../../../components/buttons/LargeBoxButton";
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 {
- LargeBoxButton,
- LargeBoxButtonProps,
-} from "@/components/buttons/LargeBoxButton";
-import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+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,16 +31,17 @@ const BUTTONS: LargeBoxButtonProps[] = [
title: "Create",
description:
"Upload your assets, enter collection metadata and deploy your collection.",
- buttonTitle: "Coming soon",
+ buttonTitle: "Open",
},
{
title: "My Collections",
description: "Manage your collections with available actions and queries.",
- buttonTitle: "Coming soon",
+ buttonTitle: "Open",
},
];
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/LaunchpadCompleteScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCompleteScreen.tsx
new file mode 100644
index 0000000000..72a96f9afb
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCompleteScreen.tsx
@@ -0,0 +1,218 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import React, { useState } from "react";
+import { FieldErrors, FormProvider, useForm } from "react-hook-form";
+import { View } from "react-native";
+import { useSelector } from "react-redux";
+
+import { TextInputLaunchpad } from "./components/inputs/TextInputLaunchpad";
+
+import { BrandText } from "@/components/BrandText";
+import { NotFound } from "@/components/NotFound";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { PrimaryButton } from "@/components/buttons/PrimaryButton";
+import { SpacerColumn } from "@/components/spacer";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { useCompleteCollection } from "@/hooks/launchpad/useCompleteCollection";
+import { useLaunchpadProjectById } from "@/hooks/launchpad/useLaunchpadProjectById";
+import { useIsMobile } from "@/hooks/useIsMobile";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { AssetsTab } from "@/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab";
+import { selectNFTStorageAPI } from "@/store/slices/settings";
+import { ScreenFC, useAppNavigation } from "@/utils/navigation";
+import {
+ neutral33,
+ neutral55,
+ neutral77,
+ primaryColor,
+} from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import {
+ CollectionAssetsMetadatasFormValues,
+ CollectionFormValues,
+ ZodCollectionAssetsMetadatasFormValues,
+} from "@/utils/types/launchpad";
+
+export const LaunchpadCompleteScreen: ScreenFC<"LaunchpadComplete"> = ({
+ route,
+}) => {
+ const isMobile = useIsMobile();
+ const { setToast } = useFeedbacks();
+ const userIPFSKey = useSelector(selectNFTStorageAPI);
+ const { id: symbol } = route.params;
+ const selectedNetworkId = useSelectedNetworkId();
+ const navigation = useAppNavigation();
+ const selectedWallet = useSelectedWallet();
+ const { completeCollection } = useCompleteCollection();
+ const { launchpadProject } = useLaunchpadProjectById({
+ networkId: selectedNetworkId,
+ projectId: symbol,
+ });
+ const assetsMetadatasForm = useForm({
+ mode: "all",
+ resolver: zodResolver(ZodCollectionAssetsMetadatasFormValues),
+ defaultValues: { assetsMetadatas: [], nftApiKey: userIPFSKey },
+ });
+ const [isLoading, setLoading] = useState(false);
+ const { setLoadingFullScreen } = useFeedbacks();
+ const assetsMetadatas = assetsMetadatasForm.watch("assetsMetadatas");
+ const nftApiKey = assetsMetadatasForm.watch("nftApiKey");
+
+ const onValid = async () => {
+ setLoading(true);
+ setLoadingFullScreen(true);
+ try {
+ const success = await completeCollection(
+ symbol,
+ assetsMetadatasForm.getValues(),
+ );
+ setLoading(false);
+ setLoadingFullScreen(false);
+ if (success) navigation.navigate("LaunchpadMyCollections");
+ } catch (e) {
+ console.error("Error completing the NFT collection", e);
+ } finally {
+ setLoading(false);
+ setLoadingFullScreen(false);
+ }
+ };
+
+ const onInvalid = (fieldsErrors: FieldErrors) => {
+ console.error("Fields errors: ", fieldsErrors);
+ setToast({
+ mode: "normal",
+ type: "error",
+ title: "Unable to complete the collection",
+ message:
+ "Some fields are not correctly filled.\nPlease complete properly the mapping file.\nCheck the description for more information.",
+ });
+ };
+
+ const onPressComplete = () =>
+ assetsMetadatasForm.handleSubmit(onValid, onInvalid)();
+
+ return (
+ >}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ // TODO: Remove after tests
+ forceNetworkId="teritori-testnet"
+ headerChildren={Collection Completion}
+ onBackPress={() => navigation.navigate("LaunchpadMyCollections")}
+ >
+
+ {!selectedWallet?.userId ? (
+
+ You are not connected
+
+ ) : !launchpadProject ? (
+
+ ) : launchpadProject.networkId !== selectedNetworkId ? (
+ Wrong network
+ ) : selectedWallet.userId !== launchpadProject?.creatorId ? (
+
+ You don't own this Collection
+
+ ) : (
+
+
+
+ Assets & Metadata
+
+
+
+ Make sure you check out{" "}
+
+ documentation
+ {" "}
+ on how to create your collection
+
+
+
+
+
+
+ label="NFT.Storage JWT"
+ sublabel={
+
+ Used to upload the cover image and the assets to your
+ NFT Storage
+
+ }
+ placeHolder="My Awesome Collection"
+ name="nftApiKey"
+ form={assetsMetadatasForm}
+ />
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx
new file mode 100644
index 0000000000..5594441f7a
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/LaunchpadCreateScreen.tsx
@@ -0,0 +1,185 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import React, { 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 {
+ const success = await createCollection(collectionForm.getValues());
+ 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]}
+ // TODO: Remove after tests
+ forceNetworkId="teritori-testnet"
+ headerChildren={Apply to Launchpad}
+ onBackPress={() => navigation.navigate("LaunchpadApply")}
+ >
+
+
+
+
+ {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..5b3e31d907
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadStepper.tsx
@@ -0,0 +1,247 @@
+import React, { Dispatch, FC, 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/LaunchpadTablesCommonColumns.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns.tsx
new file mode 100644
index 0000000000..917aafc509
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/LaunchpadTablesCommonColumns.tsx
@@ -0,0 +1,108 @@
+import React from "react";
+
+import { NetworkIcon } from "./../../../../../components/NetworkIcon";
+
+import defaultCollectionImagePNG from "@/assets/default-images/ava.png";
+import checkBadgeSVG from "@/assets/icons/certified.svg";
+import { SVG } from "@/components/SVG";
+import { RoundedGradientImage } from "@/components/images/RoundedGradientImage";
+import { SpacerRow } from "@/components/spacer";
+import { TableCell } from "@/components/table/TableCell";
+import { CellBrandText, TableTextCell } from "@/components/table/TableTextCell";
+import { TableColumns } from "@/components/table/utils";
+import { getNetwork } from "@/networks";
+import { web3ToWeb2URI } from "@/utils/ipfs";
+import { layout } from "@/utils/style/layout";
+import { CollectionDataResult } from "@/utils/types/launchpad";
+
+export const commonColumns: TableColumns = {
+ rank: {
+ label: "#",
+ minWidth: 20,
+ flex: 0.25,
+ },
+ collectionName: {
+ label: "Collection Name",
+ minWidth: 240,
+ flex: 3,
+ },
+ symbol: {
+ label: "Symbol",
+ minWidth: 80,
+ flex: 0.5,
+ },
+ collectionNetwork: {
+ label: "Collection Network",
+ minWidth: 150,
+ flex: 1.8,
+ },
+};
+
+export const LaunchpadTablesCommonColumns: React.FC<{
+ collectionData: CollectionDataResult;
+ index: number;
+}> = ({ collectionData, index }) => {
+ const network = getNetwork(collectionData.target_network);
+ return (
+ <>
+
+ {`${index + 1}`}
+
+
+
+
+ {collectionData.name}
+
+
+
+
+
+ {collectionData.symbol}
+
+
+
+
+
+
+ {network?.displayName || "UNKNOWN NETWORK"}
+
+
+ >
+ );
+};
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..f6f42dee0c
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAdditional.tsx
@@ -0,0 +1,143 @@
+import React, { FC } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import { View } from "react-native";
+
+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 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..1f4d85dd4e
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetModal.tsx
@@ -0,0 +1,193 @@
+import React 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: React.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..01c43a0b7d
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/AssetsTab.tsx
@@ -0,0 +1,750 @@
+import { parse } from "papaparse";
+import pluralize from "pluralize";
+import React, { 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 } 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<
+ {
+ title: string;
+ message: string;
+ type: "error" | "warning";
+ }[]
+ >([]);
+ const [assetsIssues, setAssetsIssues] = useState<
+ {
+ title: string;
+ message: string;
+ type: "error" | "warning";
+ }[]
+ >([]);
+ const [imagesIssues, setImagesIssues] = useState<
+ {
+ title: string;
+ message: string;
+ type: "error" | "warning";
+ }[]
+ >([]);
+
+ 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 = async (files: LocalFileData[]) => {
+ resetAllIssues();
+ setAssetsMappingDataRows([]);
+ assetsMetadatasForm.setValue("assetsMetadatas", []);
+
+ try {
+ await 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[] = [];
+
+ // Controling 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 = async (files: LocalFileData[]) => {
+ resetIssuesErrors();
+ setAssetsIssues([]);
+ setImagesIssues([]);
+ assetsMetadatasForm.setValue("assetsMetadatas", []);
+ imagesUploaderRef.current?.resetFiles();
+
+ try {
+ await 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[] = [];
+
+ // Controling 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..f3fa04d8b0
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/LaunchpadAssetsAndMetadata.tsx
@@ -0,0 +1,116 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import React, { 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, // Retreive 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..ac97003a46
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadAssetsAndMetadata/UriTab.tsx
@@ -0,0 +1,60 @@
+import React 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: React.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..e49914fe67
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadBasic.tsx
@@ -0,0 +1,145 @@
+import React, { 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="desc"
+ 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}
+ />
+
+ {/**/}
+ {/* label="External Link"*/}
+ {/* placeHolder="https://collection..."*/}
+ {/* name="externalLink"*/}
+ {/* form={collectionForm}*/}
+ {/* required={false}*/}
+ {/*/>*/}
+
+
+
+
+ );
+};
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..8254ef351e
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadDetails.tsx
@@ -0,0 +1,163 @@
+import React, { 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..55c28bf557
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/ConfigureRoyaltyDetails.tsx
@@ -0,0 +1,60 @@
+import React 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: React.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..331f5bd336
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordion.tsx
@@ -0,0 +1,46 @@
+import React, { 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..3496fd0394
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionBottom.tsx
@@ -0,0 +1,212 @@
+import React, { 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..91ef8392c1
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriodAccordionTop.tsx
@@ -0,0 +1,95 @@
+import React, { 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..e3637f0272
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMintPeriods.tsx
@@ -0,0 +1,119 @@
+import React, { 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..3b8220c2bf
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadMinting/LaunchpadMinting.tsx
@@ -0,0 +1,67 @@
+import React, { 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..229f8301d0
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadCreate/components/steps/LaunchpadTeamAndInvestment.tsx
@@ -0,0 +1,140 @@
+import React, { 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="Team links and attachments "*/}
+ {/* sublabel={*/}
+ {/* */}
+ {/* */}
+ {/* Please provide any relevant links regarding your team. You can*/}
+ {/* also post a google drive link.*/}
+ {/* */}
+ {/* */}
+ {/* }*/}
+ {/* placeHolder="Type here..."*/}
+ {/* name="teamLink"*/}
+ {/* 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}
+ />
+
+ {/**/}
+ {/* label="Whitepaper and roadmap: "*/}
+ {/* sublabel={*/}
+ {/* */}
+ {/* */}
+ {/* Please provide any relevant link regarding your white paper and*/}
+ {/* roadmap. You can also post a google drive link.*/}
+ {/* */}
+ {/* */}
+ {/* }*/}
+ {/* placeHolder="Type here..."*/}
+ {/* name="roadmapLink"*/}
+ {/* form={collectionForm}*/}
+ {/*/>*/}
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx
new file mode 100644
index 0000000000..0d49a5b928
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/LaunchpadMyCollectionsScreen.tsx
@@ -0,0 +1,126 @@
+import React from "react";
+import { View } from "react-native";
+
+import { Sort, SortDirection } from "@/api/launchpad/v1/launchpad";
+import infoSVG from "@/assets/icons/info.svg";
+import { BrandText } from "@/components/BrandText";
+import { SVG } from "@/components/SVG";
+import { ScreenContainer } from "@/components/ScreenContainer";
+import { Box } from "@/components/boxes/Box";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useLaunchpadProjectsByCreator } from "@/hooks/launchpad/useLaunchpadProjectsByCreator";
+import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork";
+import useSelectedWallet from "@/hooks/useSelectedWallet";
+import { NetworkFeature } from "@/networks";
+import { LaunchpadMyCollectionsTable } from "@/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable";
+import { ScreenFC, useAppNavigation } from "@/utils/navigation";
+import {
+ neutral17,
+ neutral77,
+ primaryColor,
+ withAlpha,
+} from "@/utils/style/colors";
+import {
+ fontSemibold13,
+ fontSemibold14,
+ fontSemibold28,
+} from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+
+export const LaunchpadMyCollectionsScreen: ScreenFC<
+ "LaunchpadMyCollections"
+> = () => {
+ const navigation = useAppNavigation();
+ const selectedNetworkId = useSelectedNetworkId();
+ const selectedWallet = useSelectedWallet();
+ const { launchpadProjects = [] } = useLaunchpadProjectsByCreator({
+ networkId: selectedNetworkId,
+ creatorId: selectedWallet?.userId || "",
+ offset: 0,
+ limit: 100, // TODO: Pagination
+ sort: Sort.SORT_UNSPECIFIED,
+ sortDirection: SortDirection.SORT_DIRECTION_UNSPECIFIED,
+ });
+
+ return (
+ >}
+ forceNetworkFeatures={[NetworkFeature.CosmWasmNFTLaunchpad]}
+ headerChildren={Apply to Launchpad}
+ onBackPress={() => navigation.navigate("LaunchpadApply")}
+ >
+
+ My collections
+
+
+
+
+ A list of your created collections. Learn more in the{" "}
+
+ documentation.
+
+
+
+
+
+ {launchpadProjects?.length ? (
+
+ ) : (
+
+
+
+
+
+
+ You haven’t created any collections so far
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx
new file mode 100644
index 0000000000..953f820f15
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/LaunchpadMyCollections/components/LaunchpadMyCollectionsTable.tsx
@@ -0,0 +1,129 @@
+import React, { useState } from "react";
+import { FlatList, View } from "react-native";
+
+import { StatusBadge } from "./../../../components/StatusBadge";
+import { CustomPressable } from "../../../../../components/buttons/CustomPressable";
+import {
+ commonColumns,
+ LaunchpadTablesCommonColumns,
+} from "../../LaunchpadCreate/components/LaunchpadTablesCommonColumns";
+
+import { LaunchpadProject } from "@/api/launchpad/v1/launchpad";
+import { PrimaryButton } from "@/components/buttons/PrimaryButton";
+import { TableCell } from "@/components/table/TableCell";
+import { TableHeader } from "@/components/table/TableHeader";
+import { TableRow } from "@/components/table/TableRow";
+import { TableWrapper } from "@/components/table/TableWrapper";
+import { TableColumns } from "@/components/table/utils";
+import { launchpadProjectStatus, parseCollectionData } from "@/utils/launchpad";
+import { useAppNavigation } from "@/utils/navigation";
+import { screenContentMaxWidthLarge } from "@/utils/style/layout";
+
+const columns: TableColumns = {
+ ...commonColumns,
+ status: {
+ label: "Status",
+ minWidth: 200,
+ flex: 2,
+ },
+ cta: {
+ label: "",
+ minWidth: 180,
+ flex: 2,
+ },
+};
+
+const breakpointM = 1120;
+
+export const LaunchpadMyCollectionsTable: React.FC<{
+ rows: LaunchpadProject[];
+}> = ({ rows }) => {
+ const renderItem = ({
+ item,
+ index,
+ }: {
+ item: LaunchpadProject;
+ index: number;
+ }) => {
+ const collectionData = parseCollectionData(item);
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+const LaunchpadReadyMyCollectionsTableRow: React.FC<{
+ launchpadProject: LaunchpadProject;
+ index: number;
+}> = ({ launchpadProject, index }) => {
+ const navigation = useAppNavigation();
+ const collectionData = parseCollectionData(launchpadProject);
+ const [isHovered, setHovered] = useState(false);
+
+ if (!collectionData) return null;
+ return (
+
+ navigation.navigate("LaunchpadApplicationReview", {
+ id: launchpadProject.id,
+ })
+ }
+ onHoverIn={() => setHovered(true)}
+ onHoverOut={() => setHovered(false)}
+ style={isHovered && { opacity: 0.5 }}
+ >
+
+
+
+
+
+
+
+
+ {launchpadProjectStatus(launchpadProject.status) === "INCOMPLETE" && (
+
+ navigation.navigate("LaunchpadComplete", {
+ id: collectionData.symbol,
+ })
+ }
+ />
+ )}
+
+
+
+ );
+};
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..91e2059e9f
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/CurrencyInputLaunchpad.tsx
@@ -0,0 +1,233 @@
+import { Decimal } from "@cosmjs/math";
+import React, { FC, Fragment, 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?: React.ReactElement;
+ sublabel?: React.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..95afecc580
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectableCurrencySmall.tsx
@@ -0,0 +1,42 @@
+import React 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: React.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..0aaedf28a4
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/CurrencyInputLaunchpad/SelectedCurrencySmall.tsx
@@ -0,0 +1,73 @@
+import React, { 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..f4b0c5245c
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/TextInputLaunchpad.tsx
@@ -0,0 +1,98 @@
+import React, { 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?: React.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..bb4c54d450
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/MultipleSelectInput.tsx
@@ -0,0 +1,167 @@
+import React, { 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 { CheckboxDappStore } from "@/screens/DAppStore/components/CheckboxDappStore";
+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?: React.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..2553de2a9f
--- /dev/null
+++ b/packages/screens/Launchpad/LaunchpadApply/components/inputs/selectInputs/SelectInputLaunchpad.tsx
@@ -0,0 +1,160 @@
+import React, { 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..9870376656 100644
--- a/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx
+++ b/packages/screens/Launchpad/LaunchpadHome/LaunchpadScreen.tsx
@@ -7,12 +7,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 +23,7 @@ export const LaunchpadScreen: ScreenFC<"Launchpad"> = () => {
return (
Launchpad}
>
= ({ projectStatus }) => {
+ const textColor =
+ projectStatus === Status.STATUS_UNSPECIFIED ||
+ projectStatus === Status.UNRECOGNIZED
+ ? neutralFF
+ : neutral00;
+
+ const backgroundColor =
+ projectStatus === Status.STATUS_INCOMPLETE
+ ? "#D2DEFC"
+ : projectStatus === Status.STATUS_COMPLETE
+ ? "#9990F5"
+ : projectStatus === Status.STATUS_REVIEWING
+ ? "#9058EC"
+ : projectStatus === Status.STATUS_CONFIRMED
+ ? "#F46FBF"
+ : neutral33;
+
+ return (
+
+
+ {launchpadProjectStatus(projectStatus)}
+
+
+ );
+};
diff --git a/packages/screens/Marketplace/CollectionsTable.tsx b/packages/screens/Marketplace/CollectionsTable.tsx
index 44229a0ff5..c739299a73 100644
--- a/packages/screens/Marketplace/CollectionsTable.tsx
+++ b/packages/screens/Marketplace/CollectionsTable.tsx
@@ -176,7 +176,7 @@ export const CollectionsTable: FC<{
renderItem={({ item, index }) => (
)}
@@ -189,10 +189,10 @@ export const CollectionsTable: FC<{
const CollectionTableRow: React.FC<{
collection: PopularCollection;
- rank: number;
+ index: number;
prices: CoingeckoPrices;
-}> = ({ collection, rank, prices }) => {
- const rowData = getRowData(collection, rank, prices);
+}> = ({ collection, index, prices }) => {
+ const rowData = getRowData(collection, index, prices);
const target = useCollectionNavigationTarget(collection.id);
const tradeDiffText = rowData["TimePeriodPercentualVolume"];
const tradeDiffColor =
@@ -232,7 +232,6 @@ const CollectionTableRow: React.FC<{
size="XS"
sourceURI={rowData.collectionNameData.image}
style={{
- // marginRight: isMobile ? layout.spacing_x1 : layout.spacing_x1_5,
marginRight: layout.spacing_x1,
}}
/>
@@ -404,7 +403,7 @@ const getDelta = (collection: PopularCollection) => {
const getRowData = (
collection: PopularCollection,
- rank: number,
+ index: number,
prices: CoingeckoPrices,
): RowData => {
const [network] = parseCollectionId(collection.id);
@@ -427,7 +426,7 @@ const getRowData = (
: undefined;
return {
id: collection.id,
- rank: rank + 1,
+ rank: index + 1,
collectionName: collection.name,
collectionNameData: {
collectionName: collection.name,
diff --git a/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx b/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx
index 635ccb565d..44fc3b2dd2 100644
--- a/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx
+++ b/packages/screens/Projects/ProjectsMakeRequestScreen/ShortPresentation.tsx
@@ -9,7 +9,6 @@ import { BrandText } from "../../../components/BrandText";
import { PrimaryButtonOutline } from "../../../components/buttons/PrimaryButtonOutline";
import { RoundedGradientImage } from "../../../components/images/RoundedGradientImage";
import { TextInputCustom } from "../../../components/inputs/TextInputCustom";
-import { FileUploader } from "../../../components/inputs/fileUploader";
import { SpacerColumn } from "../../../components/spacer";
import { useNameSearch } from "../../../hooks/search/useNameSearch";
import { useSelectedNetworkId } from "../../../hooks/useSelectedNetwork";
@@ -23,6 +22,7 @@ import {
zodProjectFormData,
} from "../hooks/useMakeRequestHook";
+import { FileUploader } from "@/components/inputs/fileUploader";
import { LoaderFullScreen } from "@/components/loaders/LoaderFullScreen";
import { useIpfs } from "@/hooks/useIpfs";
import { ButtonsGroup } from "@/screens/Projects/components/ButtonsGroup";
diff --git a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx
index 39f3e50cb9..56ac89d6ef 100644
--- a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx
+++ b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx
@@ -5,7 +5,6 @@ import { useFieldArray, useForm, useWatch } from "react-hook-form";
import { View, TouchableOpacity } from "react-native";
import { SubscriptionBottomComponent } from "./SubscriptionBottomComponent";
-import { AccordionComponent } from "../accordion/AccordionComponent";
import addSVG from "@/assets/icons/add-secondary.svg";
import settingsSVG from "@/assets/icons/settings-primary.svg";
@@ -24,6 +23,7 @@ import { usePremiumChannel } from "@/hooks/feed/usePremiumChannel";
import useSelectedWallet from "@/hooks/useSelectedWallet";
import { getNativeCurrency, getNetworkFeature, parseUserId } from "@/networks";
import { NetworkFeature } from "@/networks/features";
+import { AccordionComponent } from "@/screens/UserPublicProfile/components/accordion/AccordionComponent";
import { bigDaySeconds } from "@/utils/big-time";
import { mustGetCw721MembershipSigningClient } from "@/utils/feed/client";
import { mapTierToFormElement } from "@/utils/feed/premium";
diff --git a/packages/scripts/network-setup/deployLib.ts b/packages/scripts/network-setup/deployLib.ts
index ee0224f8ee..fa7495702a 100644
--- a/packages/scripts/network-setup/deployLib.ts
+++ b/packages/scripts/network-setup/deployLib.ts
@@ -6,6 +6,7 @@ import { bech32 } from "bech32";
import _, { cloneDeep } from "lodash";
import path from "path";
+import { instantiateNftLaunchpad } from "./deployNftLaunchpad";
import { InstantiateMsg as MarketplaceVaultInstantiateMsg } from "../../contracts-clients/nft-marketplace/NftMarketplace.types";
import {
ExecuteMsg as NameServiceExecuteMsg,
@@ -24,7 +25,9 @@ import {
cosmosNetworkGasPrice,
CosmosNetworkInfo,
getCosmosNetwork,
+ getNetworkFeature,
mustGetNonSigningCosmWasmClient,
+ NetworkFeature,
} from "@/networks";
import { zodTryParseJSON } from "@/utils/sanitize";
@@ -132,6 +135,24 @@ export const deployTeritoriEcosystem = async (
network,
);
+ console.log("Instantiating NFT Launchpad", network.nameServiceCodeId);
+ const cosmwasmNftLaunchpadFeature = cloneDeep(
+ getNetworkFeature(networkId, NetworkFeature.CosmWasmNFTLaunchpad),
+ );
+ if (!cosmwasmNftLaunchpadFeature) {
+ console.error(`Cosmwasm Launchpad feature not found on ${networkId}`);
+ } else {
+ cosmwasmNftLaunchpadFeature.launchpadContractAddress =
+ await instantiateNftLaunchpad(
+ opts,
+ wallet,
+ walletAddr,
+ "TODO DAO address",
+ network,
+ cosmwasmNftLaunchpadFeature,
+ );
+ }
+
if (opts.signer) {
await registerTNSHandle(network, opts.signer);
await testTeritoriEcosystem(network);
diff --git a/packages/utils/backend.ts b/packages/utils/backend.ts
index 5aa792efc1..857b8ff619 100644
--- a/packages/utils/backend.ts
+++ b/packages/utils/backend.ts
@@ -8,6 +8,11 @@ import {
FeedServiceClientImpl,
GrpcWebImpl as FeedGrpcWebImpl,
} from "../api/feed/v1/feed";
+import {
+ LaunchpadService,
+ LaunchpadServiceClientImpl,
+ GrpcWebImpl as LaunchpadGrpcWebImpl,
+} from "../api/launchpad/v1/launchpad";
import {
MarketplaceServiceClientImpl,
GrpcWebImpl as MarketplaceGrpcWebImpl,
@@ -18,7 +23,7 @@ import {
GrpcWebImpl as P2eGrpcWebImpl,
P2eService,
} from "../api/p2e/v1/p2e";
-import { getNetwork } from "../networks";
+import { getNetwork, getNetworkFeature, NetworkFeature } from "../networks";
const marketplaceClients: { [key: string]: MarketplaceService } = {};
@@ -107,3 +112,34 @@ export const mustGetFeedClient = (networkId: string | undefined) => {
}
return client;
};
+
+const launchpadClients: { [key: string]: LaunchpadService } = {};
+
+export const getLaunchpadClient = (networkId: string | undefined) => {
+ const network = getNetwork(networkId);
+ const cosmwasmNftLaunchpadFeature = getNetworkFeature(
+ networkId,
+ NetworkFeature.CosmWasmNFTLaunchpad,
+ );
+ if (!network || !cosmwasmNftLaunchpadFeature) {
+ return undefined;
+ }
+ if (!launchpadClients[network.id]) {
+ const rpc = new LaunchpadGrpcWebImpl(
+ cosmwasmNftLaunchpadFeature.launchpadEndpoint,
+ {
+ debug: false,
+ },
+ );
+ launchpadClients[network.id] = new LaunchpadServiceClientImpl(rpc);
+ }
+ return launchpadClients[network.id];
+};
+
+export const mustGetLaunchpadClient = (networkId: string | undefined) => {
+ const client = getLaunchpadClient(networkId);
+ if (!client) {
+ throw new Error(`failed to get feed client for network '${networkId}'`);
+ }
+ return client;
+};
diff --git a/packages/utils/formRules.ts b/packages/utils/formRules.ts
index 0f457a873f..e299f9cc68 100644
--- a/packages/utils/formRules.ts
+++ b/packages/utils/formRules.ts
@@ -1,9 +1,8 @@
import { bech32 } from "bech32";
import { ValidationRule } from "react-hook-form";
-import { LETTERS_REGEXP, NUMBERS_REGEXP } from "./regex";
-
import { DEFAULT_FORM_ERRORS } from "@/utils/errors";
+import { LETTERS_REGEXP, NUMBERS_REGEXP } from "@/utils/regex";
// validator should return false or string to trigger error
export const validateAddress = (value: string) => {
@@ -32,3 +31,10 @@ export const validateMaxNumber = (value: string, max: number) => {
}
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/launchpad.ts b/packages/utils/launchpad.ts
new file mode 100644
index 0000000000..a504c1033d
--- /dev/null
+++ b/packages/utils/launchpad.ts
@@ -0,0 +1,55 @@
+import {
+ LaunchpadProject,
+ Status,
+ StatusCount,
+} from "@/api/launchpad/v1/launchpad";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import {
+ CollectionDataResult,
+ ZodCollectionDataResult,
+} from "@/utils/types/launchpad";
+
+export const launchpadProjectStatus = (status: Status) => {
+ switch (status) {
+ case Status.STATUS_INCOMPLETE:
+ return "INCOMPLETE";
+ case Status.STATUS_COMPLETE:
+ return "COMPLETE";
+ case Status.STATUS_REVIEWING:
+ return "REVIEWING";
+ case Status.STATUS_CONFIRMED:
+ return "CONFIRMED";
+ case Status.STATUS_UNSPECIFIED:
+ return "UNSPECIFIED";
+ default:
+ return "UNSPECIFIED";
+ }
+};
+
+export const statusToCount = (status: string, statusCounts?: StatusCount[]) =>
+ statusCounts?.find(
+ (statusCount) => launchpadProjectStatus(statusCount.status) === status,
+ )?.count || 0;
+
+export const parseCollectionData = (launchpadProject: LaunchpadProject) =>
+ zodTryParseJSON(ZodCollectionDataResult, launchpadProject.collectionData);
+
+export const parseMultipleCollectionsData = (
+ launchpadProjects: LaunchpadProject[],
+) => {
+ const result: CollectionDataResult[] = [];
+ launchpadProjects.forEach((project) => {
+ if (!project) {
+ return;
+ }
+ const collectionData: CollectionDataResult | undefined = zodTryParseJSON(
+ ZodCollectionDataResult,
+ project.collectionData,
+ );
+ if (!collectionData) {
+ return;
+ }
+ result.push(collectionData);
+ });
+ return result;
+};
diff --git a/packages/utils/navigation.ts b/packages/utils/navigation.ts
index b46228241b..4c1ef48d21 100644
--- a/packages/utils/navigation.ts
+++ b/packages/utils/navigation.ts
@@ -33,6 +33,13 @@ export type RootStackParamList = {
Launchpad: undefined;
LaunchpadApply: undefined;
+ LaunchpadCreate: undefined;
+ LaunchpadComplete: { id: string };
+ LaunchpadMyCollections: undefined;
+ LaunchpadAdministrationOverview: undefined;
+ LaunchpadApplications: undefined;
+ LaunchpadApplicationReview: { id: string };
+ LaunchpadReadyApplications: undefined;
LaunchpadERC20: undefined;
LaunchpadERC20Tokens?: { network?: string };
@@ -215,6 +222,13 @@ const getNavConfig: (homeScreen: keyof RootStackParamList) => NavConfig = (
// ==== Launchpad
Launchpad: "launchpad",
LaunchpadApply: "launchpad/apply",
+ LaunchpadCreate: "launchpad/create",
+ LaunchpadComplete: "launchpad/complete/:id",
+ LaunchpadMyCollections: "launchpad/my-collections",
+ LaunchpadAdministrationOverview: "launchpad/admin",
+ LaunchpadApplications: "launchpad/admin/applications",
+ LaunchpadApplicationReview: "launchpad/review/:id",
+ LaunchpadReadyApplications: "launchpad/admin/ready-applications",
// ==== Launchpad ERC20
LaunchpadERC20: "launchpad-erc20",
diff --git a/packages/utils/regex.ts b/packages/utils/regex.ts
index a21ab7a143..420a51d0d0 100644
--- a/packages/utils/regex.ts
+++ b/packages/utils/regex.ts
@@ -1,8 +1,11 @@
export const MENTION_REGEX = /(@[\w&.-]+)/;
export const URL_REGEX =
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/;
+// export const IPFS_URI_REGEX = /^ipfs:\/\/.*/;
export const HASHTAG_REGEX = /#\S+/;
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/sidebar.ts b/packages/utils/sidebar.ts
index ec80f3c88b..5570367aa0 100644
--- a/packages/utils/sidebar.ts
+++ b/packages/utils/sidebar.ts
@@ -56,6 +56,12 @@ export const SIDEBAR_LIST: SidebarRecordType = {
icon: launchpadApplySVG,
route: "LaunchpadApply",
},
+ admin: {
+ title: "Admin Dashboard",
+ id: "admin",
+ icon: gridSVG,
+ route: "LaunchpadAdministrationOverview",
+ },
},
},
"multisig-wallet": {
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..110687bd1d
--- /dev/null
+++ b/packages/utils/types/launchpad.ts
@@ -0,0 +1,232 @@
+import { z } from "zod";
+
+import { Collection } 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 from api
+const ZodCoinDataResult = z.object({
+ amount: z.string(),
+ denom: z.string(),
+});
+
+const ZodWhitelistInfoDataResult = z.object({
+ addresses_count: z.number(),
+ addresses_ipfs: z.string(),
+ addresses_merkle_root: z.string(),
+});
+
+const ZodMintPeriodDataResult = z.object({
+ end_time: z.number().nullish(),
+ limit_per_address: z.number().nullish(),
+ max_tokens: z.number().nullish(),
+ price: ZodCoinDataResult.nullish(),
+ start_time: z.number(),
+ whitelist_info: ZodWhitelistInfoDataResult.nullish(),
+});
+
+export const ZodCollectionDataResult = z.object({
+ artwork_desc: z.string(),
+ base_token_uri: z.string().nullish(), // TODO REMOVE
+ contact_email: z.string(),
+ cover_img_uri: z.string(),
+ dao_whitelist_count: z.number(),
+ deployed_address: z.string().nullish(),
+ desc: z.string(),
+ escrow_mint_proceeds_period: z.number(),
+ investment_desc: z.string(),
+ investment_link: z.string(),
+ is_applied_previously: z.boolean(),
+ is_dox: z.boolean(),
+ is_project_derivative: z.boolean(),
+ is_ready_for_mint: z.boolean(),
+ metadatas_merkle_root: z.string().nullish(),
+ mint_periods: z.array(ZodMintPeriodDataResult),
+ name: z.string(),
+ partners: z.string(),
+ project_type: z.string(),
+ reveal_time: z.number().nullish(),
+ royalty_address: z.string().nullish(),
+ royalty_percentage: z.number().nullish(),
+ symbol: z.string(),
+ target_network: z.string(),
+ team_desc: z.string(),
+ tokens_count: z.number(),
+ website_link: z.string(),
+});
+
+export type MintPeriodDataResult = z.infer;
+
+export type CollectionDataResult = z.infer;
+
+export type CollectionToSubmit = Omit<
+ Collection,
+ "deployed_address" | "base_token_uri" | "owner"
+>;
diff --git a/rust/cw-contracts/nft-launchpad/Makefile b/rust/cw-contracts/nft-launchpad/Makefile
index d89889f4bb..ef017aa912 100644
--- a/rust/cw-contracts/nft-launchpad/Makefile
+++ b/rust/cw-contracts/nft-launchpad/Makefile
@@ -51,4 +51,5 @@ deploy.mainnet: artifacts/nft_launchpad.wasm
instantiate.mainnet: config-mainnet.json
set -o pipefail; \
TXHASH=$$(teritorid tx wasm instantiate $(CODE_ID_MAINNET) $(CONFIG_MAINNET) --label NftsBurner --admin $(ADMIN_ADDR_MAINNET) $(TX_FLAGS_MAINNET) | jq -r .txhash); \
- while ! teritorid query tx $$TXHASH $(QUERY_FLAGS_MAINNET) 2>/dev/null | jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value'; do sleep 1; done
\ No newline at end of file
+ while ! teritorid query tx $$TXHASH $(QUERY_FLAGS_MAINNET) 2>/dev/null | jq -r '.logs[0].events[] | select(.type=="instantiate").attributes[] | select(.key=="_contract_address").value'; do sleep 1; done
+
diff --git a/yarn.lock b/yarn.lock
index 240fc5ed44..3fccee07e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14585,6 +14585,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"
@@ -16135,6 +16158,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"
@@ -16165,7 +16197,7 @@ __metadata:
languageName: node
linkType: hard
-"node-gyp-build@npm:^4.3.0":
+"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0":
version: 4.8.0
resolution: "node-gyp-build@npm:4.8.0"
bin:
@@ -20232,6 +20264,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