Skip to content

Commit

Permalink
chore: implementing Firebase messaging on iOS/Android (#10184)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR aims to handle ONLY the addition of Firebase related libraries
to our codebase as well implements iOS and Android specific setup to
enable Push Notifications FCM on MetaMask Mobile. No changes on
consuming Push Notifications will take place on THIS PR since we're
breaking this implementation down. No visual changes are introduced as
well nor ways to test it, since the video updated is just to increase
the understanding of what the changes will empower.

Documentation used for implementing it, [here](https://rnfirebase.io/)

ATTENTION: Flipper DOESN'T work with Firebase, so we endorse using
[Charles Proxy](https://www.charlesproxy.com/documentation/ios/)
instead.

## **Related issues**

Fixes:

## **Manual testing steps**

1. Start an Android building. Expect no error. 
2.
3.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**


https://github.com/MetaMask/metamask-mobile/assets/44679989/dd9f7570-a4cb-4831-9cb2-23bc5ce920a4

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

---------

Co-authored-by: Cal Leung <[email protected]>
  • Loading branch information
Jonathansoufer and Cal-L authored Jul 11, 2024
1 parent 84f589c commit 2c137c5
Show file tree
Hide file tree
Showing 17 changed files with 507 additions and 58 deletions.
8 changes: 8 additions & 0 deletions .android.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export MM_FOX_CODE="EXAMPLE_FOX_CODE"
export MM_BRANCH_KEY_TEST=
export MM_BRANCH_KEY_LIVE=
# Firebase
export FCM_CONFIG_API_KEY=
export FCM_CONFIG_AUTH_DOMAIN=
export FCM_CONFIG_PROJECT_ID=
export FCM_CONFIG_STORAGE_BUCKET=
export FCM_CONFIG_MESSAGING_SENDER_ID=
export FCM_CONFIG_APP_ID=
export GOOGLE_SERVICES_B64=
8 changes: 8 additions & 0 deletions .ios.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
MM_FOX_CODE = EXAMPLE_FOX_CODE
MM_BRANCH_KEY_TEST =
MM_BRANCH_KEY_LIVE =
# Firebase
FCM_CONFIG_API_KEY=
FCM_CONFIG_AUTH_DOMAIN=
FCM_CONFIG_PROJECT_ID=
FCM_CONFIG_STORAGE_BUCKET=
FCM_CONFIG_MESSAGING_SENDER_ID=
FCM_CONFIG_APP_ID=
GOOGLE_SERVICES_B64=
8 changes: 8 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,11 @@ export SECURITY_ALERTS_API_URL="http://localhost:3000"

# Temporary mechanism to enable security alerts API prior to release.
export SECURITY_ALERTS_API_ENABLED="true"
# Firebase
export FCM_CONFIG_API_KEY=""
export FCM_CONFIG_AUTH_DOMAIN=""
export FCM_CONFIG_PROJECT_ID=""
export FCM_CONFIG_STORAGE_BUCKET=""
export FCM_CONFIG_MESSAGING_SENDER_ID=""
export FCM_CONFIG_APP_ID=""
export GOOGLE_SERVICES_B64=""
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ git clone [email protected]:MetaMask/metamask-mobile.git && \
cd metamask-mobile
```

**Firebase Messaging Setup**

Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, would be preferable that you provide your own Firebase project config file and update your `google-services.json` file in the `android/app` directory as well your .env files (ios.env, js.env, android.env), adding GOOGLE_SERVICES_B64 variable depending on the environment you are running the app (ios/android).

ATTENTION: In case you don't provide your own Firebase project config file, you can make usage of a mock file at `android/app/google-services-example.json`, following the steps below from the root of the project:

```bash
base64 -i ./android/app/google-services-example.json
```

Copy the result to your clipboard and paste it in the GOOGLE_SERVICES_B64 variable in the .env file you are running the app.

In case of any doubt, please follow the instructions in the link below to get your Firebase project config file.

[Firebase Project Quickstart](https://firebaseopensource.com/projects/firebase/quickstart-js/messaging/readme/#getting_started)

**Install dependencies**

```bash
Expand Down
1 change: 1 addition & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
apply plugin: "io.sentry.android.gradle"
apply plugin: 'com.google.gms.google-services'

import com.android.build.OutputFile

Expand Down
22 changes: 22 additions & 0 deletions android/app/google-services-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"project_info": {
"project_id": "mockproject-1234",
"project_number": "123456789000",
"name": "FirebaseQuickstarts",
"firebase_url": "https://mockproject-1234.firebaseio.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
"client_id": "android:com.google.samples.quickstart.admobexample",
"client_type": 1,
"android_client_info": {
"package_name": "com.google.samples.quickstart.admobexample",
"certificate_hash": []
}
},
}
],
"configuration_version": "1"
}
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("io.sentry:sentry-android-gradle-plugin:4.2.0")
classpath("com.google.gms:google-services:4.4.2")
}
allprojects {
repositories {
Expand Down
83 changes: 83 additions & 0 deletions app/util/notifications/methods/fcmHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
checkPlayServices,
registerAppWithFCM,
unRegisterAppWithFCM,
checkApplicationNotificationPermission,
getFcmToken,
} from './fcmHelper';

jest.mock('@react-native-firebase/app', () => ({
utils: () => ({
playServicesAvailability: {
status: 1,
isAvailable: false,
hasResolution: true,
isUserResolvableError: true,
},
makePlayServicesAvailable: jest.fn(() => Promise.resolve()),
resolutionForPlayServices: jest.fn(() => Promise.resolve()),
promptForPlayServices: jest.fn(() => Promise.resolve()),
}),
}));

jest.mock('@react-native-firebase/messaging', () => ({
__esModule: true,
default: () => ({
hasPermission: jest.fn(() => Promise.resolve(true)),
subscribeToTopic: jest.fn(),
unsubscribeFromTopic: jest.fn(),
isDeviceRegisteredForRemoteMessages: false,
registerDeviceForRemoteMessages: jest.fn(() =>
Promise.resolve('registered'),
),
unregisterDeviceForRemoteMessages: jest.fn(() =>
Promise.resolve('unregistered'),
),
deleteToken: jest.fn(() => Promise.resolve()),
requestPermission: jest.fn(() => Promise.resolve(1)),
getToken: jest.fn(() => Promise.resolve('fcm-token')),
}),
FirebaseMessagingTypes: {
AuthorizationStatus: {
AUTHORIZED: 1,
PROVISIONAL: 2,
},
},
}));

jest.mock('react-native-permissions', () => ({
PERMISSIONS: {
ANDROID: {
POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS',
},
},
request: jest.fn(() => Promise.resolve('granted')),
}));

describe('Firebase and Permission Functions', () => {
it('should check checkPlayServices function call for coverage', async () => {
await checkPlayServices();
const token = await getFcmToken();

expect(token).toBe('fcm-token');
});
it('should check registerAppWithFCM function call for coverage', async () => {
await registerAppWithFCM();

const token = await getFcmToken();

expect(token).toBe('fcm-token');
});
it('should check unRegisterAppWithFCM function call for coverage', async () => {
await unRegisterAppWithFCM();
const token = await getFcmToken();

expect(token).toBe('fcm-token');
});
it('should check checkApplicationNotificationPermission function call for coverage', async () => {
await checkApplicationNotificationPermission();
const token = await getFcmToken();

expect(token).toBe('fcm-token');
});
});
99 changes: 99 additions & 0 deletions app/util/notifications/methods/fcmHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { utils } from '@react-native-firebase/app';
import messaging, {
FirebaseMessagingTypes,
} from '@react-native-firebase/messaging';
import Logger from '../../../util/Logger';
import { PERMISSIONS, request } from 'react-native-permissions';

export async function checkPlayServices() {
const { status, isAvailable, hasResolution, isUserResolvableError } =
utils().playServicesAvailability;
if (isAvailable) return Promise.resolve();

if (isUserResolvableError || hasResolution) {
switch (status) {
case 1:
return utils().makePlayServicesAvailable();
case 2:
return utils().resolutionForPlayServices();
default:
if (isUserResolvableError) return utils().promptForPlayServices();
if (hasResolution) return utils().resolutionForPlayServices();
}
}
return Promise.reject(
new Error('Unable to find a valid play services version.'),
);
}

export async function registerAppWithFCM() {
Logger.log(
'registerAppWithFCM status',
messaging().isDeviceRegisteredForRemoteMessages,
);
if (!messaging().isDeviceRegisteredForRemoteMessages) {
await messaging()
.registerDeviceForRemoteMessages()
.then((status: unknown) => {
Logger.log('registerDeviceForRemoteMessages status', status);
})
.catch((error: Error) => {
Logger.error(error);
});
}
}

export async function unRegisterAppWithFCM() {
Logger.log(
'unRegisterAppWithFCM status',
messaging().isDeviceRegisteredForRemoteMessages,
);

if (messaging().isDeviceRegisteredForRemoteMessages) {
await messaging()
.unregisterDeviceForRemoteMessages()
.then((status: unknown) => {
Logger.log('unregisterDeviceForRemoteMessages status', status);
})
.catch((error: Error) => {
Logger.error(error);
});
}
await messaging().deleteToken();
Logger.log(
'unRegisterAppWithFCM status',
messaging().isDeviceRegisteredForRemoteMessages,
);
}

export const checkApplicationNotificationPermission = async () => {
const authStatus = await messaging().requestPermission();

const enabled =
authStatus === FirebaseMessagingTypes.AuthorizationStatus.AUTHORIZED ||
authStatus === FirebaseMessagingTypes.AuthorizationStatus.PROVISIONAL;

if (enabled) {
Logger.log('Authorization status:', authStatus);
}
request(PERMISSIONS.ANDROID.POST_NOTIFICATIONS)
.then((result) => {
Logger.log('POST_NOTIFICATIONS status:', result);
})
.catch((error: Error) => {
Logger.error(error);
});
};

export const getFcmToken = async () => {
let token = null;
await checkApplicationNotificationPermission();
await registerAppWithFCM();
try {
token = await messaging().getToken();
Logger.log('getFcmToken-->', token);
} catch (error: unknown) {
Logger.error(error as Error);
}
return token;
};
7 changes: 7 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"react-native": {
"analytics_auto_collection_enabled": false,
"messaging_auto_init_enabled": false,
"messaging_ios_auto_register_for_remote_messages": true
}
}
26 changes: 26 additions & 0 deletions ios/GoogleService-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>$(FCM_CONFIG_API_KEY)</string>
<key>GCM_SENDER_ID</key>
<string>$(FCM_CONFIG_MESSAGING_SENDER_ID)</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>PROJECT_ID</key>
<string>$(FCM_CONFIG_PROJECT_ID)</string>
<key>STORAGE_BUCKET</key>
<string>$(FCM_CONFIG_STORAGE_BUCKET)</string>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>$(FCM_CONFIG_APP_ID)</string>
</dict>
</plist>
Loading

0 comments on commit 2c137c5

Please sign in to comment.