Skip to content

Commit

Permalink
feat(native-app): Problem and No data UX related component (#14828)
Browse files Browse the repository at this point in the history
* Added general problem component and translation hook wrapper

* Update problem component

* Update problem components logic and add problem to document-details screen

* Update apps/native/app/src/ui/lib/problem/problem.tsx

rename to error

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Rename to withContainer

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored May 21, 2024
1 parent dffa964 commit 9a95a96
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 7 deletions.
14 changes: 14 additions & 0 deletions apps/native/app/src/hooks/use-translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useIntl } from 'react-intl'
import { TranslatedMessage } from '../messages'

/**
* Helper hook to simplify translations in the app.
*/
export const useTranslate = () => {
const intl = useIntl()

return (key: TranslatedMessage) =>
intl.formatMessage({
id: key,
})
}
11 changes: 11 additions & 0 deletions apps/native/app/src/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,15 @@ export const en: TranslatedMessages = {
// offline
'offline.title': 'No internet connection',
'offline.message': 'Information has not been updated.',

// problem
'problem.error.tag': 'Error',
'problem.error.title': 'Service is temporarily down',
'problem.error.message': 'Please try again later',
'problem.noData.title': 'No data',
'problem.noData.message':
'If you believe you have data that should appear here, please contact service provider.',
'problem.offline.title': 'No internet connection',
'problem.offline.message':
'An error occurred while communicating with the service provider',
}
12 changes: 11 additions & 1 deletion apps/native/app/src/messages/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,17 @@ export const is = {
'airDiscount.emptyListDescription':
'Einungis íbúar landsbyggðarinnar sem eiga lögheimili fjarri höfuðborgarsvæðinu og eyjum eiga rétt á Loftbrú.',

// Offline
// offline
'offline.title': 'Ekkert netsamband',
'offline.message': 'Upplýsingar hafa ekki verið uppfærðar.',

// problems
'problem.error.tag': 'Villa',
'problem.error.title': 'Þjónusta liggur tímabundið niðri',
'problem.error.message': 'Vinsamlegast reyndu aftur síðar',
'problem.noData.title': 'Engin gögn',
'problem.noData.message':
'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.',
'problem.offline.title': 'Samband næst ekki',
'problem.offline.message': 'Villa kom upp í samskiptum við þjónustuaðila',
}
11 changes: 5 additions & 6 deletions apps/native/app/src/screens/document-detail/document-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useApolloClient, useFragment_experimental } from '@apollo/client'
import { blue400, dynamicColor, Header, Loader, Typography } from '@ui'
import { blue400, dynamicColor, Header, Loader } from '@ui'
import { Problem } from '@ui/lib/problem/problem'
import React, { useEffect, useRef, useState } from 'react'
import { FormattedDate, useIntl } from 'react-intl'
import { Animated, Platform, StyleSheet, View } from 'react-native'
Expand Down Expand Up @@ -382,16 +383,14 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
style={[
StyleSheet.absoluteFill,
{
alignItems: 'center',
justifyContent: 'center',
maxHeight: 300,

maxHeight: 500,
},
]}
>
{error ? (
<Typography>
{intl.formatMessage({ id: 'licenseScanDetail.errorUnknown' })}
</Typography>
<Problem type="error" withContainer />
) : (
<Loader
text={intl.formatMessage({ id: 'documentDetail.loadingText' })}
Expand Down
138 changes: 138 additions & 0 deletions apps/native/app/src/ui/lib/problem/problem-template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Colors, Typography } from '@ui'
import { ReactNode } from 'react'
import { Image, View } from 'react-native'
import styled from 'styled-components/native'

type Variant = 'info' | 'error' | 'warning'

export type ProblemTemplateBaseProps = {
variant: Variant
title: string
message: string | ReactNode
withContainer?: boolean
}

interface WithIconProps extends ProblemTemplateBaseProps {
showIcon?: boolean
tag?: never
}

interface WithTagProps extends ProblemTemplateBaseProps {
tag?: string
showIcon?: never
}

export type ProblemTemplateProps = WithIconProps | WithTagProps

const getIcon = (variant: Variant) => {
switch (variant) {
case 'warning':
return require('../../assets/icons/warning.png')

case 'info':
return require('../../assets/icons/info.png')
}
}

const getColorsByVariant = (
variant: Variant,
): {
borderColor: Colors
tagBackgroundColor: Colors
tagColor: Colors
} => {
switch (variant) {
case 'error':
return {
borderColor: 'red200',
tagBackgroundColor: 'red100',
tagColor: 'red600',
}

case 'info':
return {
borderColor: 'blue200',
tagBackgroundColor: 'blue100',
tagColor: 'blue400',
}

case 'warning':
return {
borderColor: 'yellow400',
tagBackgroundColor: 'yellow300',
tagColor: 'dark400',
}
}
}

const Host = styled.View<{
borderColor: Colors
noContainer?: boolean
}>`
border-color: ${({ borderColor, theme }) => theme.color[borderColor]};
border-width: 1px;
border-radius: 24px;
justify-content: center;
align-items: center;
flex: 1;
row-gap: ${({ theme }) => theme.spacing[3]}px;
padding: ${({ theme }) => theme.spacing[2]}px;
${({ noContainer, theme }) => noContainer && `margin: ${theme.spacing[2]}px;`}
`

const Tag = styled(Typography)<{
backgroundColor: Colors
color?: Colors
}>`
background-color: ${({ backgroundColor, theme }) =>
theme.color[backgroundColor]};
padding: ${({ theme }) => theme.spacing[1]}px;
border-radius: ${({ theme }) => theme.border.radius.large};
overflow: hidden;
${({ color, theme }) => color && `color: ${theme.color[color]};`}
`

const Icon = styled(Image)(({ theme }) => ({
width: theme.spacing[3],
height: theme.spacing[3],
}))

const Content = styled(View)`
align-items: center;
row-gap: ${({ theme }) => theme.spacing[1]}px;
`

export const ProblemTemplate = ({
variant,
title,
message,
showIcon,
tag,
withContainer,
}: ProblemTemplateProps) => {
const { borderColor, tagColor, tagBackgroundColor } =
getColorsByVariant(variant)

return (
<Host borderColor={borderColor} noContainer={withContainer}>
{tag && (
<Tag
variant="eyebrow"
backgroundColor={tagBackgroundColor}
color={tagColor}
>
{tag}
</Tag>
)}
{showIcon && <Icon source={getIcon(variant)} />}
<Content>
<Typography variant="heading3" textAlign="center">
{title}
</Typography>
<Typography textAlign="center">{message}</Typography>
</Content>
</Host>
)
}
118 changes: 118 additions & 0 deletions apps/native/app/src/ui/lib/problem/problem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useEffect } from 'react'
import { useTranslate } from '../../../hooks/use-translate'
import { useOfflineStore } from '../../../stores/offline-store'
import { ProblemTemplate, ProblemTemplateBaseProps } from './problem-template'

