diff --git a/src/components/form/Select.tsx b/src/components/form/Select.tsx index fe8a6ad72..75b64eee4 100644 --- a/src/components/form/Select.tsx +++ b/src/components/form/Select.tsx @@ -64,7 +64,7 @@ const customStyles = (error: any): StylesConfig => ({ zIndex: state.isFocused ? zIndex.Select + 1 : zIndex.Select, position: "relative", width: "100%", - height: "25px" + minHeight: "25px" }), option: (provided: any, state: any) => ({ ...provided, diff --git a/src/components/modal/components/Profile/Lens/validationSchema.ts b/src/components/modal/components/Profile/Lens/validationSchema.ts index 9984d2835..e424ebb95 100644 --- a/src/components/modal/components/Profile/Lens/validationSchema.ts +++ b/src/components/modal/components/Profile/Lens/validationSchema.ts @@ -4,7 +4,7 @@ import { lensHandleMaxLength } from "../../../../../lib/config"; import { validationMessage } from "../../../../../lib/constants/validationMessage"; import { FileProps } from "../../../../form/Upload/types"; import { validationOfRequiredIpfsImage } from "../../../../product/utils/validationUtils"; -import { getCommonFieldsValidation } from "../valitationSchema"; +import { getCommonFieldsValidation } from "../validationSchema"; // const MAX_LOGO_SIZE = 300 * 1024; // 300 KB const maxLensHandleLength = 31 - lensHandleMaxLength; diff --git a/src/components/modal/components/Profile/ProfileFormFields.tsx b/src/components/modal/components/Profile/ProfileFormFields.tsx index c048debe4..a547e5aa9 100644 --- a/src/components/modal/components/Profile/ProfileFormFields.tsx +++ b/src/components/modal/components/Profile/ProfileFormFields.tsx @@ -3,7 +3,6 @@ import { useAccount } from "lib/utils/hooks/connection/connection"; import { ReactNode } from "react"; import { getIpfsGatewayUrl } from "../../../../lib/utils/ipfs"; -import { websitePattern } from "../../../../lib/validation/regex/url"; import { FormField, Input, Select, Textarea, Upload } from "../../../form"; import { CreateProfile, @@ -128,7 +127,6 @@ export function ProfileFormFields({ { + describe("test valid urls", () => { + test.each(validUrls)("%s is a valid url", (...urls) => { + urls.forEach((url) => { + expect(checkValidUrl(url, { addHttpPrefix: true })).toBe(true); + }); + }); + }); + + describe("preAppendHttps", () => { + test("url starts with https:// if it did not", () => { + const url = "example.com"; + expect(preAppendHttps(url)).toBe(`https://${url}`); + }); + test("url is not modified if it already starts with https://", () => { + const url = "https://example.com"; + expect(preAppendHttps(url)).toBe(url); + }); + test("url is not modified if it already starts with http://", () => { + const url = "http://example.com"; + expect(preAppendHttps(url)).toBe(url); + }); + }); +}); diff --git a/src/lib/validation/regex/url.ts b/src/lib/validation/regex/url.ts index acb08eb5f..e8696863b 100644 --- a/src/lib/validation/regex/url.ts +++ b/src/lib/validation/regex/url.ts @@ -1,5 +1,5 @@ export const socialLinkPattern = - "^(http://|https://)?(www.)?([a-zA-Z0-9]+).[a-zA-Z0-9]*.[a-z]{1}.([-a-z-0-9:_+.?/@]+)?$"; + "^(http://|https://)?(www.)?([a-zA-Z0-9]+).[a-zA-Z0-9]*.[a-z]{1}.([-a-z-A-Z-0-9:_+.?/@#%&=]+)?$"; export const websitePattern = socialLinkPattern; @@ -8,3 +8,15 @@ export const preAppendHttps = (url: string) => { ? url : `https://${url}`; }; + +export const checkValidUrl = ( + url: string, + { addHttpPrefix }: { addHttpPrefix: boolean } = { addHttpPrefix: true } +) => { + try { + new URL(addHttpPrefix ? preAppendHttps(url) : url); + return true; + } catch (err) { + return false; + } +}; diff --git a/src/pages/custom-store/CustomStoreFormContent.tsx b/src/pages/custom-store/CustomStoreFormContent.tsx index e801c41a6..7f8645c71 100644 --- a/src/pages/custom-store/CustomStoreFormContent.tsx +++ b/src/pages/custom-store/CustomStoreFormContent.tsx @@ -111,7 +111,9 @@ export const formValuesWithOneLogoUrl = ( } return { value: val.value.trim(), - url: preAppendHttps((val.url as string)?.trim()), + url: preAppendHttps( + `${val.prefix ?? ""}${(val.url as string)?.trim() ?? ""}` + ), label: val.label.trim() }; } @@ -132,7 +134,7 @@ export const formValuesWithOneLogoUrl = ( ) { return { label: val.label, - value: preAppendHttps(val.value || "") + value: preAppendHttps((val.value as string) || "") }; } return null; @@ -158,6 +160,7 @@ export default function CustomStoreFormContent({ hasSubmitError }: Props) { const { showModal } = useModal(); const { setFieldValue, values, isValid, setValues, isSubmitting } = useFormikContext(); + const { sellerIds } = useCurrentSellers(); const queryParams = new URLSearchParams( @@ -387,9 +390,13 @@ export default function CustomStoreFormContent({ hasSubmitError }: Props) { } ); if (option) { + const urlWithoutPrefix = + socialMediaObject.url.startsWith(option.prefix) + ? socialMediaObject.url.replace(option.prefix, "") + : socialMediaObject.url; return { ...option, - url: socialMediaObject.url + url: urlWithoutPrefix }; } return null; diff --git a/src/pages/custom-store/SocialMediaLinks.tsx b/src/pages/custom-store/SocialMediaLinks.tsx index 8c207b5cb..8933e56aa 100644 --- a/src/pages/custom-store/SocialMediaLinks.tsx +++ b/src/pages/custom-store/SocialMediaLinks.tsx @@ -9,12 +9,7 @@ import Typography from "../../components/ui/Typography"; import SocialLogo from "./SocialLogo"; import { storeFields } from "./store-fields"; import { StoreFormFields } from "./store-fields-types"; -import { - firstSubFieldBasis, - gap, - logoSize, - secondSubFieldBasis -} from "./styles"; +import { firstSubFieldBasis, gap, logoSize } from "./styles"; const Global = createGlobalStyle` .dragged { @@ -54,7 +49,7 @@ const SocialMediaLinks: React.FC = ({ Logo - + URL @@ -62,23 +57,29 @@ const SocialMediaLinks: React.FC = ({ {(links || []).map((selection, index) => { - const { label, value } = selection || {}; - + const { label, value, prefix } = selection || {}; return ( - + - + {prefix} + + + diff --git a/src/pages/custom-store/store-fields-types.ts b/src/pages/custom-store/store-fields-types.ts index 5a7fb6f88..024598d6d 100644 --- a/src/pages/custom-store/store-fields-types.ts +++ b/src/pages/custom-store/store-fields-types.ts @@ -26,8 +26,13 @@ export type StoreFields = { navigationBarPosition?: SelectType; copyright: string; showFooter?: SelectType; - socialMediaLinks?: SelectType[]; - contactInfoLinks?: SelectType[]; + socialMediaLinks?: (SelectType & { + url: string; + prefix: string; + })[]; + contactInfoLinks?: (SelectType & { + text: string; + })[]; additionalFooterLinks?: AdditionalFooterLink[]; withOwnProducts?: SelectType<"mine" | "all" | "custom">; sellerCurationList?: string; diff --git a/src/pages/custom-store/store-fields.ts b/src/pages/custom-store/store-fields.ts index 31e854232..ac5508493 100644 --- a/src/pages/custom-store/store-fields.ts +++ b/src/pages/custom-store/store-fields.ts @@ -3,12 +3,10 @@ import * as Yup from "yup"; import { SelectDataProps } from "../../components/form/types"; import { colors } from "../../lib/styles/colors"; -import { - socialLinkPattern, - websitePattern -} from "../../lib/validation/regex/url"; +import { checkValidUrl } from "../../lib/validation/regex/url"; import { validationOfFile } from "../chat/components/UploadForm/const"; import { AdditionalFooterLink } from "./AdditionalFooterLinksTypes"; +import { StoreFields } from "./store-fields-types"; export const storeFields = { isCustomStoreFront: "isCustomStoreFront", @@ -214,17 +212,72 @@ export const formModel = { requiredErrorMessage: standardRequiredErrorMessage, placeholder: "", options: [ - { label: "Facebook", value: "facebook", url: "" }, - { label: "Instagram", value: "instagram", url: "" }, - { label: "LinkedIn", value: "linkedin", url: "" }, - { label: "Medium", value: "medium", url: "" }, - { label: "Pinterest", value: "pinterest", url: "" }, - { label: "Reddit", value: "reddit", url: "" }, - { label: "Snapchat", value: "snapchat", url: "" }, - { label: "TikTok", value: "tiktok", url: "" }, - { label: "Twitch", value: "twitch", url: "" }, - { label: "Twitter", value: "twitter", url: "" }, - { label: "Youtube", value: "youtube", url: "" } + { + label: "Facebook", + value: "facebook", + url: "", + prefix: "https://www.facebook.com/" + }, + { + label: "Instagram", + value: "instagram", + url: "", + prefix: "https://www.instagram.com/" + }, + { + label: "LinkedIn", + value: "linkedin", + url: "", + prefix: "https://www.linkedin.com/" + }, + { + label: "Medium", + value: "medium", + url: "", + prefix: "https://medium.com/" + }, + { + label: "Pinterest", + value: "pinterest", + url: "", + prefix: "https://www.pinterest.com/" + }, + { + label: "Reddit", + value: "reddit", + url: "", + prefix: "https://www.reddit.com/" + }, + { + label: "Snapchat", + value: "snapchat", + url: "", + prefix: "https://www.snapchat.com/" + }, + { + label: "TikTok", + value: "tiktok", + url: "", + prefix: "https://www.tiktok.com/" + }, + { + label: "Twitch", + value: "twitch", + url: "", + prefix: "https://www.twitch.tv/" + }, + { + label: "Twitter", + value: "twitter", + url: "", + prefix: "https://twitter.com/" + }, + { + label: "Youtube", + value: "youtube", + url: "", + prefix: "https://www.youtube.com/" + } ] }, [storeFields.contactInfoLinks]: { @@ -377,9 +430,13 @@ export const validationSchema = Yup.object({ Yup.object({ label: Yup.string().required(standardRequiredErrorMessage), value: Yup.string().required(standardRequiredErrorMessage), + prefix: Yup.string().required(standardRequiredErrorMessage), url: Yup.string() - .matches(new RegExp(socialLinkPattern), notUrlErrorMessage) + .trim() .required(standardRequiredErrorMessage) + .test("FORMAT", notUrlErrorMessage, (value, { parent }) => { + return checkValidUrl(`${parent.prefix}${value ?? ""}`); + }) }) ), [storeFields.contactInfoLinks]: Yup.array( @@ -394,11 +451,15 @@ export const validationSchema = Yup.object({ label: Yup.string().required(standardRequiredErrorMessage), value: Yup.string(), url: Yup.string() - .matches(new RegExp(websitePattern), notUrlErrorMessage) + .test("FORMAT", notUrlErrorMessage, (value) => + checkValidUrl(value ?? "") + ) .when("label", (label) => { if (label) { return Yup.string() - .matches(new RegExp(websitePattern), notUrlErrorMessage) + .test("FORMAT", notUrlErrorMessage, (value) => + checkValidUrl(value ?? "") + ) .required(standardRequiredErrorMessage); } return Yup.string(); @@ -419,9 +480,10 @@ export const validationSchema = Yup.object({ .test("FORMAT", "Must be an address", (value) => value ? ethers.utils.isAddress(value) : true ), - [storeFields.openseaLinkToOriginalMainnetCollection]: Yup.string().matches( - new RegExp(websitePattern), - notUrlErrorMessage + [storeFields.openseaLinkToOriginalMainnetCollection]: Yup.string().test( + "FORMAT", + notUrlErrorMessage, + (value) => (value ? checkValidUrl(value) : true) ), [storeFields.metaTransactionsApiKey]: Yup.string() // NOTE: we may wish to show it again in the future @@ -469,15 +531,18 @@ export const initialValues: Yup.InferType = { [storeFields.showFooter]: formModel.formFields.showFooter.options.find( (option) => "default" in option && option.default ) as SelectDataProps, - [storeFields.socialMediaLinks]: [] as (SelectDataProps & { url: string })[], - [storeFields.contactInfoLinks]: [] as (SelectDataProps & { text: string })[], + [storeFields.socialMediaLinks]: [] as NonNullable< + StoreFields["socialMediaLinks"] + >, + [storeFields.contactInfoLinks]: [] as NonNullable< + StoreFields["contactInfoLinks"] + >, [storeFields.additionalFooterLinks]: [] as AdditionalFooterLink[], [storeFields.copyright]: "", [storeFields.withOwnProducts]: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion formModel.formFields.withOwnProducts.options.find( (option) => "default" in option && option.default - )!, + ) ?? formModel.formFields.withOwnProducts.options[0], [storeFields.sellerCurationList]: "", [storeFields.offerCurationList]: "", [storeFields.commitProxyAddress]: "",