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 15 commits
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
1 change: 1 addition & 0 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function RootLayout() {
<Stack.Screen name="signers" options={{ headerShown: true, title: 'Signers' }} />
<Stack.Screen name="notifications" options={{ headerShown: true, title: 'Notifications' }} />
<Stack.Screen name="app-settings" options={{ headerShown: true, title: 'Settings' }} />
<Stack.Screen name="notifications-opt-in" options={{ headerShown: true, title: '' }} />
<Stack.Screen name="+not-found" />
</Stack>
</SafeToastProvider>
Expand Down
35 changes: 35 additions & 0 deletions apps/mobile/app/notifications-opt-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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'

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

const image =
colorScheme === 'dark'
? require('@/assets/images/notifications-dark.png')
: require('@/assets/images/notifications-light.png')

return (
<OptIn
testID="notifications-opt-in"
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}
ctaButton={{
onPress: enableNotifications,
label: 'Enable notifications',
}}
secondaryButton={{
onPress: () => router.replace('/(tabs)'),
label: 'Maybe later',
}}
/>
)
}

export default NotificationsOptIn
Binary file added apps/mobile/assets/images/notifications-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/mobile/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
'<rootDir>/__mocks__/fileMock.js',
},
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|react-redux|moti/.*)',
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@notifee/react-native|react-redux|moti/.*)',
],
testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
}
61 changes: 61 additions & 0 deletions apps/mobile/src/components/FloatingContainer/FloatingContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Layout } from '@/src/store/constants'
import React, { FC, useMemo } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

import { KeyboardAvoidingView, KeyboardAvoidingViewProps, Platform, StyleSheet, View } from 'react-native'

interface FloatingContainerProps {
children: React.ReactNode
noOffset?: boolean
sticky?: boolean
keyboardAvoidEnabled?: boolean
onLayout?: KeyboardAvoidingViewProps['onLayout']
testID?: string
}

export const FloatingContainer: FC<FloatingContainerProps> = ({
children,
noOffset,
sticky,
keyboardAvoidEnabled,
onLayout,
testID,
}: FloatingContainerProps) => {
const bottomInset = useSafeAreaInsets().bottom
const deviceBottom = Layout.isSmallDevice ? 10 : 20

const bottomPadding = useMemo(() => {
return Math.max(bottomInset, deviceBottom)
}, [bottomInset])

const keyboardVerticalOffset = useMemo(() => {
return noOffset ? 0 : Platform.select({ ios: 40, default: 0 })
}, [noOffset])

return (
<KeyboardAvoidingView
testID={testID}
behavior={sticky ? 'height' : 'position'}
keyboardVerticalOffset={keyboardVerticalOffset}
enabled={keyboardAvoidEnabled}
style={[styles.floatingContainer, { paddingBottom: bottomPadding }]}
onLayout={onLayout}
>
<View style={styles.childContainer}>{children}</View>
</KeyboardAvoidingView>
)
}

const styles = StyleSheet.create({
floatingContainer: {
position: 'fixed',
bottom: -40,
width: '100%',
zIndex: 1,
},
childContainer: {
flexDirection: 'column',
justifyContent: 'space-between',
flexGrow: 1,
},
})
2 changes: 2 additions & 0 deletions apps/mobile/src/components/FloatingContainer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { FloatingContainer } from './FloatingContainer'
export { FloatingContainer }
79 changes: 79 additions & 0 deletions apps/mobile/src/components/OptIn/OptIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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'

interface OptInProps {
title: string
ctaButton: {
onPress: () => void
label: string
}
kicker?: string
description?: string
image?: ImageSourcePropType
secondaryButton?: {
onPress: () => void
label: string
}
testID?: string
isVisible?: boolean
}

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 (
<View
testID={testID}
style={styles.wrapper}
padding="$4"
gap="$8"
alignItems="center"
justifyContent="flex-start"
>
{kicker && (
<Text textAlign="center" fontWeight={700} fontSize="$4" lineHeight="$6">
{kicker}
</Text>
)}
<Text textAlign="center" fontWeight={600} fontSize="$8" lineHeight="$8">
{title}
</Text>
{description && (
<Text textAlign="center" fontWeight={400} fontSize="$4">
{description}
</Text>
)}
{image && <Image style={styles.image} source={image} />}

<FloatingContainer sticky testID="notifications-opt-in-cta-buttons">
<SafeButton onPress={ctaButton.onPress} label={ctaButton.label} />
{secondaryButton && (
<SafeButton variant="secondary" onPress={secondaryButton.onPress} label={secondaryButton.label} />
)}
</FloatingContainer>
</View>
)
},
)

const styles = StyleSheet.create({
wrapper: {
flex: 1,
},
image: {
width: '100%',
height: Math.abs(WINDOW_HEIGHT * 0.42),
},
})

OptIn.displayName = 'OptIn'
2 changes: 2 additions & 0 deletions apps/mobile/src/components/OptIn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { OptIn } from './OptIn'
export { OptIn }
11 changes: 8 additions & 3 deletions apps/mobile/src/components/SafeButton/SafeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { styled, Text, View } from 'tamagui'
interface SafeButtonProps {
onPress: () => void
label: string
variant?: 'primary' | 'secondary'
}

