Replace react-native-fcm by react-native-firebase

parent c70bf244
...@@ -2,7 +2,6 @@ import { select } from 'redux-saga/effects'; ...@@ -2,7 +2,6 @@ import { select } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan'; import { expectSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers'; import * as matchers from 'redux-saga-test-plan/matchers';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import FCM from 'react-native-fcm';
import pushNotificationsSaga from '../../app/sagas/pushNotifications'; import pushNotificationsSaga from '../../app/sagas/pushNotifications';
import { apiRequest, tokenSelector } from '../../app/utils/url'; import { apiRequest, tokenSelector } from '../../app/utils/url';
import * as pushActions from '../../app/actions/pushNotifications'; import * as pushActions from '../../app/actions/pushNotifications';
...@@ -12,22 +11,31 @@ jest.mock('../../app/utils/url', () => ({ ...@@ -12,22 +11,31 @@ jest.mock('../../app/utils/url', () => ({
tokenSelector: () => 'token', tokenSelector: () => 'token',
})); }));
jest.mock('react-native-fcm', () => ({ const mockIid = {
getFCMToken: jest.fn(), delete: jest.fn(),
requestPermissions: jest.fn(), };
deleteInstanceId: jest.fn(),
const mockMessaging = {
hasPermission: jest.fn(),
getToken: jest.fn(),
requestPermission: jest.fn(),
};
jest.mock('react-native-firebase', () => ({
iid: () => mockIid,
messaging: () => mockMessaging,
})); }));
describe('pushNotifications saga', () => { describe('pushNotifications saga', () => {
beforeAll(() => { beforeAll(() => {
FCM.getFCMToken.mockReturnValue('token'); mockMessaging.getToken.mockReturnValue('token');
}); });
describe('register', () => { describe('register', () => {
beforeEach(() => { beforeEach(() => {
Platform.OS = 'ios'; Platform.OS = 'ios';
FCM.requestPermissions.mockReset(); mockMessaging.requestPermission.mockReset();
FCM.getFCMToken.mockReset(); mockMessaging.getToken.mockReset();
apiRequest.mockReset(); apiRequest.mockReset();
}); });
...@@ -38,7 +46,7 @@ describe('pushNotifications saga', () => { ...@@ -38,7 +46,7 @@ describe('pushNotifications saga', () => {
.dispatch(pushActions.register()) .dispatch(pushActions.register())
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(FCM.requestPermissions).toBeCalled(); expect(mockMessaging.requestPermission).toBeCalled();
})); }));
it('should not request permissions when platform is Android', () => { it('should not request permissions when platform is Android', () => {
...@@ -50,31 +58,29 @@ describe('pushNotifications saga', () => { ...@@ -50,31 +58,29 @@ describe('pushNotifications saga', () => {
.dispatch(pushActions.register()) .dispatch(pushActions.register())
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(FCM.requestPermissions).not.toBeCalled(); expect(mockMessaging.requestPermission).not.toBeCalled();
}); });
}); });
it('should post a token to the server', () => { it('should post a token to the server', () => expectSaga(pushNotificationsSaga)
return expectSaga(pushNotificationsSaga) .provide([
.provide([ [select(tokenSelector), 'token'],
[select(tokenSelector), 'token'], [matchers.call.like({ fn: apiRequest, args: ['events'] }), { results: 'data' }],
[matchers.call.like({ fn: apiRequest, args: ['events'] }), { results: 'data' }], ])
]) .dispatch(pushActions.register())
.dispatch(pushActions.register()) .silentRun()
.silentRun() .then(() => {
.then(() => { expect(apiRequest).toBeCalledWith('devices',
expect(apiRequest).toBeCalledWith('devices', {
{ body: '{"type":"ios"}',
body: '{"type":"ios"}', headers: {
headers: { Accept: 'application/json',
Accept: 'application/json', Authorization: 'Token token',
Authorization: 'Token token', 'Content-Type': 'application/json',
'Content-Type': 'application/json', },
}, method: 'POST',
method: 'POST', });
}); }));
});
});
}); });
describe('invalidate', () => { describe('invalidate', () => {
...@@ -82,7 +88,7 @@ describe('pushNotifications saga', () => { ...@@ -82,7 +88,7 @@ describe('pushNotifications saga', () => {
.dispatch(pushActions.invalidate()) .dispatch(pushActions.invalidate())
.silentRun() .silentRun()
.then(() => { .then(() => {
expect(FCM.deleteInstanceId).toBeCalled(); expect(mockIid.delete).toBeCalled();
})); }));
}); });
}); });
import { NativeModules } from 'react-native';
NativeModules.RNFirebase = {
apps: [],
};
...@@ -152,12 +152,13 @@ dependencies { ...@@ -152,12 +152,13 @@ dependencies {
implementation project(':react-native-vector-icons') implementation project(':react-native-vector-icons')
implementation project(':react-native-snackbar') implementation project(':react-native-snackbar')
implementation project(':react-native-linear-gradient') implementation project(':react-native-linear-gradient')
implementation project(':react-native-fcm') implementation project(':react-native-firebase')
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.android.support:design:${rootProject.ext.supportLibVersion}" implementation "com.android.support:design:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.google.firebase:firebase-core:11.4.0' implementation "com.google.firebase:firebase-core:16.0.1"
implementation "com.google.firebase:firebase-messaging:17.1.0"
} }
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK
......
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thaliapp"> package="com.thaliapp">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22"/>
<application <application
android:name=".MainApplication" android:name=".MainApplication"
...@@ -12,7 +11,8 @@ ...@@ -12,7 +11,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:launchMode="singleTop">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
...@@ -34,18 +34,21 @@ ...@@ -34,18 +34,21 @@
</activity> </activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/> <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="com.evollu.react.fcm.MessagingService" android:enabled="true" android:exported="true"> <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService">
<service android:name="com.evollu.react.fcm.InstanceIdService" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/> <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter> </intent-filter>
</service> </service>
<service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/magenta" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" /> android:resource="@drawable/ic_notification" />
......
...@@ -3,10 +3,14 @@ package com.thaliapp; ...@@ -3,10 +3,14 @@ package com.thaliapp;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import io.invertase.firebase.RNFirebasePackage;
import io.invertase.firebase.messaging.RNFirebaseMessagingPackage;
import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage;
import io.invertase.firebase.instanceid.RNFirebaseInstanceIdPackage;
import io.sentry.RNSentryPackage; import io.sentry.RNSentryPackage;
import com.azendoo.reactnativesnackbar.SnackbarPackage; import com.azendoo.reactnativesnackbar.SnackbarPackage;
import com.i18n.reactnativei18n.ReactNativeI18n; import com.i18n.reactnativei18n.ReactNativeI18n;
import com.evollu.react.fcm.FIRMessagingPackage;
import com.BV.LinearGradient.LinearGradientPackage; import com.BV.LinearGradient.LinearGradientPackage;
import com.oblador.vectoricons.VectorIconsPackage; import com.oblador.vectoricons.VectorIconsPackage;
import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactNativeHost;
...@@ -27,12 +31,15 @@ public class MainApplication extends Application implements ReactApplication { ...@@ -27,12 +31,15 @@ public class MainApplication extends Application implements ReactApplication {
@Override @Override
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( return Arrays.asList(
new MainReactPackage(), new MainReactPackage(),
new RNSentryPackage(), new RNSentryPackage(),
new SnackbarPackage(), new SnackbarPackage(),
new ReactNativeI18n(), new ReactNativeI18n(),
new FIRMessagingPackage(), new RNFirebasePackage(),
new RNFirebaseMessagingPackage(),
new RNFirebaseNotificationsPackage(),
new RNFirebaseInstanceIdPackage(),
new LinearGradientPackage(), new LinearGradientPackage(),
new VectorIconsPackage() new VectorIconsPackage()
); );
......
...@@ -2,15 +2,12 @@ ...@@ -2,15 +2,12 @@
buildscript { buildscript {
repositories { repositories {
maven { google()
url 'https://maven.google.com/'
name 'Google'
}
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.3' classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:4.0.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
...@@ -19,23 +16,13 @@ buildscript { ...@@ -19,23 +16,13 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
maven { google()
url "https://maven.google.com" // Google's Maven repository
}
mavenLocal() mavenLocal()
jcenter() jcenter()
maven { maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android" url "$rootDir/../node_modules/react-native/android"
} }
configurations.all {
resolutionStrategy {
// Fixes https://github.com/evollu/react-native-fcm/issues/857#issuecomment-375243825
force 'com.google.firebase:firebase-messaging:11.4.0'
force 'com.google.firebase:firebase-core:11.4.0'
force 'com.google.android.gms:play-services-gcm:11.4.0'
}
}
} }
} }
......
...@@ -5,8 +5,8 @@ include ':react-native-snackbar' ...@@ -5,8 +5,8 @@ include ':react-native-snackbar'
project(':react-native-snackbar').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-snackbar/android') project(':react-native-snackbar').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-snackbar/android')
include ':react-native-locale-detector' include ':react-native-locale-detector'
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android') project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
include ':react-native-fcm' include ':react-native-firebase'
project(':react-native-fcm').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fcm/android') project(':react-native-firebase').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-firebase/android')
include ':react-native-linear-gradient' include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android') project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-vector-icons' include ':react-native-vector-icons'
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Linking, Platform, NativeModules } from 'react-native'; import {
Linking, Platform, NativeModules, Alert,
} from 'react-native';
import { applyMiddleware, createStore } from 'redux'; import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import FCM, { FCMEvent } from 'react-native-fcm'; import firebase from 'react-native-firebase';
import locale from 'react-native-locale-detector'; import locale from 'react-native-locale-detector';
import Moment from 'moment'; import Moment from 'moment';
import 'moment/locale/nl'; import 'moment/locale/nl';
...@@ -29,24 +31,6 @@ const sagaMiddleware = createSagaMiddleware(); ...@@ -29,24 +31,6 @@ const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware)); const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(sagas); sagaMiddleware.run(sagas);
FCM.on(FCMEvent.Notification, async (notif) => {
if (notif.fcm) {
FCM.presentLocalNotification({
title: notif.fcm.title,
body: notif.fcm.body,
color: notif.fcm.color,
icon: notif.fcm.icon === null ? 'ic_notification' : notif.fcm.icon,
action: notif.fcm.action,
tag: notif.fcm.tag,
show_in_foreground: true,
});
}
});
FCM.on(FCMEvent.RefreshToken, async () => {
store.dispatch(register());
});
class Main extends Component { class Main extends Component {
constructor() { constructor() {
super(); super();
...@@ -60,9 +44,19 @@ class Main extends Component { ...@@ -60,9 +44,19 @@ class Main extends Component {
componentDidMount() { componentDidMount() {
store.dispatch(sessionActions.init()); store.dispatch(sessionActions.init());
this.addDeepLinkingHandler(); this.addDeepLinkingHandler();
this.onTokenRefreshListener = firebase.messaging().onTokenRefresh(() => {
store.dispatch(register());
});
this.notificationListener = firebase.notifications().onNotification((notification) => {
Alert.alert(notification.title, notification.body, [
{ text: 'OK' },
]);
});
} }
componentWillUnmount() { componentWillUnmount() {
this.notificationListener();
this.onTokenRefreshListener();
Linking.removeEventListener('url', this.handleOpenURL); Linking.removeEventListener('url', this.handleOpenURL);
} }
......
import { call, takeEvery, select } from 'redux-saga/effects'; import { call, takeEvery, select } from 'redux-saga/effects';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import FCM from 'react-native-fcm'; import firebase from 'react-native-firebase';
import { Sentry } from 'react-native-sentry'; import { Sentry } from 'react-native-sentry';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as pushNotificationsActions from '../actions/pushNotifications'; import * as pushNotificationsActions from '../actions/pushNotifications';
const register = function* register(action) { const register = function* register(action) {
const messaging = firebase.messaging();
const token = yield select(tokenSelector); const token = yield select(tokenSelector);
const { categories } = action; const { categories } = action;
...@@ -15,18 +16,20 @@ const register = function* register(action) { ...@@ -15,18 +16,20 @@ const register = function* register(action) {
return; return;
} }
const hasPermission = yield call([messaging, 'hasPermission']);
let pushToken; let pushToken;
if (Platform.OS === 'ios') { if (Platform.OS === 'ios' && !hasPermission) {
try { try {
// this throws an error when the permissions are denied // this throws an error when the permissions are denied
yield call(FCM.requestPermissions); yield call([messaging, 'requestPermission']);
pushToken = yield call(FCM.getFCMToken); pushToken = yield call([messaging, 'getToken']);
} catch (err) { } catch (err) {
// return and do nothing since we have no token // return and do nothing since we have no token
return; return;
} }
} else { } else {
pushToken = yield call(FCM.getFCMToken); pushToken = yield call([messaging, 'getToken']);
} }
const body = { const body = {
...@@ -57,7 +60,7 @@ const register = function* register(action) { ...@@ -57,7 +60,7 @@ const register = function* register(action) {
}; };
const invalidate = function* invalidate() { const invalidate = function* invalidate() {
yield call(FCM.deleteInstanceId); yield call([firebase.iid(), 'delete']);
}; };
const pushNotificationsSaga = function* pushNotificationsSaga() { const pushNotificationsSaga = function* pushNotificationsSaga() {
......
...@@ -22,13 +22,11 @@ const pairsToObject = (obj, pair) => { ...@@ -22,13 +22,11 @@ const pairsToObject = (obj, pair) => {
return obj2; return obj2;
}; };
const getStoredItems = () => AsyncStorage.multiGet([
USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY, PUSHCATEGORYKEY,
]);
function* init() { function* init() {
try { try {
const result = yield call(getStoredItems); const result = yield call([AsyncStorage, 'multiGet'], [
USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY, PUSHCATEGORYKEY,
]);
const values = result.reduce(pairsToObject, {}); const values = result.reduce(pairsToObject, {});
const username = values[USERNAMEKEY]; const username = values[USERNAMEKEY];
......
...@@ -6,8 +6,8 @@ target 'ThaliApp' do ...@@ -6,8 +6,8 @@ target 'ThaliApp' do
# use_frameworks! # use_frameworks!
# Pods for ThaliApp # Pods for ThaliApp
pod 'Firebase/Core' pod 'Firebase/Core', '~> 5.3.0'
pod 'Firebase/Messaging' pod 'Firebase/Messaging', '~> 5.3.0'
target 'ThaliAppTests' do target 'ThaliAppTests' do
inherit! :search_paths inherit! :search_paths
......
PODS: PODS:
- Firebase/Core (4.8.0): - Firebase/Core (5.3.0):
- FirebaseAnalytics (= 4.0.5) - Firebase/CoreOnly
- FirebaseCore (= 4.0.13) - FirebaseAnalytics (= 5.0.1)
- Firebase/Messaging (4.8.0): - Firebase/CoreOnly (5.3.0):
- Firebase/Core - FirebaseCore (= 5.0.4)
- FirebaseMessaging (= 2.0.8) - Firebase/Messaging (5.3.0):
- FirebaseAnalytics (4.0.5): - Firebase/CoreOnly
- FirebaseCore (~> 4.0) - FirebaseMessaging (= 3.0.2)
- FirebaseInstanceID (~> 2.0) - FirebaseAnalytics (5.0.1):
- FirebaseCore (~> 5.0)
- FirebaseInstanceID (~> 3.0)
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
- nanopb (~> 0.3) - nanopb (~> 0.3)
- FirebaseCore (4.0.13): - FirebaseCore (5.0.4):
- "GoogleToolboxForMac/NSData+zlib (~> 2.1)" - "GoogleToolboxForMac/NSData+zlib (~> 2.1)"
- FirebaseInstanceID (2.0.8): - FirebaseInstanceID (3.1.1):
- FirebaseCore (~> 4.0) - FirebaseCore (~> 5.0)
- FirebaseMessaging (2.0.8): - FirebaseMessaging (3.0.2):
- FirebaseAnalytics (~> 4.0) - FirebaseCore (~> 5.0)
- FirebaseCore (~> 4.0) - FirebaseInstanceID (~> 3.0)
- FirebaseInstanceID (~> 2.0)
- GoogleToolboxForMac/Logger (~> 2.1) - GoogleToolboxForMac/Logger (~> 2.1)
- Protobuf (~> 3.1) - Protobuf (~> 3.1)
- GoogleToolboxForMac/Defines (2.1.3) - GoogleToolboxForMac/Defines (2.1.4)
- GoogleToolboxForMac/Logger (2.1.3): - GoogleToolboxForMac/Logger (2.1.4):
- GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/Defines (= 2.1.4)
- "GoogleToolboxForMac/NSData+zlib (2.1.3)": - "GoogleToolboxForMac/NSData+zlib (2.1.4)":
- GoogleToolboxForMac/Defines (= 2.1.3) - GoogleToolboxForMac/Defines (= 2.1.4)
- nanopb (0.3.8): - nanopb (0.3.8):
- nanopb/decode (= 0.3.8) - nanopb/decode (= 0.3.8)
- nanopb/encode (= 0.3.8) - nanopb/encode (= 0.3.8)
- nanopb/decode (0.3.8) - nanopb/decode (0.3.8)
- nanopb/encode (0.3.8) - nanopb/encode (0.3.8)
- Protobuf (3.5.0) - Protobuf (3.6.0)
DEPENDENCIES: DEPENDENCIES:
- Firebase/Core - Firebase/Core (~> 5.3.0)
- Firebase/Messaging - Firebase/Messaging (~> 5.3.0)
SPEC REPOS: SPEC REPOS:
https://github.com/cocoapods/specs.git: https://github.com/cocoapods/specs.git:
...@@ -48,15 +49,15 @@ SPEC REPOS: ...@@ -48,15 +49,15 @@ SPEC REPOS:
- Protobuf - Protobuf
SPEC CHECKSUMS: SPEC CHECKSUMS:
Firebase: 710decbbc6d9d48530e9a5dba3209740c3532e05 Firebase: 68afeeb05461db02d7c9e3215cda28068670f4aa