Skip to content

Commit

Permalink
feat: Integrate Firebase libraries and initial config to enable Push …
Browse files Browse the repository at this point in the history
…Notifications FCM. (MetaMask#10085)

<!--
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/)

## **Related issues**

Fixes:

## **Manual testing steps**

1. Go to this page...
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.
  • Loading branch information
Jonathansoufer authored Jun 27, 2024
1 parent 0703383 commit 7b0aab4
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 7 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=
9 changes: 9 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,12 @@ 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=""
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
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.3.15")
}
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);
});
}
}

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);
});
}
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);
});
};

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);
}
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
}
}
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>
<key>PROJECT_ID</key>
<string>notifications-dev-e4e6d</string>
<key>STORAGE_BUCKET</key>
<string>$(FCM_CONFIG_STORAGE_BUCKET)</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<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>
5 changes: 4 additions & 1 deletion ios/MetaMask/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#import <React/RCTRootView.h>
#import <React/RCTPushNotificationManager.h>
#import <RNBranch/RNBranch.h>
#import <Firebase.h>
#if DEBUG
#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
Expand All @@ -29,6 +30,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
} else {
foxCode = @"debug";
}
// Implements Firebase
[FIRApp configure];

// Uncomment this line to use the test key instead of the live one.
// [RNBranch useTestInstance];
Expand All @@ -55,7 +58,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
UIView* launchScreenView = [[[NSBundle mainBundle] loadNibNamed:@"LaunchScreen" owner:self options:nil] objectAtIndex:0];
launchScreenView.frame = self.window.bounds;
rootView.loadingView = launchScreenView;

[self initializeFlipper:application];

//Uncomment the following line to enable the splashscreen on ios
Expand Down
6 changes: 5 additions & 1 deletion ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def common_target_logic
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
:flipper_configuration => flipper_config,
# 27/06/2024 - Disabled in favor of Firebase Messaging usage due incompatibility. Source: https://rnfirebase.io/#altering-cocoapods-to-use-frameworks
#:flipper_configuration => flipper_config,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
)
Expand Down Expand Up @@ -104,6 +105,9 @@ end

target 'MetaMask' do
common_target_logic
# https://rnfirebase.io/
use_frameworks! :linkage => :static
$RNFirebaseAsStaticFramework = true
end

target 'MetaMask-QA' do
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@
"@react-native-community/checkbox": "^0.5.12",
"@react-native-community/netinfo": "6.0.0",
"@react-native-cookies/cookies": "^6.2.1",
"@react-native-firebase/app": "^20.1.0",
"@react-native-firebase/messaging": "^20.1.0",
"@react-native-masked-view/masked-view": "^0.2.6",
"@react-native-picker/picker": "^2.2.1",
"@react-navigation/bottom-tabs": "^5.11.11",
Expand Down
15 changes: 11 additions & 4 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ printTitle(){
echo ''
}


printError(){
ERROR_ICON=$'\342\235\214'
echo ''
Expand Down Expand Up @@ -160,7 +159,6 @@ loadJSEnv(){
export SENTRY_DISABLE_AUTO_UPLOAD=${SENTRY_DISABLE_AUTO_UPLOAD:-"true"}
}


prebuild(){
# Import provider
yarn --ignore-engines build:static-logos
Expand Down Expand Up @@ -189,6 +187,16 @@ prebuild_android(){
# Copy fonts with iconset
yes | cp -rf ./app/fonts/Metamask.ttf ./android/app/src/main/assets/fonts/Metamask.ttf

#Create google-services.json file to be used by the Firebase services.
# Check if GOOGLE_SERVICES_B64 is set
if [ ! -z "$GOOGLE_SERVICES_B64" ]; then
echo -n $GOOGLE_SERVICES_B64 | base64 -d > ./android/app/google-services.json
echo "google-services.json has been created successfully."
else
echo "GOOGLE_SERVICES_B64 is not set in the .env file."
exit 1
fi

if [ "$PRE_RELEASE" = false ] ; then
if [ -e $ANDROID_ENV_FILE ]
then
Expand Down Expand Up @@ -370,10 +378,9 @@ buildIosQA(){
fi
}


buildAndroidQA(){
remapEnvVariableQA

if [ "$PRE_RELEASE" = false ] ; then
adb uninstall io.metamask.qa
fi
Expand Down
Loading

0 comments on commit 7b0aab4

Please sign in to comment.