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"