From 7b0aab433f8d097ae25bda3a2a631fc565a36384 Mon Sep 17 00:00:00 2001
From: Jonathan Ferreira <44679989+Jonathansoufer@users.noreply.github.com>
Date: Thu, 27 Jun 2024 14:19:28 +0100
Subject: [PATCH] feat: Integrate Firebase libraries and initial config to
enable Push Notifications FCM. (#10085)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **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**
### **Before**
### **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.
---
.android.env.example | 8 ++
.ios.env.example | 8 ++
.js.env.example | 9 ++
android/app/build.gradle | 1 +
android/build.gradle | 1 +
.../notifications/methods/fcmHelper.test.ts | 83 ++++++++++++++++
app/util/notifications/methods/fcmHelper.ts | 99 +++++++++++++++++++
firebase.json | 7 ++
ios/GoogleService-Info.plist | 30 ++++++
ios/MetaMask/AppDelegate.m | 5 +-
ios/Podfile | 6 +-
package.json | 2 +
scripts/build.sh | 15 ++-
yarn.lock | 77 ++++++++++++++-
14 files changed, 344 insertions(+), 7 deletions(-)
create mode 100644 app/util/notifications/methods/fcmHelper.test.ts
create mode 100644 app/util/notifications/methods/fcmHelper.ts
create mode 100644 firebase.json
create mode 100644 ios/GoogleService-Info.plist
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"