From 9a95a964c6c8dd655e1c2c005ce3791ead2401b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sn=C3=A6r=20Seljan=20=C3=9E=C3=B3roddsson?=
<112904566+snaerseljan@users.noreply.github.com>
Date: Tue, 21 May 2024 10:17:07 +0000
Subject: [PATCH] feat(native-app): Problem and No data UX related component
(#14828)
* 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>
---
apps/native/app/src/hooks/use-translate.ts | 14 ++
apps/native/app/src/messages/en.ts | 11 ++
apps/native/app/src/messages/is.ts | 12 +-
.../document-detail/document-detail.tsx | 11 +-
.../src/ui/lib/problem/problem-template.tsx | 138 ++++++++++++++++++
.../native/app/src/ui/lib/problem/problem.tsx | 118 +++++++++++++++
6 files changed, 297 insertions(+), 7 deletions(-)
create mode 100644 apps/native/app/src/hooks/use-translate.ts
create mode 100644 apps/native/app/src/ui/lib/problem/problem-template.tsx
create mode 100644 apps/native/app/src/ui/lib/problem/problem.tsx
diff --git a/apps/native/app/src/hooks/use-translate.ts b/apps/native/app/src/hooks/use-translate.ts
new file mode 100644
index 000000000000..6c34568cd4c7
--- /dev/null
+++ b/apps/native/app/src/hooks/use-translate.ts
@@ -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,
+ })
+}
diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts
index a3201adc413f..4858b037dc7e 100644
--- a/apps/native/app/src/messages/en.ts
+++ b/apps/native/app/src/messages/en.ts
@@ -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',
}
diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts
index d3e916d0a74e..a1a6f3fb72bd 100644
--- a/apps/native/app/src/messages/is.ts
+++ b/apps/native/app/src/messages/is.ts
@@ -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',
}
diff --git a/apps/native/app/src/screens/document-detail/document-detail.tsx b/apps/native/app/src/screens/document-detail/document-detail.tsx
index a39e4eb87c5d..ae696f46eeed 100644
--- a/apps/native/app/src/screens/document-detail/document-detail.tsx
+++ b/apps/native/app/src/screens/document-detail/document-detail.tsx
@@ -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'
@@ -382,16 +383,14 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{
style={[
StyleSheet.absoluteFill,
{
- alignItems: 'center',
justifyContent: 'center',
- maxHeight: 300,
+
+ maxHeight: 500,
},
]}
>
{error ? (
-
- {intl.formatMessage({ id: 'licenseScanDetail.errorUnknown' })}
-
+
) : (
{
+ 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 (
+
+ {tag && (
+
+ {tag}
+
+ )}
+ {showIcon && }
+
+
+ {title}
+
+ {message}
+
+
+ )
+}
diff --git a/apps/native/app/src/ui/lib/problem/problem.tsx b/apps/native/app/src/ui/lib/problem/problem.tsx
new file mode 100644
index 000000000000..40963f69836c
--- /dev/null
+++ b/apps/native/app/src/ui/lib/problem/problem.tsx
@@ -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
+
+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 (
+
+ )
+ }
+
+ const noDataProps =
+ showIcon || !tag ? { showIcon: !tag ? true : showIcon } : { tag }
+
+ switch (type) {
+ case ProblemTypes.error:
+ return
+
+ case ProblemTypes.noData:
+ return (
+
+ )
+
+ default:
+ return
+ }
+}