Skip to content

Commit

Permalink
[wallet] Refactor leftover thunk to sagas (#1388)
Browse files Browse the repository at this point in the history
  • Loading branch information
martinvol authored and tkporter committed Oct 24, 2019
1 parent de7a8b9 commit b20717d
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31117,7 +31117,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-----

The following software may be included in this product: react-redux, redux, redux-thunk. A copy of the source code may be downloaded from https://github.com/reduxjs/react-redux.git (react-redux), https://github.com/reduxjs/redux.git (redux), https://github.com/reduxjs/redux-thunk.git (redux-thunk). This software contains the following license and notice below:
The following software may be included in this product: react-redux, redux. A copy of the source code may be downloaded from https://github.com/reduxjs/react-redux.git (react-redux), https://github.com/reduxjs/redux.git (redux). This software contains the following license and notice below:

The MIT License (MIT)

Expand Down
1 change: 0 additions & 1 deletion packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@
"redux": "^4.0.4",
"redux-persist": "^5.9.1",
"redux-saga": "^1.0.1",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
"sleep-promise": "^8.0.1",
"svgs": "^4.1.0",
Expand Down
13 changes: 12 additions & 1 deletion packages/mobile/src/firebase/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { PaymentRequestStatus } from 'src/account'
import { PaymentRequest, PaymentRequestStatus } from 'src/account'

export enum Actions {
AUTHORIZED = 'FIREBASE/AUTHORIZED',
PAYMENT_REQUEST_UPDATE_STATUS = 'FIREBASE/PAYMENT_REQUEST_UPDATE_STATUS',
PAYMENT_REQUEST_WRITE = 'FIREBASE/PAYMENT_REQUEST_WRITE',
}

export const firebaseAuthorized = () => ({
Expand All @@ -15,6 +16,11 @@ export interface UpdatePaymentRequestStatusAction {
status: PaymentRequestStatus
}

export interface WritePaymentRequest {
type: Actions.PAYMENT_REQUEST_WRITE
paymentInfo: PaymentRequest
}

export const updatePaymentRequestStatus = (
id: string,
status: PaymentRequestStatus
Expand All @@ -23,3 +29,8 @@ export const updatePaymentRequestStatus = (
id,
status,
})

export const writePaymentRequest = (paymentInfo: PaymentRequest): WritePaymentRequest => ({
type: Actions.PAYMENT_REQUEST_WRITE,
paymentInfo,
})
81 changes: 81 additions & 0 deletions packages/mobile/src/firebase/firebase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expectSaga } from 'redux-saga-test-plan'
import { throwError } from 'redux-saga-test-plan/providers'
import { call, select } from 'redux-saga/effects'
import { currentLanguageSelector } from 'src/app/reducers'
import { initializeCloudMessaging, registerTokenToDb, setUserLanguage } from 'src/firebase/firebase'
import { mockAccount2 } from 'test/values'

const hasPermissionMock = jest.fn(() => null)
const requestPermissionMock = jest.fn(() => null)
const getTokenMock = jest.fn(() => null)
const onTokenRefreshMock = jest.fn(() => null)
const onNotificationMock = jest.fn((fn) => null)
const onNotificationOpenedMock = jest.fn((fn) => null)
const getInitialNotificationMock = jest.fn(() => null)

const address = mockAccount2
const mockFcmToken = 'token'

const app: any = {
messaging: () => ({
hasPermission: hasPermissionMock,
requestPermission: requestPermissionMock,
getToken: getTokenMock,
onTokenRefresh: onTokenRefreshMock,
}),
notifications: () => ({
onNotification: onNotificationMock,
onNotificationOpened: onNotificationOpenedMock,
getInitialNotification: getInitialNotificationMock,
}),
}

describe(initializeCloudMessaging, () => {
afterEach(() => {
jest.clearAllMocks()
})

it("Firebase doesn't have permission", async () => {
const errorToRaise = new Error('No permission')
let catchedError

await expectSaga(initializeCloudMessaging, app, address)
.provide([
[call([app.messaging(), 'hasPermission']), false],
[call([app.messaging(), 'requestPermission']), throwError(errorToRaise)],
{
spawn(effect, next) {
// mock all spawns
return
},
},
])
.run()
.catch((error: Error) => {
catchedError = error
})

expect(errorToRaise).toEqual(catchedError)
})

it('Firebase has permission', async () => {
const mockLanguage = 'en_US'
await expectSaga(initializeCloudMessaging, app, address)
.provide([
[call([app.messaging(), 'hasPermission']), true],
[call([app.messaging(), 'getToken']), mockFcmToken],
[call(registerTokenToDb, app, address, mockFcmToken), null],
[select(currentLanguageSelector), mockLanguage],
[call(setUserLanguage, address, mockLanguage), null],
{
spawn(effect, next) {
// mock all spawns
return
},
},
])
.call(registerTokenToDb, app, address, mockFcmToken)
.call(setUserLanguage, address, mockLanguage)
.run()
})
})
110 changes: 77 additions & 33 deletions packages/mobile/src/firebase/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,44 @@ import firebase, { Firebase } from 'react-native-firebase'
import { RemoteMessage } from 'react-native-firebase/messaging'
import { Notification, NotificationOpen } from 'react-native-firebase/notifications'
import { Sentry } from 'react-native-sentry'
import { eventChannel, EventChannel } from 'redux-saga'
import { call, put, select, spawn, take } from 'redux-saga/effects'
import { NotificationReceiveState, PaymentRequest } from 'src/account'
import { showError } from 'src/alert/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { currentLanguageSelector } from 'src/app/reducers'
import { handleNotification } from 'src/firebase/notifications'
import { getReduxStore } from 'src/redux/store'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import Logger from 'src/utils/Logger'

const TAG = 'Firebase'
const TAG = 'firebase/firebase'

// only exported for testing
export function* watchFirebaseNotificationChannel(
channel: EventChannel<{ notification: Notification; stateType: NotificationReceiveState }>
) {
try {
Logger.info(`${TAG}/watchFirebaseNotificationChannel`, 'Started channel watching')
while (true) {
const data = yield take(channel)
if (!data) {
Logger.info(`${TAG}/watchFirebaseNotificationChannel`, 'Data in channel was empty')
continue
}
Logger.info(`${TAG}/watchFirebaseNotificationChannel`, 'Notification received in the channel')
yield call(handleNotification, data.notification, data.stateType)
}
} catch (error) {
Logger.error(
`${TAG}/watchFirebaseNotificationChannel`,
'Error proccesing notification channel event',
error
)
} finally {
Logger.info(`${TAG}/watchFirebaseNotificationChannel`, 'Notification channel terminated')
}
}

export const initializeAuth = async (app: Firebase, address: string) => {
Logger.info(TAG, 'Initializing Firebase auth')
Expand All @@ -30,57 +62,67 @@ export const initializeAuth = async (app: Firebase, address: string) => {
Logger.info(TAG, 'Firebase Auth initialized successfully')
}

export const initializeCloudMessaging = async (app: Firebase, address: string) => {
// TODO(cmcewen): remove once we move off thunk
const store = getReduxStore()
const language = store.getState().app.language
const dispatch = store.dispatch

export function* initializeCloudMessaging(app: Firebase, address: string) {
Logger.info(TAG, 'Initializing Firebase Cloud Messaging')
const enabled = await app.messaging().hasPermission()

// this call needs to include context: https://github.com/redux-saga/redux-saga/issues/27
const enabled = yield call([app.messaging(), 'hasPermission'])
if (!enabled) {
try {
await app.messaging().requestPermission()
yield call([app.messaging(), 'requestPermission'])
} catch (error) {
Logger.error(TAG, 'User has rejected messaging permissions', error)
throw error
}
}

const fcmToken = await app.messaging().getToken()
const fcmToken = yield call([app.messaging(), 'getToken'])
if (fcmToken) {
await registerTokenToDb(app, address, fcmToken)
yield call(registerTokenToDb, app, address, fcmToken)
// First time setting the fcmToken also set the language selection
await setUserLanguage(address, language)
const language = yield select(currentLanguageSelector)
yield call(setUserLanguage, address, language)
}

// Monitor for future token refreshes
app.messaging().onTokenRefresh(async (token) => {
Logger.info(TAG, 'Cloud Messaging token refreshed')
await registerTokenToDb(app, address, token)
})

// Listen for notification messages while the app is open
app.notifications().onNotification((notification: Notification) => {
Logger.info(TAG, 'Notification received while open')
dispatch(handleNotification(notification, NotificationReceiveState.APP_ALREADY_OPEN))
})
const channelOnNotification: EventChannel<{
notification: Notification
stateType: NotificationReceiveState
}> = eventChannel((emitter) => {
const unsuscribe = () => {
Logger.info(TAG, 'Notification channel closed, reseting callbacks. This is likely an error.')
app.notifications().onNotification(() => null)
app.notifications().onNotificationOpened(() => null)
}

app.notifications().onNotificationOpened((notification: NotificationOpen) => {
Logger.info(TAG, 'App opened via a notification')
dispatch(
handleNotification(notification.notification, NotificationReceiveState.APP_FOREGROUNDED)
)
app.notifications().onNotification((notification: Notification) => {
Logger.info(TAG, 'Notification received while open')
emitter({ notification, stateType: NotificationReceiveState.APP_ALREADY_OPEN })
})

app.notifications().onNotificationOpened((notification: NotificationOpen) => {
Logger.info(TAG, 'App opened via a notification')
emitter({
notification: notification.notification,
stateType: NotificationReceiveState.APP_FOREGROUNDED,
})
})
return unsuscribe
})
yield spawn(watchFirebaseNotificationChannel, channelOnNotification)

const initialNotification = await app.notifications().getInitialNotification()
const initialNotification = yield call([app.notifications(), 'getInitialNotification'])
if (initialNotification) {
Logger.info(TAG, 'App opened fresh via a notification')
dispatch(
handleNotification(
initialNotification.notification,
NotificationReceiveState.APP_OPENED_FRESH
)
yield call(
handleNotification,
initialNotification.notification,
NotificationReceiveState.APP_OPENED_FRESH
)
}
}
Expand All @@ -94,7 +136,7 @@ export async function onBackgroundNotification(remoteMessage: RemoteMessage) {
return Promise.resolve() // need to return a resolved promise so native code releases the JS context
}

const registerTokenToDb = async (app: Firebase, address: string, fcmToken: string) => {
export const registerTokenToDb = async (app: Firebase, address: string, fcmToken: string) => {
try {
Logger.info(TAG, 'Registering Firebase client FCM token')
const regRef = app.database().ref('registrations')
Expand All @@ -107,14 +149,16 @@ const registerTokenToDb = async (app: Firebase, address: string, fcmToken: strin
}
}

export const writePaymentRequest = (paymentInfo: PaymentRequest) => async () => {
export function* writePaymentRequest(paymentInfo: PaymentRequest) {
try {
Logger.info(TAG, `Writing pending request to database`)
const pendingRequestRef = firebase.database().ref(`pendingRequests`)
return pendingRequestRef.push(paymentInfo)
yield call(() => pendingRequestRef.push(paymentInfo))

navigate(Screens.WalletHome)
} catch (error) {
Logger.error(TAG, 'Failed to write payment request to Firebase DB', error)
throw error
yield put(showError(ErrorMessages.PAYMENT_REQUEST_FAILED))
}
}

Expand Down
30 changes: 16 additions & 14 deletions packages/mobile/src/firebase/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BigNumber from 'bignumber.js'
import { Notification } from 'react-native-firebase/notifications'
import { call, put, select } from 'redux-saga/effects'
import {
NotificationReceiveState,
NotificationTypes,
Expand All @@ -9,9 +10,10 @@ import {
import { showMessage } from 'src/alert/actions'
import { resolveCurrency } from 'src/geth/consts'
import { refreshAllBalances } from 'src/home/actions'
import { addressToE164NumberSelector } from 'src/identity/reducer'
import { getRecipientFromPaymentRequest } from 'src/paymentRequest/utils'
import { getRecipientFromAddress } from 'src/recipients/recipient'
import { DispatchType, GetStateType } from 'src/redux/reducers'
import { recipientCacheSelector } from 'src/recipients/reducer'
import {
navigateToPaymentTransferReview,
navigateToRequestedPaymentReview,
Expand All @@ -22,10 +24,10 @@ import Logger from 'src/utils/Logger'

const TAG = 'FirebaseNotifications'

const handlePaymentRequested = (
function* handlePaymentRequested(
paymentRequest: PaymentRequest,
notificationState: NotificationReceiveState
) => async (dispatch: DispatchType, getState: GetStateType) => {
) {
if (notificationState === NotificationReceiveState.APP_ALREADY_OPEN) {
return
}
Expand All @@ -35,7 +37,7 @@ const handlePaymentRequested = (
return
}

const { recipientCache } = getState().recipients
const recipientCache = yield select(recipientCacheSelector)
const targetRecipient = getRecipientFromPaymentRequest(paymentRequest, recipientCache)

navigateToRequestedPaymentReview({
Expand All @@ -47,15 +49,15 @@ const handlePaymentRequested = (
})
}

const handlePaymentReceived = (
function* handlePaymentReceived(
transferNotification: TransferNotificationData,
notificationState: NotificationReceiveState
) => async (dispatch: DispatchType, getState: GetStateType) => {
dispatch(refreshAllBalances())
) {
yield put(refreshAllBalances())

if (notificationState !== NotificationReceiveState.APP_ALREADY_OPEN) {
const { recipientCache } = getState().recipients
const { addressToE164Number } = getState().identity
const recipientCache = yield select(recipientCacheSelector)
const addressToE164Number = yield select(addressToE164NumberSelector)
const address = transferNotification.sender.toLowerCase()

navigateToPaymentTransferReview(
Expand All @@ -73,20 +75,20 @@ const handlePaymentReceived = (
}
}

export const handleNotification = (
export function* handleNotification(
notification: Notification,
notificationState: NotificationReceiveState
) => async (dispatch: DispatchType, getState: GetStateType) => {
) {
if (notificationState === NotificationReceiveState.APP_ALREADY_OPEN) {
dispatch(showMessage(notification.title))
yield put(showMessage(notification.title))
}
switch (notification.data.type) {
case NotificationTypes.PAYMENT_REQUESTED:
dispatch(handlePaymentRequested(notification.data, notificationState))
yield call(handlePaymentRequested, notification.data, notificationState)
break

case NotificationTypes.PAYMENT_RECEIVED:
dispatch(handlePaymentReceived(notification.data, notificationState))
yield call(handlePaymentReceived, notification.data, notificationState)
break

default:
Expand Down
Loading

0 comments on commit b20717d

Please sign in to comment.