로메오의 블로그

[React Native] Firebase Push Notification 추가 [Android] 본문

App & OS/Hybrid

[React Native] Firebase Push Notification 추가 [Android]

romeoh 2019. 7. 29. 04:47
반응형

Firebase 목록

[React Native] URL 배포하기 [Android/iOS]

[REACT NATIVE] FIREBASE 연동하기

 

위에서 생성한 프로젝트에서 계속 진행합니다.

 

이 포스트는 최종적으로 firebase push notification을 적용하는 과정을 알아봅니다.

포스트를 작성하는 지금 현재 react native는 0.69.4까지 출시 되었는데,

해당 버전으로 프로젝트를 만들 경우 다른 라이브러리들과 호환에 문제가 많았습니다.

그래서 제가 사용해봤던 검증된 버전으로 포스트를 진행하도록 하겠습니다.(gradle, 라이브러리 등..)

최신 버전에 대해서는 직접 적용해보시기 바랍니다.

 

 

react-native-firebase 추가

$ yarn add react-native-firebase@5.3.1
$ react-native link react-native-firebase

async-storage 추가하기

$ yarn add @react-native-community/async-storage@1.3.3
$ react-native link @react-native-community/async-storage
{
  "name": "deployTest",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "@react-native-community/async-storage": "1.3.3",
    "react": "16.8.3",
    "react-native": "0.59.8",
    "react-native-firebase": "5.3.1"
  },
  "devDependencies": {
    "@babel/core": "7.4.3",
    "@babel/runtime": "7.4.3",
    "babel-jest": "24.7.1",
    "jest": "24.7.1",
    "metro-react-native-babel-preset": "0.53.1",
    "react-test-renderer": "16.8.3"
  },
  "jest": {
    "preset": "react-native"
  }
}

변경된 package.json 파일입니다.

앞서 말한것처럼 라이브러리는 latest버전이 아닙니다. (버전앞에 ^ 심볼을 모두 제거함)

 

Android Push 설정하기

android/app/build.gradle

react-native link react-native-firebase 명령을 실행하면 

implementation project(':react-native-firebase')

줄은 자동으로 추가됩니다.

implementation 'com.google.firebase:firebase-messaging:17.3.4' 만 추가합니다.

 

....
dependencies {
    implementation project(':react-native-firebase')
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
    implementation "com.facebook.react:react-native:+"  // From node_modules

    implementation 'com.google.firebase:firebase-core:16.0.6'
    implementation 'com.google.firebase:firebase-messaging:17.3.4'
}
....
apply plugin: 'com.google.gms.google-services'

android/app/build.gradle 최종 파일은 아래와 같습니다. (주석 제거)

apply plugin: "com.android.application"
import com.android.build.OutputFile
project.ext.react = [
    entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        applicationId "com.gaeyou.deploy"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
        ndk {
            abiFilters "armeabi-v7a", "x86"
        }
        missingDimensionStrategy 'react-native-camera', 'general'
    }
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
                storeFile file(MYAPP_RELEASE_STORE_FILE)
                storePassword MYAPP_RELEASE_STORE_PASSWORD
                keyAlias MYAPP_RELEASE_KEY_ALIAS
                keyPassword MYAPP_RELEASE_KEY_PASSWORD
            }
        }
    }
    splits {
        abi {
            reset()
            enable enableSeparateBuildPerCPUArchitecture
            universalApk false  // If true, also generate a universal APK
            include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
        }
    }
    buildTypes {
        release {
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
            signingConfig signingConfigs.release
        }
    }
    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
            def abi = output.getFilter(OutputFile.ABI)
            if (abi != null) {  // null for the universal-debug, universal-release variants
                output.versionCodeOverride =
                        versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
            }
        }
    }
}

dependencies {
    implementation project(':react-native-firebase')
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
    implementation "com.facebook.react:react-native:+"  // From node_modules

    implementation 'com.google.firebase:firebase-core:16.0.6'
    implementation 'com.google.firebase:firebase-messaging:17.3.4'
}
task copyDownloadableDepsToLibs(type: Copy) {
    from configurations.compile
    into 'libs'
}

apply plugin: 'com.google.gms.google-services'

MainApplication.java

MainApplication.java에 getPackages()에 

new RNFirebaseMessagingPackage(),
new RNFirebaseNotificationsPackage()

추가합니다.

package com.gaeyou.deploy;

import android.app.Application;

import com.facebook.react.ReactApplication;
import io.invertase.firebase.RNFirebasePackage;
import io.invertase.firebase.messaging.RNFirebaseMessagingPackage;
import io.invertase.firebase.notifications.RNFirebaseNotificationsPackage;

import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import java.util.Arrays;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
            new RNFirebasePackage(),
              new RNFirebaseMessagingPackage(),
                new RNFirebaseNotificationsPackage()
      );
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}

 

푸시 아이콘 추가하기

New > Image Asset을 선택합니다.

Icon Type을 Notification Icons로 변경하고 Clip Art를 선택해서 원하는 아이콘을 선택합니다.

