Skip to content
Merged

Deploy #17322

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions data/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3051,6 +3051,9 @@ type Artwork implements Node & Searchable & Sellable {
# Whether a user can make an offer on the work through inquiry
isOfferableFromInquiry: Boolean
isOnHold: String

# Whether a partner can send an offer for this work
isPartnerOfferable: Boolean!
isPartnerPromoted: Boolean

# Whether a work is available for pickup
Expand Down Expand Up @@ -9947,6 +9950,19 @@ type Conversation implements Node {
states: [CommerceOrderStateEnum!]
): CommerceOrderConnectionWithTotalCount

# Partner offers for this conversation's artwork, scoped to the user who initiated the conversation (from_id).
partnerOffersConnection(
after: String
before: String
first: Int
last: Int

# Filter by offer type(s). Gravity defaults to bulk offers when omitted.
offerType: [PartnerOfferTypeEnum]
page: Int
size: Int
): PartnerOfferConnection

# A connection of orders for artworks in this conversation from the partner's perspective.
partnerOrdersConnection(
after: String
Expand Down Expand Up @@ -17895,6 +17911,9 @@ type Me implements Node {
before: String
first: Int
last: Int

# Filter by offer type(s). Gravity defaults to all a users partner offers when omitted.
offerType: [PartnerOfferTypeEnum]
page: Int
size: Int
sort: PartnerOfferToCollectorSorts
Expand Down
2 changes: 1 addition & 1 deletion src/Apps/Conversations/ConversationsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const useConversationsContext = () => {
const VIEWER_FRAGMENT = graphql`
fragment ConversationsContext_viewer on Viewer {
me {
partnerOffersConnection(first: 100) {
partnerOffersConnection(first: 100, offerType: [PERSONALIZED]) {
edges {
node {
artworkId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import VerifiedIcon from "@artsy/icons/VerifiedIcon"
import { Box, Flex, type FlexProps, Spacer, Text } from "@artsy/palette"
import { themeGet } from "@styled-system/theme-get"
import { useFlag } from "@unleash/proxy-client-react"
import { useConversationsContext } from "Apps/Conversations/ConversationsContext"
import { ConversationConfirmModal } from "Apps/Conversations/components/ConversationCTA/ConversationConfirmModal"
import { ConversationMakeOfferButton } from "Apps/Conversations/components/ConversationCTA/ConversationMakeOfferButton"
Expand Down Expand Up @@ -31,6 +32,8 @@ export const ConversationCTA: React.FC<
partnerOffer?.endAt ?? new Date(0).toISOString(),
)

const isPartnerOfferConvoEnabled = useFlag("topaz_partner-offer-convo")

const activeOrder = extractNodes(data.activeOrderCTA)[0]
const activePartnerOffer =
!activeOrder && partnerOffer && !partnerOfferTimer.hasEnded
Expand All @@ -50,7 +53,12 @@ export const ConversationCTA: React.FC<
const canPurchase = artwork.isAcquireable || !!activePartnerOffer
const canMakeOffer = artwork.isOfferable || artwork.isOfferableFromInquiry
const showTransactionButtons =
!activeOrder && artwork.published && (canPurchase || canMakeOffer)
!activeOrder &&
// When the new convo offer UI is on, an active offer is shown via the CTA
// bar instead, so the transaction buttons are suppressed.
!(isPartnerOfferConvoEnabled && activePartnerOffer) &&
artwork.published &&
(canPurchase || canMakeOffer)

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { screen } from "@testing-library/react"
import { useFlag } from "@unleash/proxy-client-react"
import { ConversationsProvider } from "Apps/Conversations/ConversationsContext"
import { ConversationCTA } from "Apps/Conversations/components/ConversationCTA/ConversationCTA"
import { setupTestWrapperTL } from "DevTools/setupTestWrapperTL"
Expand All @@ -10,6 +11,8 @@ jest.unmock("react-relay")
jest.mock("react-tracking")
jest.mock("System/Hooks/useSystemContext")

const mockUseFlag = useFlag as jest.Mock

describe("ConversationCTA", () => {
const { renderWithRelay } = setupTestWrapperTL({
Component: (props: any) => (
Expand Down Expand Up @@ -190,34 +193,61 @@ describe("ConversationCTA", () => {
expect(screen.queryByText("Purchase")).not.toBeInTheDocument()
expect(screen.queryByText("Make an Offer")).not.toBeInTheDocument()
})
it("renders purchase button on orders with an active partner offer", () => {
const offerViewer = () => {
const futureTime = new Date()
futureTime.setHours(futureTime.getHours() + 1)

renderWithRelay({
Conversation: mockConversationWithArtwork({
internalID: "artwork-id",
isAcquireable: false,
}),
Viewer: () => ({
me: {
partnerOffersConnection: {
edges: [
{
node: {
internalID: "partner-offer-id",
artworkId: "artwork-id",
endAt: futureTime.toISOString(),
},
return () => ({
me: {
partnerOffersConnection: {
edges: [
{
node: {
internalID: "partner-offer-id",
artworkId: "artwork-id",
endAt: futureTime.toISOString(),
},
],
},
},
],
},
}),
},
})
}

expect(screen.queryByText("Purchase")).toBeInTheDocument()
expect(screen.queryByText("Make an Offer")).not.toBeInTheDocument()
describe("with the partner-offer-convo flag on", () => {
beforeEach(() => {
mockUseFlag.mockImplementation(() => true)
})

it("hides the transaction buttons when there is an active partner offer", () => {
renderWithRelay({
Conversation: mockConversationWithArtwork({
internalID: "artwork-id",
isAcquireable: true,
}),
Viewer: offerViewer(),
})

expect(screen.queryByText("Purchase")).not.toBeInTheDocument()
expect(screen.queryByText("Make an Offer")).not.toBeInTheDocument()
})
})

describe("with the partner-offer-convo flag off", () => {
beforeEach(() => {
mockUseFlag.mockImplementation(() => false)
})

it("ignores the partner offer and shows the normal transaction buttons", () => {
renderWithRelay({
Conversation: mockConversationWithArtwork({
internalID: "artwork-id",
isAcquireable: true,
}),
Viewer: offerViewer(),
})
expect(screen.queryByText("Purchase")).toBeInTheDocument()
})
})

it("renders Make Offer button on MO-only artworks", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/Apps/Conversations/components/ConversationReply.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sentConversationMessage } from "@artsy/cohesion"
import { Button, Flex, TextArea, useToasts } from "@artsy/palette"
import { ConversationCTA } from "Apps/Conversations/components/ConversationCTA/ConversationCTA"
import { ConversationPartnerOfferCTA } from "Apps/Conversations/components/Message/ConversationPartnerOfferCTA"
import { useSendConversationMessage } from "Apps/Conversations/mutations/useSendConversationMessage"
import { useRouter } from "System/Hooks/useRouter"
import { useSystemContext } from "System/Hooks/useSystemContext"
Expand Down Expand Up @@ -53,6 +54,7 @@ export const ConversationReply: FC<
item {
... on Artwork {
id
...ConversationPartnerOfferCTA_artwork
}
}
}
Expand Down Expand Up @@ -172,6 +174,8 @@ export const ConversationReply: FC<
backgroundColor="mono5"
flexDirection="column"
>
<ConversationPartnerOfferCTA artwork={data.items?.[0]?.item} />

<ConversationCTA conversation={data} px={1} pt={1} />

<form name="reply" onSubmit={handleSubmit}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import ChevronRightIcon from "@artsy/icons/ChevronRightIcon"
import { Clickable, Flex, Message, Text } from "@artsy/palette"
import { useFlag } from "@unleash/proxy-client-react"
import { useConversationsContext } from "Apps/Conversations/ConversationsContext"
import { RouterLink } from "System/Components/RouterLink"
import { useTimer } from "Utils/Hooks/useTimer"
import type { ConversationPartnerOfferCTA_artwork$key } from "__generated__/ConversationPartnerOfferCTA_artwork.graphql"
import type { FC } from "react"
import { graphql, useFragment } from "react-relay"

interface ConversationPartnerOfferCTAProps {
artwork?: ConversationPartnerOfferCTA_artwork$key | null
}

export const ConversationPartnerOfferCTA: FC<
React.PropsWithChildren<ConversationPartnerOfferCTAProps>
> = ({ artwork }) => {
const isPartnerOfferConvoEnabled = useFlag("topaz_partner-offer-convo")

const data = useFragment(ARTWORK_FRAGMENT, artwork)

const { findPartnerOffer } = useConversationsContext()

const partnerOffer = data?.internalID
? findPartnerOffer(data.internalID)
: null

const { hasEnded } = useTimer(partnerOffer?.endAt ?? "")

if (!isPartnerOfferConvoEnabled || !partnerOffer || !data?.href) {
return null
}

if (hasEnded || !partnerOffer.isAvailable) {
return null
}

const message = partnerOffer.priceWithDiscount?.display
? `Offer Received for ${partnerOffer.priceWithDiscount.display}`
: "Offer Received"

const href = `${data.href}?partner_offer_id=${partnerOffer.internalID}`

return (
<Clickable width="100%">
<RouterLink
to={href}
target="_blank"
textDecoration="none"
data-testid="partnerOfferActionLink"
>
<Flex
flexDirection="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
>
<Message variant="info" title={message} width="100%">
<Text variant="sm">Tap to review the offer from the gallery</Text>
</Message>

<ChevronRightIcon
fill="mono100"
position="absolute"
right={0}
pr={4}
/>
</Flex>
</RouterLink>
</Clickable>
)
}

const ARTWORK_FRAGMENT = graphql`
fragment ConversationPartnerOfferCTA_artwork on Artwork {
internalID
href
}
`
Loading
Loading