From 78cf06e6f8f550d4221f9acc29b4cc24f67570da Mon Sep 17 00:00:00 2001 From: Jacob Jaffe Date: Fri, 7 Aug 2020 13:44:14 -0400 Subject: [PATCH] fix autosubscribing across sessions (#1347) * fix autosubscribing across sessions * remove debug code * remove envs from other branch * remove prop * tests --- app/locales/en.json | 2 +- .../getHealthcareAuthoritiesAction.ts | 5 +- .../toggleAutoSubscriptionBannerAction.ts | 12 ++ ...ggleHealthcareAuthorityAutoSubscription.ts | 10 -- ...toggleSelectedHealthcareAuthorityAction.ts | 10 +- app/store/index.ts | 2 +- app/store/migrations/migrations.js | 15 +++ .../reducers/healthcareAuthoritiesReducer.ts | 32 +++-- .../isAutoSubscriptionEnabledSelector.ts | 5 - app/views/Main.js | 33 +++++- app/views/Partners/PartnersOverview.tsx | 46 ------- app/views/main/AllServicesOn.tsx | 112 +++++++----------- .../ServiceOffScreens/SelectAuthority.tsx | 4 +- .../SelectAuthority.spec.js.snap | 2 +- .../__snapshots__/AllServicesOn.spec.js.snap | 64 ++-------- app/views/main/style.ts | 2 + ios/Podfile.lock | 2 +- 17 files changed, 146 insertions(+), 212 deletions(-) create mode 100644 app/store/actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction.ts delete mode 100644 app/store/actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription.ts delete mode 100644 app/store/selectors/isAutoSubscriptionEnabledSelector.ts diff --git a/app/locales/en.json b/app/locales/en.json index 6f285bca81..c4e7b52221 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -96,7 +96,7 @@ "notifications_off_subheader": "You will not receive notifications about possible exposures nor when new Health Departments are added in your area", "select_authority_button": "Add Health Department", "select_authority_header": "No local Health Department Selected", - "select_authority_subheader": "Allow PathCheck to add your local Health Department or select one yourself to receive info about COVID-19 in your area" + "select_authority_subheader": "Select your local Health Department to receive info about COVID-19 in your area" } }, "import": { diff --git a/app/store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction.ts b/app/store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction.ts index 4fcb184590..850c1cd438 100644 --- a/app/store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction.ts +++ b/app/store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction.ts @@ -63,7 +63,10 @@ const getHealthcareAuthoritiesAction = ( dispatch( toggleSelectedHealthcareAuthorityAction( { authority: localHealthAuthority, overrideValue: true }, - { triggerIntersect: false }, + { + triggerIntersect: false, + autoSubscribed: !!autoSubscriptionLocation, + }, ), ); } diff --git a/app/store/actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction.ts b/app/store/actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction.ts new file mode 100644 index 0000000000..ad34b1aca3 --- /dev/null +++ b/app/store/actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction.ts @@ -0,0 +1,12 @@ +import { createAction } from '@reduxjs/toolkit'; + +const TOGGLE_AUTO_SUBSCRIPTION_BANNER = 'TOGGLE_AUTO_SUBSCRIPTION_BANNER'; + +/** + * Enables / disables the auto-subscription banner from being shown + */ +const toggleAutoSubscriptionBannerAction = createAction<{ + overrideValue: boolean; +}>(TOGGLE_AUTO_SUBSCRIPTION_BANNER); + +export default toggleAutoSubscriptionBannerAction; diff --git a/app/store/actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription.ts b/app/store/actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription.ts deleted file mode 100644 index 31b5e984c9..0000000000 --- a/app/store/actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -const TOGGLE_HEALTHCARE_AUTHORITY_AUTO_SUBSCRIPTION = - 'TOGGLE_HEALTHCARE_AUTHORITY_AUTO_SUBSCRIPTION'; - -const toggleHealthcareAuthorityAutoSubscription = createAction<{ - autoSubscriptionEnabled: boolean; -}>(TOGGLE_HEALTHCARE_AUTHORITY_AUTO_SUBSCRIPTION); - -export default toggleHealthcareAuthorityAutoSubscription; diff --git a/app/store/actions/healthcareAuthorities/toggleSelectedHealthcareAuthorityAction.ts b/app/store/actions/healthcareAuthorities/toggleSelectedHealthcareAuthorityAction.ts index ac02a20807..374810f6bf 100644 --- a/app/store/actions/healthcareAuthorities/toggleSelectedHealthcareAuthorityAction.ts +++ b/app/store/actions/healthcareAuthorities/toggleSelectedHealthcareAuthorityAction.ts @@ -7,7 +7,8 @@ type Payload = { }; type Meta = { - triggerIntersect: boolean; + triggerIntersect?: boolean; // indicates that this action should trigger a recomputation of possible exposure notifications + autoSubscribed?: boolean; // indicates that this was triggered via geofencing auto subscription }; const TOGGLE_SELECTED_HEALTHCARE_AUTHORITY = @@ -15,9 +16,12 @@ const TOGGLE_SELECTED_HEALTHCARE_AUTHORITY = const toggleSelectedHealthcareAuthorityAction = createAction( TOGGLE_SELECTED_HEALTHCARE_AUTHORITY, - ({ authority, overrideValue }: Payload, { triggerIntersect }: Meta) => ({ + ( + { authority, overrideValue }: Payload, + { triggerIntersect, autoSubscribed }: Meta, + ) => ({ payload: { authority, overrideValue }, - meta: { triggerIntersect }, + meta: { triggerIntersect, autoSubscribed }, }), ); diff --git a/app/store/index.ts b/app/store/index.ts index a75cfd9548..39c07601d3 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -19,7 +19,7 @@ const enhancers = composeWithDevTools( ), ); -export const STORE_VERSION = 1; +export const STORE_VERSION = 2; const persistConfig = { key: 'root', storage: AsyncStorage, diff --git a/app/store/migrations/migrations.js b/app/store/migrations/migrations.js index 3f7de0dab7..1e86e5ac13 100644 --- a/app/store/migrations/migrations.js +++ b/app/store/migrations/migrations.js @@ -8,6 +8,21 @@ const migrations = { } return prevState; }, + // Migration 2: Introduce auto-subscription + 2: (prevState) => { + if (prevState && prevState.healthcareAuthorities) { + prevState.healthcareAuthorities.autoSubscription = { + bannerDismissed: false, + selectedAuthority: null, + // only auto-subscribe if user has no HAs. + active: + !prevState.healthcareAuthorities.availableCustomAuthorities || + prevState.healthcareAuthorities.availableCustomAuthorities.length === + 0, + }; + } + return prevState; + }, }; export default migrations; diff --git a/app/store/reducers/healthcareAuthoritiesReducer.ts b/app/store/reducers/healthcareAuthoritiesReducer.ts index bf386bf50e..0ec0b317f5 100644 --- a/app/store/reducers/healthcareAuthoritiesReducer.ts +++ b/app/store/reducers/healthcareAuthoritiesReducer.ts @@ -8,7 +8,7 @@ import { getHealthcareAuthorities_success, } from '../actions/healthcareAuthorities/getHealthcareAuthoritiesAction'; import toggleSelectedHealthcareAuthorityAction from '../actions/healthcareAuthorities/toggleSelectedHealthcareAuthorityAction'; -import toggleHealthcareAuthorityAutoSubscription from '../actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription'; +import toggleAutoSubscriptionBannerAction from '../actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction'; type HealthCareReducerState = { // Because we control this list to be super small and have type safety, we use the full models @@ -16,19 +16,27 @@ type HealthCareReducerState = { availableAuthorities: HealthcareAuthority[]; availableCustomAuthorities: HealthcareAuthority[]; selectedAuthorities: HealthcareAuthority[]; - autoSubscriptionEnabled: boolean; request: ApiRequest; + autoSubscription: { + active: boolean; // controls firing. We set to false after any toggle so that auto-subscribe happens max of once, and before manual. + bannerDismissed: boolean; // independently show banner until dismissed + selectedAuthority: HealthcareAuthority | null; + }; }; const initialState: HealthCareReducerState = { availableAuthorities: [], availableCustomAuthorities: [], // For testing, from a custom uploaded YAML selectedAuthorities: [], - autoSubscriptionEnabled: true, request: { status: ApiStatus.INITIAL, errorMessage: null, }, + autoSubscription: { + active: true, + bannerDismissed: false, + selectedAuthority: null, + }, }; // The 'request' property of this reducer is helpful metadata for showing loading / error cases @@ -71,7 +79,10 @@ const healthcareAuthoritiesReducer = createReducer(initialState, (builder) => }) .addCase( toggleSelectedHealthcareAuthorityAction, - (state, { payload: { authority, overrideValue } }) => { + ( + state, + { payload: { authority, overrideValue }, meta: { autoSubscribed } }, + ) => { // always remove state.selectedAuthorities = state.selectedAuthorities.filter( ({ internal_id }) => internal_id !== authority.internal_id, @@ -79,13 +90,20 @@ const healthcareAuthoritiesReducer = createReducer(initialState, (builder) => // add if needed if (overrideValue) { state.selectedAuthorities.push(authority); + state.autoSubscription.active = false; // after we subscribe, for any reason, prevent auto-subscribing + } + + // Display banner that HA was auto-subscribed to + if (autoSubscribed) { + state.autoSubscription.bannerDismissed = false; + state.autoSubscription.selectedAuthority = authority; } }, ) .addCase( - toggleHealthcareAuthorityAutoSubscription, - (state, { payload: { autoSubscriptionEnabled } }) => { - state.autoSubscriptionEnabled = autoSubscriptionEnabled; + toggleAutoSubscriptionBannerAction, + (state, { payload: { overrideValue } }) => { + state.autoSubscription.bannerDismissed = !overrideValue; }, ), ); diff --git a/app/store/selectors/isAutoSubscriptionEnabledSelector.ts b/app/store/selectors/isAutoSubscriptionEnabledSelector.ts deleted file mode 100644 index 9d632cbfdb..0000000000 --- a/app/store/selectors/isAutoSubscriptionEnabledSelector.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { RootState } from '../types'; -const isAutoSubscriptionEnabledSelector = (state: RootState): boolean => - state.healthcareAuthorities.autoSubscriptionEnabled; - -export default isAutoSubscriptionEnabledSelector; diff --git a/app/views/Main.js b/app/views/Main.js index 835e1c13e0..723c2b974b 100644 --- a/app/views/Main.js +++ b/app/views/Main.js @@ -8,14 +8,16 @@ import { AllServicesOnScreen } from './main/AllServicesOn'; import { TracingOffScreen, NotificationsOffScreen, - // SelectAuthorityScreen, + SelectAuthorityScreen, } from './main/ServiceOffScreens'; import PermissionsContext from '../gps/PermissionsContext'; import { PermissionStatus } from '../permissionStatus'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import selectedHealthcareAuthoritiesSelector from '../store/selectors/selectedHealthcareAuthoritiesSelector'; import { useStatusBarEffect } from '../navigation'; +import getHealthcareAuthoritiesAction from '../store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction'; +import Geolocation from '@react-native-community/geolocation'; export const Main = () => { useStatusBarEffect('light-content'); @@ -47,14 +49,35 @@ export const Main = () => { }; }, [navigation, updateStateInfo]); + const dispatch = useDispatch(); + const { autoSubscription } = useSelector( + (state) => state.healthcareAuthorities, + ); + + const autoSubscribe = useCallback(async () => { + if (autoSubscription.active) { + Geolocation.getCurrentPosition(({ coords }) => { + dispatch( + getHealthcareAuthoritiesAction({ autoSubscriptionLocation: coords }), + ); + }); + } + }, [autoSubscription, dispatch]); + + useEffect(() => { + autoSubscribe(); + }, [autoSubscribe]); + if (!canTrack) { return ; } else if (notification.status === PermissionStatus.DENIED) { return ; } else if (selectedAuthorities.length === 0) { - // TODO: enable this for testing versions of app - // return ; - return ; + if (autoSubscription.active) { + return ; + } else { + return ; + } } else { return ; } diff --git a/app/views/Partners/PartnersOverview.tsx b/app/views/Partners/PartnersOverview.tsx index 8fb9f8fc0d..1091af931d 100644 --- a/app/views/Partners/PartnersOverview.tsx +++ b/app/views/Partners/PartnersOverview.tsx @@ -21,11 +21,6 @@ import { Typography } from '../../components/Typography'; import { Images, Icons } from '../../assets'; import { Colors } from '../../styles'; -import { useSelector, useDispatch } from 'react-redux'; -import toggleHealthcareAuthorityAutoSubscription from '../../store/actions/healthcareAuthorities/toggleHealthcareAuthorityAutoSubscription'; -import isAutoSubscriptionEnabledSelector from '../../store/selectors/isAutoSubscriptionEnabledSelector'; -import { Switch } from 'react-native-gesture-handler'; - // For fixing image width issues const win = Dimensions.get('window'); @@ -71,20 +66,8 @@ const PartnersIllustration = (): JSX.Element => { const PartnersScreen = ({ navigation }: PartnersScreenProps): JSX.Element => { const { t } = useTranslation(); - const dispatch = useDispatch(); const navigateToViewHAs = () => navigation.navigate('PartnersEdit'); - const autoSubscriptionEnabled = useSelector( - isAutoSubscriptionEnabledSelector, - ); - const onToggleAutoSubscription = (value: boolean) => { - dispatch( - toggleHealthcareAuthorityAutoSubscription({ - autoSubscriptionEnabled: value, - }), - ); - }; - return ( { - {__DEV__ && ( - - - {t('authorities.automatically_follow')} - - - - - )} ); }; @@ -160,15 +123,6 @@ const styles = StyleSheet.create({ backgroundColor: Colors.primaryBackground, flex: 1, }, - bottomSheet: { - backgroundColor: Colors.bottomSheetBackground, - padding: 24, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: Colors.formInputBorder, - }, divider: { height: 1, backgroundColor: Colors.formInputBorder, diff --git a/app/views/main/AllServicesOn.tsx b/app/views/main/AllServicesOn.tsx index 30bdd67b38..85fb65f57c 100644 --- a/app/views/main/AllServicesOn.tsx +++ b/app/views/main/AllServicesOn.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect } from 'react'; import { Dimensions, ImageBackground, @@ -9,75 +9,45 @@ import { import { useTranslation } from 'react-i18next'; import Pulse from 'react-native-pulse'; import { SvgXml } from 'react-native-svg'; -import Geolocation from '@react-native-community/geolocation'; import { Icons, Images } from '../../assets'; import { Typography } from '../../components/Typography'; import { styles } from './style'; -import { Colors } from '../../styles'; +import { Colors, Spacing } from '../../styles'; import { Stacks, NavigationProp } from '../../navigation'; import { useSelector, useDispatch } from 'react-redux'; -import isAutoSubscriptionEnabledSelector from '../../store/selectors/isAutoSubscriptionEnabledSelector'; -import getHealthcareAuthorities from '../../store/actions/healthcareAuthorities/getHealthcareAuthoritiesAction'; import { RootState } from '../../store/types'; +import toggleAutoSubscriptionBanner from '../../store/actions/healthcareAuthorities/toggleAutoSubscriptionBannerAction'; type AllServicesOnProps = { - noHaAvailable: boolean; navigation: NavigationProp; }; -const useAutoSubscriptionBanner = ( - showAutoSubscribeBanner: boolean, - navigation: NavigationProp, -) => { - const [showBanner, setShowBanner] = useState(showAutoSubscribeBanner); - - useEffect(() => { - const listener = navigation.addListener('blur', () => setShowBanner(false)); - - return listener.remove; - }, [navigation]); - - return { - showBanner, - }; -}; - export const AllServicesOnScreen = ({ - noHaAvailable, navigation, }: AllServicesOnProps): JSX.Element => { const size = Dimensions.get('window').height; const { t } = useTranslation(); - const dispatch = useDispatch(); - - const [userAutoSubscribed, setUserAutoSubscribed] = useState(false); - const { showBanner } = useAutoSubscriptionBanner(true, navigation); - const haName = useSelector( - (state: RootState) => - state.healthcareAuthorities.selectedAuthorities[0]?.name, + const { autoSubscription, selectedAuthorities } = useSelector( + (state: RootState) => state.healthcareAuthorities, ); - const autoSubscriptionEnabled = useSelector( - isAutoSubscriptionEnabledSelector, - ); - - const autoSubscribe = useCallback(async () => { - if (autoSubscriptionEnabled && noHaAvailable && !userAutoSubscribed) { - Geolocation.getCurrentPosition(({ coords }) => { - dispatch( - getHealthcareAuthorities({ autoSubscriptionLocation: coords }), - ); - setUserAutoSubscribed(true); - }); - } - }, [autoSubscriptionEnabled, noHaAvailable, userAutoSubscribed, dispatch]); - + const dispatch = useDispatch(); useEffect(() => { - autoSubscribe(); - }, [autoSubscribe]); + const listener = navigation.addListener('blur', () => { + if ( + !autoSubscription.bannerDismissed && + autoSubscription.selectedAuthority + ) { + dispatch(toggleAutoSubscriptionBanner({ overrideValue: false })); + } + }); + return listener.remove; + }, [navigation, dispatch, autoSubscription]); + + const { bannerDismissed, selectedAuthority } = autoSubscription; return ( @@ -115,7 +85,7 @@ export const AllServicesOnScreen = ({ {t('home.gps.all_services_on_subheader')} - {noHaAvailable && ( + {selectedAuthorities.length === 0 && ( {t('home.gps.all_services_on_no_ha_available')} @@ -123,29 +93,27 @@ export const AllServicesOnScreen = ({ - {showBanner && ( - - - <> - {t('home.gps.auto_subscribe_text', { - healthAuthorityName: haName, - })} - navigation.navigate(Stacks.Partners)}> - {t('home.gps.auto_subscribe_link_text')} - - - - - navigation.navigate(Stacks.Partners)}> - - - + {!bannerDismissed && !!selectedAuthority && ( + navigation.navigate(Stacks.Partners)}> + + + <> + {t('home.gps.auto_subscribe_text', { + healthAuthorityName: selectedAuthority.name, + })} + + {t('home.gps.auto_subscribe_link_text')} + + + + + + )} ); diff --git a/app/views/main/ServiceOffScreens/SelectAuthority.tsx b/app/views/main/ServiceOffScreens/SelectAuthority.tsx index 51bd5bcb33..72da9442c5 100644 --- a/app/views/main/ServiceOffScreens/SelectAuthority.tsx +++ b/app/views/main/ServiceOffScreens/SelectAuthority.tsx @@ -3,14 +3,14 @@ import { useNavigation } from '@react-navigation/native'; import { useTranslation } from 'react-i18next'; import { ServiceOffScreen } from './Base'; -import { Screens } from '../../../navigation'; +import { Stacks } from '../../../navigation'; export const SelectAuthorityScreen = (): JSX.Element => { const { t } = useTranslation(); const navigation = useNavigation(); const onPress = () => { - navigation.navigate(Screens.PartnersEdit); + navigation.navigate(Stacks.Partners); }; return ( diff --git a/app/views/main/ServiceOffScreens/__tests__/__snapshots__/SelectAuthority.spec.js.snap b/app/views/main/ServiceOffScreens/__tests__/__snapshots__/SelectAuthority.spec.js.snap index fb747a138f..633137fd96 100644 --- a/app/views/main/ServiceOffScreens/__tests__/__snapshots__/SelectAuthority.spec.js.snap +++ b/app/views/main/ServiceOffScreens/__tests__/__snapshots__/SelectAuthority.spec.js.snap @@ -159,7 +159,7 @@ exports[`select authority screen matches snapshot 1`] = ` ] } > - Allow PathCheck to add your local Health Department or select one yourself to receive info about COVID-19 in your area + Select your local Health Department to receive info about COVID-19 in your area PathCheck is saving the places you visit to create your private location diary - - - - - You’ve been auto-subscribed to . - Click here to change. + You will not receive exposure notifications while you are outside a region served by Health Department partners - - - - - - -" - /> - + diff --git a/app/views/main/style.ts b/app/views/main/style.ts index cda0115395..53bf288473 100644 --- a/app/views/main/style.ts +++ b/app/views/main/style.ts @@ -75,6 +75,8 @@ export const styles = StyleSheet.create({ paddingLeft: 20, }, bottomSheet: { + position: 'absolute', + bottom: 0, backgroundColor: Colors.bottomSheetBackground, padding: 24, flexDirection: 'row', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cb37c2f933..99ef4153f0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -522,4 +522,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a70cd2a7867b1a342049fd907a0dace84082799b -COCOAPODS: 1.9.3 +COCOAPODS: 1.9.1