Skip to content

Commit

Permalink
feat: NFT celebration (#4886)
Browse files Browse the repository at this point in the history
### Description

If a user has an NFT defined in `nft_celebration_config` remote config,
on app open they will see a bottom sheet celebrating that NFT. Once
bottom sheet is dismissed, users will see a celebration notification
with a confetti animation with haptic feedback.

Celebration is displayed only once per installation/NFT.

Under feature flag `show_nft_celebration` (enabled for testers).

Analytics:
* celebration displayed (sent on celebration bottom sheet dismiss)
* animation displayed (send on animation end or dismiss by the user)


https://github.com/valora-inc/wallet/assets/2737872/110dd73d-8a25-4ae5-814b-2f9a7da9c073

### Test plan

* Tested manually on iOS and Android emulators
* Add unit test for celebration bottom sheet rendering

### Related issues

- Fixes RET-1000

### Backwards compatibility

Y
  • Loading branch information
bakoushin committed Feb 14, 2024
1 parent 14dfc51 commit ee6e8fc
Show file tree
Hide file tree
Showing 27 changed files with 6,146 additions and 15 deletions.
11 changes: 11 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2101,5 +2101,16 @@
"getStartedHome": {
"title": "Buy crypto or transfer tokens",
"body": "Buy or transfer compatible tokens to send crypto and explore web3!"
},
"nftCelebration": {
"bottomSheet": {
"title": "Multi-chain early adopter 🎁",
"description": "As a thank you for being a multi-chain beta tester, we’ve added a free gift to your {{appName}} wallet! The early adopter NFT can be found in your collectibles.",
"cta": "Thanks!"
},
"notification": {
"title": "Congratulations 🎉",
"description": "{{rewardName}} has successfully been added to your {{appName}} wallet."
}
}
}
2 changes: 2 additions & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export enum HomeEvents {
notification_center_spotlight_dismiss = 'notification_center_spotlight_dismiss',
hide_balances = 'hide_balances',
show_balances = 'show_balances',
nft_celebration_displayed = 'nft_celebration_displayed',
nft_celebration_animation_displayed = 'nft_celebration_animation_displayed',
}

