Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: implementing Firebase messaging on iOS/Android #10184

Merged
merged 22 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
51c53d0
chore: implementing Firebase messaging on iOS/Android
Jonathansoufer Jun 29, 2024
62e8dfb
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 1, 2024
1eda810
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 1, 2024
e52e329
chore: add a mocked google-services.json
Jonathansoufer Jul 3, 2024
0e02ae8
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 4, 2024
8a10024
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 5, 2024
2fc4fdf
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 8, 2024
40a94a3
chore: add README section fix about firebase usage
Jonathansoufer Jul 8, 2024
c070664
chore: add statements on README
Jonathansoufer Jul 9, 2024
728458f
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 10, 2024
f031ce6
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 10, 2024
3aa3415
Update ios/GoogleService-Info.plist
Jonathansoufer Jul 10, 2024
6d2144e
Update ios/GoogleService-Info.plist
Jonathansoufer Jul 10, 2024
99500ba
Update README.md
Jonathansoufer Jul 10, 2024
63fcf9e
chore: fix Loggers + file renaming
Jonathansoufer Jul 10, 2024
2dfb778
chore: remove unnecessary ios/Gemfile.lock
Jonathansoufer Jul 10, 2024
c45b9d5
chore: remove unecessary/deprecated flags
Jonathansoufer Jul 10, 2024
293af26
Merge branch 'feat/firebase-messaging' of github.com:MetaMask/metamas…
Jonathansoufer Jul 10, 2024
ce982ae
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 10, 2024
aa43462
Update README.md
Jonathansoufer Jul 11, 2024
daaeec9
Update README.md
Jonathansoufer Jul 11, 2024
1685749
Merge branch 'main' into feat/firebase-messaging
Jonathansoufer Jul 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 git@github.com: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/mock-google-services.json`, following the steps below from the root of the project:
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved

```bash
base64 -i ./android/app/mock-google-services.json
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
```

Copy the resul to your clipboard and paste it in the GOOGLE_SERVICES_B64 variable in the .env file you are running the app.
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved

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/mock-google-services.json
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
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: unknown) => {
Logger.log('registerDeviceForRemoteMessages error ', error);
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
});
}
}

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: unknown) => {
Logger.log('unregisterDeviceForRemoteMessages error ', error);
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
});
}
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: unknown) => {
Logger.log('POST_NOTIFICATIONS error ', error);
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
});
};

export const getFcmToken = async () => {
let token = null;
await checkApplicationNotificationPermission();
await registerAppWithFCM();
try {
token = await messaging().getToken();
Logger.log('getFcmToken-->', token);
} catch (error) {
Logger.log('getFcmToken Device Token error ', error);
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
}
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
}
}
12 changes: 12 additions & 0 deletions ios/Gemfile.lock
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
GEM
remote: https://rubygems.org/
specs:

PLATFORMS
arm64-darwin-23
ruby

DEPENDENCIES

BUNDLED WITH
2.5.8
30 changes: 30 additions & 0 deletions ios/GoogleService-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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>io.metamask.MetaMask</string>
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
<key>PROJECT_ID</key>
<string>notifications-dev-e4e6d</string>
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
<key>STORAGE_BUCKET</key>
<string>$(FCM_CONFIG_STORAGE_BUCKET)</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
<false></false>
<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
Loading