diff --git a/.android.env.example b/.android.env.example index aceaabefa76..b542cac8819 100644 --- a/.android.env.example +++ b/.android.env.example @@ -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= diff --git a/.ios.env.example b/.ios.env.example index bd49b067660..ee287ea38fd 100644 --- a/.ios.env.example +++ b/.ios.env.example @@ -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= diff --git a/.js.env.example b/.js.env.example index 4648ae1b482..091257f7620 100644 --- a/.js.env.example +++ b/.js.env.example @@ -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="" diff --git a/android/app/build.gradle b/android/app/build.gradle index b9d2c2f58e5..5c51605b76e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 diff --git a/android/build.gradle b/android/build.gradle index 54950802185..5e557a92d88 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 { diff --git a/app/util/notifications/methods/fcmHelper.test.ts b/app/util/notifications/methods/fcmHelper.test.ts new file mode 100644 index 00000000000..1dc2eca655b --- /dev/null +++ b/app/util/notifications/methods/fcmHelper.test.ts @@ -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'); + }); +}); diff --git a/app/util/notifications/methods/fcmHelper.ts b/app/util/notifications/methods/fcmHelper.ts new file mode 100644 index 00000000000..ad8fec8fc1a --- /dev/null +++ b/app/util/notifications/methods/fcmHelper.ts @@ -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; +}; diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000000..ccdd1ba1508 --- /dev/null +++ b/firebase.json @@ -0,0 +1,7 @@ +{ + "react-native": { + "analytics_auto_collection_enabled": false, + "messaging_auto_init_enabled": false, + "messaging_ios_auto_register_for_remote_messages": true + } +} \ No newline at end of file diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist new file mode 100644 index 00000000000..163a74206df --- /dev/null +++ b/ios/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + $(FCM_CONFIG_API_KEY) + GCM_SENDER_ID + $(FCM_CONFIG_MESSAGING_SENDER_ID) + PLIST_VERSION + 1 + BUNDLE_ID + io.metamask.MetaMask + PROJECT_ID + notifications-dev-e4e6d + STORAGE_BUCKET + $(FCM_CONFIG_STORAGE_BUCKET) + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + $(FCM_CONFIG_APP_ID) + + diff --git a/ios/MetaMask/AppDelegate.m b/ios/MetaMask/AppDelegate.m index b22d5678257..22e333bfa4a 100644 --- a/ios/MetaMask/AppDelegate.m +++ b/ios/MetaMask/AppDelegate.m @@ -4,6 +4,7 @@ #import #import #import +#import #if DEBUG #ifdef FB_SONARKIT_ENABLED #import @@ -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]; @@ -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 diff --git a/ios/Podfile b/ios/Podfile index c292007f659..e0dc4c57971 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -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}/..", ) @@ -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 diff --git a/package.json b/package.json index 552e94579b0..a090113c61d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build.sh b/scripts/build.sh index 1022d574ca6..82fbdd74414 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -53,7 +53,6 @@ printTitle(){ echo '' } - printError(){ ERROR_ICON=$'\342\235\214' echo '' @@ -160,7 +159,6 @@ loadJSEnv(){ export SENTRY_DISABLE_AUTO_UPLOAD=${SENTRY_DISABLE_AUTO_UPLOAD:-"true"} } - prebuild(){ # Import provider yarn --ignore-engines build:static-logos @@ -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 @@ -370,10 +378,9 @@ buildIosQA(){ fi } - buildAndroidQA(){ remapEnvVariableQA - + if [ "$PRE_RELEASE" = false ] ; then adb uninstall io.metamask.qa fi diff --git a/yarn.lock b/yarn.lock index 3e03fa82aef..956b4c5d038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5963,6 +5963,19 @@ dependencies: invariant "^2.2.4" +"@react-native-firebase/app@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/app/-/app-20.1.0.tgz#86b9371290f92d51821b7299eede95336949f214" + integrity sha512-FCcTtmfz/Bk2laOEKOiUrQUkAnzerkRml7d3kZzJSxaBWLFxpWJQnnXqGZmD8hNWio2QEauB8llUD71KiDk+sw== + dependencies: + opencollective-postinstall "^2.0.3" + superstruct "^0.6.2" + +"@react-native-firebase/messaging@^20.1.0": + version "20.1.0" + resolved "https://registry.yarnpkg.com/@react-native-firebase/messaging/-/messaging-20.1.0.tgz#02026259c74d1725dfc5216158b05bc6655e7951" + integrity sha512-y9FtQ9dIQSyueuLeJghvfLYnay5BqPVgl9T94p+HtUlkxinOgNDjquQFtV/QlzVOyVpLrVPmknMohvBj/fvBzg== + "@react-native-masked-view/masked-view@^0.2.6": version "0.2.6" resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.2.6.tgz#b26c52d5db3ad0926b13deea79c69620966a9221" @@ -13395,6 +13408,16 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -17253,6 +17276,23 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g== + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg== + dependencies: + for-in "^1.0.1" + foreach@~2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -18824,6 +18864,11 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -20252,7 +20297,12 @@ kind-of@^1.1.0: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" integrity sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= -kind-of@^6.0.2: +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.1, kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -21720,6 +21770,14 @@ mitt@3.0.1, mitt@^3.0.1: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA== + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -26153,6 +26211,15 @@ shaka-player@^2.5.9: dependencies: eme-encryption-scheme-polyfill "^2.0.1" +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -26980,6 +27047,14 @@ suffix@^0.1.0: resolved "https://registry.yarnpkg.com/suffix/-/suffix-0.1.1.tgz#cc58231646a0ef1102f79478ef3a9248fd9c842f" integrity sha512-j5uf6MJtMCfC4vBe5LFktSe4bGyNTBk7I2Kdri0jeLrcv5B9pWfxVa5JQpoxgtR8vaVB7bVxsWgnfQbX5wkhAA== +superstruct@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.2.tgz#c5eb034806a17ff98d036674169ef85e4c7f6a1c" + integrity sha512-lvA97MFAJng3rfjcafT/zGTSWm6Tbpk++DP6It4Qg7oNaeM+2tdJMuVgGje21/bIpBEs6iQql1PJH6dKTjl4Ig== + dependencies: + clone-deep "^2.0.1" + kind-of "^6.0.1" + superstruct@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.3.tgz#de626a5b49c6641ff4d37da3c7598e7a87697046"