export enum SettingsEvents {
Expand Down
8 changes: 8 additions & 0 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ interface HomeEventsProperties {
[HomeEvents.notification_center_opened]: { notificationsCount: number }
[HomeEvents.hide_balances]: undefined
[HomeEvents.show_balances]: undefined
[HomeEvents.nft_celebration_displayed]: {
networkId: NetworkId
contractAddress: string
}
[HomeEvents.nft_celebration_animation_displayed]: {
userInterrupted: boolean
durationInSeconds: number
}
}

interface SettingsEventsProperties {
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[HomeEvents.notification_bell_pressed]: ``,
[HomeEvents.hide_balances]: `When the eye icon is clicked to hide balances on the home screen`,
[HomeEvents.show_balances]: `When the crossed out eye icon is clicked to show balances on the home screen`,
[HomeEvents.nft_celebration_displayed]: `When user has seen an NFT celebration bottom sheet`,
[HomeEvents.nft_celebration_animation_displayed]: `When user has seen an NFT celebration confetti animation`,
[SettingsEvents.settings_profile_edit]: ``,
[SettingsEvents.profile_generate_name]: ``,
[SettingsEvents.profile_save]: ``,
Expand Down
11 changes: 10 additions & 1 deletion src/components/BottomSheetBase.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import GorhomBottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'
import GorhomBottomSheet, { BottomSheetBackdrop, BottomSheetProps } from '@gorhom/bottom-sheet'
import { BottomSheetDefaultBackdropProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types'
import React, { useCallback } from 'react'
import { Keyboard, StyleSheet } from 'react-native'
Expand All @@ -8,17 +8,23 @@ import Colors from 'src/styles/colors'
interface BottomSheetBaseProps {
forwardedRef: React.RefObject<GorhomBottomSheet>
children?: React.ReactNode | React.ReactNode[]
onChange?: BottomSheetProps['onChange']
onClose?: () => void
onOpen?: () => void
snapPoints?: (string | number)[]
handleComponent?: BottomSheetProps['handleComponent']
backgroundStyle?: BottomSheetProps['backgroundStyle']
}

const BottomSheetBase = ({
forwardedRef,
children,
onChange,
onClose,
onOpen,
snapPoints,
handleComponent,
backgroundStyle,
}: BottomSheetBaseProps) => {
const { height } = useSafeAreaFrame()
const insets = useSafeAreaInsets()
Expand Down Expand Up @@ -55,9 +61,12 @@ const BottomSheetBase = ({
snapPoints={snapPoints}
enablePanDownToClose
backdropComponent={renderBackdrop}
handleComponent={handleComponent}
handleIndicatorStyle={styles.handle}
backgroundStyle={backgroundStyle}
onAnimate={handleAnimate}
onClose={handleClose}
onChange={onChange}
maxDynamicContentSize={height - insets.top}
>
{children}
Expand Down
46 changes: 46 additions & 0 deletions src/home/WalletHome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,11 @@ describe('WalletHome', () => {

expect(store.getActions()).toEqual([notificationSpotlightSeen()])
})
})
describe('cash in bottom sheet', () => {
beforeEach(() => {
jest.mocked(getFeatureGate).mockReturnValue(true)
})

it('shows the cash in bottom sheet after the spotlight for an eligible user', async () => {
jest.mocked(fetchProviders).mockResolvedValueOnce(mockProviders)
Expand Down Expand Up @@ -444,5 +449,46 @@ describe('WalletHome', () => {
})
await waitFor(() => expect(getByTestId('cashInBtn')).toBeTruthy())
})

it('shows the cash in bottom sheet after the nft celebration for an eligible user', async () => {
jest.mocked(fetchProviders).mockResolvedValueOnce(mockProviders)

const { queryByTestId, rerender, getByTestId } = renderScreen({
...zeroBalances,
app: {
showNotificationSpotlight: false,
},
home: {
nftCelebration: {
displayed: false,
},
},
})

expect(queryByTestId('cashInBtn')).toBeFalsy()

rerender(
<Provider
store={createMockStore({
...zeroBalances,
app: {
showNotificationSpotlight: false,
},
home: {
nftCelebration: {
displayed: true,
},
},
})}
>
<WalletHome />
</Provider>
)

await act(() => {
jest.runOnlyPendingTimers()
})
await waitFor(() => expect(getByTestId('cashInBtn')).toBeTruthy())
})
})
})
14 changes: 13 additions & 1 deletion src/home/WalletHome.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useIsFocused } from '@react-navigation/native'
import _ from 'lodash'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
Expand All @@ -23,13 +24,15 @@ import {
STABLE_TRANSACTION_MIN_AMOUNT,
} from 'src/config'
import useOpenDapp from 'src/dappsExplorer/useOpenDapp'
import { refreshAllBalances, visitHome } from 'src/home/actions'
import ActionsCarousel from 'src/home/ActionsCarousel'
import CashInBottomSheet from 'src/home/CashInBottomSheet'
import DappsCarousel from 'src/home/DappsCarousel'
import NotificationBell from 'src/home/NotificationBell'
import NotificationBellSpotlight from 'src/home/NotificationBellSpotlight'
import NotificationBox from 'src/home/NotificationBox'
import { refreshAllBalances, visitHome } from 'src/home/actions'
import NftCelebration from 'src/home/celebration/NftCelebration'
import { showNftCelebrationSelector } from 'src/home/selectors'
import { importContacts } from 'src/identity/actions'
import DrawerTopBar from 'src/navigator/DrawerTopBar'
import { phoneRecipientCacheSelector } from 'src/recipients/reducer'
Expand Down Expand Up @@ -70,6 +73,10 @@ function WalletHome() {
const showNotificationCenter = getFeatureGate(StatsigFeatureGates.SHOW_NOTIFICATION_CENTER)
const showNotificationSpotlight = showNotificationCenter && canShowNotificationSpotlight

const isFocused = useIsFocused()
const canShowNftCelebration = useSelector(showNftCelebrationSelector)
const showNftCelebration = canShowNftCelebration && isFocused && !showNotificationSpotlight

useEffect(() => {
dispatch(visitHome())
}, [])
Expand Down Expand Up @@ -125,6 +132,10 @@ function WalletHome() {
return false
}

if (showNftCelebration) {
return false
}

// If user is in a sanctioned country do not show the cash in bottom sheet
if (userInSanctionedCountry) {
return false
Expand Down Expand Up @@ -230,6 +241,7 @@ function WalletHome() {
/>
<NotificationBellSpotlight isVisible={showNotificationSpotlight} />
{shouldShowCashInBottomSheet() && <CashInBottomSheet />}
{showNftCelebration && <NftCelebration />}
</SafeAreaView>
)
}
Expand Down
31 changes: 31 additions & 0 deletions src/home/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CleverTapInboxMessage } from 'src/home/cleverTapInbox'
import { IdToNotification } from 'src/home/reducers'
import { NetworkId } from 'src/transactions/types'

export enum Actions {
SET_LOADING = 'HOME/SET_LOADING',
Expand All @@ -10,6 +11,8 @@ export enum Actions {
STOP_BALANCE_AUTOREFRESH = 'HOME/STOP_BALANCE_AUTOREFRESH',
VISIT_HOME = 'HOME/VISIT_HOME',
CLEVERTAP_INBOX_MESSAGES_RECEIVED = 'HOME/CLEVERTAP_INBOX_MESSAGES_RECEIVED',
CELEBRATED_NFT_FOUND = 'HOME/CELEBRATED_NFT_FOUND',
NFT_CELEBRATION_DISPLAYED = 'HOME/NFT_CELEBRATION_DISPLAYED',
}

export interface VisitHomeAction {
Expand Down Expand Up @@ -40,12 +43,24 @@ interface CleverTapInboxMessagesReceivedAction {
messages: CleverTapInboxMessage[]
}

interface CelebratedNftFoundAction {
type: Actions.CELEBRATED_NFT_FOUND
networkId: NetworkId
contractAddress: string
}

interface NftCelebrationDisplayedAction {
type: Actions.NFT_CELEBRATION_DISPLAYED
}

export type ActionTypes =
| SetLoadingAction
| UpdateNotificationsAction
| DismissNotificationAction
| CleverTapInboxMessagesReceivedAction
| VisitHomeAction
| CelebratedNftFoundAction
| NftCelebrationDisplayedAction

export const visitHome = (): VisitHomeAction => ({
type: Actions.VISIT_HOME,
Expand Down Expand Up @@ -86,3 +101,19 @@ export const cleverTapInboxMessagesReceived = (
type: Actions.CLEVERTAP_INBOX_MESSAGES_RECEIVED,
messages,
})

export const celebratedNftFound = ({
networkId,
contractAddress,
}: {
networkId: NetworkId
contractAddress: string
}): CelebratedNftFoundAction => ({
type: Actions.CELEBRATED_NFT_FOUND,
networkId,
contractAddress,
})

export const nftCelebrationDisplayed = (): NftCelebrationDisplayedAction => ({
type: Actions.NFT_CELEBRATION_DISPLAYED,
})

0 comments on commit ee6e8fc

Please sign in to comment.