export const StyledButtonWrapper = styled(View, {
Expand All @@ -14,11 +15,15 @@ export const StyledButtonWrapper = styled(View, {
borderRadius: 8,
})

export function SafeButton({ onPress, label }: SafeButtonProps) {
export function SafeButton({ onPress, label, variant = 'primary' }: SafeButtonProps) {
const variantStyles =
variant === 'primary'
? { backgroundColor: '$primary', fontColor: '$background' }
: { backgroundColor: 'inherit', fontColor: '$primary' }
return (
<TouchableOpacity onPress={onPress}>
<StyledButtonWrapper backgroundColor="$primary">
<Text fontSize="$4" fontWeight={600} color="$background">
<StyledButtonWrapper backgroundColor={variantStyles.backgroundColor}>
<Text fontSize="$4" fontWeight={600} color={variantStyles.fontColor}>
{label}
</Text>
</StyledButtonWrapper>
Expand Down
19 changes: 15 additions & 4 deletions apps/mobile/src/features/Assets/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { selectMyAccountsMode, toggleMode } from '@/src/store/myAccountsSlice'
import { MyAccountsContainer, MyAccountsFooter } from '../MyAccounts'
import { useMyAccountsSortable } from '../MyAccounts/hooks/useMyAccountsSortable'
import { useAppDispatch, useAppSelector } from '@/src/store/hooks'
import { router } from 'expo-router'

const dropdownLabelProps = {
fontSize: '$5',
Expand Down Expand Up @@ -53,10 +54,14 @@ export const Navbar = () => {
)
}
/>

<TouchableOpacity>
<SafeFontIcon name="apps" />
</TouchableOpacity>
<View style={styles.rightButtonContainer}>
<TouchableOpacity onPress={() => router.navigate('/notifications-opt-in')}>
<SafeFontIcon name="lightbulb" />
</TouchableOpacity>
<TouchableOpacity>
<SafeFontIcon name="apps" />
</TouchableOpacity>
</View>
</SafeAreaView>
</BlurredIdenticonBackground>
</View>
Expand All @@ -72,4 +77,10 @@ const styles = StyleSheet.create({
paddingVertical: 16,
paddingBottom: 0,
},
rightButtonContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
})
14 changes: 1 addition & 13 deletions apps/mobile/src/features/Onboarding/Onboarding.container.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import React from 'react'
import { OnboardingCarousel } from './components/OnboardingCarousel'
import { items } from './components/OnboardingCarousel/items'
import { useRouter } from 'expo-router'
import { SafeButton } from '@/src/components/SafeButton'

export function Onboarding() {
const router = useRouter()

const onGetStartedPress = () => {
router.navigate('/(tabs)')
}

return (
<OnboardingCarousel items={items}>
<SafeButton onPress={onGetStartedPress} label="Get started" />
</OnboardingCarousel>
)
return <OnboardingCarousel items={items} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,25 @@ export type CarouselItem = {
name: string
description?: string
image?: React.ReactNode
imagePosition?: 'top' | 'bottom'
}

interface CarouselItemProps {
item: CarouselItem
}

export const CarouselItem = ({ item: { title, description, image } }: CarouselItemProps) => {
export const CarouselItem = ({ item: { title, description, image, imagePosition = 'top' } }: CarouselItemProps) => {
return (
<View gap="$8" alignItems="center" justifyContent="center">
{image}

{imagePosition === 'top' && image}
<YStack gap="$8" paddingHorizontal="$5">
<YStack>{title}</YStack>

<Text textAlign="center" fontSize={'$4'}>
{description}
</Text>
</YStack>
{imagePosition === 'bottom' && image}
</View>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react'
import React from 'react'
import { OnboardingCarousel } from './OnboardingCarousel'
import { items } from './items'
import { SafeButton } from '@/src/components/SafeButton'
import { action } from '@storybook/addon-actions'

const meta: Meta<typeof OnboardingCarousel> = {
title: 'Carousel',
Expand All @@ -16,10 +14,6 @@ type Story = StoryObj<typeof OnboardingCarousel>

export const Default: Story = {
render: function Render(args) {
return (
<OnboardingCarousel {...args} items={items}>
<SafeButton label="Get started" onPress={action('onPress')} />
</OnboardingCarousel>
)
return <OnboardingCarousel {...args} items={items} />
},
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { OnboardingCarousel } from './OnboardingCarousel'
import { Text, View } from 'tamagui'
import { Text } from 'tamagui'
import { render } from '@/src/tests/test-utils'

describe('OnboardingCarousel', () => {
Expand All @@ -13,12 +13,8 @@ describe('OnboardingCarousel', () => {
// react-native-collapsible-tab-view does not returns any information about the tabs children
// that is why we only test the children component here =/
it('renders without crashing', () => {
const { getByTestId } = render(
<OnboardingCarousel items={items}>
<View testID="child-element">Child Element</View>
</OnboardingCarousel>,
)
const { getByTestId } = render(<OnboardingCarousel items={items} />)

expect(getByTestId('child-element')).toBeTruthy()
expect(getByTestId('carrousel')).toBeTruthy()
})
})
Loading
Loading