diff --git a/package-lock.json b/package-lock.json index 839424949..17fc41b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "dependencies": { "@apollo/client": "^3.8.1", "@bosonprotocol/chat-sdk": "^1.4.5", - "@bosonprotocol/common": "^1.32.0", - "@bosonprotocol/react-kit": "^0.41.0", + "@bosonprotocol/common": "^1.32.0-alpha.6", + "@bosonprotocol/react-kit": "^0.42.0-alpha.0", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.7.0", @@ -3424,6 +3424,19 @@ "yup": "^1.5.0" } }, + "node_modules/@bosonprotocol/chat-sdk/node_modules/@bosonprotocol/common": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.32.0.tgz", + "integrity": "sha512-YSMpjqPyBXw5WRYlpFKdwCQwyXgSWxNDF1OqTRwFU9wdO+8R/VdG4QV0YTi+uEuy/9rFLyQZvrbxwLNVO4ZdBg==", + "dependencies": { + "@bosonprotocol/metadata": "^1.16.3", + "@ethersproject/abi": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/units": "^5.5.0" + } + }, "node_modules/@bosonprotocol/chat-sdk/node_modules/@noble/curves": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", @@ -3566,9 +3579,9 @@ } }, "node_modules/@bosonprotocol/common": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.32.0.tgz", - "integrity": "sha512-YSMpjqPyBXw5WRYlpFKdwCQwyXgSWxNDF1OqTRwFU9wdO+8R/VdG4QV0YTi+uEuy/9rFLyQZvrbxwLNVO4ZdBg==", + "version": "1.32.0-alpha.6", + "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.32.0-alpha.6.tgz", + "integrity": "sha512-fAJxG1XfmBIXPUFb5h7BP8QRkcyDVOqZpW5DwzKGltiWx0HCVgK66gUQosm+SYq30CVMhC34EJN/SwWpuwtLag==", "dependencies": { "@bosonprotocol/metadata": "^1.16.3", "@ethersproject/abi": "^5.5.0", @@ -3579,9 +3592,9 @@ } }, "node_modules/@bosonprotocol/core-sdk": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.45.0.tgz", - "integrity": "sha512-HmDFEmQZB9kWwFl8esVfrgfu1bB3ApjrJ31Rg3FNVoSnswTR2RfSx+5q6VwZAMBKRzNs08k2kbDkjuzdS3537Q==", + "version": "1.46.0-alpha.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.46.0-alpha.0.tgz", + "integrity": "sha512-8mE6qjDoxqnks7CwxcK2Di2BdyOHvCGJVjexJvG0pmDjSUWQ0YXJxbJjQnSiGSy4TemsIc/zIyq5cX+iXaeyZg==", "dependencies": { "@bosonprotocol/common": "^1.32.0", "@ethersproject/abi": "^5.5.0", @@ -3598,6 +3611,19 @@ "schema-to-yup": "^1.11.11" } }, + "node_modules/@bosonprotocol/core-sdk/node_modules/@bosonprotocol/common": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.32.0.tgz", + "integrity": "sha512-YSMpjqPyBXw5WRYlpFKdwCQwyXgSWxNDF1OqTRwFU9wdO+8R/VdG4QV0YTi+uEuy/9rFLyQZvrbxwLNVO4ZdBg==", + "dependencies": { + "@bosonprotocol/metadata": "^1.16.3", + "@ethersproject/abi": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/units": "^5.5.0" + } + }, "node_modules/@bosonprotocol/ethers-sdk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@bosonprotocol/ethers-sdk/-/ethers-sdk-1.17.0.tgz", @@ -3609,6 +3635,19 @@ "ethers": "^5.5.0" } }, + "node_modules/@bosonprotocol/ethers-sdk/node_modules/@bosonprotocol/common": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.32.0.tgz", + "integrity": "sha512-YSMpjqPyBXw5WRYlpFKdwCQwyXgSWxNDF1OqTRwFU9wdO+8R/VdG4QV0YTi+uEuy/9rFLyQZvrbxwLNVO4ZdBg==", + "dependencies": { + "@bosonprotocol/metadata": "^1.16.3", + "@ethersproject/abi": "^5.5.0", + "@ethersproject/address": "^5.5.0", + "@ethersproject/bignumber": "^5.5.0", + "@ethersproject/constants": "^5.5.0", + "@ethersproject/units": "^5.5.0" + } + }, "node_modules/@bosonprotocol/ipfs-storage": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@bosonprotocol/ipfs-storage/-/ipfs-storage-1.13.0.tgz", @@ -3639,12 +3678,12 @@ "license": "Apache-2.0" }, "node_modules/@bosonprotocol/react-kit": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.41.0.tgz", - "integrity": "sha512-Z6NRR8HwU9GLlFoMDi2YXpHiA6yWu+LQEbQx6US4wE9IPFeef3oig5WzwnJjqfTCxbB/ozUkEpa0UkM37tJFtA==", + "version": "0.42.0-alpha.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.42.0-alpha.0.tgz", + "integrity": "sha512-hqiq0SpiJpioc7IOVtHnsAIOkvBqQ9l0pSt85dZISZl8FGU+OEC5a+bAVj8fWT8qNYapWCHdMbiD+2RIhsOW8w==", "dependencies": { "@bosonprotocol/chat-sdk": "^1.3.1-alpha.20", - "@bosonprotocol/core-sdk": "^1.45.0", + "@bosonprotocol/core-sdk": "^1.46.0-alpha.0", "@bosonprotocol/ethers-sdk": "^1.17.0", "@bosonprotocol/ipfs-storage": "^1.13.0", "@davatar/react": "1.11.1", diff --git a/package.json b/package.json index 513760b17..04fa77539 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "dependencies": { "@apollo/client": "^3.8.1", "@bosonprotocol/chat-sdk": "^1.4.5", - "@bosonprotocol/common": "^1.32.0", - "@bosonprotocol/react-kit": "^0.41.0", + "@bosonprotocol/common": "^1.32.0-alpha.6", + "@bosonprotocol/react-kit": "^0.42.0-alpha.0", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.7.0", diff --git a/src/components/detail/DetailWidget/CommitDetailWidget.tsx b/src/components/detail/DetailWidget/CommitDetailWidget.tsx index 6fee50a04..7331d2b82 100644 --- a/src/components/detail/DetailWidget/CommitDetailWidget.tsx +++ b/src/components/detail/DetailWidget/CommitDetailWidget.tsx @@ -1,3 +1,4 @@ +import { PriceType } from "@bosonprotocol/common"; import { ExternalCommitDetailView, extractUserFriendlyError, @@ -290,6 +291,7 @@ export const CommitDetailWidget: React.FC = ({ const isOfferNotValidYet = dayjs( getDateTimestamp(offer?.validFromDate) ).isAfter(nowDate); + const isPriceDiscoveryOffer = offer.priceType === PriceType.Discovery; const isCommitDisabled = !hasSellerEnoughFunds || isExpiredOffer || @@ -299,7 +301,8 @@ export const CommitDetailWidget: React.FC = ({ isPreview || isOfferNotValidYet || isBuyerInsufficientFunds || - (offer.condition && !isConditionMet); + (offer.condition && !isConditionMet) || + isPriceDiscoveryOffer; const onCommitPendingSignature = () => { setIsLoading(true); showModal("WAITING_FOR_CONFIRMATION", undefined, "auto", undefined, { diff --git a/src/components/modal/ModalComponents.tsx b/src/components/modal/ModalComponents.tsx index 674988533..503aec304 100644 --- a/src/components/modal/ModalComponents.tsx +++ b/src/components/modal/ModalComponents.tsx @@ -22,6 +22,7 @@ import { IframeModal } from "./components/IframeModal/IframeModal"; import { ImageEditorModal } from "./components/ImageEditorModal/ImageEditorModal"; import InvalidRoleModal from "./components/InvalidRoleModal"; import ManageFunds from "./components/ManageFunds"; +import { PremintVouchersModal } from "./components/PremintVouchersModal/PremintVouchersModal"; import ProductCreateSuccess from "./components/ProductCreateSuccess"; import CreateProfileModal from "./components/Profile/CreateProfileModal"; import EditProfileModal from "./components/Profile/EditProfileModal"; @@ -87,7 +88,8 @@ export const MODAL_TYPES = { PRICE_IMPACT: "PRICE_IMPACT", CONFIRM_SWAP: "CONFIRM_SWAP", BUYER_SELLER_AGREEMENT: "BUYER_SELLER_AGREEMENT", - REDEEMABLE_NFT_TERMS: "REDEEMABLE_NFT_TERMS" + REDEEMABLE_NFT_TERMS: "REDEEMABLE_NFT_TERMS", + PREMINT_VOUCHERS: "PREMINT_VOUCHERS" } as const; export const MODAL_COMPONENTS = { @@ -137,5 +139,6 @@ export const MODAL_COMPONENTS = { [MODAL_TYPES.TOKEN_SAFETY]: () =>
, [MODAL_TYPES.CURRENCY_SEARCH]: () =>
, [MODAL_TYPES.PRICE_IMPACT]: () =>
, - [MODAL_TYPES.CONFIRM_SWAP]: () =>
+ [MODAL_TYPES.CONFIRM_SWAP]: () =>
, + [MODAL_TYPES.PREMINT_VOUCHERS]: PremintVouchersModal } as const; diff --git a/src/components/modal/components/PremintVouchersModal/PremintVouchersForm.tsx b/src/components/modal/components/PremintVouchersModal/PremintVouchersForm.tsx new file mode 100644 index 000000000..24b875514 --- /dev/null +++ b/src/components/modal/components/PremintVouchersModal/PremintVouchersForm.tsx @@ -0,0 +1,98 @@ +import { useFormikContext } from "formik"; +import React, { ReactNode, useEffect } from "react"; + +import { FormField, Input, Select } from "../../../form"; +import { Grid } from "../../../ui/Grid"; +import { FormType } from "./form"; + +type PremintVouchersFormProps = { + isRangeReserved: boolean; + availableQuantity: number; + alreadyMinted?: number; + children: ReactNode; + onValidityChanged?: (isValid: boolean) => void; +}; + +export const PremintVouchersForm: React.FC = ({ + isRangeReserved, + alreadyMinted = 0, + availableQuantity, + children, + onValidityChanged +}) => { + const { values, isValid, errors } = useFormikContext(); + useEffect(() => { + if (onValidityChanged) { + if (!isValid) { + console.log("Form errors: ", errors); + } + onValidityChanged(isValid); + } + }, [isValid, onValidityChanged, errors]); + return ( + + + 0 ? "1" : "0"} + step="1" + max={`${availableQuantity}`} + /> + + )} + + + {isRangeReserved && ( +
+ 0 ? "1" : "0"} + step="1" + max={values.rangeLength - alreadyMinted} + /> +
+ )} +
+ {children} +
+ ); +}; diff --git a/src/components/modal/components/PremintVouchersModal/PremintVouchersModal.tsx b/src/components/modal/components/PremintVouchersModal/PremintVouchersModal.tsx new file mode 100644 index 000000000..b1c929ecb --- /dev/null +++ b/src/components/modal/components/PremintVouchersModal/PremintVouchersModal.tsx @@ -0,0 +1,200 @@ +import { + PreMintButton, + Provider, + ReserveRangeButton, + Typography +} from "@bosonprotocol/react-kit"; +import { useConfigContext } from "components/config/ConfigContext"; +import { Form, Formik } from "formik"; +import { colors } from "lib/styles/colors"; +import { Offer } from "lib/types/offer"; +import { useSigner } from "lib/utils/hooks/connection/connection"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; + +import SimpleError from "../../../error/SimpleError"; +import { useModal } from "../../useModal"; +import { FormType, validationSchemas } from "./form"; +import { PremintVouchersForm } from "./PremintVouchersForm"; + +interface PremintVouchersModalProps { + offer?: Offer; + offerId?: string; + refetch: () => void; +} + +const PremintButtonWrapper = styled.div` + button { + background: transparent; + border-color: ${colors.orange}; + color: ${colors.orange}; + &:hover { + background: ${colors.orange}; + border-color: ${colors.orange}; + color: ${colors.white}; + } + } +`; + +const ReserveRangeButtonWrapper = styled.div` + button { + background: transparent; + border-color: ${colors.orange}; + color: ${colors.orange}; + &:hover { + background: ${colors.orange}; + border-color: ${colors.orange}; + color: ${colors.white}; + } + } +`; + +export const PremintVouchersModal: React.FC = ({ + offerId, + offer, + refetch +}) => { + const [hasError, setError] = useState(false); + const [isLoading, setLoading] = useState(false); + const { hideModal } = useModal(); + const signer = useSigner(); + const { config } = useConfigContext(); + + // If the offer doesnt have reserved range, the seller shall specify the quantity to be reserved, then the ReserveRange button shall be clicked + // When the offer has already a reserved range, we prefill the form's rangeQuantity it can't be changed + // The number of vouchers to be preminted is specified in the premintQuantity field and can't exceed the reserved range length + the number of already minted vouchers + + const [rangeLength, setRangeLength] = useState(0); + const [minted, setMinted] = useState(0); + const rangeToContract = + offer?.range && + offer.collection && + offer.range.owner === offer.collection.collectionContract.address; + const initialValues = { + to: rangeToContract + ? { value: "contract", label: "voucher contract", disabled: false } + : { value: "seller", label: "seller wallet", disabled: false }, + rangeLength: rangeLength || Number(offer?.quantityAvailable || "1"), + premintQuantity: rangeLength - minted + }; + const [values, setValues] = useState(initialValues); + useEffect(() => { + if (offer?.range) { + setRangeLength(Number(offer.range.end) - Number(offer.range.start) + 1); + setMinted(Number(offer.range.minted)); + } + }, [offer?.range]); + + return ( +
+ What are preminted vouchers? + + The seller can premint one or more vouchers for an offer. A preminted + voucher is an NFT that can be traded on an NFT marketplace. When a buyer + acquires the NFT, the resulting NFT transfer is causing the offer to be + committed to on behalf of the buyer. + +

+ + initialValues={initialValues} + onSubmit={async (_values) => { + console.log("Submitting form with values: ", _values); + // setValues(_values); + }} + validate={async (_values) => { + console.log("Validating form with values: ", _values); + setValues(_values); + }} + validateOnMount + validationSchema={ + rangeLength > 0 + ? validationSchemas.premintVouchers + : validationSchemas.reserveRange + } + enableReinitialize + validateOnChange + > + {() => { + return ( +
+ 0} + alreadyMinted={minted} + availableQuantity={Number(offer?.quantityAvailable || "0")} + onValidityChanged={(isValid) => { + setError(!isValid); + }} + > + {hasError && } + {rangeLength === 0 ? ( + + { + setLoading(true); + }} + onPendingTransaction={() => { + setLoading(true); + }} + onSuccess={async (_, { length }) => { + setLoading(false); + refetch(); + setRangeLength(length); + }} + onError={(error) => { + setLoading(false); + console.error(error); + setError(true); + }} + /> + + ) : ( + + { + setLoading(true); + }} + onPendingTransaction={() => { + setLoading(true); + }} + onSuccess={async () => { + setLoading(false); + refetch(); + hideModal(); + }} + onError={() => { + setLoading(false); + setError(true); + }} + /> + + )} + +
+ ); + }} + +
+ ); +}; diff --git a/src/components/modal/components/PremintVouchersModal/form.ts b/src/components/modal/components/PremintVouchersModal/form.ts new file mode 100644 index 000000000..f740bc004 --- /dev/null +++ b/src/components/modal/components/PremintVouchersModal/form.ts @@ -0,0 +1,26 @@ +import * as Yup from "yup"; + +export const validationSchemas = { + reserveRange: Yup.object({ + to: Yup.object({ + label: Yup.string().required(), + value: Yup.string().required(), + disabled: Yup.boolean() + }), + rangeLength: Yup.number() + .required() + .min(1, "Must be greater than 0") + .typeError("This is not a number") + }), + premintVouchers: Yup.object({ + premintQuantity: Yup.number() + .required() + .min(1, "Must be greater than 0") + .typeError("This is not a number") + }) +}; + +export type FormType = Yup.InferType< + typeof validationSchemas.premintVouchers & + typeof validationSchemas.reserveRange +>; diff --git a/src/components/product/TermsOfExchange.tsx b/src/components/product/TermsOfExchange.tsx index f97a5aa34..8e3afb06e 100644 --- a/src/components/product/TermsOfExchange.tsx +++ b/src/components/product/TermsOfExchange.tsx @@ -108,22 +108,37 @@ export default function TermsOfExchange() { ); const decimals = exchangeToken?.decimals; const step = 10 ** -(decimals || 0); + const optionsUnitCurrencyOnly = useMemo( + () => ({ + value: optionUnitKeys.fixed, + label: currency + }), + [currency] + ); const optionsUnitWithCurrency = useMemo( () => OPTIONS_UNIT.map((option) => { if (option.value === optionUnitKeys.fixed) { - return { - value: option.value, - label: currency - }; + return optionsUnitCurrencyOnly; } return option; }), - [currency] + [optionsUnitCurrencyOnly] ); + const isPriceDiscoveryOffer = + !isMultiVariant && !!values["coreTermsOfSale"].isPriceDiscoveryOffer; const optionsUnitToShow = useMemo(() => { - return isMultiVariant ? PERCENT_OPTIONS_UNIT : optionsUnitWithCurrency; - }, [isMultiVariant, optionsUnitWithCurrency]); + return isMultiVariant + ? PERCENT_OPTIONS_UNIT + : isPriceDiscoveryOffer + ? [optionsUnitCurrencyOnly] + : optionsUnitWithCurrency; + }, [ + isMultiVariant, + optionsUnitWithCurrency, + optionsUnitCurrencyOnly, + isPriceDiscoveryOffer + ]); useEffect(() => { const buyerUnit = ( optionsUnitToShow as { value: string; label: string }[] diff --git a/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx b/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx index 51af97f5f..07f2774fd 100644 --- a/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx +++ b/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx @@ -1,4 +1,8 @@ import { useConfigContext } from "components/config/ConfigContext"; +import { SwitchForm } from "components/form/Switch"; +import { Typography } from "components/ui/Typography"; +import { colors } from "lib/styles/colors"; +import { useEffect } from "react"; import styled from "styled-components"; import { useForm } from "../../../lib/utils/hooks/useForm"; @@ -23,6 +27,10 @@ const ProductInformationButtonGroup = styled(ProductButtonGroup)` margin-top: 1.563px; `; +const gridProps = { + justifyContent: "flex-end" +} as const; + interface Props { isMultiVariant: boolean; } @@ -31,10 +39,18 @@ export default function CoreTermsOfSale({ isMultiVariant }: Props) { const { config } = useConfigContext(); const OPTIONS_CURRENCIES = getOptionsCurrencies(config.envConfig); - const { nextIsDisabled } = useForm(); + const { values, setFieldValue, nextIsDisabled } = useForm(); const prefix = isMultiVariant ? "variantsCoreTermsOfSale" : "coreTermsOfSale"; + const isPriceDiscoveryOffer = + !isMultiVariant && !!values["coreTermsOfSale"].isPriceDiscoveryOffer; + useEffect(() => { + if (isPriceDiscoveryOffer) { + setFieldValue(`${prefix}.price`, 0); + } + }, [isPriceDiscoveryOffer, setFieldValue, prefix]); + return ( Core Terms of Sale @@ -43,18 +59,40 @@ export default function CoreTermsOfSale({ isMultiVariant }: Props) { ( + + use a price discovery mechanism + + )} + /> + } > -
- -
+ {!isPriceDiscoveryOffer && ( +
+ +
+ )}