From 40d2353bbf9f1b30ca3c493cca85177f17fd9f97 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Wed, 13 Nov 2024 16:53:54 -0800 Subject: [PATCH 1/6] fix: Lock ruby version to 3.1.6 and bump pod to 1.16.2 (#12282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This bumps and locks pod version to 1.16.2. It also locks ruby to 3.1.6 ## **Related issues** Fixes: ## **Manual testing steps** 1. Run `yarn setup` 2. Steps should be successful 3. App should run normally ## **Screenshots/Recordings** ### **Before** ### **After** ## **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. --- .github/workflows/ci.yml | 2 - ios/Gemfile | 4 +- ios/Gemfile.lock | 32 ++++---- ios/Podfile.lock | 158 +++++++++++++++++++-------------------- 4 files changed, 97 insertions(+), 99 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75b4cf336b9..6f13d4efd5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 #v1 with: ruby-version: '3.1.6' - bundler-cache: true env: BUNDLE_GEMFILE: ios/Gemfile - run: yarn setup @@ -181,7 +180,6 @@ jobs: name: ios-bundle path: ios/main.jsbundle - - name: Push bundle size to mobile_bundlesize_stats repo run: ./scripts/push-bundle-size.sh env: diff --git a/ios/Gemfile b/ios/Gemfile index 4d245c9f943..e05db0cadde 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,10 +1,10 @@ source 'https://rubygems.org' # Recommended to use http://rbenv.org/ to install and use this version -ruby '>= 3.1.6' +ruby '3.1.6' # Allow minor version updates up to but excluding 2.0.0 -gem 'cocoapods', '~> 1.12' +gem 'cocoapods', '1.16.2' # Allow all version updates up to but excluding 7.1.0 gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index ed3f678b6d1..0f857ed10f5 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,18 +10,18 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) base64 (0.2.0) claide (1.1.0) - cocoapods (1.15.2) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -35,8 +35,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -60,36 +60,36 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.16.3) + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-linux-gnu) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) i18n (1.14.4) concurrent-ruby (~> 1.0) - json (2.7.2) + json (2.8.1) minitest (5.22.3) molinillo (0.8.0) - nanaimo (0.3.0) + nanaimo (0.4.0) nap (1.1.0) netrc (0.11.0) nkf (0.2.0) public_suffix (4.0.7) - rexml (3.3.6) - strscan + rexml (3.3.9) ruby-macho (2.5.1) - strscan (3.1.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS arm64-darwin-22 @@ -98,7 +98,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) - cocoapods (~> 1.12) + cocoapods (= 1.16.2) RUBY VERSION ruby 3.1.6p260 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d2a95947052..84b23bf4f32 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -200,9 +200,9 @@ PODS: - lottie-react-native (5.1.5): - lottie-ios (~> 3.4.0) - React-Core - - MMKV (1.3.9): - - MMKVCore (~> 1.3.9) - - MMKVCore (1.3.9) + - MMKV (2.0.0): + - MMKVCore (~> 2.0.0) + - MMKVCore (2.0.0) - MultiplatformBleAdapter (0.2.0) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) @@ -1165,7 +1165,7 @@ SPEC CHECKSUMS: BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e boost: 7dcd2de282d72e344012f7d6564d024930a6a440 Branch: 4ac024cb3c29b0ef628048694db3c4cfa679beb0 - BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 + BVLinearGradient: 44fd36b87f318e7933c4c91a6991442a5e3f5bcf CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 25cbffbaec517695d376ab4bc428948cd0f08088 @@ -1194,63 +1194,63 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 lottie-ios: 016449b5d8be0c3dcbcfa0a9988469999cd04c5d - lottie-react-native: 3e722c63015fdb9c27638b0a77969fc412067c18 - MMKV: 817ba1eea17421547e01e087285606eb270a8dcb - MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 + lottie-react-native: b6776287d7fd31be4fd865059cd890f744242ffd + MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 + MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d nanopb: 438bc412db1928dac798aa6fd75726007be04262 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - Permission-BluetoothPeripheral: 247e379c9ecb4b1af2b87f73e4a15a00a5bc0c1f + Permission-BluetoothPeripheral: 34ab829f159c6cf400c57bac05f5ba1b0af7a86e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 8dc08ca5a393b48b1c523ab6220dfdcc0fe000ad RCTRequired: fb207f74935626041e7308c9e88dcdda680f1073 - RCTSearchApi: d2d38a5a7bffbfb144e2c770fbb30f51b1053067 + RCTSearchApi: 5fc36140c598a74fd831dca924a28ed53bc7aa18 RCTTypeSafety: 146fd11361680250b7580dd1f7f601995cfad1b1 React: f3712351445cc96ba507425675a0cd8d31321d0c React-callinvoker: dcc51a66e02d20a70aeca2abbb1388d4d3011bf8 - React-Codegen: 04b7e88a7f5d3933d058ffb9cea7b0268666de79 - React-Core: ed3aeebf41aeb621de2ab4b58216a2fd5a5fd141 - React-CoreModules: 9d1e6f44bf658431a3b99561c8058b54b5959190 - React-cxxreact: d2d14fc0c0782bd9ed7a556892769b4034ae027c + React-Codegen: ef431087b06572288cd0f789c9cf1d22b37c3019 + React-Core: 88bf9e0d862195fda28723fd95aef3111025f300 + React-CoreModules: 96a557c45f6be644a82d63066c4ac79173bba0ff + React-cxxreact: 3db957f2a0db039b95c1103ea2274e36815b8009 React-debug: 4e90d08c78aa207c064a3860e1540ff252695585 React-jsc: 9ffa4c837c5286366d27c892b6c7c34da3cd5f3d - React-jsi: 020729f637b93456de0018061d44ce36f33c2d8a - React-jsiexecutor: ce8ecfcd3b7dbc9cb65a661110be17f5afd18aa3 + React-jsi: 08cb162e1d192bf197bc0693270ab65d8e9d4d5c + React-jsiexecutor: b71b576b4447d9fed6f2f1b146550de70d49a75a React-jsinspector: b86a8abae760c28d69366bbc1d991561e51341ed - React-logger: ed7c9e01e58529065e7da6bf8318baf15024283e - react-native-aes: 0143040f4e0cb19296b69b4acc7ddd8d3df9d62d - react-native-background-timer: 1b6e6b4e10f1b74c367a1fdc3c72b67c619b222b - react-native-ble-plx: f0557dbb6bd1f26cca75a67b5f33cfc7f7f9abed - react-native-blob-jsi-helper: 13c10135af4614dbc0712afba5960784cd44f043 - react-native-blob-util: 18b510205c080a453574a7d2344d64673d0ad9af - react-native-blur: 507cf3dd4434eb9d5ca5f183e49d8bcccdd66826 - react-native-branch: 4e42fda662d96893afbbd02839806931398e3d2e - react-native-camera: b8cc03e2feec0c04403d0998e37cf519d8fd4c6f - react-native-compat: 8b6a38155e778a20a008aea837efd00e099b6fe8 - react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c - react-native-fast-crypto: 5943c42466b86ad70be60d3a5f64bd22251e5d9e - react-native-flipper: 6cfd5991388121f7f96fc5171b93380f97ebb3c6 - react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a - react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868 - react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 - react-native-launch-arguments: 4e0fd58e56dcc7f52eedef9dc8eff81eb73ced7a - react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 - react-native-netinfo: 48c5f79a84fbc3ba1d28a8b0d04adeda72885fa8 - react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d - react-native-quick-base64: 777057ea4286f806b00259ede65dc79c7c706320 - react-native-quick-crypto: 455c1b411db006dba1026a30681ececb19180187 - react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 - react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe - react-native-safe-area-context: 9e40fb181dac02619414ba1294d6c2a807056ab9 - react-native-slider: f266dd860064138a659a42714e6da47a52a51107 - react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 - react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c - react-native-webview-mm: c518409c962c1f0f95c08bb6a700b9f97aff131b - React-NativeModulesApple: 7bab439cb5de9a76299210ed1127698170777a7f + React-logger: 8c0f8173197ad28ac3212c18f8141690209dfe52 + react-native-aes: e8b2e113d532b0efb6449754492aee9c218dd502 + react-native-background-timer: 007ff829f79644caf2ed013e22f0563560336f86 + react-native-ble-plx: c08c34c162509ec466c68a7cdc86b69c12e6efdd + react-native-blob-jsi-helper: bd7509e50b0f906044c53ad7ab767786054424c9 + react-native-blob-util: 6560d6fc4b940ec140f9c3ebe21c8669b1df789b + react-native-blur: 7c03644c321696ccec9778447180e0f9339b3604 + react-native-branch: 76e1f947b40597727e6faa5cba5824d7ecf6c6b0 + react-native-camera: 1e6fefa515d3af8b4aeaca3a8bffa2925252c4ea + react-native-compat: 8050db8973090f2c764807e7fa74f163f78e4c32 + react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411 + react-native-fast-crypto: 6b448866f5310cf203714a21147ef67f735bea8e + react-native-flipper: ca4382a2b6cfd319b6e212539bc1fe7aafe36879 + react-native-get-random-values: 0fd2b6a3129988d701d10e30f0622d5f039531bc + react-native-gzip: 8d602277c2564591f04dd1cec4043acc8350dcc3 + react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400 + react-native-launch-arguments: 7eb321ed3f3ef19b3ec4a2eca71c4f9baee76b41 + react-native-mmkv: 5a46c73e3e12aa872c4485ae0e4414b4040af79a + react-native-netinfo: 26560022f28c06d8ef00a9ff1e03beefbbb60c2d + react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd + react-native-quick-base64: daf67f19ee076b77f0755bf4056f3425f164e1d8 + react-native-quick-crypto: eff065b704d3f1c6e336cfc612dce63228ab3482 + react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 + react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd + react-native-safe-area-context: 667324e20fb3dd9c39c12d6036675ed90099bcd5 + react-native-slider: 6a25a7398addb8478798315a58504efce744009d + react-native-video: 2aad0d963bf3952bd9ebb2f53fab799338e8e202 + react-native-view-shot: bb8934cb93bf8ec740c81ed94f93244778797b6c + react-native-webview-mm: d5f16bf95d45db97b53851ab87c79b2e1d964a13 + React-NativeModulesApple: ee6c836571c874dc879cf87603edff00d8dded46 React-perflogger: 6acc671f527e69c0cd93b8e62821d33d3ddf25ca React-RCTActionSheet: 569bb9db46d85565d14697e15ecf2166e035eb07 React-RCTAnimation: 0eea98143c2938a8751a33722623d3e8a38fe1e4 - React-RCTAppDelegate: 74d38dbb3d8691f72e6dda670006e85d9ea21c91 + React-RCTAppDelegate: 11e6d38c00a34e1025b9ef26bb13968f6d9ed902 React-RCTBlob: 9b3b60e806ce5c9fe5a8ee449f3e41087617441c React-RCTImage: 0220975422a367e784dfd794adfc6454fab23c1f React-RCTLinking: 1abf9834017e080ecbd5b6a28b4fb15eb843a3dd @@ -1261,42 +1261,42 @@ SPEC CHECKSUMS: React-RCTVibration: 372a12b697a170aaee792f8a9999c40e1f2692d0 React-rncore: d1ccbd5adaf4a67703790838b7c62f140e72d32a React-runtimeexecutor: d4f7ff5073fcf87e14dbf89541d434218630246e - React-runtimescheduler: b360635f6f804ec42fa875500620882a6b97d2f5 - React-utils: 8eb3c12fd4a4da6df3824f7d9a961d73a6ed6e5d - ReactCommon: 317bddf4a70fca9e542343e942a504285282971c - ReactNativePayments: db62ee22a825e9e9c3e19c276d8d020881dd0630 - RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c - RNCCheckbox: a3ca9978cb0846b981d28da4e9914bd437403d77 - RNCClipboard: ddd4d291537f1667209c9c405aaa4307297e252e - RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906 - RNDateTimePicker: 4f3c4dbd4f908be32ec8c93f086e8924bd4a2e07 - RNDefaultPreference: 2f8d6d54230edbd78708ada8d63bb275e5a8415b - RNDeviceInfo: 1e3f62b9ec32f7754fac60bd06b8f8a27124e7f0 - RNFBApp: 5f87753a8d8b37d229adf85cd0ff37709ffdf008 - RNFBMessaging: 3fa1114c0868dd21f20dfe186adf42297ea316b1 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 - RNI18n: e2f7e76389fcc6e84f2c8733ea89b92502351fd8 - RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94 - RNNotifee: 2b7df6e32a9cc24b9af6b410fa7db1cd2f411d6d - RNOS: 6f2f9a70895bbbfbdad7196abd952e7b01d45027 - RNPermissions: 4e3714e18afe7141d000beae3755e5b5fb2f5e05 - RNReanimated: f8379347f71248607d530a21e31e4140c5910c25 - RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 - RNSensors: c363d486c879e181905dea84a2535e49af1c2d25 - RNSentry: ae79ba7d46cfdf501be85ef72af1b7c8b1d80a79 - RNShare: f116bbb04f310c665ca483d0bd1e88cf59b3b334 - RNSVG: a48668fd382115bc89761ce291a81c4ca5f2fd2e - RNVectorIcons: 6607bd3a30291d0edb56f9bbe7ae411ee2b928b0 - segment-analytics-react-native: dbdd08d96fec78132e96bda092562e41c2ce0ce0 + React-runtimescheduler: 06b060b5b022f4cdb6bd9fd3405396372179cd9b + React-utils: e50991349b1b749744f35ff93d943343886deb24 + ReactCommon: 394d4d2b27d88bb8ae15fa7f864a4a7525f467f0 + ReactNativePayments: 47056cd9f1dc32dbdd716974de5df700c44f12db + RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 + RNCCheckbox: 450ce156f3e29e25efa0315c96cfbabe5a39ded1 + RNCClipboard: ba13782f62310ffd4377332497241a1051f6870b + RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 + RNDateTimePicker: 590f2000e4272050b98689cee6c8abc66c25bb22 + RNDefaultPreference: 36fe31684af1f2d14e0664aa9a816d0ec6149cc1 + RNDeviceInfo: e5219d380b51ddb7f97e650ab99a518476b90203 + RNFBApp: 0e66b9f844efdf2ac3fa2b30e64c9db41a263b3d + RNFBMessaging: 70b12c9f22c7c9d5011ac9b12ac2bafbfb081267 + RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 + RNGestureHandler: 6572a5f44759900730562b418da289c373de8d06 + RNI18n: 11ec5086508673ef71b5b567da3e8bcca8a926e1 + RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 + RNKeychain: 3194f1c9d8599f39e570b4b5ecbcdd8cd610e771 + RNNotifee: 5165d37aaf980031837be3caece2eae5a6d73ae8 + RNOS: d07e5090b5060c6f2b83116d740a32cfdb33afe3 + RNPermissions: bd0d9ca7969ff7b999aa605ee2e5919c12522bfe + RNReanimated: 7a85cf61cf3849efb530a9de2204a119f426636a + RNScreens: f112bc5442a9a0e468809c107a43f55882d6cd98 + RNSensors: 4690be00931bc60be7c7bd457701edefaff965e3 + RNSentry: 984bb0495abef6c419697bef208c581f127891d1 + RNShare: d03cdc71e750246a48b81dcd62bd792bf57a758e + RNSVG: e77adf5edb2302f0f10dd03a09e92bb9420d914e + RNVectorIcons: 24be0b504ce32d5bea38bde6c645f08b9c736392 + segment-analytics-react-native: 885c1703579dc7964b97e7ae11d857669aa9b015 Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - sovran-react-native: 791f2f726b4d57ece59676eda58d6da9dc95ad4e - TcpSockets: a8eb6b5867fe643e6cfed5db2e4de62f4d1e8fd0 + sovran-react-native: e4721a564ee6ef5b5a0d901bc677018cf371ea01 + TcpSockets: 48866ffcb39d7114741919d21069fc90189e474a Yoga: 6f5ab94cd8b1ecd04b6e973d0bc583ede2a598cc YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: e0bcc4eb12d48746028cd4f4161a292fa9ddc627 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 From 7b76e08dc3fe28ca9e5b0ecc8978f70401c3675d Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:23:27 +0000 Subject: [PATCH 2/6] chore: Remove notifications logic from wallet view (#12276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removing notifications logic from wallet view because of a performance degredation. Investigations are still in progress. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --------- Co-authored-by: Cal-L Co-authored-by: Cal Leung --- app/components/Views/Wallet/index.test.tsx | 40 +--------------- app/components/Views/Wallet/index.tsx | 53 ++-------------------- bitrise.yml | 4 +- 3 files changed, 7 insertions(+), 90 deletions(-) diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx index a7baa2653b3..a145d9d1c8e 100644 --- a/app/components/Views/Wallet/index.test.tsx +++ b/app/components/Views/Wallet/index.test.tsx @@ -1,14 +1,12 @@ import React from 'react'; import Wallet from './'; import { renderScreen } from '../../../util/test/renderWithProvider'; -import { act, screen } from '@testing-library/react-native'; +import { screen } from '@testing-library/react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import Routes from '../../../constants/navigation/Routes'; import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; -import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; -import { AppState } from 'react-native'; const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272'; @@ -186,40 +184,4 @@ describe('Wallet', () => { ); expect(accountPicker).toBeDefined(); }); - it('dispatches account syncing on mount', () => { - jest.clearAllMocks(); - //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar - render(Wallet); - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(1); - }); - it('dispatches account syncing when appState switches from inactive|background to active', () => { - jest.clearAllMocks(); - - const addEventListener = jest.spyOn(AppState, 'addEventListener'); - - //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar - render(Wallet); - - expect(addEventListener).toHaveBeenCalledWith( - 'change', - expect.any(Function), - ); - const handleAppStateChange = ( - addEventListener as jest.Mock - ).mock.calls.find(([event]) => event === 'change')[1]; - - act(() => { - handleAppStateChange('background'); - handleAppStateChange('active'); - }); - - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(2); - - act(() => { - handleAppStateChange('inactive'); - handleAppStateChange('active'); - }); - - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(3); - }); }); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 2036f343925..d06231fc9b9 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -1,10 +1,4 @@ -import React, { - useEffect, - useRef, - useCallback, - useContext, - useLayoutEffect, -} from 'react'; +import React, { useEffect, useRef, useCallback, useContext } from 'react'; import { ActivityIndicator, StyleSheet, @@ -12,8 +6,6 @@ import { TextStyle, InteractionManager, Linking, - AppState, - AppStateStatus, } from 'react-native'; import type { Theme } from '@metamask/design-tokens'; import { connect, useSelector } from 'react-redux'; @@ -93,9 +85,7 @@ import { selectIsProfileSyncingEnabled, } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; -import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; import { useAccountName } from '../../hooks/useAccountName'; -import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; import useCheckNftAutoDetectionModal from '../../hooks/useCheckNftAutoDetectionModal'; @@ -162,10 +152,7 @@ const Wallet = ({ showNftFetchingLoadingIndicator, hideNftFetchingLoadingIndicator, }: WalletProps) => { - const appState = useRef(AppState.currentState); const { navigate } = useNavigation(); - const { listNotifications } = useListNotifications(); - const { dispatchAccountSyncing } = useAccountSyncing(); const walletRef = useRef(null); const theme = useTheme(); const { toastRef } = useContext(ToastContext); @@ -415,35 +402,6 @@ const Wallet = ({ [navigation, providerConfig.chainId], ); - // Layout effect when component/view is visible - // - fetches notifications - // - dispatches account syncing - useLayoutEffect(() => { - const handleAppStateChange = (nextAppState: AppStateStatus) => { - if ( - appState.current?.match(/inactive|background/) && - nextAppState === 'active' - ) { - listNotifications(); - dispatchAccountSyncing(); - } - - appState.current = nextAppState; - }; - - const subscription = AppState.addEventListener( - 'change', - handleAppStateChange, - ); - - listNotifications(); - dispatchAccountSyncing(); - - return () => { - subscription.remove(); - }; - }, [listNotifications, dispatchAccountSyncing]); - useEffect(() => { navigation.setOptions( getWalletNavbarOptions( @@ -533,12 +491,9 @@ const Wallet = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any let stakedBalance: any = 0; - const assets = [ - ...(tokens || []), - ]; + const assets = [...(tokens || [])]; if (accountBalanceByChainId) { - balance = renderFromWei(accountBalanceByChainId.balance); const nativeAsset = { // TODO: Add name property to Token interface in controllers. @@ -575,8 +530,8 @@ const Wallet = ({ conversionRate, currentCurrency, ), - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; assets.push(stakedAsset); } diff --git a/bitrise.yml b/bitrise.yml index 124b3e2daf6..eb1a69d7959 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -135,8 +135,8 @@ stages: - run_ios_api_specs: {} - run_tag_smoke_accounts_ios: {} - run_tag_smoke_accounts_android: {} - - run_tag_smoke_notifications_ios: {} - - run_tag_smoke_notifications_android: {} + # - run_tag_smoke_notifications_ios: {} + # - run_tag_smoke_notifications_android: {} # - run_tag_smoke_assets_ios: {} - run_tag_smoke_assets_android: {} - run_tag_smoke_confirmations_ios: {} From 94e2c57caf4d870d3ef94df169877997dacc3cc8 Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:04:13 -0500 Subject: [PATCH 3/6] feat: add gas impact modal to stake confirmation input view (#12245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds a new "gas impact" modal to the staking input screen. This modal warns the user when the estimated gas fees make up 30% or more of their intended deposit amount. When gas is 30% or more it means the user likely won't profit from their stake for a long time. ## **Related issues** Jira Ticket: [STAKE-846: Warning for gas cost impact](https://consensyssoftware.atlassian.net/browse/STAKE-846) ## **Manual testing steps** 1. Set `export MM_POOLED_STAKING_UI_ENABLED=true` in your local `.js.env` file 2. From the asset list page, click Ethereum 3. Click on "Stake" or "Stake More" buttons to begin staking flow 4. Enter a very small deposit amount where the gas cost is guaranteed to be more than the deposit amount. 5. The gas impact modal should appear. ## **Screenshots/Recordings** ### **Before** The user isn't warned about the gas cost. https://github.com/user-attachments/assets/35d8ab22-2c46-409e-9e0e-c22cdf3917dc ### **After** The user is shown a warning that the gas cost is 30% or greater than the deposit amount. https://github.com/user-attachments/assets/18c85722-b7be-427b-ae53-7c0f3bdfd3d3 ## **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 - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --- .../StakeInputView/StakeInputView.test.tsx | 30 +- .../Views/StakeInputView/StakeInputView.tsx | 49 ++- .../GasImpactModal/GasImpactModal.styles.ts | 16 + .../GasImpactModal/GasImpactModal.test.tsx | 75 ++++ .../GasImpactModal/GasImpactModal.types.ts | 13 + .../GasImpactModal.test.tsx.snap | 342 ++++++++++++++++++ .../Stake/components/GasImpactModal/index.tsx | 103 ++++++ .../UI/Stake/hooks/useStakingInput.ts | 13 + app/components/UI/Stake/routes/index.tsx | 6 + app/constants/navigation/Routes.ts | 1 + locales/languages/en.json | 5 +- 11 files changed, 636 insertions(+), 17 deletions(-) create mode 100644 app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts create mode 100644 app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx create mode 100644 app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts create mode 100644 app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap create mode 100644 app/components/UI/Stake/components/GasImpactModal/index.tsx diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index c7c3005ad89..6507156a447 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -9,6 +9,9 @@ import { ChainId, PooledStakingContract } from '@metamask/stake-sdk'; import { Contract } from 'ethers'; import { MOCK_GET_VAULT_RESPONSE } from '../../__mocks__/mockData'; import { toWei } from '../../../../../util/number'; +import { strings } from '../../../../../../locales/i18n'; +// eslint-disable-next-line import/no-namespace +import * as useStakingGasFee from '../../hooks/useStakingGasFee'; function render(Component: React.ComponentType) { return renderScreen( @@ -96,7 +99,6 @@ jest.mock('../../hooks/useStakingGasFee', () => ({ __esModule: true, default: () => ({ estimatedGasFeeWei: mockGasFee, - gasLimit: 70122, isLoadingStakingGasFee: false, isStakingGasFeeError: false, refreshGasValues: jest.fn(), @@ -203,5 +205,31 @@ describe('StakeInputView', () => { screen: Routes.STAKING.MODALS.LEARN_MORE, }); }); + + it('navigates to gas impact modal when gas cost is 30% or more of deposit amount', () => { + jest.spyOn(useStakingGasFee, 'default').mockReturnValue({ + estimatedGasFeeWei: toWei('0.25'), + isLoadingStakingGasFee: false, + isStakingGasFeeError: false, + refreshGasValues: jest.fn(), + }); + + render(StakeInputView); + + fireEvent.press(screen.getByText('25%')); + + fireEvent.press(screen.getByText(strings('stake.review'))); + + expect(mockNavigate).toHaveBeenLastCalledWith('StakeModals', { + screen: Routes.STAKING.MODALS.GAS_IMPACT, + params: { + amountFiat: '750', + amountWei: '375000000000000000', + annualRewardRate: '2.5%', + annualRewardsETH: '0.00938 ETH', + annualRewardsFiat: '18.75 USD', + }, + }); + }); }); }); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index dd45e400286..9c5e09462fa 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -47,6 +47,8 @@ const StakeInputView = () => { isLoadingVaultData, handleMax, balanceValue, + isHighGasCostImpact, + isLoadingStakingGasFee, } = useStakingInputHandlers(); const navigateToLearnMoreModal = () => { @@ -55,16 +57,30 @@ const StakeInputView = () => { }); trackEvent( createEventBuilder(MetaMetricsEvents.STAKE_LEARN_MORE_CLICKED) - .addProperties({ - selected_provider: 'consensys', - text: 'Tooltip Question Mark Trigger', - location: 'Stake Input View' - }) - .build() + .addProperties({ + selected_provider: 'consensys', + text: 'Tooltip Question Mark Trigger', + location: 'Stake Input View', + }) + .build(), ); }; const handleStakePress = useCallback(() => { + if (isHighGasCostImpact()) { + navigation.navigate('StakeModals', { + screen: Routes.STAKING.MODALS.GAS_IMPACT, + params: { + amountWei: amountWei.toString(), + amountFiat: fiatAmount, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + }, + }); + return; + } + navigation.navigate('StakeScreens', { screen: Routes.STAKING.STAKE_CONFIRMATION, params: { @@ -77,15 +93,15 @@ const StakeInputView = () => { }); trackEvent( createEventBuilder(MetaMetricsEvents.REVIEW_STAKE_BUTTON_CLICKED) - .addProperties({ - selected_provider: 'consensys', - tokens_to_stake_native_value: amountEth, - tokens_to_stake_usd_value: fiatAmount, - }) - .build(), + .addProperties({ + selected_provider: 'consensys', + tokens_to_stake_native_value: amountEth, + tokens_to_stake_usd_value: fiatAmount, + }) + .build(), ); }, [ - amountEth, + isHighGasCostImpact, navigation, amountWei, fiatAmount, @@ -93,7 +109,8 @@ const StakeInputView = () => { annualRewardsFiat, annualRewardRate, trackEvent, - createEventBuilder + createEventBuilder, + amountEth, ]); const handleMaxButtonPress = () => { @@ -164,7 +181,9 @@ const StakeInputView = () => { size={ButtonSize.Lg} labelTextVariant={TextVariant.BodyMDMedium} variant={ButtonVariants.Primary} - isDisabled={isOverMaximum || !isNonZeroAmount} + isDisabled={ + isOverMaximum || !isNonZeroAmount || isLoadingStakingGasFee + } width={ButtonWidthTypes.Full} onPress={handleStakePress} /> diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts new file mode 100644 index 00000000000..54b53fd9e90 --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + content: { + paddingBottom: 16, + }, + footer: { + paddingBottom: 16, + }, + }); + +export default styleSheet; diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx new file mode 100644 index 00000000000..7018981373c --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { GasImpactModalProps } from './GasImpactModal.types'; +import GasImpactModal from './index'; +import { Metrics, SafeAreaProvider } from 'react-native-safe-area-context'; +import { fireEvent } from '@testing-library/react-native'; +import { strings } from '../../../../../../locales/i18n'; + +const mockNavigate = jest.fn(); +const mockGoBack = jest.fn(); + +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useNavigation: () => ({ + navigate: mockNavigate, + goBack: mockGoBack, + }), + }; +}); + +const props: GasImpactModalProps = { + route: { + key: '1', + params: { + amountWei: '3210000000000000', + amountFiat: '7.46', + annualRewardRate: '2.5%', + annualRewardsETH: '2.5 ETH', + annualRewardsFiat: '$5000', + }, + name: 'params', + }, +}; + +const initialMetrics: Metrics = { + frame: { x: 0, y: 0, width: 320, height: 640 }, + insets: { top: 0, left: 0, right: 0, bottom: 0 }, +}; + +const renderGasImpactModal = () => + renderWithProvider( + + , + , + ); + +describe('GasImpactModal', () => { + it('render matches snapshot', () => { + const { toJSON } = renderGasImpactModal(); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('navigates to StakeConfirmationView on approval', () => { + const { getByText } = renderGasImpactModal(); + + const proceedAnywayButton = getByText(strings('stake.proceed_anyway')); + + fireEvent.press(proceedAnywayButton); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + }); + + it('closes gas impact modal on cancel', () => { + const { getByText } = renderGasImpactModal(); + + const proceedAnywayButton = getByText(strings('stake.cancel')); + + fireEvent.press(proceedAnywayButton); + + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts new file mode 100644 index 00000000000..a00204cfbee --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts @@ -0,0 +1,13 @@ +import { RouteProp } from '@react-navigation/native'; + +interface GasImpactModalRouteParams { + amountWei: string; + amountFiat: string; + annualRewardsETH: string; + annualRewardsFiat: string; + annualRewardRate: string; +} + +export interface GasImpactModalProps { + route: RouteProp<{ params: GasImpactModalRouteParams }, 'params'>; +} diff --git a/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap new file mode 100644 index 00000000000..4050753f14d --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap @@ -0,0 +1,342 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GasImpactModal render matches snapshot 1`] = ` + + + + + + + + + + + + + + + + + + Gas cost impact + + + + + + + + + + + + Warning: the transaction gas cost will account for more than 30% of your deposit. + + + + + Cancel + + + + + Proceed anyway + + + + + + + + , + +`; diff --git a/app/components/UI/Stake/components/GasImpactModal/index.tsx b/app/components/UI/Stake/components/GasImpactModal/index.tsx new file mode 100644 index 00000000000..4e348f75426 --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/index.tsx @@ -0,0 +1,103 @@ +import React, { useRef } from 'react'; +import BottomSheet, { + BottomSheetRef, +} from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import Text, { + TextColor, + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { View } from 'react-native'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { + ButtonProps, + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button/Button.types'; +import styleSheet from './GasImpactModal.styles'; +import { useStyles } from '../../../../hooks/useStyles'; +import { useNavigation } from '@react-navigation/native'; +import Routes from '../../../../../constants/navigation/Routes'; +import { GasImpactModalProps } from './GasImpactModal.types'; +import { strings } from '../../../../../../locales/i18n'; + +const GasImpactModal = ({ route }: GasImpactModalProps) => { + const { styles } = useStyles(styleSheet, {}); + + const { navigate } = useNavigation(); + + const sheetRef = useRef(null); + + const handleClose = () => { + sheetRef.current?.onCloseBottomSheet(); + }; + + const handleNavigateToStakeReviewScreen = () => { + const { + amountWei, + annualRewardRate, + annualRewardsFiat, + annualRewardsETH, + amountFiat, + } = route.params; + + navigate('StakeScreens', { + screen: Routes.STAKING.STAKE_CONFIRMATION, + params: { + amountWei: amountWei.toString(), + amountFiat, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + }, + }); + }; + + const footerButtons: ButtonProps[] = [ + { + variant: ButtonVariants.Secondary, + label: ( + + {strings('stake.cancel')} + + ), + size: ButtonSize.Lg, + onPress: handleClose, + }, + { + variant: ButtonVariants.Primary, + label: ( + + {strings('stake.proceed_anyway')} + + ), + labelTextVariant: TextVariant.BodyMDMedium, + size: ButtonSize.Lg, + onPress: handleNavigateToStakeReviewScreen, + }, + ]; + + return ( + + + + + {strings('stake.gas_cost_impact')} + + + + {strings('stake.gas_cost_impact_warning')} + + + + + ); +}; + +export default GasImpactModal; diff --git a/app/components/UI/Stake/hooks/useStakingInput.ts b/app/components/UI/Stake/hooks/useStakingInput.ts index 3193c1a7002..5920a3f633c 100644 --- a/app/components/UI/Stake/hooks/useStakingInput.ts +++ b/app/components/UI/Stake/hooks/useStakingInput.ts @@ -97,6 +97,17 @@ const useStakingInputHandlers = () => { ? `${balanceETH} ETH` : `${balanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; + const getDepositTxGasPercentage = useCallback( + () => estimatedGasFeeWei.mul(new BN(100)).div(amountWei).toString(), + [amountWei, estimatedGasFeeWei], + ); + + // Gas fee make up 30% or more of the deposit amount. + const isHighGasCostImpact = useCallback( + () => new BN(getDepositTxGasPercentage()).gt(new BN(30)), + [getDepositTxGasPercentage], + ); + return { amountEth, amountWei, @@ -122,6 +133,8 @@ const useStakingInputHandlers = () => { handleMax, isLoadingStakingGasFee, balanceValue, + getDepositTxGasPercentage, + isHighGasCostImpact, }; }; diff --git a/app/components/UI/Stake/routes/index.tsx b/app/components/UI/Stake/routes/index.tsx index c6b98f24d2e..cf1ae55b3cb 100644 --- a/app/components/UI/Stake/routes/index.tsx +++ b/app/components/UI/Stake/routes/index.tsx @@ -8,6 +8,7 @@ import UnstakeInputView from '../Views/UnstakeInputView/UnstakeInputView'; import UnstakeConfirmationView from '../Views/UnstakeConfirmationView/UnstakeConfirmationView'; import { StakeSDKProvider } from '../sdk/stakeSdkProvider'; import MaxInputModal from '../components/MaxInputModal'; +import GasImpactModal from '../components/GasImpactModal'; const Stack = createStackNavigator(); const ModalStack = createStackNavigator(); @@ -57,6 +58,11 @@ const StakeModalStack = () => ( component={MaxInputModal} options={{ headerShown: false }} /> + ); diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 008fb429b5d..3b3dfcc7ced 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -155,6 +155,7 @@ const Routes = { MODALS: { LEARN_MORE: 'LearnMore', MAX_INPUT: 'MaxInput', + GAS_IMPACT: 'GasImpact', }, }, ///: BEGIN:ONLY_INCLUDE_IF(external-snaps) diff --git a/locales/languages/en.json b/locales/languages/en.json index 1345810012d..9c371ef2bde 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3503,7 +3503,10 @@ "description": "Max is the total amount of ETH you have, minus the gas fee required to stake. It’s a good idea to keep some extra ETH in your wallet for future transactions." }, "use_max": "Use max", - "estimated_unstaking_time": "1 to 11 days" + "estimated_unstaking_time": "1 to 11 days", + "proceed_anyway": "Proceed anyway", + "gas_cost_impact": "Gas cost impact", + "gas_cost_impact_warning": "Warning: the transaction gas cost will account for more than 30% of your deposit." }, "default_settings": { "title": "Your Wallet is ready", From ba7c2cc99b790d9d8490c5dde7cb6964269967ac Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Wed, 13 Nov 2024 23:41:14 -0600 Subject: [PATCH 4/6] feat: add loading skeleton for staking banners (#12280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds a loading component for the time when pooled stakes data is being fetched before being rendered to the user to avoid abrupt pop in of banners on the screen and make the UI experience smoother. ## **Related issues** Fixes: [STAKE-854](https://consensyssoftware.atlassian.net/browse/STAKE-854) ## **Manual testing steps** 1. Set export MM_POOLED_STAKING_UI_ENABLED=true in your local .js.env file 2. From the asset list page, click Ethereum 3. You will see a loading skeleton before the staking banners appear. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/5c63f9e7-f950-44aa-9d5a-55a65ec1bd37 ### **After** https://github.com/user-attachments/assets/28d93efc-b2e0-49a6-979b-92ebd6008a81 ## **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. [STAKE-854]: https://consensyssoftware.atlassian.net/browse/STAKE-854?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../StakingBalance/StakingBalance.tsx | 126 ++++++++++-------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 9aafb72b6e9..a014fca6425 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -43,6 +43,7 @@ import type { TokenI } from '../../../Tokens/types'; import useBalance from '../../hooks/useBalance'; import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import { selectChainId } from '../../../../../selectors/networkController'; +import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; export interface StakingBalanceProps { asset: TokenI; @@ -62,6 +63,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { exchangeRate, hasStakedPositions, hasEthToUnstake, + isLoadingPooledStakesData, } = usePooledStakes(); const { vaultData } = useVaultData(); const annualRewardRate = vaultData?.apy || ''; @@ -94,6 +96,74 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { return <>; } + const renderStakingContent = () => { + if (isLoadingPooledStakesData) { + return ( + + + + ); + } + + if (!isEligibleForPooledStaking) { + return ( + + ); + } + + return ( + <> + {unstakingRequests.map( + ({ positionTicket, withdrawalTimestamp, assetsToDisplay }) => + assetsToDisplay && ( + + ), + )} + + {hasClaimableEth && ( + + )} + + {!hasStakedPositions && ( + + )} + + + + ); + }; + return ( {hasStakedPositions && ( @@ -120,61 +190,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { )} - - {!isEligibleForPooledStaking ? ( - - ) : ( - <> - {unstakingRequests.map( - ({ positionTicket, withdrawalTimestamp, assetsToDisplay }) => - assetsToDisplay && ( - - ), - )} - - {hasClaimableEth && ( - - )} - - {!hasStakedPositions && ( - - )} - - - - )} - + {renderStakingContent()} ); }; From 42e2c7e3bd0b6910eccac877059eccfaaf368677 Mon Sep 17 00:00:00 2001 From: Nicholas Smith Date: Thu, 14 Nov 2024 09:20:59 -0600 Subject: [PATCH 5/6] fix: ensure unstake max will unstake all user shares (#12283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** 1. This PR addresses a bug where when unstaking entire balance staked causes 1 share to be left in the vault that cannot be unstaked from the UI. 2. The solution is for mobile only and is a quick fix, note that the issue currently exists in portfolio as well and will need to be fixed properly. We solve the issue by calling the getShares contract method to get the total user shares instead of converting the total assets into shares with the convertToShares contract method. We add the interface for the getShares contract method on the fly as it is not part of the contract abi in the stake-sdk where we get the contract instance from. ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/STAKE-867 ## **Manual testing steps** 1. Stake some amount of ETH or have some ETH staked already 2. Unstake All of the ETH 3. When the transaction is successful, there should be no ETH listed for the Staked Ethereum asset value. It should be exactly 0 and not < .0001 4. If there is an account that has 1 share left, it should now be able to exit that 1 share. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/4d235027-642b-4e4b-af50-7ae798ad47af ### **After** https://github.com/user-attachments/assets/b9e9df65-a1e9-4d58-8bf5-3239b5007f41 ## **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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --- .../Stake/hooks/usePoolStakedUnstake/index.ts | 31 +++++++++++-- .../usePoolStakedUnstake.test.tsx | 43 ++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts b/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts index 04e8802c5c7..641a2fd9c49 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts +++ b/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import { PooledStakingContract, ChainId } from '@metamask/stake-sdk'; import { useStakeContext } from '../useStakeContext'; import { @@ -8,6 +9,7 @@ import { import { addTransaction } from '../../../../../util/transaction-controller'; import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import trackErrorAsAnalytics from '../../../../../util/metrics/TrackError/trackErrorAsAnalytics'; +import useBalance from '../useBalance'; const generateUnstakeTxParams = ( activeAccountAddress: string, @@ -23,11 +25,33 @@ const generateUnstakeTxParams = ( }); const attemptUnstakeTransaction = - (pooledStakingContract: PooledStakingContract) => + (pooledStakingContract: PooledStakingContract, stakedBalanceWei: string) => // Note: receiver is the user address attempting to unstake. async (valueWei: string, receiver: string) => { try { - const shares = await pooledStakingContract.convertToShares(valueWei); + // STAKE-867: This is temporary logic for the unstake all action + // if we are unstaking the total assets we send the total shares + // the user has in the vault through getShares contract method + // this is a quick fix for mobile only and will be refactored to cover + // portfolio in the future. We avoid the case where contract level rounding + // error causes 1 wei dust to be left when converting assets to shares + // and attempting to unstake all assets + let shares; + if (valueWei === stakedBalanceWei) { + // create the interface for the getShares method and call getShares to get user shares + const tempInterface = new ethers.utils.Interface([ + 'function getShares(address) returns (uint256)', + ]); + const data = tempInterface.encodeFunctionData('getShares', [receiver]); + const sharesResult = await pooledStakingContract?.contract.provider.call({ + to: pooledStakingContract?.contract.address, + data, + }); + const [sharesBN] = tempInterface.decodeFunctionResult('getShares', sharesResult); + shares = sharesBN.toString(); + } else { + shares = await pooledStakingContract.convertToShares(valueWei); + } const gasLimit = await pooledStakingContract.estimateEnterExitQueueGas( shares.toString(), @@ -64,11 +88,12 @@ const attemptUnstakeTransaction = const usePoolStakedUnstake = () => { const stakeContext = useStakeContext(); + const { stakedBalanceWei } = useBalance(); const stakingContract = stakeContext.stakingContract as PooledStakingContract; return { - attemptUnstakeTransaction: attemptUnstakeTransaction(stakingContract), + attemptUnstakeTransaction: attemptUnstakeTransaction(stakingContract, stakedBalanceWei), }; }; diff --git a/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx b/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx index eb546d23f64..780791f1d0c 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx +++ b/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx @@ -7,8 +7,9 @@ import { StakingType, ChainId, } from '@metamask/stake-sdk'; -import { Contract } from 'ethers'; +import { BigNumber, Contract, ethers } from 'ethers'; import { Stake } from '../../sdk/stakeSdkProvider'; +import useBalance from '../useBalance'; const MOCK_ADDRESS_1 = '0x0'; @@ -32,6 +33,8 @@ const MOCK_DEPOSIT_CONTRACT_ADDRESS = const MOCK_RECEIVER_ADDRESS = '0x316bde155acd07609872a56bc32ccfb0b13201fa'; const MOCK_UNSTAKE_GAS_LIMIT = 73135; const MOCK_UNSTAKE_VALUE_WEI = '10000000000000000'; // 0.01 ETH +const MOCK_STAKED_BALANCE_VALUE_WEI = '20000000000000000'; // 0.02 ETH +const MOCK_UNSTAKE_ALL_VALUE_WEI = MOCK_STAKED_BALANCE_VALUE_WEI; const ENCODED_TX_UNSTAKE_DATA = { chainId: 1, @@ -82,7 +85,12 @@ jest.mock('../../../../../core/Engine', () => { const mockPooledStakingContractService: PooledStakingContract = { chainId: ChainId.ETHEREUM, connectSignerOrProvider: mockConnectSignerOrProvider, - contract: new Contract('0x0000000000000000000000000000000000000000', []), + contract: { + ...new Contract('0x0000000000000000000000000000000000000000', []), + provider: { + call: jest.fn(), + }, + } as unknown as Contract, convertToShares: mockConvertToShares, encodeClaimExitedAssetsTransactionData: jest.fn(), encodeDepositTransactionData: jest.fn(), @@ -100,10 +108,19 @@ const mockSdkContext: Stake = { setSdkType: jest.fn(), }; +const mockBalance: Pick, 'stakedBalanceWei'> = { + stakedBalanceWei: MOCK_STAKED_BALANCE_VALUE_WEI, +}; + jest.mock('../useStakeContext', () => ({ useStakeContext: () => mockSdkContext, })); +jest.mock('../useBalance', () => ({ + __esModule: true, + default: () => mockBalance, +})); + describe('usePoolStakedUnstake', () => { afterEach(() => { jest.clearAllMocks(); @@ -129,5 +146,27 @@ describe('usePoolStakedUnstake', () => { expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledTimes(1); expect(mockAddTransaction).toHaveBeenCalledTimes(1); }); + + it('attempts to create and submit an unstake all transaction', async () => { + jest.spyOn(ethers.utils, 'Interface').mockImplementation(() => ({ + encodeFunctionData: jest.fn(), + decodeFunctionResult: jest.fn().mockReturnValue([BigNumber.from(MOCK_UNSTAKE_ALL_VALUE_WEI)]), + } as unknown as ethers.utils.Interface)); + + const { result } = renderHookWithProvider(() => usePoolStakedUnstake(), { + state: mockInitialState, + }); + + await result.current.attemptUnstakeTransaction( + MOCK_UNSTAKE_ALL_VALUE_WEI, + MOCK_RECEIVER_ADDRESS, + ); + + expect(mockConvertToShares).toHaveBeenCalledTimes(0); + expect(mockEstimateEnterExitQueueGas).toHaveBeenCalledTimes(1); + expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledTimes(1); + expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledWith(BigNumber.from(MOCK_UNSTAKE_ALL_VALUE_WEI).toString(), MOCK_RECEIVER_ADDRESS, { gasLimit: MOCK_UNSTAKE_GAS_LIMIT }); + expect(mockAddTransaction).toHaveBeenCalledTimes(1); + }); }); }); From 4fa0c15ea8565a3baecf1b6eae033bc211254522 Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:48:58 +0000 Subject: [PATCH 6/6] chore: Add tags to UI Startup sentry transaction (#12286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Needs testing in QA build to verify tags Android QA build: https://app.bitrise.io/build/7d1397e9-b56a-488b-8b23-509d53b2989a?tab=log Link showing the tags: https://metamask.sentry.io/performance/trace/67e58676fea44614bac9ad8af72324cd/?dataset=transactions&field=title&field=project&field=user.display&field=timestamp&name=All%20Errors&node=txn-0ae4a10418b8431e88633814e51d032f&project=2651591&query=device%3AMAR-LX1B&queryDataset=transaction-like&sort=-timestamp&source=discover&statsPeriod=90d×tamp=1731585973&yAxis=count%28%29 ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. --------- Co-authored-by: Aslau Mario-Daniel --- app/util/sentry/utils.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 06937e5d3bc..5241f5fa41e 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -10,6 +10,7 @@ import { store } from '../../store'; import { Performance } from '../../core/Performance'; import Device from '../device'; import { TraceName } from '../trace'; +import { getTraceTags } from './tags'; /** * This symbol matches all object properties when used in a mask */ @@ -402,15 +403,19 @@ function rewriteReport(report) { */ export function excludeEvents(event) { // This is needed because store starts to initialise before performance observers completes to measure app start time - if (event?.transaction === TraceName.UIStartup && Device.isAndroid()) { - const appLaunchTime = Performance.appLaunchTime; - const formattedAppLaunchTime = (event.start_timestamp = Number( - `${appLaunchTime.toString().slice(0, 10)}.${appLaunchTime - .toString() - .slice(10)}`, - )); - if (event.start_timestamp !== formattedAppLaunchTime) { - event.start_timestamp = formattedAppLaunchTime; + if (event?.transaction === TraceName.UIStartup) { + event.tags = getTraceTags(store.getState()); + + if (Device.isAndroid()) { + const appLaunchTime = Performance.appLaunchTime; + const formattedAppLaunchTime = (event.start_timestamp = Number( + `${appLaunchTime.toString().slice(0, 10)}.${appLaunchTime + .toString() + .slice(10)}`, + )); + if (event.start_timestamp !== formattedAppLaunchTime) { + event.start_timestamp = formattedAppLaunchTime; + } } } //Modify or drop event here