Skip to content

Commit

Permalink
2350: Refactor settings
Browse files Browse the repository at this point in the history
  • Loading branch information
steffenkleinle committed Oct 17, 2024
1 parent 30ab681 commit 03f3fe5
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 152 deletions.
44 changes: 11 additions & 33 deletions native/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -19,15 +18,6 @@ type SettingsProps = {
navigation: NavigationProps<SettingsRouteType>
}

type SectionType = SectionListData<SettingsSectionType> & {
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()
Expand All @@ -46,36 +36,24 @@ const Settings = ({ navigation }: SettingsProps): ReactElement => {

const renderItem = ({ item }: { item: SettingsSectionType }) => {
const { getSettingValue, onPress, ...otherProps } = item
const value = !!(getSettingValue && getSettingValue(settings))
return <SettingItem value={value} key={otherProps.title} onPress={safeOnPress(onPress)} {...otherProps} />
const value = getSettingValue(appContext.settings)
return <SettingItem value={!!value} key={otherProps.title} onPress={safeOnPress(onPress)} {...otherProps} />
}

const renderSectionHeader = ({ section: { title } }: { section: SectionType }) => {
if (!title) {
return null
}

return <SectionHeader>{title}</SectionHeader>
}

const sections = createSettingsSections({
appContext,
navigation,
showSnackbar,
t,
})
const sections = createSettingsSections({ appContext, navigation, showSnackbar, t }).filter(
(it): it is SettingsSectionType => it !== null,
)

return (
<Layout>
<Caption title={t('layout:settings')} />
<SectionList
sections={sections}
extraData={settings}
<ItemSeparator />
<FlatList
data={sections}
extraData={appContext.settings}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
ItemSeparatorComponent={ItemSeparator}
SectionSeparatorComponent={ItemSeparator}
stickySectionHeadersEnabled={false}
ListFooterComponent={ItemSeparator}
/>
</Layout>
)
Expand Down
14 changes: 7 additions & 7 deletions native/src/utils/__tests__/createSettingsSections.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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()

Expand All @@ -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()
Expand All @@ -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()
Expand Down
213 changes: 101 additions & 112 deletions native/src/utils/createSettingsSections.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -28,7 +28,7 @@ export type SettingsSectionType = {
role?: Role
hasSwitch?: boolean
hasBadge?: boolean
getSettingValue?: (settings: SettingsType) => boolean | null
getSettingValue: (settings: SettingsType) => boolean | null
}

const volatileValues = {
Expand All @@ -49,123 +49,112 @@ const createSettingsSections = ({
navigation,
showSnackbar,
t,
}: CreateSettingsSectionsProps): Readonly<Array<SectionListData<SettingsSectionType>>> => [
{
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
}

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,
},
})
}
},
},
]),
{
title: t('sentryTitle'),
description: t('sentryDescription', {
appName: buildConfig().appName,
}),
}: CreateSettingsSectionsProps): (SettingsSectionType | null)[] => [
pushNotificationsEnabled()
? {
title: t('pushNewsTitle'),
description: t('pushNewsDescription'),
hasSwitch: true,
getSettingValue: (settings: SettingsType) => settings.errorTracking,
getSettingValue: (settings: SettingsType) => settings.allowPushNotifications,
onPress: async () => {
const errorTracking = !settings.errorTracking
updateSettings({ errorTracking })

const client = Sentry.getClient()
if (errorTracking && !client) {
initSentry()
} else if (client) {
client.getOptions().enabled = errorTracking
const allowPushNotifications = !settings.allowPushNotifications
updateSettings({ allowPushNotifications })
if (!allowPushNotifications) {
await unsubscribeNews(cityCode, languageCode)
return
}
},
},
{
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.')
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,
},
})
}
},
},
{
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)
},
},
]
: []),
],
}
: null,
{
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'),
getSettingValue: () => null,
onPress: () => navigation.navigate(CONSENT_ROUTE),
},
{
role: 'link',
title: t('about', {
appName: buildConfig().appName,
}),
getSettingValue: () => null,
onPress: async () => {
const { aboutUrls } = buildConfig()
const aboutUrl = aboutUrls[languageCode] || aboutUrls.default
await openExternalUrl(aboutUrl, showSnackbar)
},
},
{
role: 'link',
title: t('privacyPolicy'),
getSettingValue: () => null,
onPress: async () => {
const { privacyUrls } = buildConfig()
const privacyUrl = privacyUrls[languageCode] || privacyUrls.default
await openExternalUrl(privacyUrl, showSnackbar)
},
},
{
title: t('version', { version: NativeConstants.appVersion }),
getSettingValue: () => null,
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'),
getSettingValue: () => null,
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

0 comments on commit 03f3fe5

Please sign in to comment.