diff --git a/packages/mobile/src/alert/AlertBanner.test.tsx b/packages/mobile/src/alert/AlertBanner.test.tsx
index 17b3cc790d9..6d7f79ad9d4 100644
--- a/packages/mobile/src/alert/AlertBanner.test.tsx
+++ b/packages/mobile/src/alert/AlertBanner.test.tsx
@@ -1,26 +1,26 @@
import * as React from 'react'
-import 'react-native'
-import { render } from 'react-native-testing-library'
-import { AlertBanner } from 'src/alert/AlertBanner'
+import { fireEvent, render } from 'react-native-testing-library'
+import { Provider } from 'react-redux'
+import AlertBanner from 'src/alert/AlertBanner'
import { ErrorDisplayType } from 'src/alert/reducer'
+import { createMockStore } from 'test/utils'
describe('AlertBanner', () => {
- const baseProps = {
- hideAlert: jest.fn(),
- }
-
describe('when message passed in', () => {
it('renders message', () => {
const { toJSON } = render(
-
+
+
+
)
expect(toJSON()).toMatchSnapshot()
})
@@ -29,16 +29,19 @@ describe('AlertBanner', () => {
describe('when message and title passed in', () => {
it('renders title with message', () => {
const { toJSON } = render(
-
+
+
+
)
expect(toJSON()).toMatchSnapshot()
})
@@ -47,17 +50,43 @@ describe('AlertBanner', () => {
describe('when error message passed in', () => {
it('renders error message', () => {
const { toJSON } = render(
-
+
+
+
)
expect(toJSON()).toMatchSnapshot()
})
})
+
+ describe('when an action is provided', () => {
+ it('it dispatches the action when pressed', () => {
+ const store = createMockStore({
+ alert: {
+ type: 'message',
+ displayMethod: ErrorDisplayType.BANNER,
+ message: 'My message',
+ dismissAfter: 0,
+ action: { type: 'MY_ACTION' },
+ },
+ })
+ const { toJSON, getByTestId } = render(
+
+
+
+ )
+ expect(toJSON()).toMatchSnapshot()
+
+ fireEvent.press(getByTestId('SmartTopAlertTouchable'))
+ expect(store.getActions()).toEqual([{ type: 'MY_ACTION' }])
+ })
+ })
})
diff --git a/packages/mobile/src/alert/AlertBanner.tsx b/packages/mobile/src/alert/AlertBanner.tsx
index 87efc0deecb..76bf5e0a1e6 100644
--- a/packages/mobile/src/alert/AlertBanner.tsx
+++ b/packages/mobile/src/alert/AlertBanner.tsx
@@ -1,50 +1,30 @@
import SmartTopAlert, { AlertTypes } from '@celo/react-components/components/SmartTopAlert'
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useDispatch } from 'react-redux'
import { hideAlert } from 'src/alert/actions'
-import { ErrorDisplayType, State as AlertState } from 'src/alert/reducer'
-import { RootState } from 'src/redux/reducers'
+import { ErrorDisplayType } from 'src/alert/reducer'
+import useSelector from 'src/redux/useSelector'
-interface StateProps {
- alert: AlertState | null
-}
-
-interface DispatchProps {
- hideAlert: typeof hideAlert
-}
+export default function AlertBanner() {
+ const alert = useSelector((state) => state.alert)
+ const dispatch = useDispatch()
-type Props = StateProps & DispatchProps
-
-const mapStateToProps = (state: RootState): StateProps => {
- return {
- alert: state.alert,
+ const onPress = () => {
+ const action = alert?.action ?? hideAlert()
+ dispatch(action)
}
-}
-const mapDispatchToProps = {
- hideAlert,
+ return (
+
+ )
}
-
-export class AlertBanner extends React.Component {
- render() {
- const { alert, hideAlert: hideAlertAction } = this.props
-
- return (
-
- )
- }
-}
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(AlertBanner)
diff --git a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap
index bdf16d2c638..07ab5fd4c9b 100644
--- a/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap
+++ b/packages/mobile/src/alert/__snapshots__/AlertBanner.test.tsx.snap
@@ -1,5 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`AlertBanner when an action is provided it dispatches the action when pressed 1`] = `
+
+
+
+ My message
+
+
+
+`;
+
exports[`AlertBanner when error message passed in renders error message 1`] = `
@@ -135,6 +198,7 @@ exports[`AlertBanner when message and title passed in renders title with message
],
}
}
+ testID="SmartTopAlertTouchable"
>
@@ -217,6 +281,7 @@ exports[`AlertBanner when message passed in renders message 1`] = `
],
}
}
+ testID="SmartTopAlertTouchable"
>
diff --git a/packages/mobile/src/alert/actions.ts b/packages/mobile/src/alert/actions.ts
index 24aa6a21b36..97d5a8e8644 100644
--- a/packages/mobile/src/alert/actions.ts
+++ b/packages/mobile/src/alert/actions.ts
@@ -2,6 +2,7 @@ import { TOptions } from 'i18next'
import { ErrorDisplayType } from 'src/alert/reducer'
import { AppEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
+import { OpenUrlAction } from 'src/app/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { ALERT_BANNER_DURATION } from 'src/config'
import i18n, { Namespaces } from 'src/i18n'
@@ -16,6 +17,11 @@ enum AlertTypes {
ERROR = 'error',
}
+// Possible actions to dispatch when tapping the alert (or its button)
+// Could be any redux action, but limiting for now
+// As we don't yet have a type encompassing all redux actions
+type AlertAction = OpenUrlAction
+
interface ShowAlertAction {
type: Actions.SHOW
alertType: AlertTypes
@@ -23,6 +29,7 @@ interface ShowAlertAction {
message: string
dismissAfter?: number | null
buttonMessage?: string | null
+ action?: AlertAction | null
title?: string | null
underlyingError?: ErrorMessages | null
}
@@ -31,9 +38,10 @@ export const showMessage = (
message: string,
dismissAfter?: number | null,
buttonMessage?: string | null,
+ action?: AlertAction | null,
title?: string | null
): ShowAlertAction => {
- return showAlert(AlertTypes.MESSAGE, message, dismissAfter, buttonMessage, title)
+ return showAlert(AlertTypes.MESSAGE, message, dismissAfter, buttonMessage, action, title)
}
export const showError = (
@@ -48,6 +56,7 @@ export const showError = (
dismissAfter,
null,
null,
+ null,
error
)
}
@@ -79,6 +88,7 @@ const showAlert = (
message: string,
dismissAfter: number | null = ALERT_BANNER_DURATION,
buttonMessage?: string | null,
+ action?: AlertAction | null,
title?: string | null,
underlyingError?: ErrorMessages | null
): ShowAlertAction => {
@@ -89,6 +99,7 @@ const showAlert = (
message,
dismissAfter,
buttonMessage,
+ action,
title,
underlyingError,
}
diff --git a/packages/mobile/src/alert/reducer.ts b/packages/mobile/src/alert/reducer.ts
index cc13acaa47d..15f28ef20ab 100644
--- a/packages/mobile/src/alert/reducer.ts
+++ b/packages/mobile/src/alert/reducer.ts
@@ -7,19 +7,20 @@ export enum ErrorDisplayType {
'INLINE',
}
-export interface State {
+export type State = {
type: 'message' | 'error'
displayMethod: ErrorDisplayType
message: string
dismissAfter?: number | null
buttonMessage?: string | null
+ action?: object | null
title?: string | null
underlyingError?: ErrorMessages | null
-}
+} | null
const initialState = null
-export const reducer = (state: State | null = initialState, action: ActionTypes): State | null => {
+export const reducer = (state: State = initialState, action: ActionTypes): State => {
switch (action.type) {
case Actions.SHOW:
return {
@@ -28,12 +29,17 @@ export const reducer = (state: State | null = initialState, action: ActionTypes)
message: action.message,
dismissAfter: action.dismissAfter,
buttonMessage: action.buttonMessage,
+ action: action.action,
title: action.title,
underlyingError: action.underlyingError,
}
case Actions.HIDE:
return null
default:
+ if (state?.action === action) {
+ // Hide alert when the alert action is dispatched
+ return null
+ }
return state
}
}
diff --git a/packages/mobile/src/app/actions.ts b/packages/mobile/src/app/actions.ts
index a36baf99248..3fcdbac72cb 100644
--- a/packages/mobile/src/app/actions.ts
+++ b/packages/mobile/src/app/actions.ts
@@ -25,6 +25,7 @@ export enum Actions {
LOCK = 'APP/LOCK',
UNLOCK = 'APP/UNLOCK',
SET_SESSION_ID = 'SET_SESSION_ID',
+ OPEN_URL = 'APP/OPEN_URL',
}
export interface SetAppState {
@@ -87,6 +88,11 @@ export interface SetSessionId {
sessionId: string
}
+export interface OpenUrlAction {
+ type: Actions.OPEN_URL
+ url: string
+}
+
export type ActionTypes =
| SetAppState
| SetLoggedIn
@@ -101,6 +107,7 @@ export type ActionTypes =
| Lock
| Unlock
| SetSessionId
+ | OpenUrlAction
export const setAppState = (state: string) => ({
type: Actions.SET_APP_STATE,
@@ -169,3 +176,8 @@ export const setSessionId = (sessionId: string) => ({
type: Actions.SET_SESSION_ID,
sessionId,
})
+
+export const openUrl = (url: string): OpenUrlAction => ({
+ type: Actions.OPEN_URL,
+ url,
+})
diff --git a/packages/mobile/src/app/saga.ts b/packages/mobile/src/app/saga.ts
index 4ac1735bedc..acdcf68be3b 100644
--- a/packages/mobile/src/app/saga.ts
+++ b/packages/mobile/src/app/saga.ts
@@ -1,6 +1,15 @@
import { AppState, Linking } from 'react-native'
import { eventChannel } from 'redux-saga'
-import { call, cancelled, put, select, spawn, take, takeLatest } from 'redux-saga/effects'
+import {
+ call,
+ cancelled,
+ put,
+ select,
+ spawn,
+ take,
+ takeEvery,
+ takeLatest,
+} from 'redux-saga/effects'
import { AppEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import {
@@ -8,6 +17,7 @@ import {
appLock,
OpenDeepLink,
openDeepLink,
+ OpenUrlAction,
SetAppState,
setAppState,
setLanguage,
@@ -21,6 +31,7 @@ import { CodeInputType } from 'src/identity/verification'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { handlePaymentDeeplink } from 'src/send/utils'
+import { navigateToURI } from 'src/utils/linking'
import Logger from 'src/utils/Logger'
import { clockInSync } from 'src/utils/time'
import { parse } from 'url'
@@ -87,6 +98,16 @@ export function* watchDeepLinks() {
yield takeLatest(Actions.OPEN_DEEP_LINK, handleDeepLink)
}
+export function* handleOpenUrl(action: OpenUrlAction) {
+ const { url } = action
+ Logger.debug(TAG, 'Handling url', url)
+ yield call(navigateToURI, url)
+}
+
+export function* watchOpenUrl() {
+ yield takeEvery(Actions.OPEN_URL, handleOpenUrl)
+}
+
function createAppStateChannel() {
return eventChannel((emit: any) => {
AppState.addEventListener('change', emit)
@@ -130,6 +151,7 @@ export function* handleSetAppState(action: SetAppState) {
export function* appSaga() {
yield spawn(watchDeepLinks)
+ yield spawn(watchOpenUrl)
yield spawn(watchAppState)
yield takeLatest(Actions.SET_APP_STATE, handleSetAppState)
}
diff --git a/packages/mobile/src/firebase/firebase.ts b/packages/mobile/src/firebase/firebase.ts
index c53cffe3469..b1a55e6ec4c 100644
--- a/packages/mobile/src/firebase/firebase.ts
+++ b/packages/mobile/src/firebase/firebase.ts
@@ -138,14 +138,13 @@ export function* initializeCloudMessaging(app: ReactNativeFirebase.Module, addre
})
yield spawn(watchFirebaseNotificationChannel, channelOnNotification)
- const initialNotification = yield call([app.messaging(), 'getInitialNotification'])
+ // Manual type checking because yield calls can't infer return type yet :'(
+ const initialNotification: Awaited> = yield call([app.messaging(), 'getInitialNotification'])
if (initialNotification) {
- Logger.info(TAG, 'App opened fresh via a notification')
- yield call(
- handleNotification,
- initialNotification.notification,
- NotificationReceiveState.APP_OPENED_FRESH
- )
+ Logger.info(TAG, 'App opened fresh via a notification', JSON.stringify(initialNotification))
+ yield call(handleNotification, initialNotification, NotificationReceiveState.APP_OPENED_FRESH)
}
app.messaging().setBackgroundMessageHandler((remoteMessage) => {
diff --git a/packages/mobile/src/firebase/notifications.test.ts b/packages/mobile/src/firebase/notifications.test.ts
new file mode 100644
index 00000000000..99fbbd6677f
--- /dev/null
+++ b/packages/mobile/src/firebase/notifications.test.ts
@@ -0,0 +1,144 @@
+import BigNumber from 'bignumber.js'
+import { expectSaga } from 'redux-saga-test-plan'
+import { select } from 'redux-saga/effects'
+import { showMessage } from 'src/alert/actions'
+import { openUrl } from 'src/app/actions'
+import { handleNotification } from 'src/firebase/notifications'
+import { addressToE164NumberSelector } from 'src/identity/reducer'
+import { navigate } from 'src/navigator/NavigationService'
+import { Screens } from 'src/navigator/Screens'
+import { NotificationReceiveState, NotificationTypes } from 'src/notifications/types'
+import { recipientCacheSelector } from 'src/recipients/reducer'
+
+describe(handleNotification, () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('with a simple notification', () => {
+ const message = {
+ notification: { title: 'My title', body: 'My Body' },
+ }
+
+ it('shows the in-app message when the app is already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_ALREADY_OPEN)
+ .put(showMessage('My Body', undefined, null, null, 'My title'))
+ .run()
+ })
+
+ it('has no effect if the app is not already in the foreground', async () => {
+ const result = await expectSaga(
+ handleNotification,
+ message,
+ NotificationReceiveState.APP_OPENED_FRESH
+ ).run()
+
+ expect(result.toJSON()).toEqual({})
+ })
+ })
+
+ describe("with a notification with an 'open url' semantic", () => {
+ const message = {
+ notification: { title: 'My title', body: 'My Body' },
+ data: { ou: 'https://celo.org' },
+ }
+
+ it('shows the in-app message when the app is already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_ALREADY_OPEN)
+ .put(showMessage('My Body', undefined, null, openUrl('https://celo.org'), 'My title'))
+ .run()
+ })
+
+ it('directly opens the url if the app is not already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_OPENED_FRESH)
+ .put(openUrl('https://celo.org'))
+ .run()
+ })
+ })
+
+ describe('with a payment received notification', () => {
+ const message = {
+ notification: { title: 'My title', body: 'My Body' },
+ data: {
+ type: NotificationTypes.PAYMENT_RECEIVED,
+ sender: '0xTEST',
+ value: '10',
+ currency: 'dollar',
+ timestamp: 1,
+ },
+ }
+
+ it('shows the in-app message when the app is already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_ALREADY_OPEN)
+ .put(showMessage('My Body', undefined, null, null, 'My title'))
+ .run()
+
+ expect(navigate).not.toHaveBeenCalled()
+ })
+
+ it('navigates to the transaction review screen if the app is not already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_OPENED_FRESH)
+ .provide([
+ [select(addressToE164NumberSelector), {}],
+ [select(recipientCacheSelector), {}],
+ ])
+ .run()
+
+ expect(navigate).toHaveBeenCalledWith(Screens.TransactionReview, {
+ confirmationProps: {
+ address: '0xtest',
+ amount: { currencyCode: 'cUSD', value: new BigNumber('1e-17') },
+ comment: undefined,
+ recipient: undefined,
+ type: 'RECEIVED',
+ },
+ reviewProps: {
+ header: 'walletFlow5:transactionHeaderReceived',
+ timestamp: 1,
+ type: 'RECEIVED',
+ },
+ })
+ })
+ })
+
+ describe('with a payment request notification', () => {
+ const message = {
+ notification: { title: 'My title', body: 'My Body' },
+ data: {
+ type: NotificationTypes.PAYMENT_REQUESTED,
+ uid: 'abc',
+ requesterAddress: '0xTEST',
+ amount: '10',
+ currency: 'dollar',
+ comment: 'Pizza',
+ },
+ }
+
+ it('shows the in-app message when the app is already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_ALREADY_OPEN)
+ .put(showMessage('My Body', undefined, null, null, 'My title'))
+ .run()
+
+ expect(navigate).not.toHaveBeenCalled()
+ })
+
+ it('navigates to the send confirmation screen if the app is not already in the foreground', async () => {
+ await expectSaga(handleNotification, message, NotificationReceiveState.APP_OPENED_FRESH)
+ .provide([
+ [select(addressToE164NumberSelector), {}],
+ [select(recipientCacheSelector), {}],
+ ])
+ .run()
+
+ expect(navigate).toHaveBeenCalledWith(Screens.SendConfirmation, {
+ transactionData: {
+ amount: new BigNumber('10'),
+ firebasePendingRequestUid: 'abc',
+ reason: 'Pizza',
+ recipient: { address: '0xTEST', displayName: '0xTEST', kind: 'Address' },
+ type: 'PAY_REQUEST',
+ },
+ })
+ })
+ })
+})
diff --git a/packages/mobile/src/firebase/notifications.ts b/packages/mobile/src/firebase/notifications.ts
index 6b33db4cc5a..a347261e098 100644
--- a/packages/mobile/src/firebase/notifications.ts
+++ b/packages/mobile/src/firebase/notifications.ts
@@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js'
import { call, put, select } from 'redux-saga/effects'
import { showMessage } from 'src/alert/actions'
import { TokenTransactionType } from 'src/apollo/types'
+import { openUrl } from 'src/app/actions'
import { CURRENCIES, resolveCurrency } from 'src/geth/consts'
import { addressToE164NumberSelector } from 'src/identity/reducer'
import {
@@ -84,12 +85,31 @@ export function* handleNotification(
message: FirebaseMessagingTypes.RemoteMessage,
notificationState: NotificationReceiveState
) {
+ // See if this is a notification with an open url action (`ou` prop in the data)
+ const urlToOpen = message.data?.ou
+
if (notificationState === NotificationReceiveState.APP_ALREADY_OPEN) {
- const title = message.notification?.title
+ const { title, body } = message.notification ?? {}
if (title) {
- yield put(showMessage(title))
+ yield put(
+ showMessage(
+ body || title,
+ undefined,
+ null,
+ urlToOpen ? openUrl(urlToOpen) : null,
+ body ? title : null
+ )
+ )
+ }
+ } else {
+ // Notification was received while app wasn't already open (i.e. tapped to act on it)
+ // So directly handle the action if any
+ if (urlToOpen) {
+ yield put(openUrl(urlToOpen))
+ return
}
}
+
switch (message.data?.type) {
case NotificationTypes.PAYMENT_REQUESTED:
yield call(
diff --git a/packages/mobile/src/home/WalletHome.test.tsx b/packages/mobile/src/home/WalletHome.test.tsx
index 9f1c593594a..ed2998dd32c 100644
--- a/packages/mobile/src/home/WalletHome.test.tsx
+++ b/packages/mobile/src/home/WalletHome.test.tsx
@@ -50,7 +50,13 @@ describe('Testnet banner', () => {
)
expect(tree).toMatchSnapshot()
- expect(showMessageMock).toHaveBeenCalledWith('testnetAlert.1', 5000, null, 'testnetAlert.0')
+ expect(showMessageMock).toHaveBeenCalledWith(
+ 'testnetAlert.1',
+ 5000,
+ null,
+ null,
+ 'testnetAlert.0'
+ )
})
it('Renders when disconnected', async () => {
const store = createMockStoreAppDisconnected()
diff --git a/packages/mobile/src/home/WalletHome.tsx b/packages/mobile/src/home/WalletHome.tsx
index aed6b1e13dc..5d7a635d7e6 100644
--- a/packages/mobile/src/home/WalletHome.tsx
+++ b/packages/mobile/src/home/WalletHome.tsx
@@ -145,6 +145,7 @@ export class WalletHome extends React.Component {
t('testnetAlert.1', { testnet: _.startCase(DEFAULT_TESTNET) }),
ALERT_BANNER_DURATION,
null,
+ null,
t('testnetAlert.0', { testnet: _.startCase(DEFAULT_TESTNET) })
)
}
diff --git a/packages/mobile/src/send/SendAmount.test.tsx b/packages/mobile/src/send/SendAmount.test.tsx
index 2101276092b..a2daa0647e8 100644
--- a/packages/mobile/src/send/SendAmount.test.tsx
+++ b/packages/mobile/src/send/SendAmount.test.tsx
@@ -120,6 +120,7 @@ describe('SendAmount', () => {
fireEvent.press(reviewButton)
expect(store.getActions()).toEqual([
{
+ action: null,
alertType: 'error',
buttonMessage: null,
dismissAfter: 5000,
@@ -148,6 +149,7 @@ describe('SendAmount', () => {
fireEvent.press(sendButton)
expect(store.getActions()).toEqual([
{
+ action: null,
alertType: 'error',
buttonMessage: null,
dismissAfter: 5000,
diff --git a/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap b/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap
index 15c3c526fe7..fd75bada040 100644
--- a/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap
+++ b/packages/mobile/src/shared/__snapshots__/BackupPrompt.test.tsx.snap
@@ -36,6 +36,7 @@ exports[`BackupPrompt renders correctly 1`] = `
],
}
}
+ testID="SmartTopAlertTouchable"
>
diff --git a/packages/react-components/components/SmartTopAlert.tsx b/packages/react-components/components/SmartTopAlert.tsx
index a4b9e42c003..27a55b3d241 100644
--- a/packages/react-components/components/SmartTopAlert.tsx
+++ b/packages/react-components/components/SmartTopAlert.tsx
@@ -38,7 +38,7 @@ function SmartTopAlert(props: Props) {
const alertState = useMemo(() => {
// tslint bug?
// tslint:disable-next-line: no-shadowed-variable
- const { type, title, text, buttonMessage, dismissAfter, onPress, isVisible } = props
+ const { isVisible, type, title, text, buttonMessage, dismissAfter, onPress } = props
if (isVisible) {
return {
type,
@@ -53,6 +53,7 @@ function SmartTopAlert(props: Props) {
}
}, [
props.timestamp,
+ props.isVisible,
props.type,
props.title,
props.text,
@@ -138,7 +139,7 @@ function SmartTopAlert(props: Props) {
return (
-
+
{isError && }
-
- {!!title && {title} }
+
+ {!!title && {title} }
{text}
{buttonMessage && (
diff --git a/packages/react-components/components/__snapshots__/SmartTopAlert.test.tsx.snap b/packages/react-components/components/__snapshots__/SmartTopAlert.test.tsx.snap
index bcc9be62ddf..b555e7f8295 100644
--- a/packages/react-components/components/__snapshots__/SmartTopAlert.test.tsx.snap
+++ b/packages/react-components/components/__snapshots__/SmartTopAlert.test.tsx.snap
@@ -36,6 +36,7 @@ exports[`SmartTopAlert renders correctly 1`] = `
],
}
}
+ testID="SmartTopAlertTouchable"
>