diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4cded42ee..228322209 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -130,7 +130,7 @@ jobs: ENV_NAME: "production" UPDATE_COVERAGE: false REACT_APP_ENABLE_CURATION_LISTS: true - REACT_APP_SELLER_CURATION_LIST: "2,4,5,6,7,8,9,11,12,13,14,16,27,129,171,172,180,181,182,183,184,190,191" + REACT_APP_SELLER_CURATION_LIST: "2,4,5,6,7,8,9,11,12,13,14,16,27,129,171,172,180,181,182,183,184,190,191,195,196,197,202,207" REACT_APP_CREATE_PROFILE_CONFIGURATION: "LENS" REACT_APP_IPFS_GATEWAY: "https://bosonprotocol.infura-ipfs.io/ipfs" REACT_APP_IPFS_IMAGE_GATEWAY: "https://gray-permanent-fly-490.mypinata.cloud/ipfs" diff --git a/package-lock.json b/package-lock.json index 0bad20894..31a706d00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@bosonprotocol/chat-sdk": "^1.3.0", - "@bosonprotocol/react-kit": "^0.16.2-alpha.16", + "@bosonprotocol/react-kit": "0.16.2-alpha.16", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.6.1", @@ -1929,7 +1929,6 @@ "node_modules/@bosonprotocol/common": { "version": "1.21.0-alpha.11", "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.21.0-alpha.11.tgz", - "integrity": "sha512-1HxDlNf4bYMN21n919JaiQn3111E7XCBisriB2jOi7lXurk5rf8alt4WMAlIihLKdFh3Dt5frjnIR0JdR6upTg==", "dependencies": { "@bosonprotocol/metadata": "^1.9.0-alpha.4", "@ethersproject/abi": "^5.5.0", @@ -1943,7 +1942,6 @@ "node_modules/@bosonprotocol/core-sdk": { "version": "1.25.0-alpha.11", "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.25.0-alpha.11.tgz", - "integrity": "sha512-r/ytCCWRFAXA7J0XuYqY+TdCSFxV96Hg8jU1CDgr3UJotc3GpESGi3hiNbM+/1UKe8pT0a3hNFEPjkZGjyJ8Dg==", "dependencies": { "@bosonprotocol/common": "^1.21.0-alpha.11", "@ethersproject/abi": "^5.5.0", @@ -1962,7 +1960,6 @@ "node_modules/@bosonprotocol/ethers-sdk": { "version": "1.11.1-alpha.16", "resolved": "https://registry.npmjs.org/@bosonprotocol/ethers-sdk/-/ethers-sdk-1.11.1-alpha.16.tgz", - "integrity": "sha512-Ca/7jsTCgCM0V/Wwt41dDdaohcOpBAEb+NmQ2bpcAgsPwuKaoTl5jM1ojJxD4+kZi7JaPxeCVrbx0uvHbEtYqw==", "dependencies": { "@bosonprotocol/common": "^1.21.0-alpha.11" }, @@ -1973,7 +1970,6 @@ "node_modules/@bosonprotocol/ipfs-storage": { "version": "1.10.4-alpha.4", "resolved": "https://registry.npmjs.org/@bosonprotocol/ipfs-storage/-/ipfs-storage-1.10.4-alpha.4.tgz", - "integrity": "sha512-dryU75iE2PsO+Hw/xSAF8GOt64MhfrWjD6ndzblzvQMYTe1DYjbecg5+IIQdkxMi1YuViwepNthWhbpgzd51DQ==", "dependencies": { "@bosonprotocol/metadata": "^1.9.0-alpha.4", "ipfs-http-client": "^56.0.1", @@ -1984,7 +1980,6 @@ "node_modules/@bosonprotocol/metadata": { "version": "1.9.0-alpha.4", "resolved": "https://registry.npmjs.org/@bosonprotocol/metadata/-/metadata-1.9.0-alpha.4.tgz", - "integrity": "sha512-jQuWhZfxop+VxR2mbyl71h5YppOFLTfMHPRUWFkbuAjlpt7ZJydpMi5Qo6jqkBDdSYluOdnpAZg98E39mtquOw==", "dependencies": { "schema-to-yup": "^1.11.11" } @@ -1992,7 +1987,6 @@ "node_modules/@bosonprotocol/react-kit": { "version": "0.16.2-alpha.16", "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.16.2-alpha.16.tgz", - "integrity": "sha512-tDhql5gjLfAcYl9naVhNuD7Jr78NAXpPVB0pymuhXlRZrtdPmIiXUjn59pBAw8s827ykbIsIK6isZvYhzVnmMQ==", "dependencies": { "@bosonprotocol/core-sdk": "^1.25.0-alpha.11", "@bosonprotocol/ethers-sdk": "^1.11.1-alpha.16", @@ -9050,9 +9044,9 @@ } }, "node_modules/cborg": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.0.tgz", - "integrity": "sha512-/eM0JCaL99HDHxjySNQJLaolZFVdl6VA0/hEKIoiQPcQzE5LrG5QHdml0HaBt31brgB9dNe1zMr3f8IVrpotRQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.1.tgz", + "integrity": "sha512-et6Qm8MOUY2kCWa5GKk2MlBVoPjHv0hQBmlzI/Z7+5V3VJCeIkGehIB3vWknNsm2kOkAIs6wEKJFJo8luWQQ/w==", "bin": { "cborg": "cli.js" } @@ -22755,9 +22749,9 @@ } }, "node_modules/parse-duration": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.2.tgz", - "integrity": "sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.3.tgz", + "integrity": "sha512-o6NAh12na5VvR6nFejkU0gpQ8jmOY9Y9sTU2ke3L3G/d/3z8jqmbBbeyBGHU73P4JLXfc7tJARygIK3WGIkloA==" }, "node_modules/parse-filepath": { "version": "1.0.2", @@ -30206,7 +30200,6 @@ "@bosonprotocol/common": { "version": "1.21.0-alpha.11", "resolved": "https://registry.npmjs.org/@bosonprotocol/common/-/common-1.21.0-alpha.11.tgz", - "integrity": "sha512-1HxDlNf4bYMN21n919JaiQn3111E7XCBisriB2jOi7lXurk5rf8alt4WMAlIihLKdFh3Dt5frjnIR0JdR6upTg==", "requires": { "@bosonprotocol/metadata": "^1.9.0-alpha.4", "@ethersproject/abi": "^5.5.0", @@ -30220,7 +30213,6 @@ "@bosonprotocol/core-sdk": { "version": "1.25.0-alpha.11", "resolved": "https://registry.npmjs.org/@bosonprotocol/core-sdk/-/core-sdk-1.25.0-alpha.11.tgz", - "integrity": "sha512-r/ytCCWRFAXA7J0XuYqY+TdCSFxV96Hg8jU1CDgr3UJotc3GpESGi3hiNbM+/1UKe8pT0a3hNFEPjkZGjyJ8Dg==", "requires": { "@bosonprotocol/common": "^1.21.0-alpha.11", "@ethersproject/abi": "^5.5.0", @@ -30239,7 +30231,6 @@ "@bosonprotocol/ethers-sdk": { "version": "1.11.1-alpha.16", "resolved": "https://registry.npmjs.org/@bosonprotocol/ethers-sdk/-/ethers-sdk-1.11.1-alpha.16.tgz", - "integrity": "sha512-Ca/7jsTCgCM0V/Wwt41dDdaohcOpBAEb+NmQ2bpcAgsPwuKaoTl5jM1ojJxD4+kZi7JaPxeCVrbx0uvHbEtYqw==", "requires": { "@bosonprotocol/common": "^1.21.0-alpha.11" } @@ -30247,7 +30238,6 @@ "@bosonprotocol/ipfs-storage": { "version": "1.10.4-alpha.4", "resolved": "https://registry.npmjs.org/@bosonprotocol/ipfs-storage/-/ipfs-storage-1.10.4-alpha.4.tgz", - "integrity": "sha512-dryU75iE2PsO+Hw/xSAF8GOt64MhfrWjD6ndzblzvQMYTe1DYjbecg5+IIQdkxMi1YuViwepNthWhbpgzd51DQ==", "requires": { "@bosonprotocol/metadata": "^1.9.0-alpha.4", "ipfs-http-client": "^56.0.1", @@ -30258,7 +30248,6 @@ "@bosonprotocol/metadata": { "version": "1.9.0-alpha.4", "resolved": "https://registry.npmjs.org/@bosonprotocol/metadata/-/metadata-1.9.0-alpha.4.tgz", - "integrity": "sha512-jQuWhZfxop+VxR2mbyl71h5YppOFLTfMHPRUWFkbuAjlpt7ZJydpMi5Qo6jqkBDdSYluOdnpAZg98E39mtquOw==", "requires": { "schema-to-yup": "^1.11.11" } @@ -30266,7 +30255,6 @@ "@bosonprotocol/react-kit": { "version": "0.16.2-alpha.16", "resolved": "https://registry.npmjs.org/@bosonprotocol/react-kit/-/react-kit-0.16.2-alpha.16.tgz", - "integrity": "sha512-tDhql5gjLfAcYl9naVhNuD7Jr78NAXpPVB0pymuhXlRZrtdPmIiXUjn59pBAw8s827ykbIsIK6isZvYhzVnmMQ==", "requires": { "@bosonprotocol/core-sdk": "^1.25.0-alpha.11", "@bosonprotocol/ethers-sdk": "^1.11.1-alpha.16", @@ -34925,9 +34913,9 @@ "version": "2.4.0" }, "cborg": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.0.tgz", - "integrity": "sha512-/eM0JCaL99HDHxjySNQJLaolZFVdl6VA0/hEKIoiQPcQzE5LrG5QHdml0HaBt31brgB9dNe1zMr3f8IVrpotRQ==" + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.1.tgz", + "integrity": "sha512-et6Qm8MOUY2kCWa5GKk2MlBVoPjHv0hQBmlzI/Z7+5V3VJCeIkGehIB3vWknNsm2kOkAIs6wEKJFJo8luWQQ/w==" }, "ccount": { "version": "2.0.1" @@ -43625,9 +43613,9 @@ } }, "parse-duration": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.2.tgz", - "integrity": "sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.3.tgz", + "integrity": "sha512-o6NAh12na5VvR6nFejkU0gpQ8jmOY9Y9sTU2ke3L3G/d/3z8jqmbBbeyBGHU73P4JLXfc7tJARygIK3WGIkloA==" }, "parse-filepath": { "version": "1.0.2", diff --git a/package.json b/package.json index 74b8139a4..50537d3c6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@bosonprotocol/chat-sdk": "^1.3.0", - "@bosonprotocol/react-kit": "^0.16.2-alpha.16", + "@bosonprotocol/react-kit": "0.16.2-alpha.16", "@davatar/react": "^1.10.4", "@ethersproject/address": "^5.6.1", "@ethersproject/units": "^5.6.1", diff --git a/src/assets/fonts/neuropolitical_rg.ttf b/src/assets/fonts/neuropolitical_rg.ttf new file mode 100644 index 000000000..26bbb63ce Binary files /dev/null and b/src/assets/fonts/neuropolitical_rg.ttf differ diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx deleted file mode 100644 index da2b2fed9..000000000 --- a/src/components/banner/Banner.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ArrowRight } from "phosphor-react"; -import styled from "styled-components"; - -import Grid from "../../components/ui/Grid"; -import { colors } from "../../lib/styles/colors"; -import Typography from "../ui/Typography"; - -const BannerContainer = styled.a` - display: block; - text-align: center; - background-color: ${colors.secondary}; - color: ${colors.white}; - transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); - padding-left: 0.5rem; - padding-right: 0.5rem; - &:hover { - background-color: ${colors.black}; - [data-arrow-right] { - transform: translate(1rem, 0); - } - } -`; -const ArrowWrapper = styled(Grid)` - background-color: ${colors.primary}; - padding: 0.315rem; - color: ${colors.black}; - width: auto; - transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); -`; - -function Banner() { - return ( - - - - Get 50% of the purchase price of any item as a $BOSON reward - - - - - - - ); -} - -export default Banner; diff --git a/src/components/datepicker/Calendar.tsx b/src/components/datepicker/Calendar.tsx index b1910e4e2..78d2461a9 100644 --- a/src/components/datepicker/Calendar.tsx +++ b/src/components/datepicker/Calendar.tsx @@ -69,9 +69,13 @@ export default function Calendar({ {rows.map(({ text, value, current }: ICalendarCell, i: number) => { - const disabled: boolean = period + const isBeforeMinDate = minDate ? value?.isBefore(minDate, "day") : false; + const isAfterMaxDate = maxDate + ? value?.isAfter(maxDate, "day") + : false; + const disabled: boolean = isBeforeMinDate || isAfterMaxDate; return ( | null; - onChange?: (selected: Dayjs | Array) => void; + onChange?: (selected: Dayjs | Array | null) => void; error?: string; period: boolean; selectTime: boolean; @@ -37,8 +38,10 @@ const handleInitialDates = ( let endDate: Dayjs | null = null; if (Array.isArray(initialValue)) { - startDate = dayjs(initialValue[0]); - endDate = dayjs(initialValue[1]); + if (initialValue.length) { + startDate = dayjs(initialValue[0]); + endDate = dayjs(initialValue[1]); + } } else { startDate = dayjs(initialValue); } @@ -109,7 +112,7 @@ export default function DatePicker({ } }; - useEffect(() => { + useDidMountEffect(() => { if ( (!period && date !== null) || (period && date !== null && secondDate !== null) @@ -122,7 +125,13 @@ export default function DatePicker({ (period && date === null && secondDate === null) ) { setShownDate("Choose dates..."); + if (period) { + onChange?.([]); + } else { + onChange?.(null); + } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [date, secondDate, period]); useEffect(() => { diff --git a/src/components/detail/DetailWidget/DetailWidget.tsx b/src/components/detail/DetailWidget/DetailWidget.tsx index 33b19f3ed..3e4086fae 100644 --- a/src/components/detail/DetailWidget/DetailWidget.tsx +++ b/src/components/detail/DetailWidget/DetailWidget.tsx @@ -669,7 +669,7 @@ const DetailWidget: React.FC = ({ "code" in error && (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } else { showModal(modalTypes.DETAIL_WIDGET, { title: "An error occurred", diff --git a/src/components/detail/DetailWidget/TokenGated.tsx b/src/components/detail/DetailWidget/TokenGated.tsx index 4356f18a1..7743b2740 100644 --- a/src/components/detail/DetailWidget/TokenGated.tsx +++ b/src/components/detail/DetailWidget/TokenGated.tsx @@ -7,6 +7,7 @@ import { CONFIG } from "../../../lib/config"; import { colors } from "../../../lib/styles/colors"; import { Offer } from "../../../lib/types/offer"; import { IPrice } from "../../../lib/utils/convertPrice"; +import { sanitizeUrl } from "../../../lib/utils/url"; import { useCoreSDK } from "../../../lib/utils/useCoreSdk"; import { useConvertedPrice } from "../../price/useConvertedPrice"; import Grid from "../../ui/Grid"; @@ -176,8 +177,9 @@ const TokenGated = ({ {openseaLinkToOriginalMainnetCollection} diff --git a/src/components/disputeResolver/ManageDisputes/DisputesTable.tsx b/src/components/disputeResolver/ManageDisputes/DisputesTable.tsx index 47e65ca5c..2cb50805e 100644 --- a/src/components/disputeResolver/ManageDisputes/DisputesTable.tsx +++ b/src/components/disputeResolver/ManageDisputes/DisputesTable.tsx @@ -14,6 +14,7 @@ import copyToClipboard from "../../../lib/utils/copyToClipboard"; import { getDateTimestamp } from "../../../lib/utils/getDateTimestamp"; import { Disputes } from "../../../lib/utils/hooks/useExchanges"; import { useKeepQueryParamsNavigate } from "../../../lib/utils/hooks/useKeepQueryParamsNavigate"; +import { sanitizeUrl } from "../../../lib/utils/url"; import { useModal } from "../../modal/useModal"; import Price from "../../price"; import PaginationPages from "../../seller/common/PaginationPages"; @@ -175,7 +176,7 @@ export default function DisputesTable({ disputes }: Props) { whiteSpace: "pre" }} onClick={() => { - copyToClipboard(emailAddress).then(() => { + copyToClipboard(sanitizeUrl(emailAddress)).then(() => { toast(() => "Seller e-mail has been copied to clipboard"); }); }} diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 11d07d825..5bd17af97 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -5,6 +5,7 @@ import logo from "../../../src/assets/logo-white.svg"; import { BosonRoutes } from "../../lib/routing/routes"; import { breakpoint } from "../../lib/styles/breakpoint"; import { useBreakpoints } from "../../lib/utils/hooks/useBreakpoints"; +import { sanitizeUrl } from "../../lib/utils/url"; import SocialLogo, { SocialLogoValues } from "../../pages/custom-store/SocialLogo"; @@ -112,9 +113,9 @@ function Socials() { return socialMediaLinks.map(({ url, value }) => { return ( @@ -241,9 +242,10 @@ export default function FooterComponent() { return ( {footerLink.label} diff --git a/src/components/form/Datepicker.tsx b/src/components/form/Datepicker.tsx index 51dd06df7..ce5e0f8fe 100644 --- a/src/components/form/Datepicker.tsx +++ b/src/components/form/Datepicker.tsx @@ -5,7 +5,7 @@ import DatePicker from "../datepicker/DatePicker"; import Error from "./Error"; import type { DatepickerProps } from "./types"; -export default function DatepickerComponent({ +export default function Datepicker({ name, period = false, selectTime = false, @@ -17,7 +17,7 @@ export default function DatepickerComponent({ const displayError = typeof errorMessage === typeof "string" && errorMessage !== ""; - const handleChange = (date: Dayjs | Array) => { + const handleChange = (date: Dayjs | Array | null) => { if (!meta.touched) { helpers.setTouched(true); } diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index d9e7a7594..3f6405838 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -11,7 +11,6 @@ import { colors } from "../../lib/styles/colors"; import { zIndex } from "../../lib/styles/zIndex"; import { useBreakpoints } from "../../lib/utils/hooks/useBreakpoints"; import { useCustomStoreQueryParameter } from "../../pages/custom-store/useCustomStoreQueryParameter"; -import Banner from "../banner/Banner"; import { LinkWithQuery } from "../customNavigation/LinkWithQuery"; import Layout from "../Layout"; import ViewTxButton from "../transactions/ViewTxButton"; @@ -23,40 +22,6 @@ const smallWidth = "180px"; const mediumWidth = "225px"; const sideMargin = "1rem"; const closedHeaderWidth = "75px"; -const SideBannerContainer = styled.div<{ - $navigationBarPosition: string; - $isSideBarOpen: boolean; -}>` - ${({ $navigationBarPosition, $isSideBarOpen }) => { - if ("left" === $navigationBarPosition) { - return css` - ${$isSideBarOpen - ? css` - margin-left: ${smallWidth}; - ${breakpoint.m} { - margin-left: ${mediumWidth}; - } - ` - : css` - margin-left: ${closedHeaderWidth}; - `} - `; - } else if ("right" === $navigationBarPosition) { - return css` - ${$isSideBarOpen - ? css` - margin-right: ${smallWidth}; - ${breakpoint.m} { - margin-right: ${mediumWidth}; - } - ` - : css` - margin-right: ${closedHeaderWidth}; - `} - `; - } - }} -`; const Header = styled.header<{ $navigationBarPosition: string; @@ -312,20 +277,11 @@ const HeaderComponent = forwardRef( return ( <> - {withBanner && isSideNavBar && ( - - - - )}
- {withBanner && !isSideNavBar && } { switch ($modalType) { + case "RELIST_OFFER": + case "EXPORT_EXCHANGES_WITH_DELIVERY": case "REDEEM": return css` overflow: visible; diff --git a/src/components/modal/ModalComponents.tsx b/src/components/modal/ModalComponents.tsx index 9e2f8ac2f..263b26b2a 100644 --- a/src/components/modal/ModalComponents.tsx +++ b/src/components/modal/ModalComponents.tsx @@ -20,12 +20,13 @@ import ProductCreateSuccess from "./components/ProductCreateSuccess"; import ProgressBarModal from "./components/ProgressBarModal"; import RedeemModal from "./components/RedeemModal/RedeemModal"; import RedeemSuccessModal from "./components/RedeemModal/RedeemSuccessModal"; +import { RelistOfferModal } from "./components/RelistOfferModal/RelistOfferModal"; import RetractDisputeModal from "./components/RetractDisputeModal"; import RevokeProduct from "./components/RevokeProduct"; import FinanceDeposit from "./components/SellerFinance/FinanceDeposit"; import FinanceWithdraw from "./components/SellerFinance/FinanceWithdraw"; -import ConfirmationFailedModal from "./components/Transactions/ConfirmationFailedModal/ConfirmationFailedModal"; import RecentTransactionsModal from "./components/Transactions/RecentTransactionsModal/RecentTransactionsModal"; +import TransactionFailedModal from "./components/Transactions/TransactionFailedModal/TransactionFailedModal"; import TransactionSubmittedModal from "./components/Transactions/TransactionSubmittedModal/TransactionSubmittedModal"; import WaitingForConfirmationModal from "./components/Transactions/WaitingForConfirmationModal/WaitingForConfirmationModal"; import Upload from "./components/Upload"; @@ -35,7 +36,7 @@ import WhatIsRedeem from "./components/WhatIsRedeem"; export const MODAL_TYPES = { CANCEL_EXCHANGE: "CANCEL_EXCHANGE", COMPLETE_EXCHANGE: "COMPLETE_EXCHANGE", - CONFIRMATION_FAILED: "CONFIRMATION_FAILED", + TRANSACTION_FAILED: "TRANSACTION_FAILED", CREATE_PRODUCT_DRAFT: "CREATE_PRODUCT_DRAFT", CREATE_PROFILE: "CREATE_PROFILE", CUSTOM_STORE: "CUSTOM_STORE", @@ -64,13 +65,14 @@ export const MODAL_TYPES = { WAITING_FOR_CONFIRMATION: "WAITING_FOR_CONFIRMATION", WHAT_IS_REDEEM: "WHAT_IS_REDEEM", PROGRESS_BAR: "PROGRESS_BAR", - EXPORT_EXCHANGES_WITH_DELIVERY: "EXPORT_EXCHANGES_WITH_DELIVERY" + EXPORT_EXCHANGES_WITH_DELIVERY: "EXPORT_EXCHANGES_WITH_DELIVERY", + RELIST_OFFER: "RELIST_OFFER" } as const; export const MODAL_COMPONENTS = { [MODAL_TYPES.CANCEL_EXCHANGE]: CancelExchangeModal, [MODAL_TYPES.COMPLETE_EXCHANGE]: CompleteExchange, - [MODAL_TYPES.CONFIRMATION_FAILED]: ConfirmationFailedModal, + [MODAL_TYPES.TRANSACTION_FAILED]: TransactionFailedModal, [MODAL_TYPES.CREATE_PRODUCT_DRAFT]: CreateProductDraft, [MODAL_TYPES.CREATE_PROFILE]: CreateProfileModal, [MODAL_TYPES.CUSTOM_STORE]: CustomStore, @@ -99,5 +101,7 @@ export const MODAL_COMPONENTS = { [MODAL_TYPES.WAITING_FOR_CONFIRMATION]: WaitingForConfirmationModal, [MODAL_TYPES.WHAT_IS_REDEEM]: WhatIsRedeem, [MODAL_TYPES.PROGRESS_BAR]: ProgressBarModal, - [MODAL_TYPES.EXPORT_EXCHANGES_WITH_DELIVERY]: ExportExchangesWithDeliveryModal + [MODAL_TYPES.EXPORT_EXCHANGES_WITH_DELIVERY]: + ExportExchangesWithDeliveryModal, + [MODAL_TYPES.RELIST_OFFER]: RelistOfferModal } as const; diff --git a/src/components/modal/components/Chat/CancelExchangeModal.tsx b/src/components/modal/components/Chat/CancelExchangeModal.tsx index 4988b09eb..495f627c4 100644 --- a/src/components/modal/components/Chat/CancelExchangeModal.tsx +++ b/src/components/modal/components/Chat/CancelExchangeModal.tsx @@ -205,7 +205,7 @@ export default function CancelExchangeModal({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } else { showModal(modalTypes.DETAIL_WIDGET, { title: "An error occurred", diff --git a/src/components/modal/components/Chat/ResolveDisputeModal.tsx b/src/components/modal/components/Chat/ResolveDisputeModal.tsx index e7c137d63..58cf9d768 100644 --- a/src/components/modal/components/Chat/ResolveDisputeModal.tsx +++ b/src/components/modal/components/Chat/ResolveDisputeModal.tsx @@ -176,7 +176,7 @@ export default function ResolveDisputeModal({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } setResolveDisputeError(error as Error); } diff --git a/src/components/modal/components/CompleteExchange.tsx b/src/components/modal/components/CompleteExchange.tsx index 971845f49..9dfd79d1c 100644 --- a/src/components/modal/components/CompleteExchange.tsx +++ b/src/components/modal/components/CompleteExchange.tsx @@ -229,7 +229,7 @@ export default function CompleteExchange({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={() => { @@ -278,7 +278,7 @@ export default function CompleteExchange({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={() => { diff --git a/src/components/modal/components/ExpireVoucherModal.tsx b/src/components/modal/components/ExpireVoucherModal.tsx index 79867f540..2a8a05caa 100644 --- a/src/components/modal/components/ExpireVoucherModal.tsx +++ b/src/components/modal/components/ExpireVoucherModal.tsx @@ -189,7 +189,7 @@ export default function ExpireVoucherModal({ exchange }: Props) { (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={() => { diff --git a/src/components/modal/components/ExportExchangesWithDeliveryModal.tsx b/src/components/modal/components/ExportExchangesWithDeliveryModal.tsx index b99eaa507..def6ad5e5 100644 --- a/src/components/modal/components/ExportExchangesWithDeliveryModal.tsx +++ b/src/components/modal/components/ExportExchangesWithDeliveryModal.tsx @@ -1,5 +1,5 @@ import dayjs, { Dayjs } from "dayjs"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import DatePicker from "../../datepicker/DatePicker"; import BosonButton from "../../ui/BosonButton"; @@ -11,14 +11,11 @@ export default function ExportExchangesWithDeliveryModal({ }: { onExport: (from?: Dayjs) => void; }) { - const [from, setFrom] = useState(dayjs()); + const today = useMemo(() => dayjs(), []); + const [from, setFrom] = useState(today); + const initialDate = useMemo(() => dayjs().startOf("month"), []); return ( - + Option 1

@@ -34,8 +31,8 @@ export default function ExportExchangesWithDeliveryModal({ period={false} selectTime minDate={null} - maxDate={dayjs()} - initialValue={dayjs()} + maxDate={today} + initialValue={initialDate} onChange={(date) => { setFrom(date as Dayjs); }} @@ -57,9 +54,7 @@ export default function ExportExchangesWithDeliveryModal({ delivery information.This option will always include, if it exists in the chat conversation, the delivery information.

- onExport()}> - Export - + onExport()}>Export
); diff --git a/src/components/modal/components/ProductCreateSuccess.tsx b/src/components/modal/components/ProductCreateSuccess.tsx index 280f693f9..f44378132 100644 --- a/src/components/modal/components/ProductCreateSuccess.tsx +++ b/src/components/modal/components/ProductCreateSuccess.tsx @@ -15,8 +15,7 @@ import { Break, ModalGrid, ModalImageWrapper, - Widget, - WidgetButtonWrapper + Widget } from "../../detail/Detail.style"; import DetailTable from "../../detail/DetailTable"; import TokenGated from "../../detail/DetailWidget/TokenGated"; @@ -31,7 +30,7 @@ interface Props { name: string; image: string; offer: any; - onCreateNewProject: () => void; + onCreateNew?: () => void; onViewMyItem: () => void; hasMultipleVariants: boolean; } @@ -82,11 +81,6 @@ const StyledIndicator = styled(ProgressPrimitive.Indicator)` height: 100%; transition: transform 660ms cubic-bezier(0.65, 0, 0.35, 1); `; -const StyledWidgetButtonWrapper = styled(WidgetButtonWrapper)` - button { - width: 50%; - } -`; const FundTile = styled(Typography)` font-weight: 600; @@ -114,7 +108,7 @@ export default function ProductCreateSuccess({ name, image, offer, - onCreateNewProject, + onCreateNew, onViewMyItem, hasMultipleVariants }: Props) { @@ -133,10 +127,6 @@ export default function ProductCreateSuccess({ [convertedPrice, offer] ); - const handleCreateNew = () => { - onCreateNewProject(); - }; - const suggestedAmount = FixedNumber.fromString( formatUnits( BigNumber.from(offer?.sellerDeposit).mul(Number(offer?.quantityInitial)), @@ -199,14 +189,16 @@ export default function ProductCreateSuccess({ />
- + {offer.condition && ( + + )}
@@ -254,7 +246,7 @@ export default function ProductCreateSuccess({ )} - + View my item - - Create new - - - + {onCreateNew && ( + + Create new + + + )} + diff --git a/src/components/modal/components/RedeemModal/Confirmation/Confirmation.tsx b/src/components/modal/components/RedeemModal/Confirmation/Confirmation.tsx index 96551f2d9..5a7e14f35 100644 --- a/src/components/modal/components/RedeemModal/Confirmation/Confirmation.tsx +++ b/src/components/modal/components/RedeemModal/Confirmation/Confirmation.tsx @@ -218,7 +218,7 @@ ${FormModel.formFields.phone.placeholder}: ${phoneField.value}`; (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={async () => { diff --git a/src/components/modal/components/RelistOfferModal/RelistOfferModal.tsx b/src/components/modal/components/RelistOfferModal/RelistOfferModal.tsx new file mode 100644 index 000000000..3a25e167b --- /dev/null +++ b/src/components/modal/components/RelistOfferModal/RelistOfferModal.tsx @@ -0,0 +1,251 @@ +import { EvaluationMethod } from "@bosonprotocol/common"; +import { offers, productV1, subgraph } from "@bosonprotocol/react-kit"; +import * as Sentry from "@sentry/browser"; +import { Form, Formik } from "formik"; +import React from "react"; +import toast from "react-hot-toast"; +import { generatePath } from "react-router-dom"; +import uuid from "react-uuid"; + +import { UrlParameters } from "../../../../lib/routing/parameters"; +import { ProductRoutes } from "../../../../lib/routing/routes"; +import { useCreateOffers } from "../../../../lib/utils/hooks/offer/useCreateOffers"; +import { useKeepQueryParamsNavigate } from "../../../../lib/utils/hooks/useKeepQueryParamsNavigate"; +import { useCoreSDK } from "../../../../lib/utils/useCoreSdk"; +import Yup from "../../../../lib/validation/index"; +import { ExtendedOffer } from "../../../../pages/explore/WithAllOffers"; +import { CoreTermsOfSaleDates } from "../../../product/coreTermsOfSale/CoreTermsOfSaleDates"; +import { + commonCoreTermsOfSaleValidationSchema, + TOKEN_CRITERIA +} from "../../../product/utils"; +import SuccessTransactionToast from "../../../toasts/SuccessTransactionToast"; +import BosonButton from "../../../ui/BosonButton"; +import Typography from "../../../ui/Typography"; +import { useModal } from "../../useModal"; + +interface RelistOfferModalProps { + offer: ExtendedOffer; + onRelistedSuccessfully?: () => void; +} + +const validationSchema = Yup.object({ + offerValidityPeriod: + commonCoreTermsOfSaleValidationSchema["offerValidityPeriod"], + + redemptionPeriod: commonCoreTermsOfSaleValidationSchema["redemptionPeriod"] +}); + +type RelistType = Yup.InferType; + +export const RelistOfferModal: React.FC = ({ + offer, + onRelistedSuccessfully +}) => { + const coreSDK = useCoreSDK(); + const { showModal, hideModal } = useModal(); + const navigate = useKeepQueryParamsNavigate(); + const { mutateAsync: createOffers } = useCreateOffers(); + return ( + + initialValues={{ + offerValidityPeriod: [], + redemptionPeriod: [] + }} + validationSchema={validationSchema} + onSubmit={async (values) => { + try { + const validFromDate = values.offerValidityPeriod[0] + .toDate() + .getTime(); + const validUntilDate = values.offerValidityPeriod[1] + .toDate() + .getTime(); + const voucherRedeemableFromDate = values.redemptionPeriod[0] + .toDate() + .getTime(); + const voucherRedeemableUntilDate = values.redemptionPeriod[1] + .toDate() + .getTime(); + const isMultiVariant = (offer.additional?.variants?.length ?? []) > 1; + const { condition } = offer; + const isTokenGated = !!condition; + const offersToCreate: offers.CreateOfferArgs[] = []; + const productUuid = uuid(); + for (const variant of offer.additional?.variants ?? []) { + const metadataUuid = uuid(); + const originalMetadata = (await coreSDK.getMetadata( + variant.metadataHash + )) as productV1.ProductV1Metadata; + const redeemableAtValue = originalMetadata.attributes.find( + (attribute) => attribute.trait_type === "Redeemable At" + )?.value; + const origin = redeemableAtValue; + const metadataAsString = JSON.stringify(originalMetadata); + const metadata = JSON.parse( + metadataAsString + .replaceAll( + `${origin}/#/license/${originalMetadata.uuid}`, + `${origin}/#/license/${metadataUuid}` + ) + .replaceAll( + `${origin}/#/variant-uuid/${originalMetadata.uuid}`, + `${origin}/#/variant-uuid/${metadataUuid}` + ) + .replaceAll( + `${origin}/#/offer-uuid/${originalMetadata.uuid}`, + `${origin}/#/offer-uuid/${metadataUuid}` + ) + ) as productV1.ProductV1Metadata; + const metadataHash = await coreSDK.storeMetadata({ + ...metadata, + uuid: metadataUuid, + attributes: [ + ...metadata.attributes.filter( + (attribute) => + attribute.trait_type !== "Redeemable Until" && + attribute.display_type !== "date" + ), + { + trait_type: "Redeemable Until", + value: voucherRedeemableUntilDate.toString(), + display_type: "date" + } + ], + exchangePolicy: { + ...metadata.exchangePolicy, + uuid: Date.now().toString() + }, + product: { + ...metadata.product, + uuid: productUuid + } + }); + const offerData = { + price: variant.price.toString(), + sellerDeposit: variant.sellerDeposit.toString(), + buyerCancelPenalty: variant.buyerCancelPenalty.toString(), + quantityAvailable: variant.quantityAvailable.toString(), + voucherRedeemableFromDateInMS: + voucherRedeemableFromDate.toString(), + voucherRedeemableUntilDateInMS: voucherRedeemableUntilDate, + voucherValidDurationInMS: variant.voucherValidDuration.toString(), + validFromDateInMS: validFromDate.toString(), + validUntilDateInMS: validUntilDate.toString(), + disputePeriodDurationInMS: ( + Number(variant.disputePeriodDuration) * 1000 + ).toString(), + resolutionPeriodDurationInMS: + variant.resolutionPeriodDuration.toString(), + exchangeToken: variant.exchangeToken?.address, + disputeResolverId: variant.disputeResolverId, + agentId: variant.agentId, // no agent + metadataUri: `ipfs://${metadataHash}`, + metadataHash: metadataHash + }; + offersToCreate.push(offerData); + } + const handleOpenSuccessModal = ( + offerInfo: subgraph.OfferFieldsFragment + ) => { + if (!offerInfo.metadata) { + return; + } + const onViewMyItem = () => { + hideModal(); + const id = productUuid; + const pathname = generatePath(ProductRoutes.ProductDetail, { + [UrlParameters.uuid]: id + }); + navigate({ pathname }); + }; + showModal( + "PRODUCT_CREATE_SUCCESS", + { + title: `Offer ${offerInfo.id}`, + name: offerInfo.metadata.name, + message: "You have successfully relisted:", + image: offerInfo.metadata.image, + price: offerInfo.price, + offer: offerInfo, + hasMultipleVariants: isMultiVariant, + onViewMyItem + }, + "auto" + ); + }; + + await createOffers({ + isMultiVariant, + offersToCreate, + tokenGatedInfo: isTokenGated + ? { + maxCommits: condition.maxCommits, + minBalance: condition.threshold, + tokenContract: condition.tokenAddress, + tokenCriteria: + condition.method === EvaluationMethod.SpecificToken + ? TOKEN_CRITERIA[1] + : TOKEN_CRITERIA[0], + tokenId: condition.tokenId, + tokenType: condition.tokenType + } + : null, + conditionDecimals: Number(offer.exchangeToken.decimals), + onCreatedOffersWithVariants: ({ firstOffer }) => { + toast((t) => ( + { + handleOpenSuccessModal( + firstOffer || ({} as subgraph.OfferFieldsFragment) + ); + }} + /> + )); + }, + onCreatedSingleOffers: ({ offer: createdOffer }) => { + toast((t) => ( + { + handleOpenSuccessModal( + createdOffer || ({} as subgraph.OfferFieldsFragment) + ); + }} + /> + )); + } + }); + onRelistedSuccessfully?.(); + } catch (error) { + showModal("TRANSACTION_FAILED", { + errorMessage: "Something went wrong", + detailedErrorMessage: + "Please try again or try disconnecting and reconnecting your wallet before relisting the offer" + }); + console.error(error); + Sentry.captureException(error); + } + }} + > + {({ isValid }) => { + return ( +
+ + Relisting an offer duplicates all its contents except for the IDs, + Offer Validity period, and Redemption period as they may be in the + past, so you will need to define them here. + + + + Relist Offer + + + ); + }} + + ); +}; diff --git a/src/components/modal/components/RetractDisputeModal.tsx b/src/components/modal/components/RetractDisputeModal.tsx index 9890127af..064f389d4 100644 --- a/src/components/modal/components/RetractDisputeModal.tsx +++ b/src/components/modal/components/RetractDisputeModal.tsx @@ -126,7 +126,7 @@ export default function RetractDisputeModal({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } setRetractDisputeError(error as Error); } diff --git a/src/components/modal/components/RevokeProduct.tsx b/src/components/modal/components/RevokeProduct.tsx index 5ff62749e..4855972b9 100644 --- a/src/components/modal/components/RevokeProduct.tsx +++ b/src/components/modal/components/RevokeProduct.tsx @@ -141,7 +141,7 @@ export default function RevokeProduct({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={() => { diff --git a/src/components/modal/components/SellerFinance/FinanceDeposit.tsx b/src/components/modal/components/SellerFinance/FinanceDeposit.tsx index 57cc5c31a..4e00486ba 100644 --- a/src/components/modal/components/SellerFinance/FinanceDeposit.tsx +++ b/src/components/modal/components/SellerFinance/FinanceDeposit.tsx @@ -186,7 +186,7 @@ export default function FinanceDeposit({ "code" in error && (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } setDepositError(error); reload(); diff --git a/src/components/modal/components/SellerFinance/FinanceWithdraw.tsx b/src/components/modal/components/SellerFinance/FinanceWithdraw.tsx index f4295b6cc..e3e938349 100644 --- a/src/components/modal/components/SellerFinance/FinanceWithdraw.tsx +++ b/src/components/modal/components/SellerFinance/FinanceWithdraw.tsx @@ -194,7 +194,7 @@ export default function FinanceWithdraw({ "code" in error && (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } setWithdrawError(error); reload(); diff --git a/src/components/modal/components/Transactions/ConfirmationFailedModal/ConfirmationFailedModal.tsx b/src/components/modal/components/Transactions/TransactionFailedModal/TransactionFailedModal.tsx similarity index 76% rename from src/components/modal/components/Transactions/ConfirmationFailedModal/ConfirmationFailedModal.tsx rename to src/components/modal/components/Transactions/TransactionFailedModal/TransactionFailedModal.tsx index a80a4b490..eaea6a455 100644 --- a/src/components/modal/components/Transactions/ConfirmationFailedModal/ConfirmationFailedModal.tsx +++ b/src/components/modal/components/Transactions/TransactionFailedModal/TransactionFailedModal.tsx @@ -6,10 +6,16 @@ import Grid from "../../../../ui/Grid"; import Typography from "../../../../ui/Typography"; import { useModal } from "../../../useModal"; -export default function ConfirmationFailedModal() { +export default function TransactionFailedModal({ + errorMessage, + detailedErrorMessage +}: { + errorMessage: string; + detailedErrorMessage?: string; +}) { const { updateProps, store } = useModal(); useEffect(() => { - updateProps<"CONFIRMATION_FAILED">({ + updateProps<"TRANSACTION_FAILED">({ ...store, modalProps: { ...store.modalProps @@ -26,7 +32,7 @@ export default function ConfirmationFailedModal() { - Confirmation Failed + {errorMessage || "Confirmation Failed"} - Please retry this action + {detailedErrorMessage || "Please retry this action"} ); diff --git a/src/components/modal/components/VoidProduct.tsx b/src/components/modal/components/VoidProduct.tsx index ada873b9e..3b70b09d2 100644 --- a/src/components/modal/components/VoidProduct.tsx +++ b/src/components/modal/components/VoidProduct.tsx @@ -340,7 +340,7 @@ export default function VoidProduct({ (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }} onPendingSignature={() => { diff --git a/src/components/product/Preview.tsx b/src/components/product/Preview.tsx index efa98afe5..67097b73f 100644 --- a/src/components/product/Preview.tsx +++ b/src/components/product/Preview.tsx @@ -126,18 +126,18 @@ export default function Preview({ const commonTermsOfSale = isMultiVariant ? values.variantsCoreTermsOfSale : values.coreTermsOfSale; - const validFromDateInMS = Date.parse( - commonTermsOfSale.offerValidityPeriod[0] - ); - const validUntilDateInMS = Date.parse( - commonTermsOfSale.offerValidityPeriod[1] - ); - const voucherRedeemableFromDateInMS = Date.parse( - commonTermsOfSale.redemptionPeriod[0] - ); - const voucherRedeemableUntilDateInMS = Date.parse( - commonTermsOfSale.redemptionPeriod[1] - ); + const validFromDateInMS = commonTermsOfSale.offerValidityPeriod[0] + .toDate() + .getTime(); + const validUntilDateInMS = commonTermsOfSale.offerValidityPeriod[1] + .toDate() + .getTime(); + const voucherRedeemableFromDateInMS = commonTermsOfSale.redemptionPeriod[0] + .toDate() + .getTime(); + const voucherRedeemableUntilDateInMS = commonTermsOfSale.redemptionPeriod[1] + .toDate() + .getTime(); const quantityAvailable = isMultiVariant ? firstVariant.quantity : values.coreTermsOfSale.quantity; diff --git a/src/components/product/TermsOfExchange.tsx b/src/components/product/TermsOfExchange.tsx index 492e67bee..d455b2f31 100644 --- a/src/components/product/TermsOfExchange.tsx +++ b/src/components/product/TermsOfExchange.tsx @@ -224,7 +224,7 @@ export default function TermsOfExchange() {
diff --git a/src/components/product/CoreTermsOfSale.tsx b/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx similarity index 85% rename from src/components/product/CoreTermsOfSale.tsx rename to src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx index d15d91ecb..1cb684b0c 100644 --- a/src/components/product/CoreTermsOfSale.tsx +++ b/src/components/product/coreTermsOfSale/CoreTermsOfSale.tsx @@ -1,21 +1,22 @@ import { useState } from "react"; import styled from "styled-components"; -import { useCoreSDK } from "../../lib/utils/useCoreSdk"; -import { Datepicker, FormField, Input, Select, Textarea } from "../form"; -import BosonButton from "../ui/BosonButton"; +import { useCoreSDK } from "../../../lib/utils/useCoreSdk"; +import { FormField, Input, Select, Textarea } from "../../form"; +import BosonButton from "../../ui/BosonButton"; import { ContainerProductPage, ProductButtonGroup, SectionTitle -} from "./Product.styles"; +} from "../Product.styles"; import { OPTIONS_CURRENCIES, OPTIONS_TOKEN_GATED, TOKEN_CRITERIA, TOKEN_TYPES -} from "./utils"; -import { useCreateForm } from "./utils/useCreateForm"; +} from "../utils"; +import { useCreateForm } from "../utils/useCreateForm"; +import { CoreTermsOfSaleDates } from "./CoreTermsOfSaleDates"; const PriceContainer = styled.div` display: grid; @@ -97,16 +98,6 @@ export default function CoreTermsOfSale({ isMultiVariant }: Props) { {values[prefix].tokenGatedOffer.value === "true" && ( <> - {/* TODO: enable once we have more than one variant */} - {/* - - )} - - - - - - + = ({ + prefix +}) => { + const fixedPrefix = prefix ? `${prefix}.` : ""; + return ( + <> + + + + + + + + ); +}; diff --git a/src/components/product/utils/const.ts b/src/components/product/utils/const.ts index fecd7e2d2..ed435e99a 100644 --- a/src/components/product/utils/const.ts +++ b/src/components/product/utils/const.ts @@ -79,14 +79,7 @@ export const OPTIONS_TOKEN_GATED = [ value: "true", label: "Yes" } -]; - -export const TOKEN_GATED_VARIANTS = [ - { - value: "all", - label: "All" - } -]; +] as const; export const TOKEN_TYPES = [ { @@ -101,7 +94,7 @@ export const TOKEN_TYPES = [ value: "erc1155", label: "ERC1155" } -]; +] as const; export const TOKEN_CRITERIA = [ { @@ -112,7 +105,7 @@ export const TOKEN_CRITERIA = [ value: "tokenid", label: "tokenId" } -]; +] as const; export const OPTIONS_EXCHANGE_POLICY = [ { diff --git a/src/components/product/utils/initialValues.ts b/src/components/product/utils/initialValues.ts index 7322b0c97..0656f1f07 100644 --- a/src/components/product/utils/initialValues.ts +++ b/src/components/product/utils/initialValues.ts @@ -11,7 +11,6 @@ import { OPTIONS_UNIT, OPTIONS_WEIGHT, TOKEN_CRITERIA, - TOKEN_GATED_VARIANTS, TOKEN_TYPES } from "./const"; import { CreateProductForm } from "./types"; @@ -89,7 +88,6 @@ export const coreTermsOfSaleInitialValues = { currency: OPTIONS_CURRENCIES[0], quantity: 1, tokenGatedOffer: OPTIONS_TOKEN_GATED[0], - tokenGatedVariants: TOKEN_GATED_VARIANTS[0], tokenContract: "", tokenType: TOKEN_TYPES[0], minBalance: undefined, @@ -105,7 +103,6 @@ export const coreTermsOfSaleInitialValues = { export const variantsCoreTermsOfSaleInitialValues = { variantsCoreTermsOfSale: { tokenGatedOffer: OPTIONS_TOKEN_GATED[0], - tokenGatedVariants: TOKEN_GATED_VARIANTS[0], tokenContract: "", tokenType: TOKEN_TYPES[0], minBalance: undefined, diff --git a/src/components/product/utils/validationSchema.ts b/src/components/product/utils/validationSchema.ts index a75ccb2cb..76e22db7a 100644 --- a/src/components/product/utils/validationSchema.ts +++ b/src/components/product/utils/validationSchema.ts @@ -175,30 +175,23 @@ export const productInformationValidationSchema = Yup.object({ }) }); -const commonCoreTermsOfSaleValidationSchema = { +export const commonCoreTermsOfSaleValidationSchema = { tokenGatedOffer: Yup.object() .shape({ value: Yup.string(), label: Yup.string() }) .default([{ value: "", label: "" }]), - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - offerValidityPeriod: Yup.mixed().isItBeforeNow().isOfferValidityDatesValid(), // prettier-ignore - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - redemptionPeriod: Yup.mixed().isItBeforeNow().isRedemptionDatesValid(), // prettier-ignore - tokenGatedVariants: Yup.object() - .when(["tokenGatedOffer"], { - is: (tokenGated: SelectDataProps) => - tokenGated?.value === OPTIONS_TOKEN_GATED[1].value, - then: (schema) => schema.required(validationMessage.required) - }) - .shape({ - value: Yup.string(), - label: Yup.string() - }) - .default([{ value: "", label: "" }]), + offerValidityPeriod: Yup.array() + .required(validationMessage.required) + .min(2, validationMessage.required) + .isItBeforeNow() + .isOfferValidityDatesValid(), + redemptionPeriod: Yup.array() + .required(validationMessage.required) + .min(2, validationMessage.required) + .isItBeforeNow() + .isRedemptionDatesValid(), tokenContract: Yup.string() .when(["tokenGatedOffer"], { is: (tokenGated: SelectDataProps) => @@ -226,17 +219,14 @@ const commonCoreTermsOfSaleValidationSchema = { label: Yup.string() }) .default([{ value: "", label: "" }]), - tokenCriteria: Yup.object() - .when(["tokenGatedOffer"], { - is: (tokenGated: SelectDataProps) => - tokenGated?.value === OPTIONS_TOKEN_GATED[1].value, - then: (schema) => schema.required(validationMessage.required) - }) - .shape({ - value: Yup.string(), - label: Yup.string() - }) - .default([{ value: "", label: "" }]), + tokenCriteria: Yup.object({ + value: Yup.string(), + label: Yup.string() + }).when(["tokenGatedOffer"], { + is: (tokenGated: SelectDataProps) => + tokenGated?.value === OPTIONS_TOKEN_GATED[1].value, + then: (schema) => schema.required(validationMessage.required) + }), minBalance: Yup.string().when( ["tokenGatedOffer", "tokenType", "tokenCriteria"], { diff --git a/src/components/seller/SellerWrapper.tsx b/src/components/seller/SellerWrapper.tsx index f9aa6c00c..d353048b6 100644 --- a/src/components/seller/SellerWrapper.tsx +++ b/src/components/seller/SellerWrapper.tsx @@ -7,6 +7,7 @@ const SellerMain = styled.main` padding: 1.375rem 2.5rem 2.75rem 2.5rem; background: ${colors.lightGrey}; min-height: calc(100vh - 5.5rem); + overflow: hidden; `; const SellerTitle = styled(Typography)` margin: 0 0 1.25rem 0; @@ -16,6 +17,7 @@ const SellerInner = styled.div` padding: 1rem; box-shadow: 0px 0px 5px 0px rgb(0 0 0 / 2%), 0px 0px 10px 0px rgb(0 0 0 / 2%), 0px 0px 15px 0px rgb(0 0 0 / 5%); + overflow: auto; `; export const LoadingWrapper = styled.div` text-align: center; diff --git a/src/components/seller/exchanges/SellerExchanges.tsx b/src/components/seller/exchanges/SellerExchanges.tsx index a8dd0bbda..6d6d67b3b 100644 --- a/src/components/seller/exchanges/SellerExchanges.tsx +++ b/src/components/seller/exchanges/SellerExchanges.tsx @@ -180,7 +180,7 @@ export default function SellerExchanges({ const [csvData, setCsvData] = useState([] as CSVData[]); const [loading, setLoading] = useState(false); - const { initialize, bosonXmtp, isInitializing } = useChatContext(); + const { initialize, bosonXmtp, isInitializing, error } = useChatContext(); const worker = useWorker(createWorker); const location = useLocation(); @@ -568,6 +568,12 @@ export default function SellerExchanges({ } ); + useEffect(() => { + if ((bosonXmtp && loading) || error) { + setLoading(false); + } + }, [bosonXmtp, loading, error]); + useEffect(() => { if (bosonXmtp && loading && csvData.length === 0) { showExchangesExportModal(); diff --git a/src/components/seller/products/SellerProductsTable.tsx b/src/components/seller/products/SellerProductsTable.tsx index 60164d3e0..42505665a 100644 --- a/src/components/seller/products/SellerProductsTable.tsx +++ b/src/components/seller/products/SellerProductsTable.tsx @@ -244,6 +244,11 @@ const Span = styled.span` margin-right: 1rem; } `; +const RelistButton = styled(Button)` + * { + line-height: 21px; + } +`; const statusOrder = [ OffersKit.OfferState.NOT_YET_VALID, @@ -623,7 +628,7 @@ export default function SellerProductsTable({ /> ), quantity: ( - + {offer?.quantityAvailable}/{offer?.quantityInitial} ), @@ -644,56 +649,89 @@ export default function SellerProductsTable({ ), - action: !( - status === OffersKit.OfferState.EXPIRED || - status === OffersKit.OfferState.VOIDED || - offer?.quantityAvailable === "0" - ) && ( - { - if (offer) { - if (showVariant) { - showModal( - modalTypes.VOID_PRODUCT, - { - title: "Void Confirmation", - offers: offer.additional?.variants.filter( - (variant) => { - variant.validUntilDate; - return ( - !variant.voided && - !dayjs( - getDateTimestamp(offer?.validUntilDate) - ).isBefore(dayjs()) - ); - } - ) as Offer[], - refetch - }, - "s" - ); - } else { - showModal( - modalTypes.VOID_PRODUCT, - { - title: "Void Confirmation", - offerId: offer.id, - offer, - refetch - }, - "xs" - ); - } - } - }} - > - Void - - ) + action: (() => { + const withVoidButton = !( + status === OffersKit.OfferState.EXPIRED || + status === OffersKit.OfferState.VOIDED || + offer?.quantityAvailable === "0" + ); + return ( + + {withVoidButton && ( + { + if (offer) { + if (showVariant) { + showModal( + modalTypes.VOID_PRODUCT, + { + title: "Void Confirmation", + offers: offer.additional?.variants.filter( + (variant) => { + variant.validUntilDate; + return ( + !variant.voided && + !dayjs( + getDateTimestamp(offer?.validUntilDate) + ).isBefore(dayjs()) + ); + } + ) as Offer[], + refetch + }, + "s" + ); + } else { + showModal( + modalTypes.VOID_PRODUCT, + { + title: "Void Confirmation", + offerId: offer.id, + offer, + refetch + }, + "xs" + ); + } + } + }} + > + Void + + )} + [0]["onClick"]> + >[0] + ) => { + event.stopPropagation(); + if (!offer) { + return; + } + showModal(modalTypes.RELIST_OFFER, { + title: `Relist Offer "${offer.metadata.name}"`, + offer, + onRelistedSuccessfully: refetch + }); + }} + > + Relist + + + ); + })() }; }) .sort(compareOffersSortByStatus), diff --git a/src/components/toasts/SuccessTransactionToast.tsx b/src/components/toasts/SuccessTransactionToast.tsx index dd202f73a..981b372bb 100644 --- a/src/components/toasts/SuccessTransactionToast.tsx +++ b/src/components/toasts/SuccessTransactionToast.tsx @@ -1,6 +1,7 @@ import { Toast } from "react-hot-toast"; import { colors } from "../../lib/styles/colors"; +import { sanitizeUrl } from "../../lib/utils/url"; import Grid from "../ui/Grid"; import Typography from "../ui/Typography"; import SuccessToast from "./common/SuccessToast"; @@ -33,7 +34,7 @@ export default function SuccessTransactionToast({ View details ) : url ? ( - + View details ) : null} diff --git a/src/lib/styles/GlobalStyle.tsx b/src/lib/styles/GlobalStyle.tsx index 36752c3fc..b009e74ea 100644 --- a/src/lib/styles/GlobalStyle.tsx +++ b/src/lib/styles/GlobalStyle.tsx @@ -1,5 +1,7 @@ import { createGlobalStyle } from "styled-components"; +import barlowRegular from "../../assets/fonts/Barlow-Regular.ttf"; +import neuropolitical_rg from "../../assets/fonts/neuropolitical_rg.ttf"; import { breakpoint } from "../../lib/styles/breakpoint"; import { colors } from "../../lib/styles/colors"; @@ -19,7 +21,12 @@ const GlobalStyle = createGlobalStyle<{ }>` @font-face { font-family: barlow; - src: url(src/assets/fonts/Barlow-Regular.ttf); + src: url(${barlowRegular}); + font-weight: normal; + } + @font-face { + font-family: neuropolitical_rg; + src: url(${neuropolitical_rg}); font-weight: normal; } * { diff --git a/src/lib/types/externals.d.ts b/src/lib/types/externals.d.ts index 7c5b8894e..ec5f4ff93 100644 --- a/src/lib/types/externals.d.ts +++ b/src/lib/types/externals.d.ts @@ -1,3 +1,5 @@ declare module "@metamask/jazzicon"; declare module "pretty"; + +declare module "*.ttf"; diff --git a/src/lib/types/yup.d.ts b/src/lib/types/yup.d.ts new file mode 100644 index 000000000..eab5992ff --- /dev/null +++ b/src/lib/types/yup.d.ts @@ -0,0 +1,24 @@ +import type { Dayjs } from "dayjs"; +import * as yup from "yup"; +declare module "yup" { + interface StringSchema< + TType extends Maybe = string | undefined, + TContext extends AnyObject = AnyObject, + TOut extends TType = TType + > extends yup.BaseSchema { + emptyAsUndefined(): StringSchema; + } + interface ArraySchema< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends AnySchema | Lazy, + C extends AnyObject = AnyObject, + TIn extends Maybe[]> = TypeOf[] | undefined, + TOut extends Maybe[]> = Asserts[] | Optionals + > extends BaseSchema { + isItBeforeNow(): ArraySchema; + isOfferValidityDatesValid(): ArraySchema; + isRedemptionDatesValid(): ArraySchema; + } +} + +export default yup; diff --git a/src/lib/utils/hooks/offer/useCreateOffers.tsx b/src/lib/utils/hooks/offer/useCreateOffers.tsx new file mode 100644 index 000000000..5adc2c611 --- /dev/null +++ b/src/lib/utils/hooks/offer/useCreateOffers.tsx @@ -0,0 +1,332 @@ +import { offers, subgraph } from "@bosonprotocol/react-kit"; +import { useMutation } from "react-query"; +import { useAccount } from "wagmi"; + +import { authTokenTypes } from "../../../../components/modal/components/CreateProfile/Lens/const"; +import { useModal } from "../../../../components/modal/useModal"; +import { TOKEN_TYPES } from "../../../../components/product/utils"; +import { poll } from "../../../../pages/create-product/utils"; +import { + buildCondition, + CommonTermsOfSale +} from "../../../../pages/create-product/utils/buildCondition"; +import { useCoreSDK } from "../../useCoreSdk"; +import { useAddPendingTransaction } from "../transactions/usePendingTransactions"; +import { useCurrentSellers } from "../useCurrentSellers"; + +type OfferFieldsFragment = subgraph.OfferFieldsFragment; + +type UseCreateOffersProps = { + offersToCreate: offers.CreateOfferArgs[]; + isMultiVariant: boolean; + tokenGatedInfo?: CommonTermsOfSale | null; + conditionDecimals?: number; + onGetExchangeTokenDecimals?: (decimals: number | undefined) => unknown; + onCreatedOffersWithVariants?: (arg0: { + firstOffer: OfferFieldsFragment; + }) => void; + onCreatedSingleOffers?: (arg0: { offer: OfferFieldsFragment }) => void; +}; + +export function useCreateOffers() { + const coreSDK = useCoreSDK(); + const { sellers } = useCurrentSellers(); + const { address } = useAccount(); + const { showModal, hideModal } = useModal(); + const addPendingTransaction = useAddPendingTransaction(); + const isMetaTx = Boolean(coreSDK.isMetaTxConfigSet && address); + return useMutation( + async ({ + offersToCreate, + isMultiVariant, + tokenGatedInfo, + conditionDecimals, + onGetExchangeTokenDecimals, + onCreatedOffersWithVariants, + onCreatedSingleOffers + }: UseCreateOffersProps) => { + const isTokenGated = !!tokenGatedInfo; + const onBeforeBuildCondition = async () => { + let decimalsLocal: number | undefined = conditionDecimals; + if ( + tokenGatedInfo?.tokenContract && + tokenGatedInfo.tokenType?.value === TOKEN_TYPES[0].value + ) { + try { + const { decimals: tokenDecimals } = + await coreSDK.getExchangeTokenInfo(tokenGatedInfo.tokenContract); + decimalsLocal = tokenDecimals; + onGetExchangeTokenDecimals?.(decimalsLocal); + } catch (error) { + decimalsLocal = undefined; + onGetExchangeTokenDecimals?.(decimalsLocal); + } + } + return decimalsLocal; + }; + showModal("WAITING_FOR_CONFIRMATION"); + const hasSellerAccount = !!sellers?.length; + const seller = address + ? { + operator: address, + admin: address, + treasury: address, + clerk: address, + contractUri: "ipfs://sample", + royaltyPercentage: "0", + authTokenId: "0", + authTokenType: authTokenTypes.NONE + } + : null; + let txResponse; + if (isMultiVariant) { + if (!hasSellerAccount && seller) { + if (isMetaTx) { + // createSeller with meta-transaction + const nonce = Date.now(); + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateSeller({ + createSellerArgs: seller, + nonce + }); + txResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + } else { + txResponse = await coreSDK.createSeller(seller); + } + showModal("TRANSACTION_SUBMITTED", { + action: "Create seller", + txHash: txResponse.hash + }); + addPendingTransaction({ + type: subgraph.EventType.SellerCreated, + hash: txResponse.hash, + isMetaTx, + accountType: "Seller" + }); + await txResponse.wait(); + showModal("WAITING_FOR_CONFIRMATION"); + } + if (isMetaTx) { + // createOfferBatch with meta-transaction + const nonce = Date.now(); + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateOfferBatch({ + createOffersArgs: offersToCreate, + nonce + }); + txResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + } else { + txResponse = await coreSDK.createOfferBatch(offersToCreate); + } + showModal("TRANSACTION_SUBMITTED", { + action: "Create offer with variants", + txHash: txResponse.hash + }); + addPendingTransaction({ + type: subgraph.EventType.OfferCreated, + hash: txResponse.hash, + isMetaTx, + accountType: "Seller" + }); + const txReceipt = await txResponse.wait(); + const offerIds = coreSDK.getCreatedOfferIdsFromLogs(txReceipt.logs); + + if (isTokenGated) { + showModal("WAITING_FOR_CONFIRMATION"); + const decimals = await onBeforeBuildCondition(); + const condition = buildCondition(tokenGatedInfo, decimals); + + if (isMetaTx) { + const nonce = Date.now(); + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateGroup({ + createGroupArgs: { offerIds, ...condition }, + nonce + }); + txResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + } else { + txResponse = await coreSDK.createGroup({ offerIds, ...condition }); + } + showModal("TRANSACTION_SUBMITTED", { + action: "Create condition group for offers", + txHash: txResponse.hash + }); + await txResponse.wait(); + } + let createdOffers: OfferFieldsFragment[] | null = null; + await poll( + async () => { + createdOffers = ( + await Promise.all( + offerIds.map((offerId) => + coreSDK.getOfferById(offerId as string) + ) + ) + ).filter((offer) => !!offer); + return createdOffers; + }, + (offers) => { + return offers.length !== offerIds.length; + }, + 500 + ); + const [firstOffer] = createdOffers as unknown as OfferFieldsFragment[]; + onCreatedOffersWithVariants?.({ + firstOffer + }); + } else { + // no variants + const [offerData] = offersToCreate; + if (isMetaTx) { + // meta-transaction + if (!hasSellerAccount && seller) { + // createSeller with meta-transaction + const nonce = Date.now(); + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateSeller({ + createSellerArgs: seller, + nonce + }); + const createSellerResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + showModal("TRANSACTION_SUBMITTED", { + action: "Create seller", + txHash: createSellerResponse.hash + }); + addPendingTransaction({ + type: subgraph.EventType.SellerCreated, + hash: createSellerResponse.hash, + isMetaTx, + accountType: "Seller" + }); + await createSellerResponse.wait(); + showModal("WAITING_FOR_CONFIRMATION"); + } + // createOffer with meta-transaction + const nonce = Date.now(); + if (isTokenGated) { + const decimals = await onBeforeBuildCondition(); + const condition = buildCondition(tokenGatedInfo, decimals); + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateOfferWithCondition({ + offerToCreate: offerData, + condition, + nonce + }); + txResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + } else { + const { r, s, v, functionName, functionSignature } = + await coreSDK.signMetaTxCreateOffer({ + createOfferArgs: offerData, + nonce + }); + txResponse = await coreSDK.relayMetaTransaction({ + functionName, + functionSignature, + sigR: r, + sigS: s, + sigV: v, + nonce + }); + } + } else { + // no meta tx + if (isTokenGated) { + const decimals = await onBeforeBuildCondition(); + const condition = buildCondition(tokenGatedInfo, decimals); + txResponse = + !hasSellerAccount && seller + ? await coreSDK.createSellerAndOfferWithCondition( + seller, + offerData, + condition + ) + : await coreSDK.createOfferWithCondition(offerData, condition); + } else { + txResponse = + !hasSellerAccount && seller + ? await coreSDK.createSellerAndOffer(seller, offerData) + : await coreSDK.createOffer(offerData); + } + } + showModal("TRANSACTION_SUBMITTED", { + action: "Create offer", + txHash: txResponse.hash + }); + + addPendingTransaction({ + type: subgraph.EventType.OfferCreated, + hash: txResponse.hash, + isMetaTx, + accountType: "Seller" + }); + + if (!hasSellerAccount && seller) { + addPendingTransaction({ + type: subgraph.EventType.SellerCreated, + hash: txResponse.hash, + isMetaTx, + accountType: "Seller" + }); + } + + const txReceipt = await txResponse.wait(); + const offerId = coreSDK.getCreatedOfferIdFromLogs(txReceipt.logs); + let createdOffer: OfferFieldsFragment | null = null; + await poll( + async () => { + createdOffer = await coreSDK.getOfferById(offerId as string); + return createdOffer; + }, + (offer) => { + return !offer; + }, + 500 + ); + if (!createdOffer) { + return; + } + + onCreatedSingleOffers?.({ + offer: createdOffer + }); + } + + hideModal(); + } + ); +} diff --git a/src/lib/utils/hooks/useDidMountEffect.ts b/src/lib/utils/hooks/useDidMountEffect.ts new file mode 100644 index 000000000..b43583c64 --- /dev/null +++ b/src/lib/utils/hooks/useDidMountEffect.ts @@ -0,0 +1,14 @@ +import { useEffect, useRef } from "react"; + +type Params = Parameters; + +// to have a 'useEffect' that only changes when dependencies change (instead of that + initial render) +export const useDidMountEffect = (func: Params[0], deps: Params[1]) => { + const didMount = useRef(false); + + useEffect(() => { + if (didMount.current) func(); + else didMount.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +}; diff --git a/src/lib/validation/index.ts b/src/lib/validation/index.ts index 5a4f9c452..781c39ca4 100644 --- a/src/lib/validation/index.ts +++ b/src/lib/validation/index.ts @@ -1,5 +1,4 @@ -// eslint-disable-next-line -// @ts-nocheck +import type { Dayjs } from "dayjs"; import * as Yup from "yup"; import disputePeriodValue from "./disputePeriodValue"; @@ -9,17 +8,21 @@ import isOfferValidityDatesValid from "./isOfferValidityDatesValid"; import isRedemptionDatesValid from "./isRedemptionDatesValid"; import returnPeriodValue from "./returnPeriodValue"; -Yup.addMethod( - Yup.mixed, +Yup.addMethod>>( + Yup.array, "isRedemptionDatesValid", isRedemptionDatesValid ); -Yup.addMethod( - Yup.mixed, +Yup.addMethod>>( + Yup.array, "isOfferValidityDatesValid", isOfferValidityDatesValid ); -Yup.addMethod(Yup.mixed, "isItBeforeNow", isItBeforeNow); +Yup.addMethod>>( + Yup.array, + "isItBeforeNow", + isItBeforeNow +); Yup.addMethod( Yup.string, "disputePeriodValue", diff --git a/src/lib/validation/isItBeforeNow.ts b/src/lib/validation/isItBeforeNow.ts index 0698fe25d..d0feb143b 100644 --- a/src/lib/validation/isItBeforeNow.ts +++ b/src/lib/validation/isItBeforeNow.ts @@ -3,6 +3,9 @@ import dayjs from "dayjs"; function isItBeforeNow() { return this.test("isItBeforeNow", function (value: (Dayjs | null)[]) { + if (!value) { + return false; + } const startBeforeNow = value[0] instanceof dayjs ? value[0]?.isBefore(dayjs()) diff --git a/src/lib/validation/isOfferValidityDatesValid.ts b/src/lib/validation/isOfferValidityDatesValid.ts index 3fe313e06..e1b471d7b 100644 --- a/src/lib/validation/isOfferValidityDatesValid.ts +++ b/src/lib/validation/isOfferValidityDatesValid.ts @@ -5,6 +5,9 @@ function isOfferValidityDatesValid() { return this.test( "isOfferValidityDatesValid", function (value: (Dayjs | null)[]) { + if (!value) { + return false; + } const rpValue: (Dayjs | null)[] = this.parent.redemptionPeriod; const doesItEndBefore = rpValue[1] instanceof dayjs diff --git a/src/lib/validation/isRedemptionDatesValid.ts b/src/lib/validation/isRedemptionDatesValid.ts index 6b798638d..76c55bf83 100644 --- a/src/lib/validation/isRedemptionDatesValid.ts +++ b/src/lib/validation/isRedemptionDatesValid.ts @@ -3,8 +3,11 @@ import dayjs from "dayjs"; function isRedemptionDatesValid() { return this.test( - "isOfferValidityDatesValid", + "isRedemptionDatesValid", function (value: (Dayjs | null)[]) { + if (!value) { + return false; + } const ovValue = this.parent.offerValidityPeriod; const doesItEndBefore = value[1] instanceof dayjs diff --git a/src/pages/chat/ChatProvider/ChatContext.ts b/src/pages/chat/ChatProvider/ChatContext.ts index a544db8ff..2456a12fe 100644 --- a/src/pages/chat/ChatProvider/ChatContext.ts +++ b/src/pages/chat/ChatProvider/ChatContext.ts @@ -5,10 +5,12 @@ export const Context = createContext<{ bosonXmtp: BosonXmtpClient | undefined; initialize: Dispatch>; envName: string; + error: unknown; isInitializing: boolean; }>({ bosonXmtp: undefined, initialize: () => console.log("initialize has not been defined"), + error: null, envName: "", isInitializing: false }); diff --git a/src/pages/chat/ChatProvider/ChatProvider.tsx b/src/pages/chat/ChatProvider/ChatProvider.tsx index 11bfee8e6..248262568 100644 --- a/src/pages/chat/ChatProvider/ChatProvider.tsx +++ b/src/pages/chat/ChatProvider/ChatProvider.tsx @@ -14,6 +14,7 @@ export default function ChatProvider({ children }: Props) { const { data: signer } = useSigner(); const [initialize, setInitialized] = useState(0); const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(); const [bosonXmtp, setBosonXmtp] = useState(); useEffect(() => { if (signer && initialize && !bosonXmtp) { @@ -25,8 +26,12 @@ export default function ChatProvider({ children }: Props) { ) .then((bosonClient) => { setBosonXmtp(bosonClient); + setError(null); + }) + .catch((error) => { + console.error(error); + setError(error); }) - .catch(console.error) .finally(() => setLoading(false)); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -38,6 +43,7 @@ export default function ChatProvider({ children }: Props) { initialize: () => { setInitialized((prev) => prev + 1); }, + error, envName, isInitializing: isLoading }} diff --git a/src/pages/chat/components/ExchangeSidePreview.tsx b/src/pages/chat/components/ExchangeSidePreview.tsx index 9651d24a2..d00d8f9a8 100644 --- a/src/pages/chat/components/ExchangeSidePreview.tsx +++ b/src/pages/chat/components/ExchangeSidePreview.tsx @@ -418,7 +418,7 @@ export default function ExchangeSidePreview({
- {isInDispute && iAmTheBuyer && !isEscalated && !isRetracted ? ( + {isInDispute && iAmTheBuyer && !isRetracted ? ( Retract - - showModal( - "ESCALATE_MODAL", - { - title: "Escalate", - exchange: exchange, - refetch: refetchItAll - }, - "l" - ) - } - > - Escalate - + {!isEscalated ? ( + + showModal( + "ESCALATE_MODAL", + { + title: "Escalate", + exchange: exchange, + refetch: refetchItAll + }, + "l" + ) + } + > + Escalate + + ) : ( + <> + )} ) : isInRedeemed && iAmTheBuyer ? ( diff --git a/src/pages/create-product/CreateProduct.tsx b/src/pages/create-product/CreateProduct.tsx index 83ec0f0fa..259136338 100644 --- a/src/pages/create-product/CreateProduct.tsx +++ b/src/pages/create-product/CreateProduct.tsx @@ -24,12 +24,21 @@ function CreateProduct() { const showCreateProductDraftModal = useCallback(() => { if (store.shouldDisplayModal) { - showModal(modalTypes.CREATE_PRODUCT_DRAFT, { - title: "Draft", - chooseNew, - chooseDraft, - closable: false - }); + showModal( + modalTypes.CREATE_PRODUCT_DRAFT, + { + title: "Draft", + chooseNew, + chooseDraft, + closable: false + }, + "auto", + undefined, + { + xs: "100%", + s: "31.25rem" + } + ); } else { setDraftModalClosed(true); } diff --git a/src/pages/create-product/CreateProductInner.tsx b/src/pages/create-product/CreateProductInner.tsx index 914ca4436..b64336d43 100644 --- a/src/pages/create-product/CreateProductInner.tsx +++ b/src/pages/create-product/CreateProductInner.tsx @@ -8,7 +8,6 @@ import { subgraph } from "@bosonprotocol/react-kit"; import { parseUnits } from "@ethersproject/units"; -import type { Dayjs } from "dayjs"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { Form, Formik, FormikHelpers, FormikProps } from "formik"; @@ -21,13 +20,11 @@ import { generatePath, useLocation, useNavigate } from "react-router-dom"; import uuid from "react-uuid"; import { useAccount } from "wagmi"; dayjs.extend(localizedFormat); - import { BigNumber, ethers } from "ethers"; import { useEffect } from "react"; import { Token } from "../../components/convertion-rate/ConvertionRateContext"; import { FileProps } from "../../components/form/Upload/WithUploadToIpfs"; -import { authTokenTypes } from "../../components/modal/components/CreateProfile/Lens/const"; import { getLensCoverPictureUrl, getLensProfilePictureUrl, @@ -51,7 +48,7 @@ import { ProductRoutes } from "../../lib/routing/routes"; import { fetchIpfsBase64Media } from "../../lib/utils/base64"; import { useChatStatus } from "../../lib/utils/hooks/chat/useChatStatus"; import { Profile } from "../../lib/utils/hooks/lens/graphql/generated"; -import { useAddPendingTransaction } from "../../lib/utils/hooks/transactions/usePendingTransactions"; +import { useCreateOffers } from "../../lib/utils/hooks/offer/useCreateOffers"; import { useCurrentSellers } from "../../lib/utils/hooks/useCurrentSellers"; import { useIpfsStorage } from "../../lib/utils/hooks/useIpfsStorage"; import { useKeepQueryParamsNavigate } from "../../lib/utils/hooks/useKeepQueryParamsNavigate"; @@ -66,8 +63,7 @@ import { MultiStepsContainer, ProductLayoutContainer } from "./CreateProductInner.styles"; -import { createProductSteps, FIRST_STEP, poll } from "./utils"; -import { buildCondition } from "./utils/buildCondition"; +import { createProductSteps, FIRST_STEP } from "./utils"; import { validateDates } from "./utils/dataValidator"; import { CreateProductSteps } from "./utils/index"; @@ -394,7 +390,7 @@ function CreateProductInner({ [history, location, setCurrentStep] ); - const onCreateNewProject = () => { + const onCreateNew = () => { hideModal(); setCurrentStepWithHistory(FIRST_STEP); setIsPreviewVisible(false); @@ -415,10 +411,7 @@ function CreateProductInner({ const { address } = useAccount(); const { sellers, lens: lensProfiles } = useCurrentSellers(); - const addPendingTransaction = useAddPendingTransaction(); - - const hasSellerAccount = !!sellers?.length; - + const { mutateAsync: createOffers } = useCreateOffers(); const currentOperator = sellers.find((seller) => { return seller?.operator.toLowerCase() === address?.toLowerCase(); }); @@ -482,7 +475,7 @@ function CreateProductInner({ }, hasMultipleVariants: !!values.productVariants.variants.length, // these are the ones that we already had before - onCreateNewProject: onCreateNewProject, + onCreateNew: onCreateNew, onViewMyItem: () => onViewMyItem(metadataInfo.product?.uuid) }, "auto" @@ -872,330 +865,47 @@ function CreateProductInner({ }); offersToCreate.push(offerData); } - - showModal("WAITING_FOR_CONFIRMATION"); - const isMetaTx = Boolean(coreSDK.isMetaTxConfigSet && address); - const seller = address - ? { - operator: address, - admin: address, - treasury: address, - clerk: address, - contractUri: "ipfs://sample", - royaltyPercentage: "0", - authTokenId: "0", - authTokenType: authTokenTypes.NONE - } - : null; const isTokenGated = commonTermsOfSale.tokenGatedOffer.value === "true"; - let txResponse; - if (isMultiVariant) { - if (!hasSellerAccount && seller) { - if (isMetaTx) { - // createSeller with meta-transaction - const nonce = Date.now(); - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateSeller({ - createSellerArgs: seller, - nonce - }); - txResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - } else { - txResponse = await coreSDK.createSeller(seller); - } - showModal("TRANSACTION_SUBMITTED", { - action: "Create seller", - txHash: txResponse.hash - }); - addPendingTransaction({ - type: subgraph.EventType.SellerCreated, - hash: txResponse.hash, - isMetaTx, - accountType: "Seller" - }); - await txResponse.wait(); - showModal("WAITING_FOR_CONFIRMATION"); - } - if (isMetaTx) { - // createOfferBatch with meta-transaction - const nonce = Date.now(); - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateOfferBatch({ - createOffersArgs: offersToCreate, - nonce - }); - txResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - } else { - txResponse = await coreSDK.createOfferBatch(offersToCreate); - } - showModal("TRANSACTION_SUBMITTED", { - action: "Create offer with variants", - txHash: txResponse.hash - }); - addPendingTransaction({ - type: subgraph.EventType.OfferCreated, - hash: txResponse.hash, - isMetaTx, - accountType: "Seller" - }); - const txReceipt = await txResponse.wait(); - const offerIds = coreSDK.getCreatedOfferIdsFromLogs(txReceipt.logs); - - if (isTokenGated) { - showModal("WAITING_FOR_CONFIRMATION"); - if ( - commonTermsOfSale?.tokenContract && - commonTermsOfSale.tokenType?.value === TOKEN_TYPES[0].value - ) { - try { - const { decimals: decimalsLocal } = - await coreSDK.getExchangeTokenInfo( - commonTermsOfSale.tokenContract - ); - setDecimals(decimalsLocal); - } catch (error) { - setDecimals(undefined); - } - } - const condition = buildCondition(commonTermsOfSale, decimals); - - if (isMetaTx) { - const nonce = Date.now(); - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateGroup({ - createGroupArgs: { offerIds, ...condition }, - nonce - }); - txResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - } else { - txResponse = await coreSDK.createGroup({ offerIds, ...condition }); - } - showModal("TRANSACTION_SUBMITTED", { - action: "Create condition group for offers", - txHash: txResponse.hash - }); - await txResponse.wait(); - } - let createdOffers: OfferFieldsFragment[] | null = null; - await poll( - async () => { - createdOffers = ( - await Promise.all( - offerIds.map((offerId) => - coreSDK.getOfferById(offerId as string) - ) - ) - ).filter((offer) => !!offer); - return createdOffers; - }, - (offers) => { - return offers.length !== offerIds.length; - }, - 500 - ); - const [firstOffer] = createdOffers as unknown as OfferFieldsFragment[]; - toast((t) => ( - { - handleOpenSuccessModal({ - offerInfo: firstOffer || ({} as subgraph.OfferFieldsFragment), - values - }); - }} - /> - )); - } else { - const [offerData] = offersToCreate; - if (isMetaTx) { - // meta-transaction - if (!hasSellerAccount && seller) { - // createSeller with meta-transaction - const nonce = Date.now(); - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateSeller({ - createSellerArgs: seller, - nonce - }); - const createSellerResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - showModal("TRANSACTION_SUBMITTED", { - action: "Create seller", - txHash: createSellerResponse.hash - }); - addPendingTransaction({ - type: subgraph.EventType.SellerCreated, - hash: createSellerResponse.hash, - isMetaTx, - accountType: "Seller" - }); - await createSellerResponse.wait(); - showModal("WAITING_FOR_CONFIRMATION"); - } - // createOffer with meta-transaction - const nonce = Date.now(); - if (!isTokenGated) { - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateOffer({ - createOfferArgs: offerData, - nonce - }); - txResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - } else { - if ( - commonTermsOfSale?.tokenContract && - commonTermsOfSale.tokenType?.value === TOKEN_TYPES[0].value - ) { - try { - const { decimals: decimalsLocal } = - await coreSDK.getExchangeTokenInfo( - commonTermsOfSale.tokenContract - ); - setDecimals(decimalsLocal); - } catch (error) { - setDecimals(undefined); - } - } - const condition = buildCondition(commonTermsOfSale, decimals); - const { r, s, v, functionName, functionSignature } = - await coreSDK.signMetaTxCreateOfferWithCondition({ - offerToCreate: offerData, - condition, - nonce - }); - txResponse = await coreSDK.relayMetaTransaction({ - functionName, - functionSignature, - sigR: r, - sigS: s, - sigV: v, - nonce - }); - } - } else { - if (isTokenGated) { - if ( - commonTermsOfSale?.tokenContract && - commonTermsOfSale.tokenType?.value === TOKEN_TYPES[0].value - ) { - try { - const { decimals: decimalsLocal } = - await coreSDK.getExchangeTokenInfo( - commonTermsOfSale.tokenContract - ); - - setDecimals(decimalsLocal); - } catch (error) { - setDecimals(undefined); - } - } - const condition = buildCondition(commonTermsOfSale, decimals); - txResponse = - !hasSellerAccount && seller - ? await coreSDK.createSellerAndOfferWithCondition( - seller, - offerData, - condition - ) - : await coreSDK.createOfferWithCondition(offerData, condition); - } else { - txResponse = - !hasSellerAccount && seller - ? await coreSDK.createSellerAndOffer(seller, offerData) - : await coreSDK.createOffer(offerData); - } - } - showModal("TRANSACTION_SUBMITTED", { - action: "Create offer", - txHash: txResponse.hash - }); - - addPendingTransaction({ - type: subgraph.EventType.OfferCreated, - hash: txResponse.hash, - isMetaTx, - accountType: "Seller" - }); - - if (!hasSellerAccount && seller) { - addPendingTransaction({ - type: subgraph.EventType.SellerCreated, - hash: txResponse.hash, - isMetaTx, - accountType: "Seller" - }); - } - - const txReceipt = await txResponse.wait(); - const offerId = coreSDK.getCreatedOfferIdFromLogs(txReceipt.logs); - let createdOffer: OfferFieldsFragment | null = null; - await poll( - async () => { - createdOffer = await coreSDK.getOfferById(offerId as string); - return createdOffer; - }, - (offer) => { - return !offer; - }, - 500 - ); - if (!createdOffer) { - return; + await createOffers({ + isMultiVariant, + offersToCreate, + tokenGatedInfo: isTokenGated ? commonTermsOfSale : null, + conditionDecimals: decimals, + onGetExchangeTokenDecimals: setDecimals, + onCreatedOffersWithVariants: ({ firstOffer }) => { + toast((t) => ( + { + handleOpenSuccessModal({ + offerInfo: firstOffer || ({} as subgraph.OfferFieldsFragment), + values + }); + }} + /> + )); + }, + onCreatedSingleOffers: ({ offer: createdOffer }) => { + toast((t) => ( + { + handleOpenSuccessModal({ + offerInfo: + createdOffer || ({} as subgraph.OfferFieldsFragment), + values + }); + }} + /> + )); } - toast((t) => ( - { - handleOpenSuccessModal({ - offerInfo: createdOffer || ({} as subgraph.OfferFieldsFragment), - values - }); - }} - /> - )); - } - - hideModal(); + }); } catch (error: any) { // TODO: FAILURE MODAL console.error("error->", error.errors ?? error); - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } }; @@ -1221,11 +931,11 @@ function CreateProductInner({ [coreTermsOfSaleKey]: { ...values[coreTermsOfSaleKey], redemptionPeriod: - values?.[coreTermsOfSaleKey]?.redemptionPeriod?.map((d: Dayjs) => + values?.[coreTermsOfSaleKey]?.redemptionPeriod?.map((d) => dayjs(d).format() ) ?? [], offerValidityPeriod: - values?.[coreTermsOfSaleKey]?.offerValidityPeriod?.map((d: Dayjs) => + values?.[coreTermsOfSaleKey]?.offerValidityPeriod?.map((d) => dayjs(d).format() ) ?? [] } diff --git a/src/pages/create-product/utils/buildCondition.ts b/src/pages/create-product/utils/buildCondition.ts index 508994bf0..24e2fab9a 100644 --- a/src/pages/create-product/utils/buildCondition.ts +++ b/src/pages/create-product/utils/buildCondition.ts @@ -7,10 +7,22 @@ import { utils } from "ethers"; import { CreateProductForm } from "../../../components/product/utils"; +type JointTermsOfSale = + | CreateProductForm["coreTermsOfSale"] + | CreateProductForm["variantsCoreTermsOfSale"]; + +export type CommonTermsOfSale = Pick< + JointTermsOfSale, + | "tokenId" + | "minBalance" + | "tokenType" + | "tokenCriteria" + | "tokenContract" + | "maxCommits" +>; + export const buildCondition = ( - commonTermsOfSale: - | CreateProductForm["coreTermsOfSale"] - | CreateProductForm["variantsCoreTermsOfSale"], + commonTermsOfSale: CommonTermsOfSale, decimals?: number ): ConditionStruct => { let tokenType: TokenType = TokenType.FungibleToken; diff --git a/src/pages/create-product/utils/dataValidator.ts b/src/pages/create-product/utils/dataValidator.ts index 8d093edce..fb37321f4 100644 --- a/src/pages/create-product/utils/dataValidator.ts +++ b/src/pages/create-product/utils/dataValidator.ts @@ -1,3 +1,5 @@ +import type { Dayjs } from "dayjs"; + interface ReturnValues { validFromDateInMS: number; validUntilDateInMS: number; @@ -8,34 +10,30 @@ export const validateDates = ({ offerValidityPeriod, redemptionPeriod }: { - offerValidityPeriod: Array<{ $d: string }>; - redemptionPeriod: Array<{ $d: string }>; + offerValidityPeriod: Array; + redemptionPeriod: Array; }): ReturnValues => { const now = Date.now(); const numberMinutesAdd = 5; - let validFromDateInMS = Date.parse(offerValidityPeriod[0].$d); - + let validFromDateInMS = offerValidityPeriod[0].toDate().getTime(); if (validFromDateInMS < now) { validFromDateInMS = new Date(now + numberMinutesAdd * 60000).getTime(); } - let validUntilDateInMS = Date.parse(offerValidityPeriod[1].$d); - + let validUntilDateInMS = offerValidityPeriod[1].toDate().getTime(); if (validUntilDateInMS < now) { validUntilDateInMS = new Date(now + numberMinutesAdd * 2 * 60000).getTime(); } - let voucherRedeemableFromDateInMS = Date.parse(redemptionPeriod[0].$d); - + let voucherRedeemableFromDateInMS = redemptionPeriod[0].toDate().getTime(); if (voucherRedeemableFromDateInMS < now) { voucherRedeemableFromDateInMS = new Date( now + numberMinutesAdd * 60000 ).getTime(); } - let voucherRedeemableUntilDateInMS = Date.parse(redemptionPeriod[1].$d); - + let voucherRedeemableUntilDateInMS = redemptionPeriod[1].toDate().getTime(); if (voucherRedeemableUntilDateInMS < now) { voucherRedeemableUntilDateInMS = new Date( now + numberMinutesAdd * 2 * 60000 diff --git a/src/pages/create-product/utils/index.tsx b/src/pages/create-product/utils/index.tsx index 46cd56709..32773d878 100644 --- a/src/pages/create-product/utils/index.tsx +++ b/src/pages/create-product/utils/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import ConfirmProductDetails from "../../../components/product/ConfirmProductDetails"; -import CoreTermsOfSale from "../../../components/product/CoreTermsOfSale"; +import CoreTermsOfSale from "../../../components/product/coreTermsOfSale/CoreTermsOfSale"; import ProductImages from "../../../components/product/ProductImages"; import ProductInformation from "../../../components/product/ProductInformation"; import ProductType from "../../../components/product/ProductType"; diff --git a/src/pages/custom-store/store-fields.ts b/src/pages/custom-store/store-fields.ts index dd9a16a37..51066127c 100644 --- a/src/pages/custom-store/store-fields.ts +++ b/src/pages/custom-store/store-fields.ts @@ -190,6 +190,10 @@ export const formModel = { { label: "Barlow", value: "barlow" // defined in src/lib/styles/GlobalStyle.tsx + }, + { + label: "Neuropolitical", + value: "neuropolitical_rg" // defined in src/lib/styles/GlobalStyle.tsx } ] }, diff --git a/src/pages/dispute-centre/DisputeCentre.tsx b/src/pages/dispute-centre/DisputeCentre.tsx index a8dc4f813..a2978dff4 100644 --- a/src/pages/dispute-centre/DisputeCentre.tsx +++ b/src/pages/dispute-centre/DisputeCentre.tsx @@ -303,7 +303,7 @@ function DisputeCentre() { (error as unknown as { code: string }).code === "ACTION_REJECTED"; if (hasUserRejectedTx) { - showModal("CONFIRMATION_FAILED"); + showModal("TRANSACTION_FAILED"); } setSubmitError(error as Error); diff --git a/src/pages/profile/seller/SellerSocial.tsx b/src/pages/profile/seller/SellerSocial.tsx index eab5eb813..706e45e02 100644 --- a/src/pages/profile/seller/SellerSocial.tsx +++ b/src/pages/profile/seller/SellerSocial.tsx @@ -7,6 +7,7 @@ import { Profile, ProfileFieldsFragment } from "../../../lib/utils/hooks/lens/graphql/generated"; +import { sanitizeUrl } from "../../../lib/utils/url"; import { preAppendHttps } from "../../../lib/validation/regex/url"; import { DetailShareWrapper, @@ -26,7 +27,12 @@ function RenderSocial({ icon: Icon }: RenderSocialProps) { return ( - + {Icon ? Icon : } ); @@ -60,7 +66,7 @@ export default function SellerSocial({ {/* TODO: Removed as we don't have discord in lens profile */} {/* */} {lensUrl !== false && ( - } href={lensUrl} /> + } href={sanitizeUrl(lensUrl)} /> )}