Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Mobile): add notifications opt-In screen #4837

Merged
merged 16 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/mobile/app/notifications-opt-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import React from 'react'
import { useColorScheme } from 'react-native'
import { OptIn } from '@/src/components/OptIn'
import useNotifications from '@/src/hooks/useNotifications'
import { router } from 'expo-router'
import { router, useFocusEffect } from 'expo-router'

function NotificationsOptIn() {
const { enableNotifications, isAppNotificationEnabled } = useNotifications(true)
const { enableNotifications, isAppNotificationEnabled } = useNotifications()
const colorScheme = useColorScheme()

useFocusEffect(() => {
if (isAppNotificationEnabled) {
router.replace('/(tabs)')
}
})

const image =
colorScheme === 'dark'
? require('@/assets/images/notifications-dark.png')
Expand All @@ -19,13 +25,13 @@ function NotificationsOptIn() {
title="Stay in the loop with account activity"
description="Get notified when you receive assets, and when transactions require your action."
image={image}
isVisible={!isAppNotificationEnabled}
isVisible
ctaButton={{
onPress: enableNotifications,
label: 'Enable notifications',
}}
secondaryButton={{
onPress: () => router.replace('/(tabs)'),
onPress: () => router.back(),
label: 'Maybe later',
}}
/>
Expand Down
5 changes: 1 addition & 4 deletions apps/mobile/src/components/OptIn/OptIn.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import { ImageSourcePropType, StyleSheet } from 'react-native'
import { View, Image, Text } from 'tamagui'
import { useRouter } from 'expo-router'
import { SafeButton } from '@/src/components/SafeButton'
import { WINDOW_HEIGHT } from '@/src/store/constants'
import { FloatingContainer } from '../FloatingContainer'
Expand All @@ -25,10 +24,8 @@ interface OptInProps {

export const OptIn: React.FC<OptInProps> = React.memo(
({ testID, kicker, title, description, image, ctaButton, secondaryButton, isVisible }: OptInProps) => {
const router = useRouter()

if (!isVisible) {
router.push('/(tabs)')
return
}

return (
Expand Down
23 changes: 22 additions & 1 deletion apps/mobile/src/features/Assets/Assets.container.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React from 'react'
import React, { useEffect } from 'react'

import { SafeTab } from '@/src/components/SafeTab'

import { TokensContainer } from '@/src/features/Assets/components/Tokens'
import { NFTsContainer } from '@/src/features/Assets/components/NFTs'
import { AssetsHeaderContainer } from '@/src/features/Assets/components/AssetsHeader'
import useNotifications from '@/src/hooks/useNotifications'
import { useRouter } from 'expo-router'
import { useAppDispatch } from '@/src/store/hooks'
import { updatePromptAttempts } from '@/src/store/notificationsSlice'

const tabItems = [
{
Expand All @@ -18,5 +22,22 @@ const tabItems = [
]

export function AssetsContainer() {
const { isAppNotificationEnabled, promptAttempts } = useNotifications()
const dispatch = useAppDispatch()
const router = useRouter()

/*
* If the user has not enabled notifications and has not been prompted to enable them,
* redirect to the opt-in screen
* */

const shouldShowOptIn = !isAppNotificationEnabled && !promptAttempts

useEffect(() => {
if (shouldShowOptIn) {
dispatch(updatePromptAttempts(1))
router.navigate('/notifications-opt-in')
}
}, [])
return <SafeTab items={tabItems} headerHeight={200} renderHeader={AssetsHeaderContainer} />
Comment on lines +25 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird. The AssetsContainer should not care about notifications.
Get some inspiration from here:
https://docs.expo.dev/router/reference/authentication/
https://reactnavigation.org/docs/auth-flow
I think that the _layout.tsx should be the place where we decide what first screen we show to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely get your point here, but we don't wanna spam our users before the Onboarding, and if we use _layout, this takes place before anything. The other alternative would be calling it at HomeScreen, wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the user clicks on "get started/continue" in the onboarding flow we should set a flag "onboarding_done": true. Next time the user starts we should not show the onboarding screens. So we will be deciding in the _layout.tsx which screen to show anyway.

}
11 changes: 10 additions & 1 deletion apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MyAccountsContainer, MyAccountsFooter } from '../MyAccounts'
import { useMyAccountsSortable } from '../MyAccounts/hooks/useMyAccountsSortable'
import { useAppDispatch, useAppSelector } from '@/src/store/hooks'
import { router } from 'expo-router'
import { selectAppNotificationStatus } from '@/src/store/notificationsSlice'

const dropdownLabelProps = {
fontSize: '$5',
Expand All @@ -25,8 +26,16 @@ export const Navbar = () => {
const dispatch = useAppDispatch()
const isEdit = useAppSelector(selectMyAccountsMode)
const activeSafe = useAppSelector(selectActiveSafe)
const isAppNotificationEnabled = useAppSelector(selectAppNotificationStatus)
const { safes, onDragEnd } = useMyAccountsSortable()

const handleNotificationAccess = () => {
if (!isAppNotificationEnabled) {
router.navigate('/notifications-opt-in')
}
// TODO: navigate to notifications list when notifications are enabled
}

const toggleEditMode = () => {
dispatch(toggleMode())
}
Expand Down Expand Up @@ -55,7 +64,7 @@ export const Navbar = () => {
}
/>
<View style={styles.rightButtonContainer}>
<TouchableOpacity onPress={() => router.navigate('/notifications-opt-in')}>
<TouchableOpacity onPress={handleNotificationAccess}>
<SafeFontIcon name="lightbulb" />
</TouchableOpacity>
<TouchableOpacity>
Expand Down
42 changes: 5 additions & 37 deletions apps/mobile/src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,33 @@ import {
selectAppNotificationStatus,
selectFCMToken,
selectPromptAttempts,
selectLastTimePromptAttempted,
selectRemoteMessages,
toggleAppNotifications,
updatePromptAttempts,
} from '@/src/store/notificationsSlice'
import NotificationsService from '@/src/services/notifications/NotificationService'
import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'
import Logger from '@/src/utils/logger'
import { router } from 'expo-router'

interface NotificationsProps {
isAppNotificationEnabled: boolean
fcmToken: string | null
remoteMessages: FirebaseMessagingTypes.RemoteMessage[]
enableNotifications: () => void
promptAttempts: number
}

const useNotifications = (isOnboarding?: boolean): NotificationsProps => {
const useNotifications = (): NotificationsProps => {
const dispatch = useAppDispatch()
/**
* We need to check if the user has enabled notifications for the device in order to keep listening for messages
* since the user can disable notifications at any time on their device, we need to handle app behavior accordingly
* if device notifications are disabled, the user has been prompt more than 3 times within a month to enable the app notifications
* we should only ask the user to enable notifications again after a month has passed
*
* If the user has disabled notifications for the app, we should disable app notifications
*/
const isAppNotificationEnabled = useAppSelector(selectAppNotificationStatus)
const fcmToken = useAppSelector(selectFCMToken)
const remoteMessages = useAppSelector(selectRemoteMessages)

const promptAttempts = useAppSelector(selectPromptAttempts)
const lastTimePromptAttempted = useAppSelector(selectLastTimePromptAttempted)

const enableNotifications = useCallback(() => {
const checkNotifications = async () => {
const isDeviceNotificationEnabled = await NotificationsService.isDeviceNotificationEnabled()
if (!isDeviceNotificationEnabled) {
/**
* If the user has been prompt more than 3 times within a month to enable the device notifications
* we should only ask the user to enable it again after a month has passed
*
* This also disables app notifications if the user has disabled device notifications and denied to re-enabled it after 3 attempts
*/
if (
promptAttempts &&
promptAttempts >= 3 &&
lastTimePromptAttempted &&
new Date().getTime() - new Date(lastTimePromptAttempted).getTime() < 2592000000
) {
if (isAppNotificationEnabled) {
dispatch(toggleAppNotifications(false))
}

if (isOnboarding) {
router.navigate('/(tabs)')
}

return
}
dispatch(updatePromptAttempts(1))

const { permission } = await NotificationsService.getAllPermissions()

Expand All @@ -90,7 +58,7 @@ const useNotifications = (isOnboarding?: boolean): NotificationsProps => {
checkNotifications()
}, [isAppNotificationEnabled])

return { enableNotifications, isAppNotificationEnabled, fcmToken, remoteMessages }
return { enableNotifications, promptAttempts, isAppNotificationEnabled, fcmToken, remoteMessages }
}

export default useNotifications
Loading