enum ProblemTypes {
error = 'error',
noData = 'no_data',
}

type ProblemBaseProps = {
/**
* Type of problem
* @default 'error'
* 'error' is a generic error that is not caused by the user
* 'no_data' is a 200 response, i.e. no data
*/
type?: `${ProblemTypes}`
error?: Error
title?: string
message?: string
logError?: boolean
} & Pick<ProblemTemplateBaseProps, 'withContainer'>

interface ErrorProps extends ProblemBaseProps {
type?: 'error'
showIcon?: never
error?: Error
title?: string
message?: string
tag?: string
}

interface NoDataBaseProps extends ProblemBaseProps {
type: 'no_data'
error?: never
title?: string
message?: string
}

interface NoDataWithIconProps extends NoDataBaseProps {
showIcon?: boolean
tag?: never
}

interface NoDataWithTagProps extends NoDataBaseProps {
showIcon?: never
tag?: string
}

type NoDataProps = NoDataWithIconProps | NoDataWithTagProps

type ProblemProps = ErrorProps | NoDataProps

export const Problem = ({
type = ProblemTypes.error,
error,
title,
message,
tag,
logError = false,
withContainer,
showIcon,
}: ProblemProps) => {
const t = useTranslate()
const { isConnected } = useOfflineStore()

const defaultProps = { withContainer }

const fallbackProps = {
...defaultProps,
title: title ?? t('problem.error.title'),
message: message ?? t('problem.error.message'),
tag: tag ?? t('problem.error.tag'),
variant: 'error',
} as const

useEffect(() => {
if (logError && error) {
console.error(error)
}
}, [logError, error])

// When offline prioritize showing offline template
if (!isConnected) {
return (
<ProblemTemplate
{...defaultProps}
showIcon
variant="warning"
title={title ?? t('problem.offline.title')}
message={message ?? t('problem.offline.message')}
/>
)
}

const noDataProps =
showIcon || !tag ? { showIcon: !tag ? true : showIcon } : { tag }

switch (type) {
case ProblemTypes.error:
return <ProblemTemplate {...fallbackProps} />

case ProblemTypes.noData:
return (
<ProblemTemplate
{...defaultProps}
{...noDataProps}
variant="info"
title={title ?? t('problem.noData.title')}
message={message ?? t('problem.noData.message')}
/>
)

default:
return <ProblemTemplate {...fallbackProps} />
}
}

0 comments on commit 9a95a96

Please sign in to comment.