Name을 변경합니다.

Finish를 눌러서 푸시 아이콘을 추가합니다.

 

Push Large Icon 추가

120 * 120 사이즈의 투명 png 파일을 준비해서 android/app/src/main/res/mipmap-*/ 폴더에 넣어줍니다.

파일명은 logo_push_large.png 으로 하겠습니다.

logo_push_large.png

nonojapan 시국에 도라에몽이라니!! 

그래도 전 일본여행은 안갑니다 -_-

color.xml 만들기

values 폴더에서 new > Values resource file을 선택합니다.

File name을 colors로 입력하고 ok를 누릅니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPush">#ff0000</color>
</resources>

colors.xml 파일에 colorPush를 추가합니다.

strings.xml 

strings.xml 파일을 열어서 Open editor를 누릅니다.

+ 아이콘을 눌러서 

key: default_notification_channel_id
default value: deployPushTest

추가합니다.

<resources>
    <string name="app_name">deployTest</string>
    <string name="default_notification_channel_id">deployPushTest</string>
</resources>

strings.xml에 default_notification_channel_id가 추가 되었습니다.

 

NotificationService.java 추가

package에 Java Class를 추가합니다.

NotificationService을 추가합니다.

package com.gaeyou.deploy;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class NotificationService extends FirebaseMessagingService {

    private static final String TAG = "NotificationService";
    public static final String REMOTE_NOTIFICATION_EVENT = "notifications-remote-notification";

    @Override
    public void onMessageReceived(RemoteMessage message) {
        Log.d(TAG, "onMessageReceived event received");
//        Log.d(TAG, message.toString());
        try {
            Intent intent = new Intent(this, MainActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);

            Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            NotificationCompat.Builder notificationBuilder;
            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

            String messageTitle = message.getData().get("title");
            String messageBody = message.getData().get("message");
            Log.d(TAG, messageTitle);
            Log.d(TAG, messageBody);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                notificationBuilder = new NotificationCompat.Builder(this, message.getData().get("android_channel_id"))
                        .setSmallIcon(R.drawable.ic_notification)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.logo_push_large))
                        .setContentTitle(messageTitle)
                        .setContentText(messageBody)
                        .setAutoCancel(true)
                        .setSound(defaultSoundUri)
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .setContentIntent(pendingIntent)
                        .setContentText(messageBody);

            } else {
                notificationBuilder = new NotificationCompat.Builder(this)
                        .setSmallIcon(R.drawable.ic_notification)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.logo_push_large))
                        .setContentTitle(messageTitle)
                        .setContentText(messageBody)
                        .setAutoCancel(true)
                        .setSound(defaultSoundUri)
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .setContentIntent(pendingIntent)
                        .setContentText(messageBody);
            }

            notificationManager.notify(0, notificationBuilder.build());
        } catch (Exception e) {
            Log.d(TAG, "Error ", e);
        }
    }
}

AndroidManifest.xml

