-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(wallet-mobile): Add in-app notifications (#3775)
- Loading branch information
1 parent
4ed8aa3
commit 0ce90f4
Showing
26 changed files
with
644 additions
and
253 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import * as React from 'react' | ||
import {Path, Svg} from 'react-native-svg' | ||
|
||
import {IconProps} from './type' | ||
|
||
export const Bell = ({size = 36, color = 'black'}: IconProps) => { | ||
return ( | ||
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none"> | ||
<Path | ||
d="M20.529 15.207l-1.802-1.81v-4.46a6.855 6.855 0 00-1.624-4.53 6.869 6.869 0 00-4.202-2.35A6.754 6.754 0 005.878 5.95a6.733 6.733 0 00-.605 2.777v4.67l-1.802 1.81a1.64 1.64 0 001.161 2.79h3.364v.34A3.838 3.838 0 0012 21.997a3.846 3.846 0 004.004-3.66v-.34h3.363a1.643 1.643 0 001.603-1.952 1.64 1.64 0 00-.441-.838zm-6.527 3.13A1.879 1.879 0 0112 19.997a1.883 1.883 0 01-2.002-1.66v-.34h4.004v.34zm-8.499-2.34l1.182-1.18a2 2 0 00.59-1.42v-4.67a4.725 4.725 0 011.622-3.56 4.676 4.676 0 013.744-1.17 4.857 4.857 0 014.084 4.9v4.5a2 2 0 00.58 1.42l1.192 1.18H5.503z" | ||
fill={color} | ||
/> | ||
</Svg> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
apps/wallet-mobile/src/features/Notifications/useCases/NotificationUIHandler.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import {useNotificationManager} from '@yoroi/notifications' | ||
import {Notifications} from '@yoroi/types' | ||
import * as React from 'react' | ||
import {useMemo} from 'react' | ||
|
||
import {useNotificationDisplaySettings} from '../../Settings/useCases/changeWalletSettings/Notifications/NotificationsDisplaySettings' | ||
import {useWalletManager} from '../../WalletManager/context/WalletManagerProvider' | ||
import {NotificationPopup} from './common/NotificationPopup' | ||
import {NotificationStack} from './common/NotificationStack' | ||
|
||
const displayLimit = 3 | ||
const displayTime = 20 * 1000 | ||
|
||
export const NotificationUIHandler = () => { | ||
const enabled = useNotificationDisplaySettings() | ||
const {events, removeEvent} = useCollectNewNotifications({enabled}) | ||
const reversed = useMemo(() => [...events].reverse(), [events]) | ||
const displayed = reversed.slice(0, displayLimit) | ||
|
||
if (displayed.length === 0) { | ||
return null | ||
} | ||
|
||
return ( | ||
<NotificationStack> | ||
{displayed.map((event) => ( | ||
<NotificationPopup | ||
key={event.id} | ||
event={event} | ||
onCancel={() => removeEvent(event.id)} | ||
onPress={() => removeEvent(event.id)} | ||
/> | ||
))} | ||
</NotificationStack> | ||
) | ||
} | ||
|
||
const useCollectNewNotifications = ({enabled}: {enabled: boolean}) => { | ||
const manager = useNotificationManager() | ||
const walletManager = useWalletManager() | ||
const selectedWalletId = walletManager.selected.wallet?.id | ||
const [events, setEvents] = React.useState<Notifications.Event[]>([]) | ||
|
||
React.useEffect(() => { | ||
if (!enabled) return | ||
const pushEvent = (event: Notifications.Event) => { | ||
setEvents((e) => [...e, event]) | ||
setTimeout(() => setEvents((e) => e.filter((ev) => ev.id !== event.id)), displayTime) | ||
} | ||
|
||
const subscription = manager.newEvents$.subscribe((event) => { | ||
if (event.trigger === Notifications.Trigger.RewardsUpdated && event.metadata.walletId === selectedWalletId) { | ||
pushEvent(event) | ||
} | ||
|
||
if (event.trigger === Notifications.Trigger.TransactionReceived && event.metadata.walletId === selectedWalletId) { | ||
pushEvent(event) | ||
} | ||
}) | ||
return () => { | ||
subscription.unsubscribe() | ||
} | ||
}, [manager, setEvents, selectedWalletId, enabled]) | ||
|
||
const removeEvent = (id: number) => { | ||
setEvents((e) => e.filter((ev) => ev.id !== id)) | ||
} | ||
|
||
return {events, removeEvent} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationPopup.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import {useTheme} from '@yoroi/theme' | ||
import {Notifications} from '@yoroi/types' | ||
import * as React from 'react' | ||
import {StyleSheet, TouchableOpacity, View} from 'react-native' | ||
|
||
import {Icon} from '../../../../components/Icon' | ||
import {Text} from '../../../../components/Text' | ||
import {useWalletNavigation} from '../../../../kernel/navigation' | ||
import {SwipeOutWrapper} from './SwipeOutWrapper' | ||
import {useStrings} from './useStrings' | ||
|
||
type Props = { | ||
event: Notifications.Event | ||
onPress: () => void | ||
onCancel: () => void | ||
} | ||
|
||
export const NotificationPopup = ({event, onPress, onCancel}: Props) => { | ||
const navigation = useWalletNavigation() | ||
const strings = useStrings() | ||
|
||
if (event.trigger === Notifications.Trigger.TransactionReceived) { | ||
return ( | ||
<SwipeOutWrapper onSwipeOut={onCancel}> | ||
<NotificationItem | ||
onPress={() => { | ||
onPress() | ||
navigation.navigateToTxHistory() | ||
}} | ||
icon={<TransactionReceivedIcon />} | ||
title={strings.assetsReceived} | ||
description={strings.tapToView} | ||
/> | ||
</SwipeOutWrapper> | ||
) | ||
} | ||
|
||
if (event.trigger === Notifications.Trigger.RewardsUpdated) { | ||
return ( | ||
<SwipeOutWrapper onSwipeOut={onCancel}> | ||
<NotificationItem | ||
onPress={() => { | ||
onPress() | ||
navigation.navigateToStakingDashboard() | ||
}} | ||
icon={<RewardsUpdatedIcon />} | ||
title={strings.stakingRewardsReceived} | ||
description={strings.tapToView} | ||
/> | ||
</SwipeOutWrapper> | ||
) | ||
} | ||
|
||
return null | ||
} | ||
|
||
const NotificationItem = ({ | ||
onPress, | ||
icon, | ||
title, | ||
description, | ||
}: { | ||
onPress: () => void | ||
icon: React.ReactNode | ||
title: string | ||
description: string | ||
}) => { | ||
const {styles} = useStyles() | ||
return ( | ||
<TouchableOpacity style={styles.container} onPress={onPress}> | ||
{icon} | ||
|
||
<View style={styles.content}> | ||
<Text style={styles.title}>{title}</Text> | ||
|
||
<Text style={styles.description}>{description}</Text> | ||
</View> | ||
</TouchableOpacity> | ||
) | ||
} | ||
|
||
const TransactionReceivedIcon = () => { | ||
const {styles, colors} = useStyles() | ||
return ( | ||
<View style={[styles.icon, {backgroundColor: colors.iconBackground}]}> | ||
<Icon.Received color={colors.iconColor} /> | ||
</View> | ||
) | ||
} | ||
|
||
const RewardsUpdatedIcon = () => { | ||
const {styles, colors} = useStyles() | ||
return ( | ||
<View style={[styles.icon, {backgroundColor: colors.iconBackground}]}> | ||
<Icon.Staking color={colors.iconColor} /> | ||
</View> | ||
) | ||
} | ||
|
||
const useStyles = () => { | ||
const {atoms, color} = useTheme() | ||
const styles = StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
height: 76, | ||
borderRadius: 6, | ||
...atoms.p_lg, | ||
...atoms.gap_lg, | ||
...atoms.flex_row, | ||
backgroundColor: color.bg_color_max, | ||
}, | ||
icon: { | ||
width: 40, | ||
height: 40, | ||
borderRadius: 20, | ||
...atoms.align_center, | ||
...atoms.justify_center, | ||
}, | ||
content: { | ||
...atoms.flex_col, | ||
...atoms.gap_xs, | ||
}, | ||
title: { | ||
...atoms.body_2_md_regular, | ||
...atoms.font_semibold, | ||
}, | ||
description: { | ||
...atoms.link_2_md, | ||
color: color.gray_600, | ||
}, | ||
}) | ||
|
||
return {styles, colors: {iconColor: color.secondary_600, iconBackground: color.secondary_100}} | ||
} |
39 changes: 39 additions & 0 deletions
39
apps/wallet-mobile/src/features/Notifications/useCases/common/NotificationStack.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import {useTheme} from '@yoroi/theme' | ||
import * as React from 'react' | ||
import {StyleSheet, View} from 'react-native' | ||
import {SafeAreaView} from 'react-native-safe-area-context' | ||
|
||
type Props = { | ||
children: React.ReactNode | ||
} | ||
|
||
export const NotificationStack = ({children}: Props) => { | ||
const {styles} = useStyles() | ||
return ( | ||
<View style={styles.absolute}> | ||
<SafeAreaView edges={['top']}> | ||
<View style={styles.flex}>{children}</View> | ||
</SafeAreaView> | ||
</View> | ||
) | ||
} | ||
|
||
const useStyles = () => { | ||
const {atoms} = useTheme() | ||
const styles = StyleSheet.create({ | ||
absolute: { | ||
...atoms.absolute, | ||
top: 0, | ||
left: 0, | ||
right: 0, | ||
...atoms.z_50, | ||
...atoms.p_lg, | ||
}, | ||
flex: { | ||
...atoms.gap_sm, | ||
...atoms.flex_col, | ||
}, | ||
}) | ||
|
||
return {styles} | ||
} |
54 changes: 54 additions & 0 deletions
54
apps/wallet-mobile/src/features/Notifications/useCases/common/SwipeOutWrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as React from 'react' | ||
import {Animated, Dimensions, PanResponder} from 'react-native' | ||
|
||
type Props = { | ||
children: React.ReactNode | ||
onSwipeOut: () => void | ||
} | ||
|
||
export const SwipeOutWrapper = ({children, onSwipeOut}: Props) => { | ||
const {pan, panResponder} = usePanAnimation({onRelease: onSwipeOut}) | ||
|
||
return ( | ||
<Animated.View | ||
style={{ | ||
transform: [{translateX: pan.x}], | ||
}} | ||
{...panResponder.panHandlers} | ||
> | ||
{children} | ||
</Animated.View> | ||
) | ||
} | ||
|
||
const usePanAnimation = ({onRelease}: {onRelease: () => void}) => { | ||
const pan = React.useRef(new Animated.ValueXY()).current | ||
const screenWidth = Dimensions.get('window').width | ||
const screenLimitInPercentAfterWhichShouldRelease = 0.3 | ||
|
||
const panResponder = React.useRef( | ||
PanResponder.create({ | ||
onMoveShouldSetPanResponder: () => true, | ||
onPanResponderMove: (e, gestureState) => { | ||
if (gestureState.dx > 0) { | ||
Animated.event([null, {dx: pan.x, dy: pan.y}], {useNativeDriver: false})(e, gestureState) | ||
} | ||
}, | ||
onPanResponderRelease: (e, gestureState) => { | ||
if (gestureState.dx > screenWidth * screenLimitInPercentAfterWhichShouldRelease) { | ||
Animated.spring(pan, { | ||
toValue: {x: screenWidth, y: 0}, | ||
useNativeDriver: false, | ||
}).start(() => onRelease()) | ||
} else { | ||
Animated.spring(pan, { | ||
toValue: {x: 0, y: 0}, | ||
useNativeDriver: false, | ||
}).start() | ||
} | ||
}, | ||
}), | ||
).current | ||
|
||
return {pan, panResponder} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.