diff --git a/package-lock.json b/package-lock.json index 0a2ab676f..db9c25b8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@apollo/client": "^3.8.1", "@bosonprotocol/chat-sdk": "^1.3.1-alpha.9", - "@bosonprotocol/react-kit": "^0.22.0-alpha.2", + "@bosonprotocol/react-kit": "^0.22.0-alpha.3", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.6.1", @@ -2317,9 +2317,9 @@ } }, "node_modules/@bosonprotocol/core-sdk": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.32.0.tgz", - "integrity": "sha512-1P6siYsiyyjFXvb4NAcg5Io8RPDmoGpvpvlANsE9u60HbIp54QEAxYJF3CjaVHKHWZCMY2jbao715qinxgm1rA==", + "version": "1.33.0-alpha.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.33.0-alpha.0.tgz", + "integrity": "sha512-bvQMk2qZR+CseC5dHNjuk/GFEZseV3PhuNK391sD7TYvx17qtd5STLl76f8yG+WbrATwqP0+Ol+nafVGdHGFOw==", "dependencies": { "@bosonprotocol/common": "^1.23.2", "@ethersproject/abi": "^5.5.0", @@ -2367,12 +2367,12 @@ } }, "node_modules/@bosonprotocol/react-kit": { - "version": "0.22.0-alpha.2", - "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.22.0-alpha.2.tgz", - "integrity": "sha512-PuxJyP8CUQuGt1MxEuNMdwF+dRGAVP2i+dWl6i0Xf6p4XNlyBvOU8RZrrRM/FfJPWbuurbk2F+imFc+U7FiO+w==", + "version": "0.22.0-alpha.3", + "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.22.0-alpha.3.tgz", + "integrity": "sha512-3XIUzycn9DmxsC6/ujaSvT1xqF0HPEO/JvGdxkYh3AJ1irP7BAFDbNSmQVAS2OlYbB8u89Yjxh2yzwsscF0W2A==", "dependencies": { "@bosonprotocol/chat-sdk": "^1.3.1-alpha.9", - "@bosonprotocol/core-sdk": "^1.32.0", + "@bosonprotocol/core-sdk": "^1.33.0-alpha.0", "@bosonprotocol/ethers-sdk": "^1.12.9", "@bosonprotocol/ipfs-storage": "^1.10.10", "@davatar/react": "1.11.1", @@ -47377,9 +47377,9 @@ } }, "@bosonprotocol/core-sdk": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.32.0.tgz", - "integrity": "sha512-1P6siYsiyyjFXvb4NAcg5Io8RPDmoGpvpvlANsE9u60HbIp54QEAxYJF3CjaVHKHWZCMY2jbao715qinxgm1rA==", + "version": "1.33.0-alpha.0", + "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.33.0-alpha.0.tgz", + "integrity": "sha512-bvQMk2qZR+CseC5dHNjuk/GFEZseV3PhuNK391sD7TYvx17qtd5STLl76f8yG+WbrATwqP0+Ol+nafVGdHGFOw==", "requires": { "@bosonprotocol/common": "^1.23.2", "@ethersproject/abi": "^5.5.0", @@ -47424,12 +47424,12 @@ } }, "@bosonprotocol/react-kit": { - "version": "0.22.0-alpha.2", - "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.22.0-alpha.2.tgz", - "integrity": "sha512-PuxJyP8CUQuGt1MxEuNMdwF+dRGAVP2i+dWl6i0Xf6p4XNlyBvOU8RZrrRM/FfJPWbuurbk2F+imFc+U7FiO+w==", + "version": "0.22.0-alpha.3", + "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.22.0-alpha.3.tgz", + "integrity": "sha512-3XIUzycn9DmxsC6/ujaSvT1xqF0HPEO/JvGdxkYh3AJ1irP7BAFDbNSmQVAS2OlYbB8u89Yjxh2yzwsscF0W2A==", "requires": { "@bosonprotocol/chat-sdk": "^1.3.1-alpha.9", - "@bosonprotocol/core-sdk": "^1.32.0", + "@bosonprotocol/core-sdk": "^1.33.0-alpha.0", "@bosonprotocol/ethers-sdk": "^1.12.9", "@bosonprotocol/ipfs-storage": "^1.10.10", "@davatar/react": "1.11.1", diff --git a/package.json b/package.json index dccfb4b75..5cae56ed3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dependencies": { "@apollo/client": "^3.8.1", "@bosonprotocol/chat-sdk": "^1.3.1-alpha.9", - "@bosonprotocol/react-kit": "^0.22.0-alpha.2", + "@bosonprotocol/react-kit": "^0.22.0-alpha.3", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.6.1", diff --git a/src/components/detail/DetailWidget/DetailWidget.tsx b/src/components/detail/DetailWidget/DetailWidget.tsx index 1eb8b3ac7..a6d5eb6c0 100644 --- a/src/components/detail/DetailWidget/DetailWidget.tsx +++ b/src/components/detail/DetailWidget/DetailWidget.tsx @@ -642,7 +642,7 @@ const DetailWidget: React.FC = ({ const offerCurationList = useCustomStoreQueryParameter("offerCurationList"); const { isConditionMet } = useCheckTokenGatedOffer({ commitProxyAddress, - condition: offer.condition + offer }); const numSellers = new Set( sellerCurationList diff --git a/src/components/detail/DetailWidget/TokenGated.tsx b/src/components/detail/DetailWidget/TokenGated.tsx index 31f70e17c..e09e8c63c 100644 --- a/src/components/detail/DetailWidget/TokenGated.tsx +++ b/src/components/detail/DetailWidget/TokenGated.tsx @@ -1,4 +1,4 @@ -import { EvaluationMethod, TokenType } from "@bosonprotocol/common"; +import { EvaluationMethod, GatingType, TokenType } from "@bosonprotocol/common"; import { useConfigContext } from "components/config/ConfigContext"; import { Check, X } from "phosphor-react"; import { CSSProperties, useEffect, useMemo, useState } from "react"; @@ -55,22 +55,30 @@ const getBuildMessage = const { method, tokenType, - minTokenId: tokenId, + minTokenId, + maxTokenId, + gatingType, tokenAddress, threshold } = condition; + const perWalletOrPerToken = + gatingType === GatingType.PerAddress ? " (per wallet)" : " (per token)"; + + const TokenLink = ( + + {tokenAddress.slice(0, 10)}... + + ); + if (tokenType === TokenType.FungibleToken) { return ( <> - {tokenInfo.convertedValue.price} {tokenInfo.symbol} tokens ( - - {tokenAddress.slice(0, 10)}... - - ) + {tokenInfo.convertedValue.price} {tokenInfo.symbol} tokens + {TokenLink} ); } @@ -78,40 +86,39 @@ const getBuildMessage = if (method === EvaluationMethod.Threshold) { return ( <> - {threshold} tokens from{" "} - - {tokenAddress.slice(0, 10)}... - + {threshold} tokens {perWalletOrPerToken} from {TokenLink} ); } - if (method === EvaluationMethod.SpecificToken) { + if (method === EvaluationMethod.TokenRange) { + if (minTokenId === maxTokenId) { + return ( + <> + Token ID {perWalletOrPerToken}: {minTokenId} from {TokenLink} + + ); + } return ( <> - Token ID: {tokenId} from{" "} - - {tokenAddress.slice(0, 15)}... - + From token ID {minTokenId} to token ID {maxTokenId}{" "} + {perWalletOrPerToken} from {TokenLink} ); } } if (tokenType === TokenType.MultiToken) { + if (minTokenId === maxTokenId) { + return ( + <> + {threshold} x token(s) with id: {minTokenId} {perWalletOrPerToken}{" "} + from {TokenLink} + + ); + } return ( <> - {threshold} x token(s) with id: {tokenId} from{" "} - - {tokenAddress.slice(0, 10)}... - + {threshold} x token(s) from token ID {minTokenId} to token ID{" "} + {maxTokenId} {perWalletOrPerToken} from {TokenLink} ); } diff --git a/src/components/product/tokenGating/TokenGating.tsx b/src/components/product/tokenGating/TokenGating.tsx index 8d7267b8b..13a7ef3df 100644 --- a/src/components/product/tokenGating/TokenGating.tsx +++ b/src/components/product/tokenGating/TokenGating.tsx @@ -8,7 +8,13 @@ import { FormField, Input, Select } from "../../form"; import BosonButton from "../../ui/BosonButton"; import Grid from "../../ui/Grid"; import { ProductButtonGroup, SectionTitle } from "../Product.styles"; -import { TOKEN_CRITERIA, TOKEN_TYPES } from "../utils"; +import { + TOKEN_CRITERIA, + TOKEN_GATING_PER_OPTIONS, + TOKEN_TYPES, + TokenCriteriaTokenRange, + TokenGatingPerToken +} from "../utils"; const ContainerProductPage = styled.div` width: 100%; @@ -52,9 +58,9 @@ const prefix = "tokenGating."; const [{ value: minBalance }] = TOKEN_CRITERIA; const [{ value: erc20 }, { value: erc721 }, { value: erc1155 }] = TOKEN_TYPES; - +// logic extracted from https://miro.com/app/board/uXjVMDNFjuw=/ export default function TokenGating() { - const { nextIsDisabled, values, handleBlur } = useForm(); + const { nextIsDisabled, values, handleBlur, setFieldValue } = useForm(); const { tokenGating } = values; const core = useCoreSDK(); const [symbol, setSymbol] = useState(undefined); @@ -83,6 +89,18 @@ export default function TokenGating() { } })(); }, [core, tokenGating.tokenContract, tokenGating.tokenType]); + + useEffect(() => { + if ( + tokenGating.tokenType?.value === erc721 && + tokenGating.gatingType?.value === TokenGatingPerToken.value + ) { + setFieldValue(`${prefix}tokenCriteria`, TokenCriteriaTokenRange); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tokenGating.gatingType?.value, setFieldValue]); + const walletOrToken = + tokenGating.gatingType?.value === "wallet" ? "wallet" : "token"; return ( Token Gating @@ -119,81 +137,151 @@ export default function TokenGating() { <> - {tokenGating.tokenType?.value === erc721 && ( - - + - - - {tokenGating.tokenCriteria?.value === minBalance ? ( - ) : ( + {symbol && ( + + + + )} + + - + - )} - + + )} - {tokenGating.tokenType?.value === erc20 && ( - - + - - - {symbol && ( - - )} - + + + + + {tokenGating.tokenCriteria?.value === minBalance ? ( + + + + ) : ( + + + + + + + + + )} + + + + + + + )} - {tokenGating.tokenType?.value === erc1155 ? ( + {tokenGating.tokenType?.value === erc1155 && ( <> - + - + + + + - + + + + @@ -213,17 +312,6 @@ export default function TokenGating() { - ) : ( - - - - - )} {/* diff --git a/src/components/product/utils/const.ts b/src/components/product/utils/const.ts index 02edab92d..5a1b17750 100644 --- a/src/components/product/utils/const.ts +++ b/src/components/product/utils/const.ts @@ -1,4 +1,4 @@ -import { EvaluationMethod, TokenType } from "@bosonprotocol/common"; +import { EvaluationMethod, GatingType, TokenType } from "@bosonprotocol/common"; import { ProtocolConfig } from "@bosonprotocol/react-kit"; import countries from "lib/constants/countries.json"; import { onlyFairExchangePolicyLabel } from "lib/constants/policies"; @@ -117,17 +117,34 @@ export const TokenTypeEnumToString = { [TokenType.MultiToken]: "erc1155" } as const; +export const TokenCriteriaMinBalance = { + value: "minbalance", + label: "Collection balance", + method: EvaluationMethod.Threshold +} as const; +export const TokenCriteriaTokenRange = { + value: "tokenrange", + label: "Token range", + method: EvaluationMethod.SpecificToken +} as const; export const TOKEN_CRITERIA = [ - { - value: "minbalance", - label: "Collection balance", - method: EvaluationMethod.Threshold - }, - { - value: "tokenid", - label: "Specific token", - method: EvaluationMethod.SpecificToken - } + TokenCriteriaMinBalance, + TokenCriteriaTokenRange +] as const; + +export const TokenGatingPerWallet = { + value: "wallet", + label: "Per wallet", + gatingType: GatingType.PerAddress +} as const; +export const TokenGatingPerToken = { + value: "token", + label: "Per token", + gatingType: GatingType.PerTokenId +} as const; +export const TOKEN_GATING_PER_OPTIONS = [ + TokenGatingPerWallet, + TokenGatingPerToken ] as const; export const OPTIONS_EXCHANGE_POLICY = [ diff --git a/src/components/product/utils/productHelpOptions.tsx b/src/components/product/utils/productHelpOptions.tsx index 66ca86f3d..00fb2edb5 100644 --- a/src/components/product/utils/productHelpOptions.tsx +++ b/src/components/product/utils/productHelpOptions.tsx @@ -143,7 +143,7 @@ export const tokenGatingHelp = [
  • ERC721
    • - One ca select whether or not they want to target a specific token + One can select whether or not they want to target a specific token from a collection or any token from a collection
    • diff --git a/src/components/product/utils/validationSchema.ts b/src/components/product/utils/validationSchema.ts index b8c435b3d..16b13cfb6 100644 --- a/src/components/product/utils/validationSchema.ts +++ b/src/components/product/utils/validationSchema.ts @@ -1,3 +1,4 @@ +import { GatingType } from "@bosonprotocol/common"; import { parseUnits } from "@ethersproject/units"; import { maxLengthErrorMessage, @@ -20,6 +21,7 @@ import { OPTIONS_PERIOD, OPTIONS_UNIT, TOKEN_CRITERIA, + TOKEN_GATING_PER_OPTIONS, TOKEN_TYPES } from "./const"; import { @@ -212,7 +214,9 @@ export const tokenGatingValidationSchema = Yup.object({ .required(validationMessage.required) .matches(/^\+?[1-9]\d*$/, "Value must greater than 0"), tokenType: Yup.object({ - value: Yup.string(), + value: Yup.mixed().oneOf( + TOKEN_TYPES.map((type) => type.value) + ), label: Yup.string() }) .required(validationMessage.required) @@ -236,7 +240,7 @@ export const tokenGatingValidationSchema = Yup.object({ ) .typeError("Value must be an integer greater than or equal to 1") }), - tokenId: Yup.string().when(["tokenType", "tokenCriteria"], { + minTokenId: Yup.string().when(["tokenType", "tokenCriteria"], { is: (tokenType: SelectDataProps, tokenCriteria: SelectDataProps) => tokenType?.value === TOKEN_TYPES[2].value || (tokenType?.value === TOKEN_TYPES[1].value && @@ -258,8 +262,37 @@ export const tokenGatingValidationSchema = Yup.object({ ) .typeError("Value must be an integer greater than or equal to 0") .required(validationMessage.required) - }) - + }), + maxTokenId: Yup.string().when(["tokenType", "tokenCriteria"], { + is: (tokenType: SelectDataProps, tokenCriteria: SelectDataProps) => + tokenType?.value === TOKEN_TYPES[2].value || + (tokenType?.value === TOKEN_TYPES[1].value && + tokenCriteria?.value === TOKEN_CRITERIA[1].value), + then: (schema) => + schema + .test( + "", + "Value must greater than or equal to 0 or a hex value up to 64 chars", + (value) => { + if (!value) { + return false; + } + return ( + /0[xX][0-9a-fA-F]{1,64}$/.test(value) || + /^(0|\+?[1-9]\d*)$/.test(value) + ); + } + ) + .typeError("Value must be an integer greater than or equal to 0") + .required(validationMessage.required) + }), + gatingType: Yup.object({ + value: Yup.mixed< + typeof TOKEN_GATING_PER_OPTIONS[number]["value"] + >().oneOf(TOKEN_GATING_PER_OPTIONS.map((type) => type.value)), + label: Yup.string(), + gatingType: Yup.number().oneOf(Object.values(GatingType).map(Number)) + }).required(validationMessage.required) // tokenGatingDesc: Yup.string().required(validationMessage.required) }) }); diff --git a/src/lib/utils/hooks/offer/useCheckTokenGatedOffer.ts b/src/lib/utils/hooks/offer/useCheckTokenGatedOffer.ts index 78adf798b..8f6f55a9b 100644 --- a/src/lib/utils/hooks/offer/useCheckTokenGatedOffer.ts +++ b/src/lib/utils/hooks/offer/useCheckTokenGatedOffer.ts @@ -9,13 +9,14 @@ import { useAccount, useSigner } from "../connection/connection"; interface Props { commitProxyAddress?: string | undefined; - condition?: Offer["condition"] | undefined; + offer: Offer; } export default function useCheckTokenGatedOffer({ commitProxyAddress, - condition + offer }: Props) { + const { id, condition } = offer; const signer = useSigner(); const { account: address } = useAccount(); @@ -58,7 +59,7 @@ export default function useCheckTokenGatedOffer({ } try { - const met = await core.checkTokenGatedCondition(condition, address); + const met = await core.checkTokenGatedCondition(id, address); setConditionMet(met); } catch (error) { console.error(error); diff --git a/src/lib/utils/hooks/offer/useCreateOffers.tsx b/src/lib/utils/hooks/offer/useCreateOffers.tsx index d1bac8576..b78173f0b 100644 --- a/src/lib/utils/hooks/offer/useCreateOffers.tsx +++ b/src/lib/utils/hooks/offer/useCreateOffers.tsx @@ -8,7 +8,7 @@ import { TOKEN_TYPES } from "../../../../components/product/utils"; import LoadingToast from "../../../../components/toasts/common/LoadingToast"; import { buildCondition, - PartialTokenGating + FullTokenGating } from "../../../../pages/create-product/utils/buildCondition"; import { useCoreSDK } from "../../useCoreSdk"; import { useAccount } from "../connection/connection"; @@ -28,7 +28,7 @@ type UseCreateOffersProps = { sellerToCreate: accounts.CreateSellerArgs | null; offersToCreate: offers.CreateOfferArgs[]; isMultiVariant: boolean; - tokenGatedInfo?: PartialTokenGating | null; + tokenGatedInfo?: FullTokenGating | null; conditionDecimals?: number; onGetExchangeTokenDecimals?: (decimals: number | undefined) => unknown; onCreatedOffersWithVariants?: (arg0: { diff --git a/src/pages/create-product/CreateProduct.tsx b/src/pages/create-product/CreateProduct.tsx index ebf675851..dff3a5329 100644 --- a/src/pages/create-product/CreateProduct.tsx +++ b/src/pages/create-product/CreateProduct.tsx @@ -36,7 +36,7 @@ export default function CreateProductWrapper() { FallbackComponent={() => (

      - Something when wrong, please refresh the page to try again or go + Something went wrong, please refresh the page to try again or go back to the home page

      diff --git a/src/pages/create-product/utils/buildCondition.ts b/src/pages/create-product/utils/buildCondition.ts index 0f521adbc..44a7d943b 100644 --- a/src/pages/create-product/utils/buildCondition.ts +++ b/src/pages/create-product/utils/buildCondition.ts @@ -1,69 +1,75 @@ import { ConditionStruct, EvaluationMethod, + GatingType, TokenType } from "@bosonprotocol/common"; import { utils } from "ethers"; -import { TokenGating } from "../../../components/product/utils"; +import { + TokenCriteriaTokenRange, + TokenGating +} from "../../../components/product/utils"; -export type PartialTokenGating = Pick< - TokenGating["tokenGating"], - | "tokenId" - | "minBalance" - | "tokenType" - | "tokenCriteria" - | "tokenContract" - | "maxCommits" ->; +export type FullTokenGating = TokenGating["tokenGating"]; export const buildCondition = ( - partialTokenGating: PartialTokenGating, + tokenGating: FullTokenGating, decimals?: number ): ConditionStruct => { let tokenType: TokenType = TokenType.FungibleToken; let method: EvaluationMethod = EvaluationMethod.None; let threshold; - let tokenId = partialTokenGating.tokenId || "0"; + let gatingType; + let minTokenId; + let maxTokenId; let formatedValue = null; - if (decimals && partialTokenGating.minBalance) { - formatedValue = utils.parseUnits(partialTokenGating.minBalance, decimals); + if (decimals && tokenGating.minBalance) { + formatedValue = utils.parseUnits(tokenGating.minBalance, decimals); } - switch (partialTokenGating.tokenType.value) { + switch (tokenGating.tokenType.value) { case "erc1155": tokenType = TokenType.MultiToken; method = EvaluationMethod.Threshold; - threshold = partialTokenGating.minBalance; + threshold = tokenGating.minBalance; + minTokenId = tokenGating.minTokenId; + maxTokenId = tokenGating.maxTokenId; + gatingType = tokenGating.gatingType.gatingType; break; case "erc721": tokenType = TokenType.NonFungibleToken; - if (partialTokenGating.tokenCriteria.value === "tokenid") { - method = EvaluationMethod.SpecificToken; - // if erc721 and SpecificToken we should set the threshold as zero + gatingType = tokenGating.gatingType.gatingType; + if (tokenGating.tokenCriteria.value === TokenCriteriaTokenRange.value) { + method = EvaluationMethod.TokenRange; + // if erc721 and TokenRange we should set the threshold as zero threshold = "0"; + minTokenId = tokenGating.minTokenId; + maxTokenId = tokenGating.maxTokenId; } else { method = EvaluationMethod.Threshold; - threshold = partialTokenGating.minBalance; - tokenId = "0"; + threshold = tokenGating.minBalance; + minTokenId = "0"; + maxTokenId = "0"; } break; default: tokenType = TokenType.FungibleToken; method = EvaluationMethod.Threshold; - threshold = formatedValue || partialTokenGating.minBalance; - tokenId = "0"; + threshold = formatedValue || tokenGating.minBalance; + minTokenId = "0"; + maxTokenId = "0"; break; } return { method, tokenType, - tokenAddress: partialTokenGating.tokenContract || "", - gatingType: 0, // default: PerAddress (legacy) - minTokenId: tokenId, - maxTokenId: tokenId, + tokenAddress: tokenGating.tokenContract || "", + gatingType: gatingType ?? GatingType.PerAddress, + minTokenId: minTokenId ?? "0", + maxTokenId: maxTokenId ?? "0", threshold: threshold || "", - maxCommits: partialTokenGating.maxCommits || "" + maxCommits: tokenGating.maxCommits || "" }; };