Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-32111) feat(XPCine): add a button anchor to access to screenings #7251

Merged
merged 3 commits into from
Nov 28, 2024
Merged
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
30 changes: 30 additions & 0 deletions src/features/offer/components/OfferCine/CineContentCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FC } from 'react'
import styled from 'styled-components/native'

import { useOfferCTA } from 'features/offer/components/OfferContent/OfferCTAProvider'
import { ButtonWithLinearGradient } from 'ui/components/buttons/buttonWithLinearGradient/ButtonWithLinearGradient'
import { StickyBottomWrapper } from 'ui/components/StickyBottomWrapper/StickyBottomWrapper'
import { Spacer } from 'ui/theme'

export const CineContentCTA: FC = () => {
const { onPress, wording } = useOfferCTA()

return (
<StickyBottomWrapper>
<CallToActionContainer>
<Spacer.Column numberOfSpaces={6} />
<ButtonWithLinearGradient wording={wording} onPress={onPress} />
<Spacer.Column numberOfSpaces={6} />
</CallToActionContainer>
</StickyBottomWrapper>
)
}

const CallToActionContainer = styled.View(({ theme }) => ({
alignSelf: 'center',
paddingHorizontal: theme.contentPage.marginHorizontal,
width: '100%',
...(!theme.isMobileViewport && {
maxWidth: theme.contentPage.maxWidth,
}),
}))
28 changes: 24 additions & 4 deletions src/features/offer/components/OfferCine/OfferCineBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React, { FC } from 'react'
import React, { FC, useEffect } from 'react'
import { View, ViewStyle } from 'react-native'
import { InView } from 'react-native-intersection-observer'
import styled, { useTheme } from 'styled-components/native'

import { OfferResponseV2 } from 'api/gen'
import { MovieCalendarProvider } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext'
import { OfferCineContent } from 'features/offer/components/OfferCine/OfferCineContent'
import { useOfferCTA } from 'features/offer/components/OfferContent/OfferCTAProvider'
import { cinemaCTAButtonName } from 'features/venue/components/VenueOffers/VenueOffers'
import { AppThemeType } from 'theme'
import { Anchor } from 'ui/components/anchor/Anchor'
import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext'
import { getSpacing, Spacer, TypoDS } from 'ui/theme'
import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs'

