From 3fa984da5fac7fc1fc7ba2b3c61c2ed80374c0b3 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:24:34 -0600 Subject: [PATCH 01/13] promote address form fields to generic component --- .../Components/Form/Utils/initialValues.ts | 2 +- .../Auction/Hooks/useCreateTokenAndSubmit.ts | 2 +- src/Apps/Invoice/Components/AddressForm.tsx | 2 +- .../Invoice/Components/InvoicePaymentForm.tsx | 2 +- .../Invoice/Hooks/useCreateTokenAndSubmit.ts | 2 +- .../Order/Components/CreditCardPicker.tsx | 4 +- .../CreditCardPicker.jest.enzyme.enzyme.tsx | 3 +- .../Utils/__tests__/formValidators.jest.tsx | 2 +- src/Apps/Order/Utils/formValidators.tsx | 2 +- .../Address/AddressAutocompleteInput.tsx | 2 +- src/Components/Address/AddressForm.tsx | 35 +--- src/Components/Address/AddressFormFields.tsx | 178 ++++++++++++++++++ .../Address/useAddressAutocomplete.ts | 2 +- src/Components/Address/utils.ts | 35 ++++ src/Components/__tests__/Utils/addressForm.ts | 2 +- .../__tests__/Utils/addressForm2.ts | 2 +- 16 files changed, 229 insertions(+), 48 deletions(-) create mode 100644 src/Components/Address/AddressFormFields.tsx diff --git a/src/Apps/Auction/Components/Form/Utils/initialValues.ts b/src/Apps/Auction/Components/Form/Utils/initialValues.ts index e895cb54a69..1d4a21f1138 100644 --- a/src/Apps/Auction/Components/Form/Utils/initialValues.ts +++ b/src/Apps/Auction/Components/Form/Utils/initialValues.ts @@ -1,5 +1,5 @@ import { FormikHelpers } from "formik" -import { Address, emptyAddress } from "Components/Address/AddressForm" +import { Address, emptyAddress } from "Components/Address/utils" export interface AuctionFormValues { address: Address diff --git a/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts b/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts index 7e93c286866..5ee1e1a4b9e 100644 --- a/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts +++ b/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts @@ -13,7 +13,7 @@ import { AuctionRegistrationRoute_me$data } from "__generated__/AuctionRegistrat import { AuctionRegistrationRoute_sale$data } from "__generated__/AuctionRegistrationRoute_sale.graphql" import { AuctionBidRoute_me$data } from "__generated__/AuctionBidRoute_me.graphql" import { AuctionBidRoute_sale$data } from "__generated__/AuctionBidRoute_sale.graphql" -import { toStripeAddress } from "Components/Address/AddressForm" +import { toStripeAddress } from "Components/Address/utils" import { useRefreshUserData } from "Apps/Auction/Queries/useRefreshUserData" import { errorMessageForCard, diff --git a/src/Apps/Invoice/Components/AddressForm.tsx b/src/Apps/Invoice/Components/AddressForm.tsx index e970eb76826..ec323e93017 100644 --- a/src/Apps/Invoice/Components/AddressForm.tsx +++ b/src/Apps/Invoice/Components/AddressForm.tsx @@ -1,6 +1,6 @@ import { Column, GridColumns, Input } from "@artsy/palette" import { CountrySelect } from "Components/CountrySelect" -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" export interface AddressFormValues { diff --git a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx index 231d2fdcdca..5d229d5a292 100644 --- a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx +++ b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx @@ -2,7 +2,7 @@ import { Button, Spacer } from "@artsy/palette" import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" import { AddressFormWithCreditCard } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useCreateTokenAndSubmit } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" -import { emptyAddress } from "Components/Address/AddressForm" +import { emptyAddress } from "Components/Address/utils" import { Formik, Form } from "formik" import { useRouter } from "System/Hooks/useRouter" diff --git a/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts b/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts index 87e892f50be..7444ac6afaa 100644 --- a/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts +++ b/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts @@ -6,7 +6,7 @@ import { useStripe, } from "@stripe/react-stripe-js" import createLogger from "Utils/logger" -import { toStripeAddress } from "Components/Address/AddressForm" +import { toStripeAddress } from "Components/Address/utils" import { stripeCardElementNotFound, stripeNotLoadedErrorMessage, diff --git a/src/Apps/Order/Components/CreditCardPicker.tsx b/src/Apps/Order/Components/CreditCardPicker.tsx index c29e0e31ba6..61d5ca744e9 100644 --- a/src/Apps/Order/Components/CreditCardPicker.tsx +++ b/src/Apps/Order/Components/CreditCardPicker.tsx @@ -2,14 +2,14 @@ import { CreditCardPicker_me$data } from "__generated__/CreditCardPicker_me.grap import { CreditCardPicker_order$data } from "__generated__/CreditCardPicker_order.graphql" import { CreditCardPickerCreateCreditCardMutation } from "__generated__/CreditCardPickerCreateCreditCardMutation.graphql" import { - Address, AddressChangeHandler, AddressErrors, AddressForm, AddressTouched, - emptyAddress, } from "Components/Address/AddressForm" +import { Address, emptyAddress } from "Components/Address/utils" + import { CreditCardInput } from "Components/CreditCardInput" import { validateAddress } from "Apps/Order/Utils/formValidators" import * as DeprecatedSchema from "@artsy/cohesion/dist/DeprecatedSchema" diff --git a/src/Apps/Order/Components/__tests__/CreditCardPicker.jest.enzyme.enzyme.tsx b/src/Apps/Order/Components/__tests__/CreditCardPicker.jest.enzyme.enzyme.tsx index 4ac1f26084d..860bdb52742 100644 --- a/src/Apps/Order/Components/__tests__/CreditCardPicker.jest.enzyme.enzyme.tsx +++ b/src/Apps/Order/Components/__tests__/CreditCardPicker.jest.enzyme.enzyme.tsx @@ -13,7 +13,8 @@ import { fillIn, validAddress, } from "Components/__tests__/Utils/addressForm" -import { Address, AddressForm } from "Components/Address/AddressForm" +import { AddressForm } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { RootTestPage } from "DevTools/RootTestPage" import { graphql } from "react-relay" import { diff --git a/src/Apps/Order/Utils/__tests__/formValidators.jest.tsx b/src/Apps/Order/Utils/__tests__/formValidators.jest.tsx index e23acf824c8..632301b58f6 100644 --- a/src/Apps/Order/Utils/__tests__/formValidators.jest.tsx +++ b/src/Apps/Order/Utils/__tests__/formValidators.jest.tsx @@ -1,4 +1,4 @@ -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { validateAddress, validatePresence, diff --git a/src/Apps/Order/Utils/formValidators.tsx b/src/Apps/Order/Utils/formValidators.tsx index 018688f1fd6..595e8f2f973 100644 --- a/src/Apps/Order/Utils/formValidators.tsx +++ b/src/Apps/Order/Utils/formValidators.tsx @@ -1,4 +1,4 @@ -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { isEmpty } from "lodash" export const validatePresence = (value: string): string | null => { diff --git a/src/Components/Address/AddressAutocompleteInput.tsx b/src/Components/Address/AddressAutocompleteInput.tsx index c0fb00f7ea2..bf45ba083bd 100644 --- a/src/Components/Address/AddressAutocompleteInput.tsx +++ b/src/Components/Address/AddressAutocompleteInput.tsx @@ -5,7 +5,7 @@ import { Input, usePrevious, } from "@artsy/palette" -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { useFeatureFlag } from "System/Hooks/useFeatureFlag" import { getENV } from "Utils/getENV" import { useCallback, useEffect, useMemo, useState } from "react" diff --git a/src/Components/Address/AddressForm.tsx b/src/Components/Address/AddressForm.tsx index f4be26c02d2..3d6e884d4d6 100644 --- a/src/Components/Address/AddressForm.tsx +++ b/src/Components/Address/AddressForm.tsx @@ -8,7 +8,6 @@ import { } from "@artsy/palette" import { CountrySelect } from "Components/CountrySelect" import * as React from "react" -import { CreateTokenCardData } from "@stripe/stripe-js" import { isEqual } from "lodash" import { AddressAutocompleteSuggestion, @@ -23,20 +22,10 @@ import { SelectedItemFromAddressAutoCompletion, } from "@artsy/cohesion" import { useAnalyticsContext } from "System/Hooks/useAnalyticsContext" +import { Address, emptyAddress } from "Components/Address/utils" const ENABLE_SECONDARY_SUGGESTIONS = false -export interface Address { - name: string - country: string - postalCode: string - addressLine1: string - addressLine2: string - city: string - region: string - phoneNumber?: string -} - export type AddressErrors = Partial
export type AddressTouched = Partial<{ [T in keyof Address]: boolean }> export type AddressChangeHandler = ( @@ -44,28 +33,6 @@ export type AddressChangeHandler = ( key: keyof Address ) => void -export const emptyAddress: Address = { - name: "", - country: "", - postalCode: "", - addressLine1: "", - addressLine2: "", - city: "", - region: "", - phoneNumber: "", -} - -export const toStripeAddress = (address: Address): CreateTokenCardData => { - return { - name: address.name, - address_line1: address.addressLine1, - address_line2: address.addressLine2, - address_country: address.country, - address_city: address.city, - address_state: address.region, - address_zip: address.postalCode, - } -} export interface AddressFormProps { onChange: AddressChangeHandler value?: Partial
diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx new file mode 100644 index 00000000000..84af3e48a5d --- /dev/null +++ b/src/Components/Address/AddressFormFields.tsx @@ -0,0 +1,178 @@ +import { ContextModule } from "@artsy/cohesion" +import { Column, GridColumns, Input } from "@artsy/palette" +import { AddressAutocompleteInput } from "Components/Address/AddressAutocompleteInput" +import { Address } from "Components/Address/utils" +import { CountrySelect } from "Components/CountrySelect" +import { useFormikContext } from "formik" +import { useAnalyticsContext } from "System/Hooks/useAnalyticsContext" + +interface FormikContextWithAddress { + address: Address + phoneNumber: string +} + +const useFormContext = () => { + const context = useFormikContext() + return context +} + +export const AddressForm = () => { + const { + handleChange, + handleBlur, + errors, + values, + touched, + setValues, + setFieldValue, + } = useFormContext() + + const { contextPageOwnerId, contextPageOwnerType } = useAnalyticsContext() + + const autocompleteTrackingValues = { + contextModule: ContextModule.auctionRegistration, + contextOwnerType: contextPageOwnerType, + contextPageOwnerId: contextPageOwnerId || "", + } + + return ( + + + + + + + + + + + { + const selectedAddress = option.address + setValues({ + ...values, + address: { + ...values.address, + addressLine1: selectedAddress.addressLine1, + addressLine2: selectedAddress.addressLine2, + city: selectedAddress.city, + region: selectedAddress.region, + postalCode: selectedAddress.postalCode, + country: selectedAddress.country, + }, + }) + }} + error={touched.address?.addressLine1 && errors.address?.addressLine1} + onClear={() => { + setFieldValue("address.addressLine1", "") + }} + /> + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/Components/Address/useAddressAutocomplete.ts b/src/Components/Address/useAddressAutocomplete.ts index e511955730f..1885f12d37d 100644 --- a/src/Components/Address/useAddressAutocomplete.ts +++ b/src/Components/Address/useAddressAutocomplete.ts @@ -1,5 +1,5 @@ import { AutocompleteInputOptionType } from "@artsy/palette" -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { useFeatureFlag } from "System/Hooks/useFeatureFlag" import { getENV } from "Utils/getENV" import { useCallback, useEffect, useMemo, useState } from "react" diff --git a/src/Components/Address/utils.ts b/src/Components/Address/utils.ts index 1fe85e6bdde..e6af58ffdaa 100644 --- a/src/Components/Address/utils.ts +++ b/src/Components/Address/utils.ts @@ -1,6 +1,41 @@ import { validatePhoneNumber } from "Components/PhoneNumberInput" +import { CreateTokenCardData } from "@stripe/stripe-js" import * as Yup from "yup" +export interface Address { + name: string + country: string + postalCode: string + addressLine1: string + addressLine2: string + city: string + region: string + phoneNumber?: string +} + +export const emptyAddress: Address = { + name: "", + country: "", + postalCode: "", + addressLine1: "", + addressLine2: "", + city: "", + region: "", + phoneNumber: "", +} + +export const toStripeAddress = (address: Address): CreateTokenCardData => { + return { + name: address.name, + address_line1: address.addressLine1, + address_line2: address.addressLine2, + address_country: address.country, + address_city: address.city, + address_state: address.region, + address_zip: address.postalCode, + } +} + export const yupPhoneValidator = Yup.string() .required("Phone Number is required") .test({ diff --git a/src/Components/__tests__/Utils/addressForm.ts b/src/Components/__tests__/Utils/addressForm.ts index 896677af73d..af82a1e1588 100644 --- a/src/Components/__tests__/Utils/addressForm.ts +++ b/src/Components/__tests__/Utils/addressForm.ts @@ -1,5 +1,5 @@ /* Address utilities for the old address form without improvements like state select */ -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { CountrySelect } from "Components/CountrySelect" import { Input } from "@artsy/palette" import { screen, waitFor } from "@testing-library/react" diff --git a/src/Components/__tests__/Utils/addressForm2.ts b/src/Components/__tests__/Utils/addressForm2.ts index 3d327f2d8d9..5c95340faa9 100644 --- a/src/Components/__tests__/Utils/addressForm2.ts +++ b/src/Components/__tests__/Utils/addressForm2.ts @@ -3,7 +3,7 @@ * with improvements like state select */ -import { Address } from "Components/Address/AddressForm" +import { Address } from "Components/Address/utils" import { CountrySelect } from "Components/CountrySelect" import { Input } from "@artsy/palette" import { screen, waitFor } from "@testing-library/react" From fdf0fcae7eb26c44f9798ea2c8f5dc940e03047d Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:25:15 -0600 Subject: [PATCH 02/13] fix non-null assertions --- src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts | 15 ++++++++++----- src/Apps/Order/Components/CreditCardPicker.tsx | 7 ++++--- src/Components/__tests__/Utils/addressForm.ts | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts b/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts index 5ee1e1a4b9e..6461e1c9b92 100644 --- a/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts +++ b/src/Apps/Auction/Hooks/useCreateTokenAndSubmit.ts @@ -80,11 +80,16 @@ export const useCreateTokenAndSubmit = ({ helpers.setSubmitting(true) + if (!values.address) { + helpers.setStatus("SUBMISSION_FAILED") + return + } + try { const { error, token } = await stripe.createToken( cardNumberElement, - toStripeAddress(values.address!) - )! + toStripeAddress(values.address) + ) if (error) { helpers.setFieldError("creditCard", error.message) @@ -94,7 +99,7 @@ export const useCreateTokenAndSubmit = ({ await submitAddCreditCardAndUpdateProfileMutation({ variables: { creditCardInput: { - token: token?.id!, + token: token.id, }, profileInput: { phone: values.phoneNumber, @@ -102,8 +107,8 @@ export const useCreateTokenAndSubmit = ({ }, rejectIf: res => { if (res.createCreditCard?.creditCardOrError?.mutationError) { - const mutationErrorDetail = res.createCreditCard?.creditCardOrError - ?.mutationError?.detail! + const mutationErrorDetail = + res.createCreditCard.creditCardOrError.mutationError.detail helpers.setFieldError( "creditCard", diff --git a/src/Apps/Order/Components/CreditCardPicker.tsx b/src/Apps/Order/Components/CreditCardPicker.tsx index 61d5ca744e9..eda93dae0c3 100644 --- a/src/Apps/Order/Components/CreditCardPicker.tsx +++ b/src/Apps/Order/Components/CreditCardPicker.tsx @@ -91,7 +91,7 @@ export class CreditCardPicker extends React.Component< return this.props.me.creditCards?.edges?.length ? { type: "existing", - id: this.props.me.creditCards.edges[0]?.node?.internalID!, + id: this.props.me.creditCards.edges[0]?.node?.internalID as string, } : { type: "new" } } @@ -121,6 +121,7 @@ export class CreditCardPicker extends React.Component< try { this.setState({ isCreatingStripeToken: true }) const stripeBillingAddress = this.getStripeBillingAddress() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const cardNumberElement = this.props.elements.getElement( CardNumberElement )! @@ -167,7 +168,7 @@ export class CreditCardPicker extends React.Component< const creditCardOrError = ( await this.createCreditCard({ input: { - token: stripeResult?.token?.id!, + token: stripeResult.token.id, oneTimeUse: !saveNewCreditCard, }, }) @@ -192,7 +193,7 @@ export class CreditCardPicker extends React.Component< } else return { type: "success", - creditCardId: creditCardOrError?.creditCard?.internalID!, + creditCardId: creditCardOrError?.creditCard?.internalID as string, } } diff --git a/src/Components/__tests__/Utils/addressForm.ts b/src/Components/__tests__/Utils/addressForm.ts index af82a1e1588..fb0319defbf 100644 --- a/src/Components/__tests__/Utils/addressForm.ts +++ b/src/Components/__tests__/Utils/addressForm.ts @@ -66,5 +66,5 @@ export const fillAddressForm = async (address: Address) => { userEvent.paste(city, address.city) userEvent.paste(region, address.region) userEvent.paste(postalCode, address.postalCode) - userEvent.paste(phoneNumber, address.phoneNumber!) + address.phoneNumber && userEvent.paste(phoneNumber, address.phoneNumber) } From 402b75a6339ace77ad65ea3758839ad170e87cb6 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:55:24 -0600 Subject: [PATCH 03/13] migrate auction registration form to use global address component --- .../Auction/Components/Form/AddressForm.tsx | 167 ------------------ .../Form/AddressFormWithCreditCard.tsx | 8 +- .../Form/ConditionsOfSaleCheckbox.tsx | 5 +- .../Auction/Components/Form/ErrorStatus.tsx | 5 +- .../__tests__/AddressForm.jest.enzyme.tsx | 5 +- .../AddressFormWithCreditCard.jest.enzyme.tsx | 8 +- .../ConditionsOfSaleCheckbox.jest.enzyme.tsx | 11 +- .../__tests__/ErrorStatus.jest.enzyme.tsx | 9 +- ...ormContext.ts => useAuctionFormContext.ts} | 2 +- .../Bid/Components/PricingTransparency.tsx | 6 +- src/Components/Address/AddressFormFields.tsx | 2 +- 11 files changed, 33 insertions(+), 195 deletions(-) delete mode 100644 src/Apps/Auction/Components/Form/AddressForm.tsx rename src/Apps/Auction/Hooks/{useFormContext.ts => useAuctionFormContext.ts} (81%) diff --git a/src/Apps/Auction/Components/Form/AddressForm.tsx b/src/Apps/Auction/Components/Form/AddressForm.tsx deleted file mode 100644 index a25cd18f1e2..00000000000 --- a/src/Apps/Auction/Components/Form/AddressForm.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { ContextModule } from "@artsy/cohesion" -import { Column, GridColumns, Input } from "@artsy/palette" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" -import { AddressAutocompleteInput } from "Components/Address/AddressAutocompleteInput" -import { CountrySelect } from "Components/CountrySelect" -import { useAnalyticsContext } from "System/Hooks/useAnalyticsContext" - -export const AddressForm = () => { - const { - handleChange, - handleBlur, - errors, - values, - touched, - setValues, - setFieldValue, - } = useFormContext() - - const { contextPageOwnerId, contextPageOwnerType } = useAnalyticsContext() - - const autocompleteTrackingValues = { - contextModule: ContextModule.auctionRegistration, - contextOwnerType: contextPageOwnerType, - contextPageOwnerId: contextPageOwnerId || "", - } - - return ( - - - - - - - - - - - { - const selectedAddress = option.address - setValues({ - ...values, - address: { - ...values.address, - addressLine1: selectedAddress.addressLine1, - addressLine2: selectedAddress.addressLine2, - city: selectedAddress.city, - region: selectedAddress.region, - postalCode: selectedAddress.postalCode, - country: selectedAddress.country, - }, - }) - }} - error={touched.address?.addressLine1 && errors.address?.addressLine1} - onClear={() => { - setFieldValue("address.addressLine1", "") - }} - /> - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx index ecd5de26e14..f576b7f2f55 100644 --- a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx +++ b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx @@ -1,7 +1,7 @@ import { Join, Spacer, Text } from "@artsy/palette" import { CreditCardInput } from "Components/CreditCardInput" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" -import { AddressForm } from "./AddressForm" +import { AddressFormFields } from "Components/Address/AddressFormFields" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" export const AddressFormWithCreditCard: React.FC> = () => { const { @@ -10,7 +10,7 @@ export const AddressFormWithCreditCard: React.FC}> @@ -44,7 +44,7 @@ export const AddressFormWithCreditCard: React.FC - + ) } diff --git a/src/Apps/Auction/Components/Form/ConditionsOfSaleCheckbox.tsx b/src/Apps/Auction/Components/Form/ConditionsOfSaleCheckbox.tsx index 767c50ca161..9914e9c3dcf 100644 --- a/src/Apps/Auction/Components/Form/ConditionsOfSaleCheckbox.tsx +++ b/src/Apps/Auction/Components/Form/ConditionsOfSaleCheckbox.tsx @@ -1,5 +1,6 @@ import { Checkbox, Spacer, Text } from "@artsy/palette" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" + import { RouterLink } from "System/Components/RouterLink" export const ConditionsOfSaleCheckbox: React.FC> = () => { @@ -9,7 +10,7 @@ export const ConditionsOfSaleCheckbox: React.FC errors, setFieldTouched, setFieldValue, - } = useFormContext() + } = useAuctionFormContext() const showErrorMessage = !!(touched.agreeToTerms && errors.agreeToTerms) diff --git a/src/Apps/Auction/Components/Form/ErrorStatus.tsx b/src/Apps/Auction/Components/Form/ErrorStatus.tsx index 7b76160a57c..95798613a2a 100644 --- a/src/Apps/Auction/Components/Form/ErrorStatus.tsx +++ b/src/Apps/Auction/Components/Form/ErrorStatus.tsx @@ -1,10 +1,11 @@ import { Banner, BannerProps, Flex, Text } from "@artsy/palette" import { errorMessageForBidding } from "Apps/Auction/Components/Form/Utils/errorMessages" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" + import { RouterLink } from "System/Components/RouterLink" export const ErrorStatus = () => { - const { status } = useFormContext() + const { status } = useAuctionFormContext() if (!status) { return null diff --git a/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx index ab2d0c73fda..aca7df7b439 100644 --- a/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx +++ b/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx @@ -1,8 +1,9 @@ import { mount } from "enzyme" import { AddressForm } from "Apps/Auction/Components/Form/AddressForm" -jest.mock("Apps/Auction/Hooks/useFormContext", () => ({ - useFormContext: () => { +// TODO: Migrate these tests to new Components/Address/AddressFormFields.jest.tsx file +jest.mock("Apps/Auction/Hooks/useAuctionFormContext", () => ({ + useAuctionFormContext: () => { return { handleChange: jest.fn(), handleBlur: jest.fn(), diff --git a/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx index 5f0ddb54203..c402353a460 100644 --- a/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx +++ b/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx @@ -1,8 +1,8 @@ import { mount } from "enzyme" import { AddressFormWithCreditCard } from "Apps/Auction/Components/Form/AddressFormWithCreditCard" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" -jest.mock("Apps/Auction/Hooks/useFormContext") +jest.mock("Apps/Auction/Hooks/useAuctionFormContext") jest.mock("Components/CreditCardInput", () => ({ CreditCardInput: () => null, @@ -12,7 +12,7 @@ jest.mock("../AddressForm", () => ({ })) describe("AddressFormWithCreditCard", () => { - const mockUseFormContext = useFormContext as jest.Mock + const mockuseAuctionFormContext = useAuctionFormContext as jest.Mock const setFieldTouched = jest.fn() const setFieldValue = jest.fn() const setFieldError = jest.fn() @@ -22,7 +22,7 @@ describe("AddressFormWithCreditCard", () => { } beforeAll(() => { - mockUseFormContext.mockImplementation(() => { + mockuseAuctionFormContext.mockImplementation(() => { return { setFieldTouched, setFieldValue, diff --git a/src/Apps/Auction/Components/Form/__tests__/ConditionsOfSaleCheckbox.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/ConditionsOfSaleCheckbox.jest.enzyme.tsx index 5416efa74bd..a08b64b9678 100644 --- a/src/Apps/Auction/Components/Form/__tests__/ConditionsOfSaleCheckbox.jest.enzyme.tsx +++ b/src/Apps/Auction/Components/Form/__tests__/ConditionsOfSaleCheckbox.jest.enzyme.tsx @@ -1,12 +1,13 @@ import { mount } from "enzyme" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" + import { ConditionsOfSaleCheckbox } from "Apps/Auction/Components/Form/ConditionsOfSaleCheckbox" -jest.mock("Apps/Auction/Hooks/useFormContext") +jest.mock("Apps/Auction/Hooks/useAuctionFormContext") jest.mock("System/Hooks/useFeatureFlag") describe("ConditionsOfSaleCheckbox", () => { - const mockUseFormContext = useFormContext as jest.Mock + const mockuseAuctionFormContext = useAuctionFormContext as jest.Mock const setFieldTouched = jest.fn() const setFieldValue = jest.fn() const formProps = { @@ -22,13 +23,13 @@ describe("ConditionsOfSaleCheckbox", () => { } beforeAll(() => { - mockUseFormContext.mockImplementation(() => { + mockuseAuctionFormContext.mockImplementation(() => { return formProps }) }) it("shows error message if error", () => { - mockUseFormContext.mockImplementation(() => { + mockuseAuctionFormContext.mockImplementation(() => { return { ...formProps, touched: { diff --git a/src/Apps/Auction/Components/Form/__tests__/ErrorStatus.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/ErrorStatus.jest.enzyme.tsx index b5d8e00662b..0bd3507f1fc 100644 --- a/src/Apps/Auction/Components/Form/__tests__/ErrorStatus.jest.enzyme.tsx +++ b/src/Apps/Auction/Components/Form/__tests__/ErrorStatus.jest.enzyme.tsx @@ -1,15 +1,16 @@ import { mount } from "enzyme" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" + import { ErrorStatus } from "Apps/Auction/Components/Form/ErrorStatus" -jest.mock("Apps/Auction/Hooks/useFormContext") +jest.mock("Apps/Auction/Hooks/useAuctionFormContext") describe("ErrorStatus", () => { - const mockUseFormContext = useFormContext as jest.Mock + const mockuseAuctionFormContext = useAuctionFormContext as jest.Mock let status const getWrapper = () => { - mockUseFormContext.mockImplementation(() => { + mockuseAuctionFormContext.mockImplementation(() => { return { status, } diff --git a/src/Apps/Auction/Hooks/useFormContext.ts b/src/Apps/Auction/Hooks/useAuctionFormContext.ts similarity index 81% rename from src/Apps/Auction/Hooks/useFormContext.ts rename to src/Apps/Auction/Hooks/useAuctionFormContext.ts index e31e1c3801e..bb82b6fb5c2 100644 --- a/src/Apps/Auction/Hooks/useFormContext.ts +++ b/src/Apps/Auction/Hooks/useAuctionFormContext.ts @@ -1,7 +1,7 @@ import { AuctionFormValues } from "Apps/Auction/Components/Form/Utils/initialValues" import { useFormikContext } from "formik" -export const useFormContext = () => { +export const useAuctionFormContext = () => { const context = useFormikContext() return context } diff --git a/src/Apps/Auction/Routes/Bid/Components/PricingTransparency.tsx b/src/Apps/Auction/Routes/Bid/Components/PricingTransparency.tsx index 344c2e8da81..34ea9f4d394 100644 --- a/src/Apps/Auction/Routes/Bid/Components/PricingTransparency.tsx +++ b/src/Apps/Auction/Routes/Bid/Components/PricingTransparency.tsx @@ -7,7 +7,7 @@ import { } from "__generated__/PricingTransparencyQuery.graphql" import { useSystemContext } from "System/Hooks/useSystemContext" import { SystemQueryRenderer } from "System/Relay/SystemQueryRenderer" -import { useFormContext } from "Apps/Auction/Hooks/useFormContext" +import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" import { Text, @@ -86,8 +86,8 @@ export const PricingTransparencyQueryRenderer = ({ artworkId, }: Omit) => { const { relayEnvironment } = useSystemContext() - const { values } = useFormContext() - const bidAmountMinor = parseInt(values.selectedBid!) + const { values } = useAuctionFormContext() + const bidAmountMinor = parseInt(values.selectedBid || "0") // Hack to prevent invalid refetch / preloader state during route transition // when the url changes after user places a successful bid and we redirect diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index 84af3e48a5d..42d11095957 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -16,7 +16,7 @@ const useFormContext = () => { return context } -export const AddressForm = () => { +export const AddressFormFields = () => { const { handleChange, handleBlur, From 307fabe00b21355d79587f4f8edf585d1af458e1 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:47:57 -0600 Subject: [PATCH 04/13] types cleanup to guarantee form used in correct ctx - maybe move phone number into address object on address type since it is already redundantly in there? --- .../Form/AddressFormWithCreditCard.tsx | 3 +- .../Components/Form/Utils/initialValues.ts | 2 +- .../Routes/Shipping/Utils/computeOrderData.ts | 4 +-- .../Routes/Shipping/Utils/shippingUtils.ts | 32 +++++------------ src/Components/Address/AddressFormFields.tsx | 36 +++++++++++-------- 5 files changed, 34 insertions(+), 43 deletions(-) diff --git a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx index f576b7f2f55..b398cbfc978 100644 --- a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx +++ b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx @@ -2,6 +2,7 @@ import { Join, Spacer, Text } from "@artsy/palette" import { CreditCardInput } from "Components/CreditCardInput" import { AddressFormFields } from "Components/Address/AddressFormFields" import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" +import { AuctionFormValues } from "Apps/Auction/Components/Form/Utils/initialValues" export const AddressFormWithCreditCard: React.FC> = () => { const { @@ -44,7 +45,7 @@ export const AddressFormWithCreditCard: React.FC - + /> ) } diff --git a/src/Apps/Auction/Components/Form/Utils/initialValues.ts b/src/Apps/Auction/Components/Form/Utils/initialValues.ts index 1d4a21f1138..16275e8ecb0 100644 --- a/src/Apps/Auction/Components/Form/Utils/initialValues.ts +++ b/src/Apps/Auction/Components/Form/Utils/initialValues.ts @@ -5,7 +5,7 @@ export interface AuctionFormValues { address: Address agreeToTerms: boolean creditCard?: boolean - phoneNumber?: string + phoneNumber: string selectedBid?: string } diff --git a/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts b/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts index 64b9650e19d..08d2aea97fb 100644 --- a/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts +++ b/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts @@ -2,7 +2,7 @@ import { ShippingContextProps } from "Apps/Order/Routes/Shipping/ShippingContext import { FulfillmentType, PickupValues, - ShippingAddressFormValues, + Address, addressWithFallbackValues, matchAddressFields, } from "Apps/Order/Routes/Shipping/Utils/shippingUtils" @@ -33,7 +33,7 @@ type SavedFulfillmentData = | { fulfillmentType: FulfillmentType.SHIP isArtsyShipping: boolean - attributes: ShippingAddressFormValues + attributes: Address selectedSavedAddressID: string | null } | null diff --git a/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts b/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts index 4b72e31a6bd..b12b683269e 100644 --- a/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts +++ b/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts @@ -2,7 +2,7 @@ import * as Yup from "yup" import { AddressVerifiedBy } from "Apps/Order/Components/AddressVerificationFlow" import { ShippingContext_me$data } from "__generated__/ShippingContext_me.graphql" import { pick, omitBy, isNil, isEqual } from "lodash" -import { postalCodeValidator } from "Components/Address/utils" +import { Address, postalCodeValidator } from "Components/Address/utils" export enum FulfillmentType { SHIP = "SHIP", @@ -29,7 +29,7 @@ export interface PickupValues { export interface ShipValues { fulfillmentType: FulfillmentType.SHIP - attributes: ShippingAddressFormValues + attributes: Address meta: FormMetaValues } @@ -46,17 +46,6 @@ interface FormMetaValues { export type FulfillmentValues = ShipValues | PickupValues -export interface ShippingAddressFormValues { - name: string - phoneNumber: string - addressLine1: string - addressLine2: string - city: string - region: string - country: string - postalCode: string -} - // TODO: Replace with what we use in SettingsShippingAddressForm when we have // a rich phone input export const BASIC_PHONE_VALIDATION_SHAPE = { @@ -80,7 +69,7 @@ export const ADDRESS_VALIDATION_SHAPE = { country: Yup.string().required("Country is required"), } -const ORDER_EMPTY_ADDRESS: ShippingAddressFormValues = { +const ORDER_EMPTY_ADDRESS: Address = { name: "", phoneNumber: "", addressLine1: "", @@ -92,22 +81,17 @@ const ORDER_EMPTY_ADDRESS: ShippingAddressFormValues = { } export const onlyAddressValues = (values: any) => { - return pick( - values, - Object.keys(ORDER_EMPTY_ADDRESS) - ) + return pick
(values, Object.keys(ORDER_EMPTY_ADDRESS)) } /** * Takes an address object and returns a new address object with all the * non-null values from the original address object. Useful for converting - * a SavedAddress from relay to a ShippingAddressFormValues object. + * a SavedAddress from relay to a Address object. */ -export const addressWithFallbackValues = ( - address: any -): ShippingAddressFormValues => ({ +export const addressWithFallbackValues = (address: any): Address => ({ ...ORDER_EMPTY_ADDRESS, - ...omitBy(onlyAddressValues(address), isNil), + ...omitBy
(onlyAddressValues(address), isNil), }) export type SavedAddressType = NonNullable< @@ -189,7 +173,7 @@ export const getInitialShippingValues = ( export const matchAddressFields = (...addressPair: [object, object]) => { const [a1, a2] = addressPair.map(a => addressWithFallbackValues(a)) - const fields: Array = [ + const fields: Array = [ "addressLine1", "addressLine2", "city", diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index 42d11095957..06f31876fb3 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -11,12 +11,7 @@ interface FormikContextWithAddress { phoneNumber: string } -const useFormContext = () => { - const context = useFormikContext() - return context -} - -export const AddressFormFields = () => { +export const AddressFormFields = () => { const { handleChange, handleBlur, @@ -25,7 +20,16 @@ export const AddressFormFields = () => { touched, setValues, setFieldValue, - } = useFormContext() + } = useFormikContext() + + // Formik types don't understand our specific nested structure + // so we need to cast these to what we know to be the correct types + const touchedAddress = touched.address as + | Partial> + | undefined + const errorsAddress = errors.address as + | Partial> + | undefined const { contextPageOwnerId, contextPageOwnerType } = useAnalyticsContext() @@ -47,7 +51,7 @@ export const AddressFormFields = () => { value={values.address?.name} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.name && errors.address?.name} + error={touchedAddress?.name && errorsAddress?.name} required /> @@ -61,7 +65,7 @@ export const AddressFormFields = () => { value={values.address.country} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.country && errors.address?.country} + error={touchedAddress?.country && errorsAddress?.country} required /> @@ -96,7 +100,7 @@ export const AddressFormFields = () => { }, }) }} - error={touched.address?.addressLine1 && errors.address?.addressLine1} + error={touchedAddress?.addressLine1 && errorsAddress?.addressLine1} onClear={() => { setFieldValue("address.addressLine1", "") }} @@ -112,7 +116,7 @@ export const AddressFormFields = () => { value={values.address?.addressLine2} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.addressLine2 && errors.address?.addressLine2} + error={touchedAddress?.addressLine2 && errorsAddress?.addressLine2} /> @@ -125,7 +129,7 @@ export const AddressFormFields = () => { value={values.address?.city} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.city && errors.address?.city} + error={touchedAddress?.city && errorsAddress?.city} required /> @@ -139,7 +143,7 @@ export const AddressFormFields = () => { value={values.address?.region} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.region && errors.address?.region} + error={touchedAddress?.region && errorsAddress?.region} required /> @@ -153,7 +157,7 @@ export const AddressFormFields = () => { value={values.address?.postalCode} onChange={handleChange} onBlur={handleBlur} - error={touched.address?.postalCode && errors.address?.postalCode} + error={touchedAddress?.postalCode && errorsAddress?.postalCode} required /> @@ -169,7 +173,9 @@ export const AddressFormFields = () => { value={values.phoneNumber} onChange={handleChange} onBlur={handleBlur} - error={touched.phoneNumber && errors.phoneNumber} + error={ + touched.phoneNumber && (errors.phoneNumber as string | undefined) + } required /> From 4f3c9ccb4627e6cef9392d66e3fed38cd5136490 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:46:47 -0600 Subject: [PATCH 05/13] port old enzyme test to new form --- .../__tests__/AddressForm.jest.enzyme.tsx | 43 ------------- .../Components/AddressFormWithCreditCard.tsx | 5 +- src/Components/Address/AddressFormFields.tsx | 12 +++- .../__tests__/AddressFormFields.jest.tsx | 62 +++++++++++++++++++ 4 files changed, 75 insertions(+), 47 deletions(-) delete mode 100644 src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx create mode 100644 src/Components/Address/__tests__/AddressFormFields.jest.tsx diff --git a/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx deleted file mode 100644 index aca7df7b439..00000000000 --- a/src/Apps/Auction/Components/Form/__tests__/AddressForm.jest.enzyme.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { mount } from "enzyme" -import { AddressForm } from "Apps/Auction/Components/Form/AddressForm" - -// TODO: Migrate these tests to new Components/Address/AddressFormFields.jest.tsx file -jest.mock("Apps/Auction/Hooks/useAuctionFormContext", () => ({ - useAuctionFormContext: () => { - return { - handleChange: jest.fn(), - handleBlur: jest.fn(), - errors: {}, - values: { - address: {}, - }, - touched: {}, - } - }, -})) - -describe("AddressForm", () => { - const getWrapper = () => { - return mount() - } - - it("renders the correct components", () => { - const wrapper = getWrapper() - expect(wrapper.find("CountrySelect")).toHaveLength(1) - - const text = wrapper.text() - - ;[ - "Full Name", - "Country", - "ZIP/Postal code", - "Street address", - "Apt, floor, suite, etc. (optional)", - "City", - "State, region or province", - "Phone number", - ].forEach(label => { - expect(text).toContain(label) - }) - }) -}) diff --git a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx index f836349b9e1..f5098644cd4 100644 --- a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx +++ b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx @@ -1,7 +1,8 @@ import { Join, Spacer } from "@artsy/palette" import { CreditCardInput } from "Components/CreditCardInput" -import { AddressForm } from "./AddressForm" +import { AddressForm, AddressFormValues } from "./AddressForm" import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" +import { AddressFormFields } from "Components/Address/AddressFormFields" export const AddressFormWithCreditCard: React.FC> = () => { const { @@ -36,7 +37,7 @@ export const AddressFormWithCreditCard: React.FC - + /> ) } diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index 06f31876fb3..e002fbe6927 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -44,7 +44,8 @@ export const AddressFormFields = () => { () => { () => { () => { flip={false} required disableAutocomplete={values.address.region === "AK"} - name="address.addressLine1" placeholder="Add street address" title="Street address" value={values.address.addressLine1} @@ -110,6 +113,7 @@ export const AddressFormFields = () => { () => { () => { () => { () => { { + return { + useFormikContext: jest.fn(() => ({ + handleChange: jest.fn(), + handleBlur: jest.fn(), + errors: {}, + values: { + address: {}, + }, + touched: {}, + setValues: jest.fn(), + setFieldValue: jest.fn(), + })), + } +}) + +const INPUT_EXPECTATIONS = [ + { label: "Full name", placeholder: "Add full name" }, + { + label: "Country", + placeholder: null, + }, + { + label: "ZIP/Postal code", + placeholder: "Add ZIP/Postal code", + }, + { + label: "Street address", + placeholder: "Add street address", + }, + { + label: "Apt, floor, suite, etc. (optional)", + placeholder: "Add apartment, floor, suite, etc.", + }, + { label: "City", placeholder: "Add city" }, + { + label: "State, region or province", + placeholder: "Add state, region or province", + }, + { label: "Phone number", placeholder: "Add phone number" }, +] + +describe("AddressForm", () => { + it("renders the correct components with label & placeholder copy", () => { + render() + + INPUT_EXPECTATIONS.forEach(({ label, placeholder }) => { + const input = screen.getByLabelText(label) + expect(input).toBeInTheDocument() + if (placeholder) { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).toHaveAttribute("placeholder", placeholder) + } else { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).not.toHaveAttribute("placeholder") + } + }) + }) +}) From 74c79a5ec8246fa534eee2eadb052cd313542de4 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:54:46 -0600 Subject: [PATCH 06/13] add more tests --- .../Form/AddressFormWithCreditCard.tsx | 6 +- .../Components/AddressFormWithCreditCard.tsx | 6 +- src/Components/Address/AddressFormFields.tsx | 61 ++-- .../__tests__/AddressFormFields.jest.tsx | 262 ++++++++++++++---- src/Components/Address/__tests__/utils.ts | 61 ++++ src/Components/Address/utils.ts | 18 ++ 6 files changed, 333 insertions(+), 81 deletions(-) create mode 100644 src/Components/Address/__tests__/utils.ts diff --git a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx index b398cbfc978..f4425419d63 100644 --- a/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx +++ b/src/Apps/Auction/Components/Form/AddressFormWithCreditCard.tsx @@ -4,7 +4,9 @@ import { AddressFormFields } from "Components/Address/AddressFormFields" import { useAuctionFormContext } from "Apps/Auction/Hooks/useAuctionFormContext" import { AuctionFormValues } from "Apps/Auction/Components/Form/Utils/initialValues" -export const AddressFormWithCreditCard: React.FC> = () => { +export const AddressFormWithCreditCard: React.FC> = () => { const { setFieldValue, setFieldTouched, @@ -45,7 +47,7 @@ export const AddressFormWithCreditCard: React.FC - /> + withPhoneNumber /> ) } diff --git a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx index f5098644cd4..fdb856b9d9d 100644 --- a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx +++ b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx @@ -1,10 +1,12 @@ import { Join, Spacer } from "@artsy/palette" import { CreditCardInput } from "Components/CreditCardInput" -import { AddressForm, AddressFormValues } from "./AddressForm" +import { AddressFormValues } from "./AddressForm" import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" import { AddressFormFields } from "Components/Address/AddressFormFields" -export const AddressFormWithCreditCard: React.FC> = () => { +export const AddressFormWithCreditCard: React.FC> = () => { const { setFieldValue, setFieldTouched, diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index e002fbe6927..12d928766f3 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -1,17 +1,34 @@ import { ContextModule } from "@artsy/cohesion" import { Column, GridColumns, Input } from "@artsy/palette" import { AddressAutocompleteInput } from "Components/Address/AddressAutocompleteInput" -import { Address } from "Components/Address/utils" +import { + Address, + basicPhoneValidator, + yupAddressValidator, +} from "Components/Address/utils" import { CountrySelect } from "Components/CountrySelect" import { useFormikContext } from "formik" import { useAnalyticsContext } from "System/Hooks/useAnalyticsContext" interface FormikContextWithAddress { address: Address - phoneNumber: string + phoneNumber?: string } -export const AddressFormFields = () => { +interface Props { + withPhoneNumber?: boolean +} + +export const addressFormFieldsValidator = ( + args: { withPhoneNumber?: boolean } = {} +) => ({ + address: yupAddressValidator, + ...(args.withPhoneNumber && { phoneNumber: basicPhoneValidator }), +}) + +export const AddressFormFields = ( + props: Props +) => { const { handleChange, handleBlur, @@ -169,24 +186,26 @@ export const AddressFormFields = () => { /> - - - + {props.withPhoneNumber && ( + + + + )} ) } diff --git a/src/Components/Address/__tests__/AddressFormFields.jest.tsx b/src/Components/Address/__tests__/AddressFormFields.jest.tsx index 1ef8881e774..1a42ff64873 100644 --- a/src/Components/Address/__tests__/AddressFormFields.jest.tsx +++ b/src/Components/Address/__tests__/AddressFormFields.jest.tsx @@ -1,62 +1,212 @@ -import { act, render, screen } from "@testing-library/react" -import { AddressFormFields } from "Components/Address/AddressFormFields" - -jest.mock("formik", () => { - return { - useFormikContext: jest.fn(() => ({ - handleChange: jest.fn(), - handleBlur: jest.fn(), - errors: {}, - values: { - address: {}, - }, - touched: {}, - setValues: jest.fn(), - setFieldValue: jest.fn(), - })), - } -}) - -const INPUT_EXPECTATIONS = [ - { label: "Full name", placeholder: "Add full name" }, - { - label: "Country", - placeholder: null, - }, - { - label: "ZIP/Postal code", - placeholder: "Add ZIP/Postal code", - }, - { - label: "Street address", - placeholder: "Add street address", - }, - { - label: "Apt, floor, suite, etc. (optional)", - placeholder: "Add apartment, floor, suite, etc.", - }, - { label: "City", placeholder: "Add city" }, - { - label: "State, region or province", - placeholder: "Add state, region or province", - }, - { label: "Phone number", placeholder: "Add phone number" }, -] +import { Button } from "@artsy/palette" +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { + ADDRESS_FORM_INPUTS, + fillAddressFormFields, +} from "Components/Address/__tests__/utils" +import { + AddressFormFields, + addressFormFieldsValidator, +} from "Components/Address/AddressFormFields" +import { Address, emptyAddress } from "Components/Address/utils" +import { flushPromiseQueue } from "DevTools/flushPromiseQueue" +import { Formik } from "formik" +import * as Yup from "yup" describe("AddressForm", () => { - it("renders the correct components with label & placeholder copy", () => { - render() - - INPUT_EXPECTATIONS.forEach(({ label, placeholder }) => { - const input = screen.getByLabelText(label) - expect(input).toBeInTheDocument() - if (placeholder) { - // eslint-disable-next-line jest/no-conditional-expect - expect(input).toHaveAttribute("placeholder", placeholder) - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(input).not.toHaveAttribute("placeholder") + const mockOnSubmit = jest.fn() + beforeEach(() => { + mockOnSubmit.mockClear() + }) + + describe("without phone number input", () => { + beforeEach(() => { + render( + + {formikBag => ( + <> + + + + )} + + ) + }) + + it("renders the correct components with label & placeholder copy", () => { + const { phoneNumber, ...remainingInputs } = ADDRESS_FORM_INPUTS + + Object.values(remainingInputs).forEach(({ label, placeholder }) => { + const input = screen.getByLabelText(label) + expect(input).toBeInTheDocument() + if (placeholder) { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).toHaveAttribute("placeholder", placeholder) + } else { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).not.toHaveAttribute("placeholder") + } + }) + }) + + it("can be filled out with valid values", async () => { + const address: Address = { + name: "Mr Art Collector", + country: "US", + postalCode: "10013", + addressLine1: "123 Main St", + addressLine2: "Apt 23", + city: "New York", + region: "NY", + } + + await fillAddressFormFields(address) + + await userEvent.click(screen.getByText("Submit")) + + await flushPromiseQueue() + expect(mockOnSubmit).toHaveBeenCalledWith( + { + address: { + addressLine1: "123 Main St", + addressLine2: "Apt 23", + city: "New York", + country: "US", + name: "Mr Art Collector", + phoneNumber: "", + postalCode: "10013", + region: "NY", + }, + }, + expect.anything() + ) + }) + + it("validates required fields", async () => { + await userEvent.click(screen.getByText("Submit")) + + await flushPromiseQueue() + expect(mockOnSubmit).not.toHaveBeenCalled() + + screen.getByText("Full name is required") + screen.getByText("Country is required") + screen.getByText("Street address is required") + screen.getByText("City is required") + expect( + screen.queryByText("Phone number is required") + ).not.toBeInTheDocument() + expect(screen.queryByText("State is required")).not.toBeInTheDocument() + }) + }) + + describe("with phone number input", () => { + beforeEach(() => { + render( + + {formikBag => ( + <> + + + + )} + + ) + }) + + it("renders the correct components with label & placeholder copy", () => { + Object.values(ADDRESS_FORM_INPUTS).forEach(({ label, placeholder }) => { + const input = screen.getByLabelText(label) + expect(input).toBeInTheDocument() + if (placeholder) { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).toHaveAttribute("placeholder", placeholder) + } else { + // eslint-disable-next-line jest/no-conditional-expect + expect(input).not.toHaveAttribute("placeholder") + } + }) + }) + + it("can be filled out with valid values", async () => { + const address: Address = { + name: "Mr Art Collector", + country: "US", + postalCode: "10013", + addressLine1: "123 Main St", + addressLine2: "Apt 23", + city: "New York", + region: "NY", + phoneNumber: "5555937743", } + + await fillAddressFormFields(address) + + await userEvent.click(screen.getByText("Submit")) + + await flushPromiseQueue() + expect(mockOnSubmit).toHaveBeenCalledWith( + { + address: { + addressLine1: "123 Main St", + addressLine2: "Apt 23", + city: "New York", + country: "US", + name: "Mr Art Collector", + phoneNumber: "", + postalCode: "10013", + region: "NY", + }, + phoneNumber: "5555937743", + }, + expect.anything() + ) + }) + + it("validates required fields", async () => { + await userEvent.click(screen.getByText("Submit")) + + await flushPromiseQueue() + expect(mockOnSubmit).not.toHaveBeenCalled() + + screen.getByText("Full name is required") + screen.getByText("Country is required") + screen.getByText("Street address is required") + screen.getByText("City is required") + screen.getByText("Phone number is required") + expect(screen.queryByText("State is required")).not.toBeInTheDocument() + expect(screen.queryByText("ZIP code is required")).not.toBeInTheDocument() + + await fillAddressFormFields({ country: "US" }) + await userEvent.click(screen.getByText("Submit")) + + await flushPromiseQueue() + expect(mockOnSubmit).not.toHaveBeenCalled() + + expect(screen.queryByText("Country is required")).not.toBeInTheDocument() + screen.getByText("State is required") + screen.getByText("ZIP code is required") }) }) }) diff --git a/src/Components/Address/__tests__/utils.ts b/src/Components/Address/__tests__/utils.ts new file mode 100644 index 00000000000..1057eb58bac --- /dev/null +++ b/src/Components/Address/__tests__/utils.ts @@ -0,0 +1,61 @@ +import { screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { Address } from "Components/Address/utils" +import { flushPromiseQueue } from "DevTools/flushPromiseQueue" + +export const ADDRESS_FORM_INPUTS: Record< + keyof Address, + { label: string; placeholder: string | null } +> = { + name: { label: "Full name", placeholder: "Add full name" }, + country: { + label: "Country", + placeholder: null, + }, + postalCode: { + label: "ZIP/Postal code", + placeholder: "Add ZIP/Postal code", + }, + addressLine1: { + label: "Street address", + placeholder: "Add street address", + }, + addressLine2: { + label: "Apt, floor, suite, etc. (optional)", + placeholder: "Add apartment, floor, suite, etc.", + }, + city: { label: "City", placeholder: "Add city" }, + region: { + label: "State, region or province", + placeholder: "Add state, region or province", + }, + phoneNumber: { label: "Phone number", placeholder: "Add phone number" }, +} + +/** + * Fill the `` component's inputs with the provided address + */ +export const fillAddressFormFields = async (address: Partial
) => { + const { country, phoneNumber, ...defaultTextInputs } = address + + if (country) { + const countrySelect = await screen.findByLabelText( + ADDRESS_FORM_INPUTS.country.label + ) + await userEvent.selectOptions(countrySelect, [country]) + await flushPromiseQueue() + } + + if (phoneNumber) { + const phoneNumberInput = await screen.findByLabelText( + ADDRESS_FORM_INPUTS.phoneNumber.label + ) + await userEvent.paste(phoneNumberInput, phoneNumber) + } + await Promise.all( + Object.entries(defaultTextInputs).map(async ([key, value]) => { + const input = await screen.findByLabelText(ADDRESS_FORM_INPUTS[key].label) + await userEvent.paste(input, value) + }) + ) +} diff --git a/src/Components/Address/utils.ts b/src/Components/Address/utils.ts index e6af58ffdaa..0bb7dc83f2d 100644 --- a/src/Components/Address/utils.ts +++ b/src/Components/Address/utils.ts @@ -36,6 +36,10 @@ export const toStripeAddress = (address: Address): CreateTokenCardData => { } } +export const basicPhoneValidator = Yup.string() + .required("Phone number is required") + .matches(/^[+\-\(\)\d\s]+$/, "Please enter a valid phone number") + export const yupPhoneValidator = Yup.string() .required("Phone Number is required") .test({ @@ -65,3 +69,17 @@ export const postalCodeValidator = Yup.string().when("country", { otherwise: Yup.string(), }), }) + +export const yupAddressValidator = Yup.object().shape({ + name: Yup.string().required("Full name is required"), + addressLine1: Yup.string().required("Street address is required"), + addressLine2: Yup.string().nullable(), + city: Yup.string().required("City is required"), + postalCode: postalCodeValidator, + region: Yup.string().when("country", { + is: country => ["US", "CA"].includes(country), + then: Yup.string().required("State is required"), + otherwise: Yup.string(), + }), + country: Yup.string().required("Country is required"), +}) From d9ee50c1fb371508e1b3d23e7e13bb6a26f6cf2f Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:02:16 -0600 Subject: [PATCH 07/13] implement no-phone invoice address form --- src/Apps/Invoice/Components/InvoicePaymentForm.tsx | 8 +++++++- src/Components/Address/AddressFormFields.tsx | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx index 5d229d5a292..e8161eefab6 100644 --- a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx +++ b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx @@ -2,6 +2,7 @@ import { Button, Spacer } from "@artsy/palette" import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" import { AddressFormWithCreditCard } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useCreateTokenAndSubmit } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" +import { addressFormFieldsValidator } from "Components/Address/AddressFormFields" import { emptyAddress } from "Components/Address/utils" import { Formik, Form } from "formik" import { useRouter } from "System/Hooks/useRouter" @@ -12,7 +13,9 @@ export interface InvoicePaymentFormProps { amountMinor: number } -export const InvoicePaymentForm: React.FC> = props => { +export const InvoicePaymentForm: React.FC> = props => { const { match, router } = useRouter() const token = match.params.token const invoiceRoute = `/invoice/${token}` @@ -28,6 +31,9 @@ export const InvoicePaymentForm: React.FC onSubmit={handleSubmit} initialValues={{ address: emptyAddress, creditCard: false }} + validationSchema={{ + ...addressFormFieldsValidator({ withPhoneNumber: false }), + }} > {({ isSubmitting, isValid }) => { return ( diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index 12d928766f3..1f032ded1df 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -19,9 +19,9 @@ interface Props { withPhoneNumber?: boolean } -export const addressFormFieldsValidator = ( - args: { withPhoneNumber?: boolean } = {} -) => ({ +export const addressFormFieldsValidator = (args: { + withPhoneNumber: boolean +}) => ({ address: yupAddressValidator, ...(args.withPhoneNumber && { phoneNumber: basicPhoneValidator }), }) From b4a2c84619d3db9ac518e976372fe983d380e5b8 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:14:23 -0600 Subject: [PATCH 08/13] revert changes to shipping route types for now --- .../Routes/Shipping/Utils/computeOrderData.ts | 4 +-- .../Routes/Shipping/Utils/shippingUtils.ts | 32 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts b/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts index 08d2aea97fb..64b9650e19d 100644 --- a/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts +++ b/src/Apps/Order/Routes/Shipping/Utils/computeOrderData.ts @@ -2,7 +2,7 @@ import { ShippingContextProps } from "Apps/Order/Routes/Shipping/ShippingContext import { FulfillmentType, PickupValues, - Address, + ShippingAddressFormValues, addressWithFallbackValues, matchAddressFields, } from "Apps/Order/Routes/Shipping/Utils/shippingUtils" @@ -33,7 +33,7 @@ type SavedFulfillmentData = | { fulfillmentType: FulfillmentType.SHIP isArtsyShipping: boolean - attributes: Address + attributes: ShippingAddressFormValues selectedSavedAddressID: string | null } | null diff --git a/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts b/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts index b12b683269e..4b72e31a6bd 100644 --- a/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts +++ b/src/Apps/Order/Routes/Shipping/Utils/shippingUtils.ts @@ -2,7 +2,7 @@ import * as Yup from "yup" import { AddressVerifiedBy } from "Apps/Order/Components/AddressVerificationFlow" import { ShippingContext_me$data } from "__generated__/ShippingContext_me.graphql" import { pick, omitBy, isNil, isEqual } from "lodash" -import { Address, postalCodeValidator } from "Components/Address/utils" +import { postalCodeValidator } from "Components/Address/utils" export enum FulfillmentType { SHIP = "SHIP", @@ -29,7 +29,7 @@ export interface PickupValues { export interface ShipValues { fulfillmentType: FulfillmentType.SHIP - attributes: Address + attributes: ShippingAddressFormValues meta: FormMetaValues } @@ -46,6 +46,17 @@ interface FormMetaValues { export type FulfillmentValues = ShipValues | PickupValues +export interface ShippingAddressFormValues { + name: string + phoneNumber: string + addressLine1: string + addressLine2: string + city: string + region: string + country: string + postalCode: string +} + // TODO: Replace with what we use in SettingsShippingAddressForm when we have // a rich phone input export const BASIC_PHONE_VALIDATION_SHAPE = { @@ -69,7 +80,7 @@ export const ADDRESS_VALIDATION_SHAPE = { country: Yup.string().required("Country is required"), } -const ORDER_EMPTY_ADDRESS: Address = { +const ORDER_EMPTY_ADDRESS: ShippingAddressFormValues = { name: "", phoneNumber: "", addressLine1: "", @@ -81,17 +92,22 @@ const ORDER_EMPTY_ADDRESS: Address = { } export const onlyAddressValues = (values: any) => { - return pick
(values, Object.keys(ORDER_EMPTY_ADDRESS)) + return pick( + values, + Object.keys(ORDER_EMPTY_ADDRESS) + ) } /** * Takes an address object and returns a new address object with all the * non-null values from the original address object. Useful for converting - * a SavedAddress from relay to a Address object. + * a SavedAddress from relay to a ShippingAddressFormValues object. */ -export const addressWithFallbackValues = (address: any): Address => ({ +export const addressWithFallbackValues = ( + address: any +): ShippingAddressFormValues => ({ ...ORDER_EMPTY_ADDRESS, - ...omitBy
(onlyAddressValues(address), isNil), + ...omitBy(onlyAddressValues(address), isNil), }) export type SavedAddressType = NonNullable< @@ -173,7 +189,7 @@ export const getInitialShippingValues = ( export const matchAddressFields = (...addressPair: [object, object]) => { const [a1, a2] = addressPair.map(a => addressWithFallbackValues(a)) - const fields: Array = [ + const fields: Array = [ "addressLine1", "addressLine2", "city", From 5e26bfb30056d1636147b51be95544c4bd4e73df Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:26:56 -0600 Subject: [PATCH 09/13] cleanup --- src/Apps/Invoice/Components/InvoicePaymentForm.tsx | 7 ++++--- src/Components/Address/AddressAutocompleteInput.tsx | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx index e8161eefab6..4347d0872aa 100644 --- a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx +++ b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx @@ -1,10 +1,11 @@ +import { Formik, Form } from "formik" +import * as Yup from "yup" import { Button, Spacer } from "@artsy/palette" import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" import { AddressFormWithCreditCard } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useCreateTokenAndSubmit } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" import { addressFormFieldsValidator } from "Components/Address/AddressFormFields" import { emptyAddress } from "Components/Address/utils" -import { Formik, Form } from "formik" import { useRouter } from "System/Hooks/useRouter" export interface InvoicePaymentFormProps { @@ -31,9 +32,9 @@ export const InvoicePaymentForm: React.FC onSubmit={handleSubmit} initialValues={{ address: emptyAddress, creditCard: false }} - validationSchema={{ + validationSchema={Yup.object().shape({ ...addressFormFieldsValidator({ withPhoneNumber: false }), - }} + })} > {({ isSubmitting, isValid }) => { return ( diff --git a/src/Components/Address/AddressAutocompleteInput.tsx b/src/Components/Address/AddressAutocompleteInput.tsx index bf45ba083bd..4c3e6ebcf22 100644 --- a/src/Components/Address/AddressAutocompleteInput.tsx +++ b/src/Components/Address/AddressAutocompleteInput.tsx @@ -312,6 +312,7 @@ export const AddressAutocompleteInput = ({ onChange={onChange} error={error} data-testid={dataTestId} + required={!!autocompleteProps.required} /> ) } From defdffee340aea81ec017c2b4c0875a72bcdaf35 Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:55:43 -0600 Subject: [PATCH 10/13] fix tests --- .../__tests__/AddressFormWithCreditCard.jest.enzyme.tsx | 6 +++--- .../Hooks/__tests__/useCreateTokenAndSubmit.jest.tsx | 9 ++++++--- .../Hooks/__tests__/useCreateTokenAndSubmit.jest.ts | 7 ++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx b/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx index c402353a460..13a00652304 100644 --- a/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx +++ b/src/Apps/Auction/Components/Form/__tests__/AddressFormWithCreditCard.jest.enzyme.tsx @@ -7,8 +7,8 @@ jest.mock("Apps/Auction/Hooks/useAuctionFormContext") jest.mock("Components/CreditCardInput", () => ({ CreditCardInput: () => null, })) -jest.mock("../AddressForm", () => ({ - AddressForm: () => null, +jest.mock("Components/Address/AddressFormFields", () => ({ + AddressFormFields: () => null, })) describe("AddressFormWithCreditCard", () => { @@ -35,7 +35,7 @@ describe("AddressFormWithCreditCard", () => { it("renders correct components", () => { const wrapper = getWrapper() expect(wrapper.find("CreditCardInput")).toHaveLength(1) - expect(wrapper.find("AddressForm")).toHaveLength(1) + expect(wrapper.find("AddressFormFields")).toHaveLength(1) }) describe("credit card error handling", () => { diff --git a/src/Apps/Auction/Hooks/__tests__/useCreateTokenAndSubmit.jest.tsx b/src/Apps/Auction/Hooks/__tests__/useCreateTokenAndSubmit.jest.tsx index 4e3da4634e5..c8949ad25d8 100644 --- a/src/Apps/Auction/Hooks/__tests__/useCreateTokenAndSubmit.jest.tsx +++ b/src/Apps/Auction/Hooks/__tests__/useCreateTokenAndSubmit.jest.tsx @@ -11,9 +11,10 @@ import { flushPromiseQueue } from "DevTools/flushPromiseQueue" import { useAuctionTracking } from "Apps/Auction/Hooks/useAuctionTracking" import { useRefreshUserData } from "Apps/Auction/Queries/useRefreshUserData" -jest.mock("Components/Address/AddressForm", () => ({ - toStripeAddress: jest.fn(), -})) +jest.mock("Components/Address/utils", () => { + const actual = jest.requireActual("Components/Address/utils") + return { ...actual, toStripeAddress: jest.fn() } +}) jest.mock("Apps/Auction/Queries/useRefreshUserData") jest.mock("Apps/Auction/Hooks/useAuctionTracking") @@ -39,6 +40,7 @@ describe("useCreateTokenAndSubmit", () => { const values = { phoneNumber: "+1 (123) 456-7890", + address: {}, } const helpers = { @@ -252,6 +254,7 @@ describe("useCreateTokenAndSubmit", () => { it("sets submitting to false at the very end", async () => { await setupHook() await flushPromiseQueue() + expect(helpers.setSubmitting).toHaveBeenCalledWith(false) }) }) diff --git a/src/Apps/Invoice/Hooks/__tests__/useCreateTokenAndSubmit.jest.ts b/src/Apps/Invoice/Hooks/__tests__/useCreateTokenAndSubmit.jest.ts index 40d6550d36d..b742ef9f5ab 100644 --- a/src/Apps/Invoice/Hooks/__tests__/useCreateTokenAndSubmit.jest.ts +++ b/src/Apps/Invoice/Hooks/__tests__/useCreateTokenAndSubmit.jest.ts @@ -8,9 +8,10 @@ import { UseCreateTokenAndSubmitProps, } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" -jest.mock("Components/Address/AddressForm", () => ({ - toStripeAddress: jest.fn(), -})) +jest.mock("Components/Address/utils", () => { + const actual = jest.requireActual("Components/Address/utils") + return { ...actual, toStripeAddress: jest.fn() } +}) jest.mock("Apps/Invoice/Hooks/useMakeInvoicePayment") From 13be370981698e1e24e9e92181215709fdba191c Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:47:17 -0600 Subject: [PATCH 11/13] delete unused AddressForm --- src/Apps/Invoice/Components/AddressForm.tsx | 113 ------------------ .../Components/AddressFormWithCreditCard.tsx | 6 +- .../Invoice/Components/InvoicePaymentForm.tsx | 2 +- .../Invoice/Hooks/useCreateTokenAndSubmit.ts | 2 +- src/Apps/Invoice/Hooks/useFormContext.ts | 2 +- 5 files changed, 8 insertions(+), 117 deletions(-) delete mode 100644 src/Apps/Invoice/Components/AddressForm.tsx diff --git a/src/Apps/Invoice/Components/AddressForm.tsx b/src/Apps/Invoice/Components/AddressForm.tsx deleted file mode 100644 index ec323e93017..00000000000 --- a/src/Apps/Invoice/Components/AddressForm.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Column, GridColumns, Input } from "@artsy/palette" -import { CountrySelect } from "Components/CountrySelect" -import { Address } from "Components/Address/utils" -import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" - -export interface AddressFormValues { - address: Address - creditCard?: boolean -} - -export const AddressForm = () => { - const { handleChange, handleBlur, errors, values, touched } = useFormContext() - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx index fdb856b9d9d..5ae4aaea9ac 100644 --- a/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx +++ b/src/Apps/Invoice/Components/AddressFormWithCreditCard.tsx @@ -1,9 +1,13 @@ import { Join, Spacer } from "@artsy/palette" import { CreditCardInput } from "Components/CreditCardInput" -import { AddressFormValues } from "./AddressForm" import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" import { AddressFormFields } from "Components/Address/AddressFormFields" +import { Address } from "Components/Address/utils" +export interface AddressFormValues { + address: Address + creditCard?: boolean +} export const AddressFormWithCreditCard: React.FC> = () => { diff --git a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx index 4347d0872aa..04dcba2b851 100644 --- a/src/Apps/Invoice/Components/InvoicePaymentForm.tsx +++ b/src/Apps/Invoice/Components/InvoicePaymentForm.tsx @@ -1,7 +1,7 @@ import { Formik, Form } from "formik" import * as Yup from "yup" import { Button, Spacer } from "@artsy/palette" -import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" +import { AddressFormValues } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { AddressFormWithCreditCard } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useCreateTokenAndSubmit } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" import { addressFormFieldsValidator } from "Components/Address/AddressFormFields" diff --git a/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts b/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts index 7444ac6afaa..f9d1a9635bd 100644 --- a/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts +++ b/src/Apps/Invoice/Hooks/useCreateTokenAndSubmit.ts @@ -12,7 +12,7 @@ import { stripeNotLoadedErrorMessage, } from "Apps/Auction/Components/Form/Utils/errorMessages" import { FormikHelpers } from "formik" -import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" +import { AddressFormValues } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useMakeInvoicePayment } from "Apps/Invoice/Hooks/useMakeInvoicePayment" import { InvoicePaymentFormProps } from "Apps/Invoice/Components/InvoicePaymentForm" import { useToasts } from "@artsy/palette" diff --git a/src/Apps/Invoice/Hooks/useFormContext.ts b/src/Apps/Invoice/Hooks/useFormContext.ts index a38a23d8422..bbb4f8fe135 100644 --- a/src/Apps/Invoice/Hooks/useFormContext.ts +++ b/src/Apps/Invoice/Hooks/useFormContext.ts @@ -1,4 +1,4 @@ -import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" +import { AddressFormValues } from "Apps/Invoice/Components/AddressFormWithCreditCard" import { useFormikContext } from "formik" export const useFormContext = () => { From 8a9f7f91453d088b4119ee2c4d3abc9f93939ebd Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:02:09 -0600 Subject: [PATCH 12/13] cleanup --- src/Components/Address/__tests__/AddressFormFields.jest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Address/__tests__/AddressFormFields.jest.tsx b/src/Components/Address/__tests__/AddressFormFields.jest.tsx index 1a42ff64873..aacac55892f 100644 --- a/src/Components/Address/__tests__/AddressFormFields.jest.tsx +++ b/src/Components/Address/__tests__/AddressFormFields.jest.tsx @@ -14,7 +14,7 @@ import { flushPromiseQueue } from "DevTools/flushPromiseQueue" import { Formik } from "formik" import * as Yup from "yup" -describe("AddressForm", () => { +describe("AddressFormFields", () => { const mockOnSubmit = jest.fn() beforeEach(() => { mockOnSubmit.mockClear() From f1202358d880f0c9473940fa856499f2c6fc0d5d Mon Sep 17 00:00:00 2001 From: Erik <9088720+erikdstock@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:24:35 -0600 Subject: [PATCH 13/13] cleanup + docs --- src/Components/Address/AddressFormFields.tsx | 43 ++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Components/Address/AddressFormFields.tsx b/src/Components/Address/AddressFormFields.tsx index 1f032ded1df..b5b3639f817 100644 --- a/src/Components/Address/AddressFormFields.tsx +++ b/src/Components/Address/AddressFormFields.tsx @@ -19,13 +19,50 @@ interface Props { withPhoneNumber?: boolean } -export const addressFormFieldsValidator = (args: { - withPhoneNumber: boolean -}) => ({ +/** + * Validation schema for address form fields. Arguments match the + * component - e.g. to include phone number validation + * @example + * ```tsx + * const validationSchema = yup.object().shape({ + * ...addressFormFieldsValidator({ withPhoneNumber: true }), + * saveAddress: boolean + * }) + * // later... + * + * + * withPhoneNumber /> + * ``` + */ +export const addressFormFieldsValidator = (args: Props = {}) => ({ address: yupAddressValidator, ...(args.withPhoneNumber && { phoneNumber: basicPhoneValidator }), }) +/** + * Form fields for collecting address information. This component is intended + * to be used within a Formik form, and the `Values` interface of that form + * should fulfill a `FormikContextWithAddress` interface: + * - the relevant nested `address` object + * - plus a `phoneNumber` if the `withPhoneNumber` prop is passed + * For a composable validation schema, see `addressFormFieldsValidator()`. + * + * @example + * ```tsx + * interface MyFormValues { + * address: Address + * phoneNumber: string + * saveAddress: boolean + * } + * + * {...otherFormikProps}> + * withPhoneNumber /> + * + * + */ export const AddressFormFields = ( props: Props ) => {