Skip to content

Commit

Permalink
Merge pull request #2738 from digitalfabrik/2699-appointment-booking-…
Browse files Browse the repository at this point in the history
…offer

2699: Support appointment booking offer
  • Loading branch information
steffenkleinle authored Mar 27, 2024
2 parents c850970 + 0dce7c1 commit e55cc22
Show file tree
Hide file tree
Showing 21 changed files with 319 additions and 104 deletions.
17 changes: 4 additions & 13 deletions native/src/components/Categories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { ReactElement } from 'react'
import { View } from 'react-native'

import { CATEGORIES_ROUTE, RouteInformationType } from 'shared'
import { CATEGORIES_ROUTE, getCategoryTiles, RouteInformationType } from 'shared'
import { CategoriesMapModel, CategoryModel, CityModel } from 'shared/api'

import TileModel from '../models/TileModel'
import testID from '../testing/testID'
import { LanguageResourceCacheStateType } from '../utils/DataContainer'
import CategoryListItem from './CategoryListItem'
Expand Down Expand Up @@ -34,29 +33,21 @@ const Categories = ({
goBack,
}: CategoriesProps): ReactElement => {
const children = categories.getChildren(category)
const cityCode = cityModel.code

const navigateToCategory = ({ path }: { path: string }) =>
navigateTo({
route: CATEGORIES_ROUTE,
cityCode: cityModel.code,
cityCode,
languageCode: language,
cityContentPath: path,
})

if (category.isRoot()) {
const tiles = children.map(
category =>
new TileModel({
title: category.title,
path: category.path,
thumbnail: category.thumbnail,
isExternalUrl: false,
}),
)
return (
<View {...testID('Dashboard-Page')}>
<Tiles
tiles={tiles}
tiles={getCategoryTiles({ categories: children, cityCode })}
language={language}
onTilePress={navigateToCategory}
resourceCache={resourceCache[category.path]}
Expand Down
6 changes: 1 addition & 5 deletions native/src/components/DashboardNavigationTiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {
OFFERS_ROUTE,
POIS_ROUTE,
RouteInformationType,
TileModel,
TU_NEWS_TYPE,
} from 'shared'
import { CityModel } from 'shared/api'

import { CalendarIcon, NewsIcon, OffersIcon, POIsIcon } from '../assets'
import buildConfig from '../constants/buildConfig'
import TileModel from '../models/TileModel'
import NavigationTiles from './NavigationTiles'

const Spacing = styled.View`
Expand Down Expand Up @@ -54,7 +54,6 @@ const DashboardNavigationTiles = ({
languageCode,
newsType: localNewsEnabled ? LOCAL_NEWS_TYPE : TU_NEWS_TYPE,
}),
notifications: 0,
}),
)
}
Expand All @@ -72,7 +71,6 @@ const DashboardNavigationTiles = ({
cityCode,
languageCode,
}),
notifications: 0,
}),
)
}
Expand All @@ -90,7 +88,6 @@ const DashboardNavigationTiles = ({
cityCode,
languageCode,
}),
notifications: 0,
}),
)
}
Expand All @@ -108,7 +105,6 @@ const DashboardNavigationTiles = ({
cityCode,
languageCode,
}),
notifications: 0,
}),
)
}
Expand Down
8 changes: 5 additions & 3 deletions native/src/components/NavigationTile.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { ReactElement } from 'react'
import React, { JSXElementConstructor, ReactElement } from 'react'
import { View } from 'react-native'
import { SvgProps } from 'react-native-svg'
import styled from 'styled-components/native'

import TileModel from '../models/TileModel'
import { TileModel } from 'shared'

import SimpleImage from './SimpleImage'
import Pressable from './base/Pressable'

Expand Down Expand Up @@ -43,7 +45,7 @@ const StyledIcon = styled(SimpleImage)`
`

type NavigationTileProps = {
tile: TileModel
tile: TileModel<JSXElementConstructor<SvgProps>>
width: number
}

Expand Down
8 changes: 5 additions & 3 deletions native/src/components/NavigationTiles.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { ReactElement, useRef, useState } from 'react'
import React, { JSXElementConstructor, ReactElement, useRef, useState } from 'react'
import { Dimensions, NativeScrollEvent, NativeSyntheticEvent, ScrollView } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { SvgProps } from 'react-native-svg'
import styled from 'styled-components/native'

import { TileModel } from 'shared'

import { ArrowBackIcon } from '../assets'
import TileModel from '../models/TileModel'
import HighlightBox from './HighlightBox'
import NavigationTile from './NavigationTile'
import Icon from './base/Icon'
Expand Down Expand Up @@ -33,7 +35,7 @@ const StyledIcon = styled(Icon)<{ disabled: boolean }>`
`

type NavigationTilesProps = {
tiles: TileModel[]
tiles: TileModel<JSXElementConstructor<SvgProps>>[]
}

const NavigationTiles = ({ tiles }: NavigationTilesProps): ReactElement => {
Expand Down
24 changes: 17 additions & 7 deletions native/src/components/Tile.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React, { ReactElement } from 'react'
import styled from 'styled-components/native'

import TileModel from '../models/TileModel'
import { TileModel } from 'shared'

import useSnackbar from '../hooks/useSnackbar'
import { PageResourceCacheStateType } from '../utils/DataContainer'
import openExternalUrl from '../utils/openExternalUrl'
import { reportError } from '../utils/sentry'
import SimpleImage from './SimpleImage'
import Pressable from './base/Pressable'

Expand Down Expand Up @@ -30,11 +34,17 @@ type TileProps = {
resourceCache: PageResourceCacheStateType | undefined
}

const Tile = ({ onTilePress, tile, resourceCache }: TileProps): ReactElement => (
<TileContainer onPress={() => onTilePress(tile)}>
<Thumbnail source={tile.thumbnail} resourceCache={resourceCache} />
<TileTitle android_hyphenationFrequency='full'>{tile.title}</TileTitle>
</TileContainer>
)
const Tile = ({ onTilePress, tile, resourceCache }: TileProps): ReactElement => {
const showSnackbar = useSnackbar()
const openTile = () =>
tile.isExternalUrl ? openExternalUrl(tile.path, showSnackbar).catch(reportError) : onTilePress(tile)

return (
<TileContainer onPress={openTile}>
<Thumbnail source={tile.thumbnail} resourceCache={resourceCache} />
<TileTitle android_hyphenationFrequency='full'>{tile.title}</TileTitle>
</TileContainer>
)
}

export default Tile
3 changes: 2 additions & 1 deletion native/src/components/Tiles.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { ReactElement } from 'react'
import styled from 'styled-components/native'

import { TileModel } from 'shared'

import { contentDirection } from '../constants/contentDirection'
import TileModel from '../models/TileModel'
import { PageResourceCacheStateType } from '../utils/DataContainer'
import Caption from './Caption'
import Tile from './Tile'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { mocked } from 'jest-mock'
import React from 'react'
import TestRenderer from 'react-test-renderer'

import { TileModel } from 'shared'
import { CityModel, LanguageModelBuilder } from 'shared/api'

import buildConfig from '../../constants/buildConfig'
import TileModel from '../../models/TileModel'
import DashboardNavigationTiles from '../DashboardNavigationTiles'
import NavigationTiles from '../NavigationTiles'

Expand Down
48 changes: 48 additions & 0 deletions native/src/components/__tests__/Tile.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { fireEvent } from '@testing-library/react-native'
import React from 'react'

import { TileModel } from 'shared'

import render from '../../testing/render'
import openExternalUrl from '../../utils/openExternalUrl'
import Tile from '../Tile'

jest.mock('../../utils/openExternalUrl', () => jest.fn(async () => undefined))

describe('Tile', () => {
const onTilePress = jest.fn()

beforeEach(() => {
jest.clearAllMocks()
})

it('should call onTilePress', () => {
const tile = new TileModel({
title: 'my category tile',
path: '/example/category/path',
thumbnail: null,
isExternalUrl: false,
})
const { getByText } = render(<Tile tile={tile} onTilePress={onTilePress} resourceCache={{}} />)
fireEvent.press(getByText(tile.title))

expect(onTilePress).toHaveBeenCalledTimes(1)
expect(onTilePress).toHaveBeenCalledWith(tile)
expect(openExternalUrl).not.toHaveBeenCalled()
})

it('should open external url', () => {
const tile = new TileModel({
title: 'my category tile',
path: 'https://example.com/test',
thumbnail: null,
isExternalUrl: true,
})
const { getByText } = render(<Tile tile={tile} onTilePress={onTilePress} resourceCache={{}} />)
fireEvent.press(getByText(tile.title))

expect(openExternalUrl).toHaveBeenCalledTimes(1)
expect(openExternalUrl).toHaveBeenCalledWith(tile.path, expect.anything())
expect(onTilePress).not.toHaveBeenCalled()
})
})
10 changes: 7 additions & 3 deletions native/src/routes/Offers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import React, { ReactElement } from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'

import { SPRUNGBRETT_OFFER_ROUTE, MALTE_HELP_FORM_OFFER_ROUTE } from 'shared'
import {
SPRUNGBRETT_OFFER_ROUTE,
MALTE_HELP_FORM_OFFER_ROUTE,
TileModel,
APPOINTMENT_BOOKING_OFFER_ALIAS,
} from 'shared'
import { OfferModel } from 'shared/api'

import Tiles from '../components/Tiles'
import TileModel from '../models/TileModel'

type OffersProps = {
offers: Array<OfferModel>
Expand All @@ -20,7 +24,7 @@ const Offers = ({ offers, navigateToOffer, languageCode }: OffersProps): ReactEl
const { t } = useTranslation('offers')

const tiles = offers
.filter(offer => offer.alias !== MALTE_HELP_FORM_OFFER_ROUTE)
.filter(offer => offer.alias !== MALTE_HELP_FORM_OFFER_ROUTE && offer.alias !== APPOINTMENT_BOOKING_OFFER_ALIAS)
.map(offer => {
let path = offer.path
if (internalOffers.includes(offer.alias)) {
Expand Down
3 changes: 1 addition & 2 deletions native/src/routes/OffersContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ReactElement } from 'react'

import { EXTERNAL_OFFER_ROUTE, OFFERS_ROUTE, OffersRouteType, SPRUNGBRETT_OFFER_ROUTE } from 'shared'
import { EXTERNAL_OFFER_ROUTE, OFFERS_ROUTE, OffersRouteType, SPRUNGBRETT_OFFER_ROUTE, TileModel } from 'shared'
import { createOffersEndpoint, ErrorCode } from 'shared/api'

import { NavigationProps, RouteProps } from '../constants/NavigationTypes'
Expand All @@ -9,7 +9,6 @@ import useHeader from '../hooks/useHeader'
import useLoadExtraCityContent from '../hooks/useLoadExtraCityContent'
import useNavigate from '../hooks/useNavigate'
import useSnackbar from '../hooks/useSnackbar'
import TileModel from '../models/TileModel'
import urlFromRouteInformation from '../navigation/url'
import openExternalUrl from '../utils/openExternalUrl'
import LoadingErrorHandler from './LoadingErrorHandler'
Expand Down
7 changes: 7 additions & 0 deletions release-notes/unreleased/2699-appointment-booking-offer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
issue_key: 2699
show_in_stores: false
platforms:
- android
- ios
- web
en: Support appointment booking and other external embedded offers
3 changes: 3 additions & 0 deletions shared/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export const MAX_DATE_RECURRENCES_COLLAPSED = 3

export const SPRUNGBRETT_OFFER_ALIAS = 'sprungbrett'
export const MALTE_HELP_FORM_OFFER_ALIAS = 'help'
export const APPOINTMENT_BOOKING_OFFER_ALIAS = 'terminbuchung'

export const INTERNAL_OFFERS = [SPRUNGBRETT_OFFER_ALIAS, MALTE_HELP_FORM_OFFER_ALIAS]
8 changes: 2 additions & 6 deletions shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export { default as parseHTML } from './utils/parseHTML'
export { embedInCollection } from './utils/geoJson'
export { prepareMapFeatures, prepareMapFeature, MIN_DISTANCE_THRESHOLD } from './utils/geoJson'
export { default as getExcerpt } from './utils/getExcerpt'
export {
MAX_DATE_RECURRENCES,
MAX_DATE_RECURRENCES_COLLAPSED,
SPRUNGBRETT_OFFER_ALIAS,
MALTE_HELP_FORM_OFFER_ALIAS,
} from './constants'
export * from './constants'
export { default as TileModel } from './models/TileModel'
export type ExternalSourcePermissions = Record<string, boolean>
20 changes: 5 additions & 15 deletions native/src/models/TileModel.ts → shared/models/TileModel.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,28 @@
import { JSXElementConstructor } from 'react'
import { SvgProps } from 'react-native-svg'

export default class TileModel {
export default class TileModel<T = string | null> {
_title: string
_path: string
_thumbnail: JSXElementConstructor<SvgProps> | string | null
_thumbnail: T
_isExternalUrl: boolean
_postData?: Map<string, string>
_onTilePress?: () => void
_notifications?: number

constructor(params: {
title: string
path: string
thumbnail: JSXElementConstructor<SvgProps> | string | null
thumbnail: T
isExternalUrl: boolean
postData?: Map<string, string>
onTilePress?: () => void
notifications?: number
}) {
this._title = params.title
this._path = params.path
this._thumbnail = params.thumbnail
this._isExternalUrl = params.isExternalUrl
this._postData = params.postData
this._onTilePress = params.onTilePress
this._notifications = params.notifications
}

get thumbnail(): JSXElementConstructor<SvgProps> | string | null {
get thumbnail(): T {
return this._thumbnail
}

Expand All @@ -44,15 +38,11 @@ export default class TileModel {
return this._isExternalUrl
}

get postData(): Map<string, string> | null | undefined {
get postData(): Map<string, string> | undefined {
return this._postData
}

get onTilePress(): (() => void) | undefined {
return this._onTilePress
}

get notifications(): number | null | undefined {
return this._notifications
}
}
Loading

0 comments on commit e55cc22

Please sign in to comment.