AndroidManifest.xml파일을 위와 같이 수정합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.gaeyou.deploy">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <uses-permission android:name="android.permission.VIBRATE" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
        android:windowSoftInputMode="adjustResize"
        android:launchMode="singleTask">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
      <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

        <service android:name=".NotificationService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

        <service android:name="io.invertase.firebase.messaging.RNFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        <service android:name="io.invertase.firebase.messaging.RNFirebaseInstanceIdService">
            <intent-filter>
                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
            </intent-filter>
        </service>
        <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_notification" />
        <!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
             notification message. See README(https://goo.gl/6BKBk7) for more. -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/colorPush" />

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="@string/default_notification_channel_id"/>

        <receiver android:name="io.invertase.firebase.notifications.RNFirebaseNotificationReceiver"/>
        <receiver android:enabled="true" android:exported="true"  android:name="io.invertase.firebase.notifications.RNFirebaseNotificationsRebootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.QUICKBOOT_POWERON"/>
                <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

 

 

 

react native 수정

App.js

import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import firebase from 'react-native-firebase';
import AsyncStorage from '@react-native-community/async-storage';

export default class App extends Component {

    componentDidMount() {

        // 권한체크
        this.checkPermission();

        // notification listener 등록
        this.createNotificationListeners();
    }

    componentWillUnmount() {
        this.notificationListener;
        this.notificationOpenedListener;
    }

    /**
     * token을 가져오기 위해 권한을 체크한다.
     */
    async checkPermission() {
        const enabled = await firebase.messaging().hasPermission();
        if (enabled) {
            this.getToken();
        } else {
            this.requestPermission();
        }
    }

    /**
     * 토큰 가지고 오기
     */
    async getToken() {
        // storage에 저장된 token을 가지고옴
        let fcmToken = await AsyncStorage.getItem('fcmToken');
        if (!fcmToken) {
            // 저장된 token이 없으면 token을 발급
            fcmToken = await firebase.messaging().getToken();
            if (fcmToken) {
                console.log('fcmToken:', fcmToken);
                // 발급한 token을 저장함
                await AsyncStorage.setItem('fcmToken', fcmToken);
                this.setState({
                    fcmToken: fcmToken
                })
            }
        }
        console.log('fcmToken:', fcmToken);
    }

    /**
     * 권한을 요청함
     */
    async requestPermission() {
        try {
            await firebase.messaging().requestPermission();
            // 권한을 얻어서 token을 발급함
            this.getToken();
        } catch (error) {
            // 권한 없음
            console.log('permission rejected');
        }
    }

    /**
     * notification listener 등록
     */
    async createNotificationListeners() {
        this.notificationDisplayedListener = firebase.notifications().onNotificationDisplayed(notification => {
            this.toast('on notifi')
            // Process your notification as required
            // ANDROID: Remote notifications do not contain the channel ID. You will have to specify this manually if you'd like to re-display the notification.
        });

        /**
         * 앱이 실행되고 있는 상태에서 알림을 받음
         */
        this.notificationListener = firebase.notifications().onNotification((notification) => {
            const { title, body } = notification;
            console.log('onNotification:');

            const localNotification = new firebase.notifications.Notification({
                sound: 'default',
                show_in_foreground: true,
            })
                .setNotificationId(notification.notificationId)
                .setTitle(notification.title)
                .setBody(notification.body)
                .setSound('default')
                .setData(notification.data)
                .android.setChannelId('deployPushTest') // string.xml에서 설정한 channel id
                .android.setSmallIcon('@drawable/ic_notification') // 추가한 notification icon
                .android.setColor('@color/colorPush') // colors.xml에서 설정한 color
                .android.setAutoCancel(true)
                .android.setDefaults(firebase.notifications.Android.Defaults.All)
                .android.setPriority(firebase.notifications.Android.Priority.High);


            firebase.notifications()
                .displayNotification(localNotification)
                .catch(err => console.error(err));
            firebase.notifications().removeDeliveredNotification(localNotification.notificationId);
        });

        const channel = new firebase.notifications.Android.Channel('deployPushTest', 'deploy test app name', firebase.notifications.Android.Importance.High)
            .setDescription('deploy test app description')
        // .setSound('sampleaudio.mp3');
        firebase.notifications().android.createChannel(channel);

        /*
         * 앱이 background에 있을때 알림창을 터치한다.
         */
        this.notificationOpenedListener = firebase.notifications().onNotificationOpened(notificationOpen => {
            const { title, body } = notificationOpen.notification;
            console.log('onNotificationOpened:', notificationOpen);
            const action = notificationOpen.action
            const notification = notificationOpen.notification
            var seen = []
            alert(JSON.stringify(notificationOpen, function (key, val) {
                if (val != null && typeof val === 'object') {
                    if (seen.indexOf(val) >= 0) {
                        return
                    }
                    seen.push(val)
                }
                return val
            }))
            firebase.notifications().removeDeliveredNotification(notification.notificationId);
        });

        /*
         * 앱이 종료되었을때 알림을 터치한다.
         */
        firebase.notifications().getInitialNotification()
            .then(notificationOpen => {
                if (notificationOpen) {
                    const action = notificationOpen.action;
                    const notification = notificationOpen.notification;
                }
            });

        /*
         * foreground에 있을때 터치한다.
         */
        this.messageListener = firebase.messaging().onMessage(message => {
            console.log(JSON.stringify(message));
        });

    }

    render() {
        return (
            <View style={styles.container}>
                <Text style={styles.welcome}>Welcome to React Native!</Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },
    welcome: {
        fontSize: 20,
        textAlign: 'center',
        margin: 10,
    },
});

App.js파일을 위와 같이 수정하고 앱을 실행합니다.

 

Push 테스트

Firebase에서 Cloud Messaging을 눌러서 Send your first message를 누릅니다.

제목과 텍스트를 입력하고 다음을 누릅니다.

타겟을 안드로이드 앱을 선택하고 하단에 검토 버튼을 누릅니다.

게시를 눌러서 push 메시지를 안드로이드에 보냅니다.

하지만 android 앱은 아무런 반응이 없습니다.

이건 서버에서 보내는 푸시메세지의 data 필드가 없어서 생기는 현상입니다.

이 부분은 다음 포스트에서 postman으로 데이터를 보내는 방법으로 알아보겠습니다.

 

홈버튼을 눌러서 앱을 종료합니다.

방금 보낸 알림을 복제합니다.

처음에 보낸 알림을 검토해서 다시 게시합니다.

설정한 아이콘과 함께 푸시가 왔습니다.

커튼을 내려보면 Hello world 알림을 받은것을 알 수 있습니다.

색상도 적용되었습니다.

그런데 도라에몽 아이콘은 표시되지 않습니다.

이것도 postman에서 날릴때 해결됩니다.

푸시받은 내용을 눌러봅니다.

앱이 실행되면서 푸시 정보를 alert으로 표시합니다.

다음 포스트에서는 앱이 실행되어 있을때 푸시를 받을수 있도록 설정해 보겠습니다.

 

 

Firebase 목록

반응형
Comments