Expand All @@ -17,12 +22,27 @@ type Props = {

export const OfferCineBlock: FC<Props> = ({ title, onSeeVenuePress, offer }) => {
const theme = useTheme()
const { setButton, showButton } = useOfferCTA()
const scrollToAnchor = useScrollToAnchor()

useEffect(() => {
setButton(cinemaCTAButtonName, () => {
scrollToAnchor('offer-cine-availabilities')
})
}, [scrollToAnchor, setButton])

return (
<Container testID="offer-new-xp-cine-block">
<TitleContainer>
<TypoDS.Title3 {...getHeadingAttrs(2)}>{title}</TypoDS.Title3>
</TitleContainer>
<Anchor name="offer-cine-availabilities">
<InView
onChange={(inView) => {
showButton(!inView)
}}>
<TitleContainer>
<TypoDS.Title3 {...getHeadingAttrs(2)}>{title}</TypoDS.Title3>
</TitleContainer>
</InView>
</Anchor>
<Spacer.Column numberOfSpaces={4} />
<MovieCalendarProvider nbOfDays={15} containerStyle={getCalendarStyle(theme)}>
<OfferCineContent offer={offer} onSeeVenuePress={onSeeVenuePress} />
Expand Down
48 changes: 48 additions & 0 deletions src/features/offer/components/OfferContent/OfferCTAProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { createContext, useContext, ReactNode, useState, useCallback, useMemo } from 'react'

interface OfferCTAContextValue {
wording: string
onPress: () => void
setButton: (wording: string, onPress: () => void) => void
showButton: (isVisible: boolean) => void
isButtonVisible: boolean
}

const OfferCTAContext = createContext<OfferCTAContextValue | undefined>(undefined)

interface OfferCTAProviderProps {
children: ReactNode
}

export const OfferCTAProvider: React.FC<OfferCTAProviderProps> = ({ children }) => {
const [wording, setWording] = useState<string>('')
const [isVisible, setIsVisible] = useState<boolean>(false)
const [onPress, setOnPress] = useState<() => void>(() => () => {
return
})

const setButton = useCallback(
(newWording: string, newOnPress: () => void) => {
if (newWording !== wording) {
setWording(newWording)
setOnPress(() => newOnPress)
}
},
[wording]
)

const value = useMemo(
() => ({ wording, onPress, setButton, showButton: setIsVisible, isButtonVisible: isVisible }),
[isVisible, onPress, setButton, wording]
)

return <OfferCTAContext.Provider value={value}>{children}</OfferCTAContext.Provider>
}

export const useOfferCTA = (): OfferCTAContextValue => {
const context = useContext(OfferCTAContext)
if (!context) {
throw new Error('useOfferCTAButton must be used within an OfferCTAProvider')
}
return context
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { NavigationContainer } from '@react-navigation/native'
import React, { ComponentProps } from 'react'
import { InViewProps } from 'react-native-intersection-observer'

import {
OfferResponseV2,
RecommendationApiParams,
SubcategoriesResponseModelv2,
SubcategoryIdEnum,
SubcategoryIdEnumv2,
} from 'api/gen'
import * as useGoBack from 'features/navigation/useGoBack'
Expand All @@ -14,12 +14,15 @@ import { PlaylistType } from 'features/offer/enums'
import { mockSubcategory } from 'features/offer/fixtures/mockSubcategory'
import { offerResponseSnap } from 'features/offer/fixtures/offerResponse'
import * as useArtistResults from 'features/offer/helpers/useArtistResults/useArtistResults'
import { cinemaCTAButtonName } from 'features/venue/components/VenueOffers/VenueOffers'
import {
mockedAlgoliaOffersWithSameArtistResponse,
mockedAlgoliaResponse,
} from 'libs/algolia/fixtures/algoliaFixtures'
import { analytics } from 'libs/analytics'
import * as useFeatureFlagAPI from 'libs/firebase/firestore/featureFlags/useFeatureFlag'
import { DEFAULT_REMOTE_CONFIG } from 'libs/firebase/remoteConfig/remoteConfig.constants'
import * as useRemoteConfigContextModule from 'libs/firebase/remoteConfig/RemoteConfigProvider'
import { Position } from 'libs/location'
import { SuggestedPlace } from 'libs/place/types'
import { BatchEvent, BatchUser } from 'libs/react-native-batch'
Expand All @@ -28,6 +31,7 @@ import { mockAuthContextWithoutUser } from 'tests/AuthContextUtils'
import { mockServer } from 'tests/mswServer'
import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC'
import { act, fireEvent, render, screen, userEvent, waitFor } from 'tests/utils'
import * as AnchorContextModule from 'ui/components/anchor/AnchorContext'

import { OfferContent } from './OfferContent'

Expand Down Expand Up @@ -85,23 +89,29 @@ jest.spyOn(useArtistResults, 'useArtistResults').mockReturnValue({
artistTopOffers: mockedAlgoliaOffersWithSameArtistResponse.slice(0, 4),
})

/**
* This mock permit to simulate the visibility of the playlist
* it is an alternative solution which allows you to replace the scroll simulation
* it's not optimal, if you have better idea don't hesitate to update
*/
const mockInView = jest.fn()
const InViewMock = ({
onChange,
children,
}: {
onChange: VoidFunction
children: React.ReactNode
}) => {
mockInView.mockImplementation(onChange)
return <React.Fragment>{children}</React.Fragment>
}

jest.mock('react-native-intersection-observer', () => {
const InView = (props: InViewProps) => {
mockInView.mockImplementation(props.onChange)
return null
}
return {
...jest.requireActual('react-native-intersection-observer'),
InView,
InView: InViewMock,
mockInView,
}
})

const useScrollToAnchorSpy = jest.spyOn(AnchorContextModule, 'useScrollToAnchor')
const useRemoteConfigContextSpy = jest.spyOn(useRemoteConfigContextModule, 'useRemoteConfigContext')

jest.mock('libs/firebase/remoteConfig/RemoteConfigProvider', () => ({
useRemoteConfigContext: jest.fn().mockReturnValue({
sameAuthorPlaylist: 'withPlaylistAsFirst',
Expand Down Expand Up @@ -525,6 +535,77 @@ describe('<OfferContent />', () => {

expect(screen.queryByTestId('booking-button')).not.toBeOnTheScreen()
})

describe('movie screening access button', () => {
beforeAll(() => {
useRemoteConfigContextSpy.mockReturnValue({
...DEFAULT_REMOTE_CONFIG,
showAccessScreeningButton: true,
})
})

it('should show button', async () => {
renderOfferContent({
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.SEANCE_CINE },
})

await act(async () => {
mockInView(false)
})

await screen.findByText('Trouve ta séance')

expect(await screen.findByText(cinemaCTAButtonName)).toBeOnTheScreen()
})

it('should not show button', async () => {
renderOfferContent({
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.SEANCE_CINE },
})

await act(async () => {
mockInView(true)
})

await screen.findByText('Trouve ta séance')

expect(screen.queryByText(cinemaCTAButtonName)).not.toBeOnTheScreen()
})

it('should scroll to anchor', async () => {
renderOfferContent({
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.SEANCE_CINE },
})

await act(async () => {
mockInView(false)
})

const button = await screen.findByText(cinemaCTAButtonName)

await userEvent.press(button)

expect(useScrollToAnchorSpy).toHaveBeenCalledWith()
})

it('should not display the button if the remote config flag is deactivated', async () => {
useRemoteConfigContextSpy.mockReturnValueOnce({
...DEFAULT_REMOTE_CONFIG,
showAccessScreeningButton: false,
})
renderOfferContent({
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.SEANCE_CINE },
})

await act(async () => {
mockInView(true)
})

await screen.findByText('Trouve ta séance')

expect(screen.queryByText(cinemaCTAButtonName)).not.toBeOnTheScreen()
})
})
})

type RenderOfferContentType = Partial<ComponentProps<typeof OfferContent>> & {
Expand Down
24 changes: 13 additions & 11 deletions src/features/offer/components/OfferContent/OfferContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components/native'

import { UseNavigationType } from 'features/navigation/RootNavigator/types'
import { OfferContentBase } from 'features/offer/components/OfferContent/OfferContentBase'
import { OfferCTAProvider } from 'features/offer/components/OfferContent/OfferCTAProvider'
import { OfferCTAButton } from 'features/offer/components/OfferCTAButton/OfferCTAButton'
import { useOfferBatchTracking } from 'features/offer/helpers/useOfferBatchTracking/useOfferBatchTracking'
import { OfferContentProps } from 'features/offer/types'
Expand All @@ -18,11 +19,10 @@ export const OfferContent: FunctionComponent<OfferContentProps> = ({
subcategory,
}) => {
const { navigate } = useNavigation<UseNavigationType>()
const { trackEventHasSeenOfferOnce } = useOfferBatchTracking(subcategory.id)

const handlePress = (defaultIndex = 0) => {
navigate('OfferPreview', { id: offer.id, defaultIndex })
}
const { trackEventHasSeenOfferOnce } = useOfferBatchTracking(subcategory.id)

const footer = useMemo(
() => (
Expand All @@ -36,15 +36,17 @@ export const OfferContent: FunctionComponent<OfferContentProps> = ({
)

return (
<OfferContentBase
offer={offer}
searchGroupList={searchGroupList}
contentContainerStyle={CONTENT_CONTAINER_STYLE}
onOfferPreviewPress={handlePress}
footer={footer}
BodyWrapper={BodyWrapper}
subcategory={subcategory}
/>
<OfferCTAProvider>
<OfferContentBase
offer={offer}
searchGroupList={searchGroupList}
contentContainerStyle={CONTENT_CONTAINER_STYLE}
onOfferPreviewPress={handlePress}
BodyWrapper={BodyWrapper}
footer={footer}
subcategory={subcategory}
/>
</OfferCTAProvider>
)
}

Expand Down
35 changes: 19 additions & 16 deletions src/features/offer/components/OfferContent/OfferContent.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components/native'

import { OfferImageResponse } from 'api/gen'
import { OfferContentBase } from 'features/offer/components/OfferContent/OfferContentBase'
import { OfferCTAProvider } from 'features/offer/components/OfferContent/OfferCTAProvider'
import { OfferCTAButton } from 'features/offer/components/OfferCTAButton/OfferCTAButton'
import { useOfferBatchTracking } from 'features/offer/helpers/useOfferBatchTracking/useOfferBatchTracking'
import { OfferContentProps } from 'features/offer/types'
Expand Down Expand Up @@ -53,22 +54,24 @@ export const OfferContent: FunctionComponent<OfferContentProps> = ({
)

return (
<React.Fragment>
<ImagesCarouselModal
hideModal={hideModal}
isVisible={visible}
imagesURL={offerImages}
defaultIndex={carouselDefaultIndex}
/>
<StyledOfferContentBase
offer={offer}
searchGroupList={searchGroupList}
subcategory={subcategory}
onOfferPreviewPress={handlePress}
BodyWrapper={BodyWrapper}
footer={footer}
/>
</React.Fragment>
<OfferCTAProvider>
<React.Fragment>
<ImagesCarouselModal
hideModal={hideModal}
isVisible={visible}
imagesURL={offerImages}
defaultIndex={carouselDefaultIndex}
/>
<StyledOfferContentBase
offer={offer}
searchGroupList={searchGroupList}
subcategory={subcategory}
onOfferPreviewPress={handlePress}
BodyWrapper={BodyWrapper}
footer={footer}
/>
</React.Fragment>
</OfferCTAProvider>
)
}

Expand Down
Loading