diff --git a/native/src/components/SettingItem.tsx b/native/src/components/SettingItem.tsx index f73dac6eba..d69a10cfc5 100644 --- a/native/src/components/SettingItem.tsx +++ b/native/src/components/SettingItem.tsx @@ -35,6 +35,11 @@ const FlexEndContainer = styled.View` padding: 0 5px; ` +const BadgeContainer = styled.View` + flex-direction: row; + align-items: center; +` + const Badge = styled.View<{ enabled: boolean }>` width: 8px; height: 8px; @@ -42,50 +47,61 @@ const Badge = styled.View<{ enabled: boolean }>` background-color: ${props => (props.enabled ? 'limegreen' : 'red')}; ` +type SettingItemValueProps = { + onPress: () => void + hasBadge: boolean + value: boolean +} + +const SettingsItemValue = ({ value, hasBadge, onPress }: SettingItemValueProps): ReactElement => { + const { t } = useTranslation('settings') + if (hasBadge) { + return ( + + + {value ? t('enabled') : t('disabled')} + + ) + } + return +} + type SettingItemProps = { title: string description?: string onPress: () => void bigTitle?: boolean role?: Role - hasSwitch?: boolean hasBadge?: boolean - value: boolean + value: boolean | null } -const SettingItem = (props: SettingItemProps): ReactElement => { - const { title, description, onPress, value, hasBadge, hasSwitch, bigTitle, role } = props - const { t } = useTranslation('settings') - - return ( - - - +const SettingItem = ({ + title, + description, + onPress, + value, + bigTitle, + role, + hasBadge = false, +}: SettingItemProps): ReactElement => ( + + + + + {title} + + {!!description && ( - {title} + {description} - {!!description && ( - - {description} - - )} - - - {hasSwitch && } - {hasBadge && ( - - - {value ? t('enabled') : t('disabled')} - - )} - - - - ) -} + )} + + + {value !== null && } + + + +) export default SettingItem diff --git a/native/src/routes/Settings.tsx b/native/src/routes/Settings.tsx index 66e176e8fb..d88696f056 100644 --- a/native/src/routes/Settings.tsx +++ b/native/src/routes/Settings.tsx @@ -1,7 +1,6 @@ import React, { ReactElement } from 'react' import { useTranslation } from 'react-i18next' -import { SectionList, SectionListData } from 'react-native' -import styled from 'styled-components/native' +import { FlatList } from 'react-native' import { SettingsRouteType } from 'shared' @@ -19,15 +18,6 @@ type SettingsProps = { navigation: NavigationProps } -type SectionType = SectionListData & { - title?: string | null -} - -const SectionHeader = styled.Text` - padding: 20px; - color: ${props => props.theme.colors.textColor}; -` - const Settings = ({ navigation }: SettingsProps): ReactElement => { const appContext = useCityAppContext() const showSnackbar = useSnackbar() @@ -46,36 +36,24 @@ const Settings = ({ navigation }: SettingsProps): ReactElement => { const renderItem = ({ item }: { item: SettingsSectionType }) => { const { getSettingValue, onPress, ...otherProps } = item - const value = !!(getSettingValue && getSettingValue(settings)) + const value = getSettingValue ? !!getSettingValue(settings) : null return } - const renderSectionHeader = ({ section: { title } }: { section: SectionType }) => { - if (!title) { - return null - } - - return {title} - } - - const sections = createSettingsSections({ - appContext, - navigation, - showSnackbar, - t, - }) + const sections = createSettingsSections({ appContext, navigation, showSnackbar, t }).filter( + (it): it is SettingsSectionType => it !== null, + ) return ( - + ) diff --git a/native/src/utils/__tests__/createSettingsSections.spec.ts b/native/src/utils/__tests__/createSettingsSections.spec.ts index bd6eb9f7db..5cc737f42b 100644 --- a/native/src/utils/__tests__/createSettingsSections.spec.ts +++ b/native/src/utils/__tests__/createSettingsSections.spec.ts @@ -48,20 +48,20 @@ describe('createSettingsSections', () => { navigation, showSnackbar, t, - })[0]!.data + }) describe('allowPushNotifications', () => { it('should not include push notification setting if disabled', () => { mockedPushNotificationsEnabled.mockImplementation(() => false) const sections = createSettings() - expect(sections.find(it => it.title === 'privacyPolicy')).toBeTruthy() - expect(sections.find(it => it.title === 'pushNewsTitle')).toBeFalsy() + expect(sections.find(it => it?.title === 'privacyPolicy')).toBeTruthy() + expect(sections.find(it => it?.title === 'pushNewsTitle')).toBeFalsy() }) it('should set correct setting on press', async () => { mockedPushNotificationsEnabled.mockImplementation(() => true) const sections = createSettings() - const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')! + const pushNotificationSection = sections.find(it => it?.title === 'pushNewsTitle')! await pushNotificationSection!.onPress() expect(updateSettings).toHaveBeenCalledTimes(1) expect(updateSettings).toHaveBeenCalledWith({ allowPushNotifications: false }) @@ -77,7 +77,7 @@ describe('createSettingsSections', () => { it('should unsubscribe from push notification topic', async () => { mockedPushNotificationsEnabled.mockImplementation(() => true) const sections = createSettings() - const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')! + const pushNotificationSection = sections.find(it => it?.title === 'pushNewsTitle')! expect(mockUnsubscribeNews).not.toHaveBeenCalled() @@ -95,7 +95,7 @@ describe('createSettingsSections', () => { it('should subscribe to push notification topic if permission is granted', async () => { mockedPushNotificationsEnabled.mockImplementation(() => true) const sections = createSettings({ allowPushNotifications: false }) - const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')! + const pushNotificationSection = sections.find(it => it?.title === 'pushNewsTitle')! expect(mockRequestPushNotificationPermission).not.toHaveBeenCalled() expect(mockSubscribeNews).not.toHaveBeenCalled() @@ -120,7 +120,7 @@ describe('createSettingsSections', () => { it('should open settings and return false if permissions not granted', async () => { mockedPushNotificationsEnabled.mockImplementation(() => true) const sections = createSettings({ allowPushNotifications: false }) - const pushNotificationSection = sections.find(it => it.title === 'pushNewsTitle')! + const pushNotificationSection = sections.find(it => it?.title === 'pushNewsTitle')! expect(mockRequestPushNotificationPermission).not.toHaveBeenCalled() expect(mockSubscribeNews).not.toHaveBeenCalled() diff --git a/native/src/utils/createSettingsSections.ts b/native/src/utils/createSettingsSections.ts index e10a547358..8c06a7e647 100644 --- a/native/src/utils/createSettingsSections.ts +++ b/native/src/utils/createSettingsSections.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/react-native' import { TFunction } from 'i18next' -import { Role, SectionListData } from 'react-native' +import { Role } from 'react-native' import { openSettings } from 'react-native-permissions' import { CONSENT_ROUTE, JPAL_TRACKING_ROUTE, LICENSES_ROUTE, SettingsRouteType } from 'shared' @@ -26,7 +26,6 @@ export type SettingsSectionType = { onPress: () => Promise | void bigTitle?: boolean role?: Role - hasSwitch?: boolean hasBadge?: boolean getSettingValue?: (settings: SettingsType) => boolean | null } @@ -49,123 +48,105 @@ const createSettingsSections = ({ navigation, showSnackbar, t, -}: CreateSettingsSectionsProps): Readonly>> => [ - { - title: null, - data: [ - ...(!pushNotificationsEnabled() - ? [] - : [ - { - title: t('pushNewsTitle'), - description: t('pushNewsDescription'), - hasSwitch: true, - getSettingValue: (settings: SettingsType) => settings.allowPushNotifications, - onPress: async () => { - const allowPushNotifications = !settings.allowPushNotifications - updateSettings({ allowPushNotifications }) - if (!allowPushNotifications) { - await unsubscribeNews(cityCode, languageCode) - return - } +}: CreateSettingsSectionsProps): (SettingsSectionType | null)[] => [ + pushNotificationsEnabled() + ? { + title: t('pushNewsTitle'), + description: t('pushNewsDescription'), + getSettingValue: (settings: SettingsType) => settings.allowPushNotifications, + onPress: async () => { + const allowPushNotifications = !settings.allowPushNotifications + updateSettings({ allowPushNotifications }) + if (!allowPushNotifications) { + await unsubscribeNews(cityCode, languageCode) + return + } - const status = await requestPushNotificationPermission(updateSettings) + const status = await requestPushNotificationPermission(updateSettings) - if (status) { - await subscribeNews({ cityCode, languageCode, allowPushNotifications, skipSettingsCheck: true }) - } else { - updateSettings({ allowPushNotifications: false }) - // If the user has rejected the permission once, it can only be changed in the system settings - showSnackbar({ - text: 'noPushNotificationPermission', - positiveAction: { - label: t('layout:settings'), - onPress: openSettings, - }, - }) - } + if (status) { + await subscribeNews({ cityCode, languageCode, allowPushNotifications, skipSettingsCheck: true }) + } else { + updateSettings({ allowPushNotifications: false }) + // If the user has rejected the permission once, it can only be changed in the system settings + showSnackbar({ + text: 'noPushNotificationPermission', + positiveAction: { + label: t('layout:settings'), + onPress: openSettings, }, - }, - ]), - { - title: t('sentryTitle'), - description: t('sentryDescription', { - appName: buildConfig().appName, - }), - hasSwitch: true, - getSettingValue: (settings: SettingsType) => settings.errorTracking, - onPress: async () => { - const errorTracking = !settings.errorTracking - updateSettings({ errorTracking }) - - const client = Sentry.getClient() - if (errorTracking && !client) { - initSentry() - } else if (client) { - client.getOptions().enabled = errorTracking + }) } }, - }, - { - title: t('externalResourcesTitle'), - description: t('externalResourcesDescription'), - onPress: () => { - navigation.navigate(CONSENT_ROUTE) - }, - }, - { - role: 'link', - title: t('about', { - appName: buildConfig().appName, - }), - onPress: async () => { - const { aboutUrls } = buildConfig() - const aboutUrl = aboutUrls[languageCode] || aboutUrls.default - await openExternalUrl(aboutUrl, showSnackbar) - }, - }, - { - role: 'link', - title: t('privacyPolicy'), - onPress: async () => { - const { privacyUrls } = buildConfig() - const privacyUrl = privacyUrls[languageCode] || privacyUrls.default - await openExternalUrl(privacyUrl, showSnackbar) - }, - }, - { - title: t('version', { - version: NativeConstants.appVersion, - }), - onPress: () => { - volatileValues.versionTaps += 1 + } + : null, + { + title: t('sentryTitle'), + description: t('sentryDescription', { appName: buildConfig().appName }), + getSettingValue: (settings: SettingsType) => settings.errorTracking, + onPress: async () => { + const errorTracking = !settings.errorTracking + updateSettings({ errorTracking }) - if (volatileValues.versionTaps === TRIGGER_VERSION_TAPS) { - volatileValues.versionTaps = 0 - throw Error('This error was thrown for testing purposes. Please ignore this error.') - } - }, - }, - { - title: t('openSourceLicenses'), - onPress: () => navigation.navigate(LICENSES_ROUTE), - }, - // Only show the jpal tracking setting for users that opened it via deep link before - ...(buildConfig().featureFlags.jpalTracking && settings.jpalTrackingCode - ? [ - { - title: t('tracking'), - description: t('trackingShortDescription', { appName: buildConfig().appName }), - getSettingValue: (settings: SettingsType) => settings.jpalTrackingEnabled, - hasBadge: true, - onPress: () => { - navigation.navigate(JPAL_TRACKING_ROUTE) - }, - }, - ] - : []), - ], + const client = Sentry.getClient() + if (errorTracking && !client) { + initSentry() + } else if (client) { + client.getOptions().enabled = errorTracking + } + }, + }, + { + title: t('externalResourcesTitle'), + description: t('externalResourcesDescription'), + onPress: () => navigation.navigate(CONSENT_ROUTE), + }, + { + role: 'link', + title: t('about', { + appName: buildConfig().appName, + }), + onPress: async () => { + const { aboutUrls } = buildConfig() + const aboutUrl = aboutUrls[languageCode] || aboutUrls.default + await openExternalUrl(aboutUrl, showSnackbar) + }, + }, + { + role: 'link', + title: t('privacyPolicy'), + onPress: async () => { + const { privacyUrls } = buildConfig() + const privacyUrl = privacyUrls[languageCode] || privacyUrls.default + await openExternalUrl(privacyUrl, showSnackbar) + }, }, + { + title: t('version', { version: NativeConstants.appVersion }), + onPress: () => { + volatileValues.versionTaps += 1 + + if (volatileValues.versionTaps === TRIGGER_VERSION_TAPS) { + volatileValues.versionTaps = 0 + throw Error('This error was thrown for testing purposes. Please ignore this error.') + } + }, + }, + { + title: t('openSourceLicenses'), + onPress: () => navigation.navigate(LICENSES_ROUTE), + }, + buildConfig().featureFlags.jpalTracking && settings.jpalTrackingCode + ? { + title: t('tracking'), + description: t('trackingShortDescription', { appName: buildConfig().appName }), + getSettingValue: (settings: SettingsType) => settings.jpalTrackingEnabled, + hasBadge: true, + onPress: () => { + navigation.navigate(JPAL_TRACKING_ROUTE) + }, + } + : null, ] export default createSettingsSections