import React , { FC , useRef , useEffect , useState } from 'react' ; import { StyleSheet , ScrollView , View , Animated , RefreshControl , ActivityIndicator , Platform } from 'react-native' ; import { useTranslation } from 'react-i18next' ; import { useNavigation , useFocusEffect , useIsFocused } from '@react-navigation/native' ; import AsyncStorage from '@react-native-community/async-storage' ; import { useExposure , StatusState , StatusType , Status } from 'react-native-exposure-notification-service' ; import * as SecureStore from 'expo-secure-store' ; import { differenceInCalendarDays } from 'date-fns' ; import Container from '../atoms/container' ; import Text from '../atoms/text' ; import { text , colors } from '../../theme' ; import { SPACING_HORIZONTAL } from '../../theme/layouts/shared' ; import { Header } from '../molecules/header' ; import { Grid } from '../molecules/grid' ; import { Message } from '../atoms/message' ; import Spacing from '../atoms/spacing' ; import Markdown from '../atoms/markdown' ; import { ScreenNames } from '../../navigation' ; import { TouchableWithoutFeedback } from 'react-native-gesture-handler' ; import { useApplication } from '../../providers/context' ; import { ArrowLink } from '../molecules/arrow-link' ; import { ExposureNotificationsModal , BluetoothNotificationsModal , PushNotificationsModal } from '../organisms/modals' ; import { useSettings } from '../../providers/settings' ; import { useAppState , useVersion , useA11yElement , A11yView } from '../../hooks' ; import { useReminder } from '../../providers/reminder' ; import { NewVersionCard } from '../molecules/new-version-card' ; import { getExposureDate } from '../../utils/exposure' ; const RestrictionsImage = require ( '../../assets/images/restrictions/image.png' ) ; const ANIMATION_DURATION = 300 ; const PROMPT_OFFSET = 1000 ; const getMessage = ( { onboarded , enabled , status , messages , stage , paused } ) : string => { if ( paused ) { return 'dashboard:message:paused' ; } if ( onboarded ) { if ( enabled && status . state === StatusState . active ) { return 'dashboard:message:standard' ; } return 'dashboard:message:inactive' ; } else { if ( stage < 1 ) { if ( enabled && status . state === StatusState . active ) { return messages [ 0 ] ; } return 'dashboard:alternateTourStart' ; } else { return messages [ stage ] ; } } } ; export const Dashboard = ( ) => { const { t } = useTranslation ( ) ; const { initialised , enabled , status , contacts , getCloseContacts , permissions , readPermissions } = useExposure ( ) ; const [ appState ] = useAppState ( ) ; const { checked , paused } = useReminder ( ) ; const navigation = useNavigation ( ) ; const { onboarded , setContext , loadAppData } = useApplication ( ) ; const { isolationDuration , isolationCompleteDuration , latestVersion : appLatestVersion } = useSettings ( ) ; const [ refreshing , setRefreshing ] = useState ( false ) ; const { focusRef : tourFocus , focusA11yElement : focusTourElem } = useA11yElement ( ) ; const { focusRef : dashboardFocus , focusA11yElement : focusDashboardElem } = useA11yElement ( ) ; const isFocused = useIsFocused ( ) ; const messageOpacity = useRef ( new Animated . Value ( 0 ) ) . current ; const contentOpacity = useRef ( new Animated . Value ( 0 ) ) . current ; const gridOpacity = useRef ( new Animated . Value ( 0 ) ) . current ; const exposureEnabled = useRef ( enabled ) ; const bluetoothDisabled = useRef ( status . state === 'disabled' && status . type ?. includes ( StatusType . bluetooth ) ) ; const pushNotificationsDisabled = useRef ( permissions . notifications . status === 'not_allowed' ) ; const [ state , setState ] = useState < { stage : number ; exposurePrompt : boolean ; bluetoothPrompt : boolean ; pushNotificationsPrompt : boolean ; disabled : boolean ; current : string ; isolationMessage : string | null ; isolationComplete : boolean ; default : string ; messages : string [ ] ; } > ( { stage : onboarded ? - 1 : 0 , exposurePrompt : false , bluetoothPrompt : false , pushNotificationsPrompt : false , disabled : false , current : t ( getMessage ( { onboarded , enabled , status , messages : t ( 'dashboard:tour' , { returnObjects : true } ) , stage : onboarded ? - 1 : 0 , paused } ) ) , isolationMessage : null , isolationComplete : false , messages : t ( 'dashboard:tour' , { returnObjects : true } ) , default : t ( 'dashboard:message:standard' ) } ) ; const version = useVersion ( ) ; const resetToNormal = ( ) => setState ( ( s ) => ( { ... s , isolationComplete : false , isolationMessage : null } ) ) ; const setExposed = ( ) => setState ( ( s ) => ( { ... s , isolationComplete : false , isolationMessage : t ( 'dashboard:exposed' ) } ) ) ; const setIsolationComplete = ( ) => setState ( ( s ) => ( { ... s , isolationComplete : true , isolationMessage : t ( 'dashboard:isolationComplete' ) } ) ) ; const processContactsForMessaging = async ( ) => { let currentStatus = null ; try { currentStatus = await SecureStore . getItemAsync ( 'niexposuredate' ) ; } catch ( err ) { await SecureStore . deleteItemAsync ( 'niexposuredate' ) ; console . log ( 'processContactsForMessaging' , err ) ; } if ( currentStatus ) { const daysDiff = differenceInCalendarDays ( new Date ( ) , new Date ( Number ( currentStatus ) ) ) ; const withIsolation = isolationDuration + isolationCompleteDuration ; if ( daysDiff >= withIsolation ) { await SecureStore . deleteItemAsync ( 'niexposuredate' ) ; return resetToNormal ( ) ; } if ( daysDiff >= isolationDuration && daysDiff < withIsolation ) { return setIsolationComplete ( ) ; } if ( contacts && contacts . length > 0 ) { return setExposed ( ) ; } } return resetToNormal ( ) ; } ; const checkLatestExposure = async ( ) => { const latestExposure = getExposureDate ( contacts ) ; if ( latestExposure ) { await SecureStore . setItemAsync ( 'niexposuredate' , String ( latestExposure . getTime ( ) ) ) ; } processContactsForMessaging ( ) ; } ; const onRefresh = ( ) => { setRefreshing ( true ) ; loadAppData ( ) . then ( ( ) => setRefreshing ( false ) ) ; } ; useEffect ( ( ) => { onRefresh ( ) ; } , [ ] ) ; useEffect ( ( ) => { getCloseContacts ( ) ; } , [ status ] ) ; useEffect ( ( ) => { checkLatestExposure ( ) ; } , [ contacts , status ] ) ; useFocusEffect ( React . useCallback ( ( ) => { if ( ! isFocused || appState !== 'active' ) { return ; } readPermissions ( ) ; } , [ isFocused , appState , readPermissions ] ) ) ; useEffect ( ( ) => { setState ( ( s ) => ( { ... s , current : t ( getMessage ( { onboarded , enabled , status , messages : state . messages , stage : state . stage , paused } ) ) } ) ) ; exposureEnabled . current = enabled ; bluetoothDisabled . current = status . state === 'disabled' && status . type ?. includes ( StatusType . bluetooth ) ; pushNotificationsDisabled . current = permissions . notifications . status === 'not_allowed' ; if ( ! exposureEnabled . current && onboarded ) { setTimeout ( ( ) => { if ( ! exposureEnabled . current ) { setState ( ( s ) => ( { ... s , exposurePrompt : true } ) ) ; } } , PROMPT_OFFSET ) ; } else if ( bluetoothDisabled . current && onboarded ) { setTimeout ( ( ) => { if ( bluetoothDisabled . current ) { setState ( ( s ) => ( { ... s , bluetoothPrompt : true } ) ) ; } } , PROMPT_OFFSET ) ; } else if ( pushNotificationsDisabled . current && onboarded ) { setTimeout ( ( ) => { if ( pushNotificationsDisabled . current && exposureEnabled . current && ! bluetoothDisabled . current ) { setState ( ( s ) => ( { ... s , pushNotificationsPrompt : true } ) ) ; } } , PROMPT_OFFSET ) ; } else if ( onboarded && exposureEnabled . current ) { setState ( ( s ) => ( { ... s , exposurePrompt : false } ) ) ; } setTimeout ( ( ) => checkLatestExposure ( ) , 100 ) ; } , [ enabled , onboarded , status , permissions ] ) ; const animateTourIn = ( ) => { setState ( ( s ) => ( { ... s , disabled : true } ) ) ; Animated . parallel ( [ Animated . timing ( messageOpacity , { toValue : 1 , duration : ANIMATION_DURATION , useNativeDriver : true } ) , Animated . timing ( gridOpacity , { toValue : 1 , duration : ANIMATION_DURATION , useNativeDriver : true } ) ] ) . start ( ( ) => { setState ( ( s ) => ( { ... s , disabled : false } ) ) ; } ) ; } ; const animateTourOut = ( ) => { setState ( ( s ) => ( { ... s , disabled : true } ) ) ; Animated . parallel ( [ Animated . timing ( messageOpacity , { toValue : 0 , duration : ANIMATION_DURATION , useNativeDriver : true } ) , Animated . timing ( gridOpacity , { toValue : 0 , duration : ANIMATION_DURATION , useNativeDriver : true } ) ] ) . start ( ( ) => { if ( state . stage < state . messages . length - 1 ) { setState ( ( s ) => ( { ... s , stage : state . stage + 1 , current : getMessage ( { onboarded , enabled , status , messages : state . messages , stage : state . stage + 1 } ) } ) ) ; animateTourIn ( ) ; } else { setState ( ( s ) => ( { ... s , stage : - 1 , current : s . default } ) ) ; setContext ( { onboarded : true } ) ; animateDashboard ( ) ; } } ) ; } ; const animateDashboard = ( ) => { setState ( ( s ) => ( { ... s , disabled : true } ) ) ; Animated . parallel ( [ Animated . timing ( messageOpacity , { toValue : 1 , duration : ANIMATION_DURATION , useNativeDriver : true } ) , Animated . timing ( contentOpacity , { toValue : 1 , duration : ANIMATION_DURATION , useNativeDriver : true } ) , Animated . timing ( gridOpacity , { toValue : 1 , duration : ANIMATION_DURATION , useNativeDriver : true } ) ] ) . start ( ( ) => { AsyncStorage . setItem ( 'scot.onboarded' , 'true' ) ; setState ( ( s ) => ( { ... s , disabled : false } ) ) ; } ) ; } ; useEffect ( ( ) => { if ( onboarded ) { setTimeout ( ( ) => animateDashboard ( ) , 200 ) ; } else { setTimeout ( ( ) => animateTourIn ( ) , 200 ) ; } } , [ ] ) ; useEffect ( ( ) => { if ( ! state . disabled ) { focusTourElem ( ) ; } } , [ focusTourElem , state . disabled ] ) ; useEffect ( ( ) => { if ( onboarded && ! state . disabled ) { focusDashboardElem ( ) ; } } , [ focusDashboardElem , onboarded , state . disabled ] ) ; const handleTour = ( ) => { animateTourOut ( ) ; } ; if ( ! initialised || ! checked ) { return ( < > < Container center = "both" > < ActivityIndicator color = { colors . darkGrey } size = "large" / > < /Container> < />); } return ( < > < Header /> < ScrollView refreshControl = { < RefreshControl refreshing = { onboarded && refreshing } onRefresh = { onRefresh } / > } > { onboarded && appLatestVersion && version && appLatestVersion !== version ?. display && ( < View style = { blockStyles . block } > < NewVersionCard /> < Spacing s = { 16 } / > < /View>)} < Spacing s = { onboarded ? 15 : 65 } / > { ! onboarded && state . stage > - 1 && ( < > < View style = { styles . dots } > { [ - 1 , 0 , 1 , 2 ] . map ( ( x ) => ( < Animated . View key = { `step-${ x }` } style = { [ styles . dot , { backgroundColor : state . stage > x ? colors . primaryPurple : colors . lighterPurple } ] } / > ) ) } < /View> < A11yView ref = { tourFocus } accessible accessibilityHint = { t ( 'dashboard:tourA11y' , { returnObjects : true } ) [ state . stage ] } / > < Animated . View style = { { opacity : messageOpacity } } > < TouchableWithoutFeedback onPress = { ( ) => handleTour ( ) } > < Markdown markdownStyles = { markDownStyles } > { state . current } < /Markdown> < /TouchableWithoutFeedback> < Spacing s = { 30 } / > < ArrowLink onPress = { ( ) => { if ( ! state . disabled ) { handleTour ( ) ; } } } accessibilityHint = { t ( 'dashboard:tourActionHint' ) } invert > < Text variant = "h3" color = "pink" style = { styles . nextLink } > { t ( 'dashboard:tourAction' ) } < /Text> < /ArrowLink> < /Animated.View> < />)} { onboarded && state . isolationMessage && ( < > < Animated . View style = { { opacity : messageOpacity } } > < View accessible ref = { dashboardFocus } > < Markdown markdownStyles = { state . isolationComplete ? markDownStyles : markDownStylesExposed } > { state . isolationMessage } < /Markdown> < /View> { ! state . isolationComplete && ( < > < Spacing s = { 30 } /> < ArrowLink onPress = { ( ) => navigation . navigate ( ScreenNames . closeContact ) } accessibilityHint = { t ( 'dashboard:exposedAction' ) } invert > < Text variant = "h3" color = "pink" style = { styles . nextLink } > { t ( 'dashboard:tourAction' ) } < /Text> < /ArrowLink> < />)} { state . isolationComplete && ( < > < Spacing s = { 20 } /> < Text style = { blockStyles . block } inline color = "darkGrey" > { t ( 'dashboard:isolationCompleteSupplemental' ) } < /Text> < />)} < /Animated.View> < Spacing s = { 30 } / > < Animated . View style = { [ { opacity : contentOpacity } , blockStyles . block ] } > < Message /> < /Animated.View> < />)} { onboarded && ! state . isolationMessage && ( < Animated . View style = { { opacity : messageOpacity } } > < View accessible ref = { dashboardFocus } > < Markdown markdownStyles = { markDownStyles } > { state . current } < /Markdown> < /View> { state . stage === - 1 && ! paused && ( < > < Spacing s = { 20 } /> < Text style = { blockStyles . block } inline color = "darkGrey" > { t ( `dashboard:message:bluetooth:${ Platform . OS } `)}
                </Text>
              </>)}
            {state.stage === -1 && paused && (<>
                <Spacing s={20}/>
                <Text style={blockStyles.block} inline color="darkGrey">
                  {t('dashboard:message:pausedSupplemental')}
                </Text>
              </>)}
          </Animated.View>)}
        <Spacing s={30}/>
        <Grid onboarded={onboarded} stage={state.stage} opacity={gridOpacity} onboardingCallback={() => handleTour()}/>
        {state.isolationMessage && <Spacing s={34}/>}
        {onboarded && !state.isolationMessage && (<>
            <Animated.View style={[{ opacity: contentOpacity }, blockStyles.block]}>
              <Spacing s={30}/>
              <Message />
              <Spacing s={16}/>
              <Message image={RestrictionsImage} markdown={t('restrictions:message')} accessibilityLabel={t('restrictions:a11y:label')} accessibilityHint={t('restrictions:a11y:hint')} link={t('links:r')}/>
              <Spacing s={45}/>
            </Animated.View>
          </>)}
        {onboarded && (<Text variant="h4" color="primaryPurple" align="center">
            {t('dashboard:thanks')}
          </Text>)}
        <Spacing s={60}/>
      </ScrollView>
      {checked && !paused && state.exposurePrompt && (<ExposureNotificationsModal isVisible={state.exposurePrompt} onBackdropPress={() => setState((s) => ({ ...s, exposurePrompt: false }))} onClose={() => setState((s) => ({ ...s, exposurePrompt: false }))}/>)}
      {checked && !paused && state.bluetoothPrompt && (<BluetoothNotificationsModal isVisible={state.bluetoothPrompt} onBackdropPress={() => setState((s) => ({ ...s, bluetoothPrompt: false }))} onClose={() => setState((s) => ({ ...s, bluetoothPrompt: false }))}/>)}
      {checked && !paused && state.pushNotificationsPrompt && (<PushNotificationsModal isVisible={state.pushNotificationsPrompt} onBackdropPress={() => setState((s) => ({ ...s, pushNotificationsPrompt: false }))} onClose={() => setState((s) => ({ ...s, pushNotificationsPrompt: false }))}/>)}
    </>);
};
const blockStyles = StyleSheet.create({
    block: {
        marginHorizontal: SPACING_HORIZONTAL
    }
});
const markDownStyles = StyleSheet.create({
    ...blockStyles,
    text: {
        ...text.h1Heading,
        ...blockStyles.block,
        color: colors.primaryPurple
    }
});
const markDownStylesExposed = StyleSheet.create({
    ...markDownStyles,
    text: {
        ...markDownStyles.text,
        color: colors.errorRed
    }
});
const styles = StyleSheet.create({
    nextLink: {
        paddingLeft: SPACING_HORIZONTAL,
        paddingRight: 5
    },
    dots: {
        flexDirection: 'row',
        marginBottom: 30,
        paddingLeft: 46,
        paddingRight: SPACING_HORIZONTAL
    },
    dot: {
        width: 5,
        height: 5,
        borderRadius: 5,
        backgroundColor: colors.primaryPurple,
        marginRight: 10
    }
});
