From 4ef1c4cf609276c1d9cce1c357a4d5ff0b8a7bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 11 Nov 2024 21:59:25 +0000 Subject: [PATCH] feat(native-app): add vaccinations to app (#16659) * feat: update finance card UI * feat: final touches to finance screen * feat: first version of vaccinations unfiltered * feat: animate arrow when expanding card * feat: adding loading states and proper link handling * feat: add tab buttons except animation is left * feat: add animation for tab buttons * feat: remove bottom tab bar from vaccinations page * feat: add feature flag for vaccinations * fix: key warning * feat: add english translations * feat: rename vaccinations card container to vaccinations card * feat: rename finance status card container to be just finance status card * fix: don't show badge if status or statusName is undefined * fix: warning about shadow for expandable card * fix: accessibility role for tab buttons should be tablist instead of tabbar * fix: remove eas file * fix: rename isOpen * fix: better accessibility for tab buttons * set selected state to accessibilityState for tab-buttons * fix: use ButtonProps from Button in select-button * feat: add error handling to vaccinations screen * fix: minor fixes * fix: reusable buttonStyle for health buttons --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/package.json | 1 + .../app/src/graphql/queries/health.graphql | 28 ++ apps/native/app/src/messages/en.ts | 19 +- apps/native/app/src/messages/is.ts | 19 +- .../finance-status-card-container.tsx | 318 --------------- .../components/finance-status-card.tsx | 380 ++++++++++++++++++ .../{light-button.tsx => select-button.tsx} | 5 +- .../app/src/screens/finance/finance.tsx | 29 +- .../src/screens/health/health-overview.tsx | 31 +- .../components/vaccination-card.tsx | 284 +++++++++++++ .../src/screens/vaccinations/vaccinations.tsx | 156 +++++++ apps/native/app/src/ui/index.ts | 3 +- apps/native/app/src/ui/lib/badge/badge.tsx | 29 +- apps/native/app/src/ui/lib/button/button.tsx | 2 +- ...ce-status-card.tsx => expandable-card.tsx} | 121 ++++-- apps/native/app/src/ui/lib/link/link-text.tsx | 12 +- .../app/src/ui/lib/markdown/markdown.tsx | 51 +++ .../src/ui/lib/tab-buttons/tab-buttons.tsx | 104 +++++ .../app/src/utils/component-registry.ts | 1 + .../src/utils/lifecycle/setup-components.tsx | 2 + .../app/src/utils/lifecycle/setup-routes.ts | 11 + yarn.lock | 56 +++ 22 files changed, 1276 insertions(+), 386 deletions(-) delete mode 100644 apps/native/app/src/screens/finance/components/finance-status-card-container.tsx create mode 100644 apps/native/app/src/screens/finance/components/finance-status-card.tsx rename apps/native/app/src/screens/finance/components/{light-button.tsx => select-button.tsx} (89%) create mode 100644 apps/native/app/src/screens/vaccinations/components/vaccination-card.tsx create mode 100644 apps/native/app/src/screens/vaccinations/vaccinations.tsx rename apps/native/app/src/ui/lib/card/{finance-status-card.tsx => expandable-card.tsx} (51%) create mode 100644 apps/native/app/src/ui/lib/markdown/markdown.tsx create mode 100644 apps/native/app/src/ui/lib/tab-buttons/tab-buttons.tsx diff --git a/apps/native/app/package.json b/apps/native/app/package.json index 6fdecceb6f9b..3e42a3ae7714 100644 --- a/apps/native/app/package.json +++ b/apps/native/app/package.json @@ -78,6 +78,7 @@ "react-native-interactable": "2.0.1", "react-native-keyboard-manager": "6.5.4-3", "react-native-keychain": "8.1.1", + "react-native-markdown-display": "7.0.2", "react-native-mmkv-storage": "0.9.1", "react-native-navigation": "7.40.0", "react-native-navigation-hooks": "6.3.0", diff --git a/apps/native/app/src/graphql/queries/health.graphql b/apps/native/app/src/graphql/queries/health.graphql index 15106fbfb6df..850b10e951d6 100644 --- a/apps/native/app/src/graphql/queries/health.graphql +++ b/apps/native/app/src/graphql/queries/health.graphql @@ -49,3 +49,31 @@ query GetPaymentStatus { maximumMonthlyPayment } } + +query GetVaccinations { + healthDirectorateVaccinations { + vaccinations { + id + name + description + isFeatured + status + statusName + statusColor + lastVaccinationDate + vaccinationsInfo { + id + name + date + age { + years + months + } + url + comment + rejected + } + comments + } + } +} diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index e4f9b8d91e0e..26488788f35b 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -609,7 +609,7 @@ export const en: TranslatedMessages = { 'updateApp.button': 'Update', 'updateApp.buttonSkip': 'Skip', - // health + // health - overview 'health.overview.screenTitle': 'Health', 'health.overview.title': 'My health', 'health.overview.description': @@ -628,6 +628,7 @@ export const en: TranslatedMessages = { 'health.overview.paymentCredit': 'Credit', 'health.overview.paymentDebt': 'Debt', 'health.overview.therapy': 'Therapy', + 'health.overview.vaccinations': 'Vaccinations', 'health.overview.aidsAndNutrition': 'Aids and nutrition', 'health.overview.medicinePurchase': 'Medicine purchase', 'health.overview.period': 'Period', @@ -635,4 +636,20 @@ export const en: TranslatedMessages = { 'health.overview.levelStatusValue': 'Level {level}, you pay {percentage}%', 'health.overview.medicinePurchaseNoActivePeriodWarning': 'A new payment period begins with the next medicine purchase', + + // health - vaccinations + 'health.vaccinations.screenTitle': 'Vaccinations', + 'health.vaccinations.title': 'Vaccinations', + 'health.vaccinations.description': + 'Here you can see a list of vaccines you have received, vaccination status and other information.', + 'health.vaccinations.generalVaccinations': 'General vaccinations', + 'health.vaccinations.otherVaccinations': 'Other vaccinations', + 'health.vaccinations.number': 'No.', + 'health.vaccinations.date': 'Date', + 'health.vaccinations.age': 'Age', + 'health.vaccinations.vaccine': 'Vaccine', + 'health.vaccinations.noVaccinations': 'No vaccinations recorded', + 'health.vaccinations.noVaccinationsDescription': + 'If you believe you have data that should appear here, please contact service provider.', + 'health.vaccinations.directorateOfHealth': 'The directorate of Health', } diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 8feb23255970..9cc3d7391845 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -609,7 +609,7 @@ export const is = { 'updateApp.button': 'Uppfæra', 'updateApp.buttonSkip': 'Sleppa', - // health + // health - overview 'health.overview.screenTitle': 'Heilsa', 'health.overview.title': 'Heilsan mín', 'health.overview.description': @@ -628,6 +628,7 @@ export const is = { 'health.overview.paymentCredit': 'Inneign', 'health.overview.paymentDebt': 'Skuld', 'health.overview.therapy': 'Þjálfun', + 'health.overview.vaccinations': 'Bólusetningar', 'health.overview.aidsAndNutrition': 'Hjálpartæki og næring', 'health.overview.medicinePurchase': 'Lyfjakaup', 'health.overview.period': 'Tímabil', @@ -636,4 +637,20 @@ export const is = { 'Greiðsluþrep {level}, þú greiðir {percentage}%', 'health.overview.medicinePurchaseNoActivePeriodWarning': 'Nýtt greiðslutímabil hefst við næstu lyfjakaup', + + // health - vaccinations + 'health.vaccinations.screenTitle': 'Bólusetningar', + 'health.vaccinations.title': 'Bólusetningar', + 'health.vaccinations.description': + 'Hér getur þú séð lista yfir bóluefni sem þú hefur fengið, stöðu bólusetningar og aðrar upplýsingar.', + 'health.vaccinations.generalVaccinations': 'Almennar bólusetningar', + 'health.vaccinations.otherVaccinations': 'Aðrar bólusetningar', + 'health.vaccinations.number': 'Nr.', + 'health.vaccinations.date': 'Dags.', + 'health.vaccinations.age': 'Aldur', + 'health.vaccinations.vaccine': 'Bóluefni', + 'health.vaccinations.noVaccinations': 'Engar bólusetningar skráðar', + 'health.vaccinations.noVaccinationsDescription': + 'Ef þú telur þig eiga gögn sem ættu að birtast hér, vinsamlegast hafðu samband við þjónustuaðila.', + 'health.vaccinations.directorateOfHealth': 'Embætti landlæknis', } diff --git a/apps/native/app/src/screens/finance/components/finance-status-card-container.tsx b/apps/native/app/src/screens/finance/components/finance-status-card-container.tsx deleted file mode 100644 index 23d40634ab1e..000000000000 --- a/apps/native/app/src/screens/finance/components/finance-status-card-container.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { - FinanceStatusCard, - Skeleton, - Typography, - blue400, - dynamicColor, -} from '@ui' -import { useState } from 'react' -import { FormattedMessage, useIntl } from 'react-intl' -import { Image, Linking, Pressable, View } from 'react-native' -import styled from 'styled-components/native' -import chevronDown from '../../../assets/icons/chevron-down.png' -import { - ChargeType, - GetFinanceStatusDetails, - Organization, -} from '../../../graphql/types/finance.types' -import { useGetFinanceStatusDetailsQuery } from '../../../graphql/types/schema' -import { navigateTo } from '../../../lib/deep-linking' -import { showPicker } from '../../../lib/show-picker' -import { LightButton } from './light-button' - -const Row = styled.View<{ border?: boolean }>` - flex-direction: row; - flex-wrap: wrap; - margin-left: -8px; - margin-right: -8px; - - border-bottom-color: ${dynamicColor(({ theme }) => ({ - light: theme.color.blue100, - dark: theme.shades.dark.shade300, - }))}; - border-bottom-width: ${({ border }) => (border ? 1 : 0)}px; -` - -const Cell = styled.View` - margin-right: 8px; - margin-left: 8px; - margin-top: 4px; - margin-bottom: 4px; -` - -const TouchableRow = styled.TouchableHighlight` - flex-direction: row; - flex-wrap: wrap; - margin-left: -8px; - margin-right: -8px; - padding-top: 8px; - padding-bottom: 8px; - border-bottom-color: ${dynamicColor(({ theme }) => ({ - light: theme.color.blue100, - dark: theme.shades.dark.shade300, - }))}; - border-bottom-width: 1px; -` - -const AboutBox = styled.View` - padding: 16px; - border-top-color: ${dynamicColor(({ theme }) => ({ - light: theme.color.blueberry200, - dark: theme.shades.dark.shade300, - }))}; - border-top-width: 1px; - margin-top: 0px; -` - -export function FinanceStatusCardContainer({ - chargeType, - org, -}: { - chargeType: ChargeType - org: Organization -}) { - const intl = useIntl() - const [open, setOpen] = useState(false) - const res = useGetFinanceStatusDetailsQuery({ - variables: { - input: { - orgID: org.id, - chargeTypeID: chargeType.id, - }, - }, - skip: !open, - }) - const financeStatusDetails: GetFinanceStatusDetails | undefined = - res.data?.getFinanceStatusDetails - - const chargeItemSubjects = [ - ...new Set( - (financeStatusDetails?.chargeItemSubjects ?? []).map( - (i) => i.chargeItemSubject, - ), - ), - ] - const [selectedChargeItemSubject, setSelectedChargeItemSubject] = useState(0) - - return ( - - } - value={`${intl.formatNumber(chargeType.totals)} kr.`} - onPress={() => { - setOpen((p) => !p) - }} - open={open} - > - - - - - { - showPicker({ - title: intl.formatMessage({ - id: 'finance.statusCard.paymentBase', - defaultMessage: 'Gjaldgrunnur', - }), - items: chargeItemSubjects.map((label, value) => ({ - label, - value, - id: String(value), - })), - selectedId: String(selectedChargeItemSubject), - cancel: true, - }).then((value) => { - if (value.selectedItem) { - setSelectedChargeItemSubject(Number(value.selectedItem.id)) - } - }) - // void - }} - /> - - - - - - - - - - - - - {res.loading - ? Array.from({ length: 3 }).map((_, index) => ( - - - - - - )) - : financeStatusDetails?.chargeItemSubjects?.map((charge, index) => { - if ( - charge.chargeItemSubject !== - chargeItemSubjects[selectedChargeItemSubject] - ) { - return null - } - return ( - { - navigateTo( - `/finance/status/${org.id}/${chargeType.id}/${index}`, - ) - }} - > - <> - - {charge.finalDueDate} - - - - {intl.formatNumber(charge.totals)} kr. - - - - - - ) - })} - - - - - - {res.loading ? ( - - ) : ( - - {intl.formatNumber(chargeType.totals)} kr. - - )} - - - - - - - - {res.loading ? ( - <> - - - - ) : ( - {org.name} - )} - - {org.homepage ? ( - - - - : - - - Linking.openURL( - `https://${org.email.replace(/https?:\/\//, '')}`, - ) - } - > - - {org.homepage} - - - - ) : null} - {org.email ? ( - - - - : - - Linking.openURL(`mailto:${org.email}`)}> - - {org.email} - - - - ) : null} - {org.phone ? ( - - - - : - - Linking.openURL(`tel:${org.phone}`)}> - - {org.phone} - - - - ) : null} - - - - ) -} diff --git a/apps/native/app/src/screens/finance/components/finance-status-card.tsx b/apps/native/app/src/screens/finance/components/finance-status-card.tsx new file mode 100644 index 000000000000..e8f884674ca8 --- /dev/null +++ b/apps/native/app/src/screens/finance/components/finance-status-card.tsx @@ -0,0 +1,380 @@ +import { + Skeleton, + Typography, + blue400, + dynamicColor, + ExpandableCard, +} from '@ui' +import { useState } from 'react' +import { FormattedMessage, useIntl } from 'react-intl' +import { Image, Linking, Pressable, View } from 'react-native' +import styled, { useTheme } from 'styled-components/native' +import chevronDown from '../../../assets/icons/chevron-down.png' +import { + ChargeType, + GetFinanceStatusDetails, + Organization, +} from '../../../graphql/types/finance.types' +import { useGetFinanceStatusDetailsQuery } from '../../../graphql/types/schema' +import { navigateTo } from '../../../lib/deep-linking' +import { showPicker } from '../../../lib/show-picker' +import { SelectButton } from './select-button' +import { useBrowser } from '../../../lib/use-browser' + +const Row = styled.View<{ border?: boolean }>` + flex-direction: row; + flex-wrap: wrap; + border-bottom-color: ${dynamicColor(({ theme }) => ({ + light: theme.color.blue100, + dark: theme.shades.dark.shade300, + }))}; + border-bottom-width: ${({ border }) => (border ? 1 : 0)}px; +` + +const Cell = styled.View` + margin-right: ${({ theme }) => theme.spacing[1]}px; + margin-left: ${({ theme }) => theme.spacing[1]}px; + margin-top: ${({ theme }) => theme.spacing.smallGutter}px; + margin-bottom: ${({ theme }) => theme.spacing.smallGutter}px; +` + +const AboutItem = styled.View` + margin-top: ${({ theme }) => theme.spacing[2]}px; + min-width: 50%; + flex: 1; +` + +const TouchableRow = styled.TouchableHighlight` + flex-direction: row; + flex-wrap: wrap; + padding-top: ${({ theme }) => theme.spacing[2]}px; + padding-bottom: ${({ theme }) => theme.spacing[2]}px; + border-bottom-color: ${dynamicColor(({ theme }) => ({ + light: theme.color.blue200, + dark: theme.shades.dark.shade300, + }))}; + border-bottom-width: 1px; +` + +const AboutBox = styled.View` + padding: ${({ theme }) => theme.spacing[2]}px; + margin-top: 0px; +` + +const RowItem = styled.View` + margin-right: ${({ theme }) => theme.spacing[1]}px; + margin-left: ${({ theme }) => theme.spacing[1]}px; + flex: 1; +` + +const TableHeading = styled.View` + flex-direction: row; + flex: 1; + margin-top: ${({ theme }) => theme.spacing[1]}px; + padding-bottom: ${({ theme }) => theme.spacing[1]}px; + border-bottom-color: ${dynamicColor(({ theme }) => ({ + light: theme.color.blue200, + dark: theme.shades.dark.shade300, + }))}; + border-bottom-width: 1px; +` + +const Total = styled(Typography)` + margin-top: ${({ theme }) => theme.spacing[1]}px; + margin-bottom: ${({ theme }) => theme.spacing.smallGutter}px; +` + +export function FinanceStatusCard({ + chargeType, + org, + componentId, +}: { + chargeType: ChargeType + org: Organization + componentId: string +}) { + const intl = useIntl() + const theme = useTheme() + const [open, setOpen] = useState(false) + const { openBrowser } = useBrowser() + const res = useGetFinanceStatusDetailsQuery({ + variables: { + input: { + orgID: org.id, + chargeTypeID: chargeType.id, + }, + }, + skip: !open, + }) + const financeStatusDetails: GetFinanceStatusDetails | undefined = + res.data?.getFinanceStatusDetails + + const chargeItemSubjects = [ + ...new Set( + (financeStatusDetails?.chargeItemSubjects ?? []).map( + (i) => i.chargeItemSubject, + ), + ), + ] + const [selectedChargeItemSubject, setSelectedChargeItemSubject] = useState(0) + + const shouldShowAboutOrgBox = + org.homepage || org.email || org.phone || org.name + + return ( + + } + value={`${intl.formatNumber(chargeType.totals)} kr.`} + onPress={() => { + setOpen((p) => !p) + }} + open={open} + > + + + + + { + showPicker({ + title: intl.formatMessage({ + id: 'finance.statusCard.paymentBase', + defaultMessage: 'Gjaldgrunnur', + }), + items: chargeItemSubjects.map((label, value) => ({ + label, + value, + id: String(value), + })), + selectedId: String(selectedChargeItemSubject), + cancel: true, + }).then((value) => { + if (value.selectedItem) { + setSelectedChargeItemSubject(Number(value.selectedItem.id)) + } + }) + // void + }} + /> + + + + + + + + + + + + + + + {res.loading + ? Array.from({ length: 3 }).map((_, index) => ( + + + + + + )) + : financeStatusDetails?.chargeItemSubjects?.map((charge, index) => { + if ( + charge.chargeItemSubject !== + chargeItemSubjects[selectedChargeItemSubject] + ) { + return null + } + return ( + { + navigateTo( + `/finance/status/${org.id}/${chargeType.id}/${index}`, + ) + }} + > + <> + + + {charge.finalDueDate} + + + + + {intl.formatNumber(charge.totals)} kr. + + + + + + ) + })} + + + + + + {res.loading ? ( + + ) : ( + + {intl.formatNumber(chargeType.totals)} kr. + + )} + + + + {shouldShowAboutOrgBox && !res.loading && ( + + + + + {res.loading ? ( + <> + + + + ) : ( + + {org.name} + + )} + + {org.homepage ? ( + + + + : + + + openBrowser( + `https://${org.email.replace(/https?:\/\//, '')}`, + componentId, + ) + } + > + + {org.homepage} + + + + ) : null} + {org.email ? ( + + + + : + + Linking.openURL(`mailto:${org.email}`)} + style={{ flexWrap: 'nowrap' }} + > + + {org.email} + + + + ) : null} + {org.phone ? ( + + + + : + + Linking.openURL(`tel:${org.phone}`)} + style={{ flexWrap: 'nowrap' }} + > + + {org.phone} + + + + ) : null} + + + )} + + ) +} diff --git a/apps/native/app/src/screens/finance/components/light-button.tsx b/apps/native/app/src/screens/finance/components/select-button.tsx similarity index 89% rename from apps/native/app/src/screens/finance/components/light-button.tsx rename to apps/native/app/src/screens/finance/components/select-button.tsx index 9ae9d9a40545..c24ad5d71366 100644 --- a/apps/native/app/src/screens/finance/components/light-button.tsx +++ b/apps/native/app/src/screens/finance/components/select-button.tsx @@ -1,8 +1,8 @@ import { DynamicColorIOS, Platform } from 'react-native' import { useTheme } from 'styled-components/native' -import { Button, blue400 } from '../../../ui' +import { Button, ButtonProps, blue400 } from '../../../ui' -export const LightButton = (props: any) => { +export const SelectButton = (props: ButtonProps) => { const theme = useTheme() return (